diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 550eda882b..c1d2457ed4 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,26 +6,27 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get release URL - id: get-release-url - run: | - if [ "${{ github.event.release.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview/latest" - else - URL="https://zed.dev/releases/stable/latest" - fi - echo "::set-output name=URL::$URL" - - name: Get content - uses: 2428392/gh-truncate-string-action@v1.2.0 - id: get-content - with: - stringToTruncate: | - 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! + - name: Get release URL + id: get-release-url + run: | + if [ "${{ github.event.release.prerelease }}" == "true" ]; then + URL="https://zed.dev/releases/preview/latest" + else + URL="https://zed.dev/releases/stable/latest" + fi + echo "::set-output name=URL::$URL" + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.3.0 + id: get-content + with: + stringToTruncate: | + 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! - ${{ github.event.release.body }} - maxLength: 2000 - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v5.3.0 - with: - webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: ${{ steps.get-content.outputs.string }} + ${{ github.event.release.body }} + maxLength: 2000 + truncationSymbol: "..." + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v5.3.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + content: ${{ steps.get-content.outputs.string }} diff --git a/Cargo.lock b/Cargo.lock index d0e7939bb5..7259836f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1169,7 +1169,7 @@ dependencies = [ "futures 0.3.28", "gpui2", "language2", - "live_kit_client", + "live_kit_client2", "log", "media", "postage", @@ -1541,13 +1541,12 @@ dependencies = [ "schemars", "serde", "serde_derive", - "settings", "settings2", "smol", "sum_tree", "sysinfo", "tempfile", - "text", + "text2", "thiserror", "time", "tiny_http", @@ -1603,7 +1602,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.27.0" +version = "0.28.0" dependencies = [ "anyhow", "async-trait", @@ -3092,7 +3091,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "text", + "text2", "time", "util", ] @@ -3384,6 +3383,26 @@ dependencies = [ "url", ] +[[package]] +name = "git3" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clock", + "collections", + "futures 0.3.28", + "git2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "smol", + "sum_tree", + "text2", + "unindent", + "util", +] + [[package]] name = "glob" version = "0.3.1" @@ -4225,7 +4244,7 @@ dependencies = [ "settings2", "shellexpand", "util", - "workspace", + "workspace2", ] [[package]] @@ -4358,7 +4377,7 @@ dependencies = [ "env_logger 0.9.3", "futures 0.3.28", "fuzzy2", - "git", + "git3", "globset", "gpui2", "indoc", @@ -4379,7 +4398,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", - "text", + "text2", "theme2", "tree-sitter", "tree-sitter-elixir", @@ -4601,6 +4620,39 @@ dependencies = [ "simplelog", ] +[[package]] +name = "live_kit_client2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-broadcast", + "async-trait", + "block", + "byteorder", + "bytes 1.5.0", + "cocoa", + "collections", + "core-foundation", + "core-graphics", + "foreign-types", + "futures 0.3.28", + "gpui2", + "hmac 0.12.1", + "jwt", + "live_kit_server", + "log", + "media", + "nanoid", + "objc", + "parking_lot 0.11.2", + "postage", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.7", + "simplelog", +] + [[package]] name = "live_kit_server" version = "0.1.0" @@ -5047,6 +5099,53 @@ dependencies = [ "workspace", ] +[[package]] +name = "multi_buffer2" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "client2", + "clock", + "collections", + "convert_case 0.6.0", + "copilot2", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "git3", + "gpui2", + "indoc", + "itertools 0.10.5", + "language2", + "lazy_static", + "log", + "lsp2", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "postage", + "project2", + "pulldown-cmark", + "rand 0.8.5", + "rich_text2", + "schemars", + "serde", + "serde_derive", + "settings2", + "smallvec", + "smol", + "snippet", + "sum_tree", + "text2", + "theme2", + "tree-sitter", + "tree-sitter-html", + "tree-sitter-rust", + "tree-sitter-typescript", + "unindent", + "util", +] + [[package]] name = "multimap" version = "0.8.3" @@ -6218,8 +6317,8 @@ dependencies = [ "fsevent", "futures 0.3.28", "fuzzy2", - "git", "git2", + "git3", "globset", "gpui2", "ignore", @@ -6247,7 +6346,7 @@ dependencies = [ "sum_tree", "tempdir", "terminal2", - "text", + "text2", "thiserror", "toml 0.5.11", "unindent", @@ -6878,6 +6977,24 @@ dependencies = [ "util", ] +[[package]] +name = "rich_text2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui2", + "language2", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme2", + "util", +] + [[package]] name = "ring" version = "0.16.20" @@ -7493,7 +7610,6 @@ dependencies = [ "collections", "editor", "futures 0.3.28", - "globset", "gpui", "language", "log", @@ -7746,7 +7862,6 @@ dependencies = [ "anyhow", "collections", "feature_flags2", - "fs", "fs2", "futures 0.3.28", "gpui2", @@ -8756,6 +8871,29 @@ dependencies = [ "util", ] +[[package]] +name = "text2" +version = "0.1.0" +dependencies = [ + "anyhow", + "clock", + "collections", + "ctor", + "digest 0.9.0", + "env_logger 0.9.3", + "gpui2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "regex", + "rope", + "smallvec", + "sum_tree", + "util", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -8785,10 +8923,11 @@ name = "theme2" version = "0.1.0" dependencies = [ "anyhow", - "fs", + "fs2", "gpui2", "indexmap 1.9.3", "parking_lot 0.11.2", + "refineable", "schemars", "serde", "serde_derive", @@ -8798,21 +8937,6 @@ dependencies = [ "util", ] -[[package]] -name = "theme_converter" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap 4.4.4", - "convert_case 0.6.0", - "gpui2", - "log", - "rust-embed", - "serde", - "simplelog", - "theme2", -] - [[package]] name = "theme_selector" version = "0.1.0" @@ -9630,6 +9754,7 @@ dependencies = [ "itertools 0.11.0", "rand 0.8.5", "serde", + "settings2", "smallvec", "strum", "theme2", @@ -9791,6 +9916,7 @@ dependencies = [ "dirs 3.0.2", "futures 0.3.28", "git2", + "globset", "isahc", "lazy_static", "log", @@ -10752,6 +10878,44 @@ dependencies = [ "text", ] +[[package]] +name = "workspace2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion 1.0.5", + "bincode", + "call2", + "client2", + "collections", + "db2", + "env_logger 0.9.3", + "fs2", + "futures 0.3.28", + "gpui2", + "indoc", + "install_cli2", + "itertools 0.10.5", + "language2", + "lazy_static", + "log", + "node_runtime", + "parking_lot 0.11.2", + "postage", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "terminal2", + "theme2", + "ui2", + "util", + "uuid 1.4.1", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -10832,7 +10996,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.111.0" +version = "0.112.0" dependencies = [ "activity_indicator", "ai", @@ -11029,7 +11193,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", - "text", + "text2", "theme2", "thiserror", "tiny_http", @@ -11067,6 +11231,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "workspace2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b41eef1338..be9deba5ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "crates/menu", "crates/menu2", "crates/multi_buffer", + "crates/multi_buffer2", "crates/node_runtime", "crates/notifications", "crates/outline", @@ -94,14 +95,13 @@ members = [ "crates/text", "crates/theme", "crates/theme2", - "crates/theme_converter", "crates/theme_selector", "crates/ui2", "crates/util", "crates/semantic_index", "crates/vim", "crates/vcs_menu", - "crates/workspace", + "crates/workspace2", "crates/welcome", "crates/xtask", "crates/zed", diff --git a/assets/icons/at-sign.svg b/assets/icons/at-sign.svg new file mode 100644 index 0000000000..5adac38f62 --- /dev/null +++ b/assets/icons/at-sign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell-off.svg b/assets/icons/bell-off.svg new file mode 100644 index 0000000000..db1021f2d3 --- /dev/null +++ b/assets/icons/bell-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell-ring.svg b/assets/icons/bell-ring.svg new file mode 100644 index 0000000000..da51fdc5be --- /dev/null +++ b/assets/icons/bell-ring.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index ea1c6dd42e..4c7d5472db 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,8 +1 @@ - - - + \ No newline at end of file diff --git a/assets/icons/mail-open.svg b/assets/icons/mail-open.svg new file mode 100644 index 0000000000..b63915bd73 --- /dev/null +++ b/assets/icons/mail-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/ai/src/test.rs b/crates/ai/src/test.rs index d4165f3cca..3f331da117 100644 --- a/crates/ai/src/test.rs +++ b/crates/ai/src/test.rs @@ -153,10 +153,17 @@ impl FakeCompletionProvider { pub fn send_completion(&self, completion: impl Into) { let mut tx = self.last_completion_tx.lock(); - tx.as_mut().unwrap().try_send(completion.into()).unwrap(); + + println!("COMPLETION TX: {:?}", &tx); + + let a = tx.as_mut().unwrap(); + a.try_send(completion.into()).unwrap(); + + // tx.as_mut().unwrap().try_send(completion.into()).unwrap(); } pub fn finish_completion(&self) { + println!("FINISHING COMPLETION"); self.last_completion_tx.lock().take().unwrap(); } } @@ -181,8 +188,10 @@ impl CompletionProvider for FakeCompletionProvider { &self, _prompt: Box, ) -> BoxFuture<'static, anyhow::Result>>> { + println!("COMPLETING"); let (tx, rx) = mpsc::channel(1); *self.last_completion_tx.lock() = Some(tx); + println!("TX: {:?}", *self.last_completion_tx.lock()); async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed() } fn box_clone(&self) -> Box { diff --git a/crates/ai2/Cargo.toml b/crates/ai2/Cargo.toml index 4f06840e8e..aee265db6e 100644 --- a/crates/ai2/Cargo.toml +++ b/crates/ai2/Cargo.toml @@ -12,9 +12,9 @@ doctest = false test-support = [] [dependencies] -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } -language2 = { path = "../language2" } +language = { package = "language2", path = "../language2" } async-trait.workspace = true anyhow.workspace = true futures.workspace = true @@ -35,4 +35,4 @@ rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } bincode = "1.3.3" [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/ai2/src/auth.rs b/crates/ai2/src/auth.rs index e4670bb449..baa1fe7b83 100644 --- a/crates/ai2/src/auth.rs +++ b/crates/ai2/src/auth.rs @@ -1,5 +1,4 @@ -use async_trait::async_trait; -use gpui2::AppContext; +use gpui::AppContext; #[derive(Clone, Debug)] pub enum ProviderCredential { @@ -8,10 +7,9 @@ pub enum ProviderCredential { NotNeeded, } -#[async_trait] -pub trait CredentialProvider: Send + Sync { +pub trait CredentialProvider { fn has_credentials(&self) -> bool; - async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; - async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); - async fn delete_credentials(&self, cx: &mut AppContext); + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); + fn delete_credentials(&self, cx: &mut AppContext); } diff --git a/crates/ai2/src/embedding.rs b/crates/ai2/src/embedding.rs index 7ea4786178..6768b7ce7b 100644 --- a/crates/ai2/src/embedding.rs +++ b/crates/ai2/src/embedding.rs @@ -81,7 +81,7 @@ mod tests { use super::*; use rand::prelude::*; - #[gpui2::test] + #[gpui::test] fn test_similarity(mut rng: StdRng) { assert_eq!( Embedding::from(vec![1., 0., 0., 0., 0.]) diff --git a/crates/ai2/src/prompts/base.rs b/crates/ai2/src/prompts/base.rs index 29091d0f5b..75bad00154 100644 --- a/crates/ai2/src/prompts/base.rs +++ b/crates/ai2/src/prompts/base.rs @@ -2,7 +2,7 @@ use std::cmp::Reverse; use std::ops::Range; use std::sync::Arc; -use language2::BufferSnapshot; +use language::BufferSnapshot; use util::ResultExt; use crate::models::LanguageModel; diff --git a/crates/ai2/src/prompts/file_context.rs b/crates/ai2/src/prompts/file_context.rs index 4a741beb24..f108a62f6f 100644 --- a/crates/ai2/src/prompts/file_context.rs +++ b/crates/ai2/src/prompts/file_context.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; -use language2::BufferSnapshot; -use language2::ToOffset; +use language::BufferSnapshot; +use language::ToOffset; use crate::models::LanguageModel; use crate::models::TruncationDirection; diff --git a/crates/ai2/src/prompts/repository_context.rs b/crates/ai2/src/prompts/repository_context.rs index 1bb75de7d2..0d831c2cb2 100644 --- a/crates/ai2/src/prompts/repository_context.rs +++ b/crates/ai2/src/prompts/repository_context.rs @@ -2,8 +2,8 @@ use crate::prompts::base::{PromptArguments, PromptTemplate}; use std::fmt::Write; use std::{ops::Range, path::PathBuf}; -use gpui2::{AsyncAppContext, Model}; -use language2::{Anchor, Buffer}; +use gpui::{AsyncAppContext, Model}; +use language::{Anchor, Buffer}; #[derive(Clone)] pub struct PromptCodeSnippet { diff --git a/crates/ai2/src/providers/open_ai/completion.rs b/crates/ai2/src/providers/open_ai/completion.rs index eca5611027..3e49fc5290 100644 --- a/crates/ai2/src/providers/open_ai/completion.rs +++ b/crates/ai2/src/providers/open_ai/completion.rs @@ -1,10 +1,9 @@ use anyhow::{anyhow, Result}; -use async_trait::async_trait; use futures::{ future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, Stream, StreamExt, }; -use gpui2::{AppContext, Executor}; +use gpui::{AppContext, BackgroundExecutor}; use isahc::{http::StatusCode, Request, RequestExt}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; @@ -105,7 +104,7 @@ pub struct OpenAIResponseStreamEvent { pub async fn stream_completion( credential: ProviderCredential, - executor: Arc, + executor: Arc, request: Box, ) -> Result>> { let api_key = match credential { @@ -198,11 +197,11 @@ pub async fn stream_completion( pub struct OpenAICompletionProvider { model: OpenAILanguageModel, credential: Arc>, - executor: Arc, + executor: Arc, } impl OpenAICompletionProvider { - pub fn new(model_name: &str, executor: Arc) -> Self { + pub fn new(model_name: &str, executor: Arc) -> Self { let model = OpenAILanguageModel::load(model_name); let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); Self { @@ -213,7 +212,6 @@ impl OpenAICompletionProvider { } } -#[async_trait] impl CredentialProvider for OpenAICompletionProvider { fn has_credentials(&self) -> bool { match *self.credential.read() { @@ -221,52 +219,45 @@ impl CredentialProvider for OpenAICompletionProvider { _ => false, } } - async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { + + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { let existing_credential = self.credential.read().clone(); - - let retrieved_credential = cx - .run_on_main(move |cx| match existing_credential { - ProviderCredential::Credentials { .. } => { - return existing_credential.clone(); - } - _ => { - if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { - return ProviderCredential::Credentials { api_key }; - } - - if let Some(Some((_, api_key))) = cx.read_credentials(OPENAI_API_URL).log_err() - { - if let Some(api_key) = String::from_utf8(api_key).log_err() { - return ProviderCredential::Credentials { api_key }; - } else { - return ProviderCredential::NoCredentials; - } + let retrieved_credential = match existing_credential { + ProviderCredential::Credentials { .. } => existing_credential.clone(), + _ => { + if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { + ProviderCredential::Credentials { api_key } + } else if let Some(Some((_, api_key))) = + cx.read_credentials(OPENAI_API_URL).log_err() + { + if let Some(api_key) = String::from_utf8(api_key).log_err() { + ProviderCredential::Credentials { api_key } } else { - return ProviderCredential::NoCredentials; + ProviderCredential::NoCredentials } + } else { + ProviderCredential::NoCredentials } - }) - .await; - + } + }; *self.credential.write() = retrieved_credential.clone(); retrieved_credential } - async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { *self.credential.write() = credential.clone(); let credential = credential.clone(); - cx.run_on_main(move |cx| match credential { + match credential { ProviderCredential::Credentials { api_key } => { cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) .log_err(); } _ => {} - }) - .await; + } } - async fn delete_credentials(&self, cx: &mut AppContext) { - cx.run_on_main(move |cx| cx.delete_credentials(OPENAI_API_URL).log_err()) - .await; + + fn delete_credentials(&self, cx: &mut AppContext) { + cx.delete_credentials(OPENAI_API_URL).log_err(); *self.credential.write() = ProviderCredential::NoCredentials; } } diff --git a/crates/ai2/src/providers/open_ai/embedding.rs b/crates/ai2/src/providers/open_ai/embedding.rs index fc49c15134..8f62c8dc0d 100644 --- a/crates/ai2/src/providers/open_ai/embedding.rs +++ b/crates/ai2/src/providers/open_ai/embedding.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::AsyncReadExt; -use gpui2::Executor; -use gpui2::{serde_json, AppContext}; +use gpui::BackgroundExecutor; +use gpui::{serde_json, AppContext}; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; @@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider { model: OpenAILanguageModel, credential: Arc>, pub client: Arc, - pub executor: Arc, + pub executor: Arc, rate_limit_count_rx: watch::Receiver>, rate_limit_count_tx: Arc>>>, } @@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage { } impl OpenAIEmbeddingProvider { - pub fn new(client: Arc, executor: Arc) -> Self { + pub fn new(client: Arc, executor: Arc) -> Self { let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); @@ -146,7 +146,6 @@ impl OpenAIEmbeddingProvider { } } -#[async_trait] impl CredentialProvider for OpenAIEmbeddingProvider { fn has_credentials(&self) -> bool { match *self.credential.read() { @@ -154,52 +153,45 @@ impl CredentialProvider for OpenAIEmbeddingProvider { _ => false, } } - async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { let existing_credential = self.credential.read().clone(); - let retrieved_credential = cx - .run_on_main(move |cx| match existing_credential { - ProviderCredential::Credentials { .. } => { - return existing_credential.clone(); - } - _ => { - if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { - return ProviderCredential::Credentials { api_key }; - } - - if let Some(Some((_, api_key))) = cx.read_credentials(OPENAI_API_URL).log_err() - { - if let Some(api_key) = String::from_utf8(api_key).log_err() { - return ProviderCredential::Credentials { api_key }; - } else { - return ProviderCredential::NoCredentials; - } + let retrieved_credential = match existing_credential { + ProviderCredential::Credentials { .. } => existing_credential.clone(), + _ => { + if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { + ProviderCredential::Credentials { api_key } + } else if let Some(Some((_, api_key))) = + cx.read_credentials(OPENAI_API_URL).log_err() + { + if let Some(api_key) = String::from_utf8(api_key).log_err() { + ProviderCredential::Credentials { api_key } } else { - return ProviderCredential::NoCredentials; + ProviderCredential::NoCredentials } + } else { + ProviderCredential::NoCredentials } - }) - .await; + } + }; *self.credential.write() = retrieved_credential.clone(); retrieved_credential } - async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { *self.credential.write() = credential.clone(); - let credential = credential.clone(); - cx.run_on_main(move |cx| match credential { + match credential { ProviderCredential::Credentials { api_key } => { cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) .log_err(); } _ => {} - }) - .await; + } } - async fn delete_credentials(&self, cx: &mut AppContext) { - cx.run_on_main(move |cx| cx.delete_credentials(OPENAI_API_URL).log_err()) - .await; + + fn delete_credentials(&self, cx: &mut AppContext) { + cx.delete_credentials(OPENAI_API_URL).log_err(); *self.credential.write() = ProviderCredential::NoCredentials; } } diff --git a/crates/ai2/src/test.rs b/crates/ai2/src/test.rs index ee88529aec..3d59febbe9 100644 --- a/crates/ai2/src/test.rs +++ b/crates/ai2/src/test.rs @@ -5,7 +5,7 @@ use std::{ use async_trait::async_trait; use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; -use gpui2::AppContext; +use gpui::AppContext; use parking_lot::Mutex; use crate::{ @@ -100,16 +100,15 @@ impl FakeEmbeddingProvider { } } -#[async_trait] impl CredentialProvider for FakeEmbeddingProvider { fn has_credentials(&self) -> bool { true } - async fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { + fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { ProviderCredential::NotNeeded } - async fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} - async fn delete_credentials(&self, _cx: &mut AppContext) {} + fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} + fn delete_credentials(&self, _cx: &mut AppContext) {} } #[async_trait] @@ -162,16 +161,15 @@ impl FakeCompletionProvider { } } -#[async_trait] impl CredentialProvider for FakeCompletionProvider { fn has_credentials(&self) -> bool { true } - async fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { + fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { ProviderCredential::NotNeeded } - async fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} - async fn delete_credentials(&self, _cx: &mut AppContext) {} + fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} + fn delete_credentials(&self, _cx: &mut AppContext) {} } impl CompletionProvider for FakeCompletionProvider { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 03eb3c238f..6ab96093a7 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -142,7 +142,7 @@ pub struct AssistantPanel { zoomed: bool, has_focus: bool, toolbar: ViewHandle, - completion_provider: Box, + completion_provider: Arc, api_key_editor: Option>, languages: Arc, fs: Arc, @@ -204,7 +204,7 @@ impl AssistantPanel { let semantic_index = SemanticIndex::global(cx); // Defaulting currently to GPT4, allow for this to be set via config. - let completion_provider = Box::new(OpenAICompletionProvider::new( + let completion_provider = Arc::new(OpenAICompletionProvider::new( "gpt-4", cx.background().clone(), )); @@ -259,7 +259,13 @@ impl AssistantPanel { cx: &mut ViewContext, ) { let this = if let Some(this) = workspace.panel::(cx) { - if this.update(cx, |assistant, _| assistant.has_credentials()) { + if this.update(cx, |assistant, cx| { + if !assistant.has_credentials() { + assistant.load_credentials(cx); + }; + + assistant.has_credentials() + }) { this } else { workspace.focus_panel::(cx); @@ -320,13 +326,10 @@ impl AssistantPanel { }; let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let provider = Arc::new(OpenAICompletionProvider::new( - "gpt-4", - cx.background().clone(), - )); + let provider = self.completion_provider.clone(); // Retrieve Credentials Authenticates the Provider - // provider.retrieve_credentials(cx); + provider.retrieve_credentials(cx); let codegen = cx.add_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) @@ -1439,7 +1442,7 @@ struct Conversation { pending_save: Task>, path: Option, _subscriptions: Vec, - completion_provider: Box, + completion_provider: Arc, } impl Entity for Conversation { @@ -1450,7 +1453,7 @@ impl Conversation { fn new( language_registry: Arc, cx: &mut ModelContext, - completion_provider: Box, + completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { @@ -1544,7 +1547,7 @@ impl Conversation { None => Some(Uuid::new_v4().to_string()), }; let model = saved_conversation.model; - let completion_provider: Box = Box::new( + let completion_provider: Arc = Arc::new( OpenAICompletionProvider::new(model.full_name(), cx.background().clone()), ); completion_provider.retrieve_credentials(cx); @@ -2201,7 +2204,7 @@ struct ConversationEditor { impl ConversationEditor { fn new( - completion_provider: Box, + completion_provider: Arc, language_registry: Arc, fs: Arc, workspace: WeakViewHandle, @@ -3406,7 +3409,7 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3535,7 +3538,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3633,7 +3636,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3716,7 +3719,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index 0466259b24..25c9deef7f 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -118,7 +118,7 @@ impl Codegen { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { - let chunks = strip_markdown_codeblock(response.await?); + let chunks = strip_invalid_spans_from_codeblock(response.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -279,12 +279,13 @@ impl Codegen { } } -fn strip_markdown_codeblock( +fn strip_invalid_spans_from_codeblock( stream: impl Stream>, ) -> impl Stream> { let mut first_line = true; let mut buffer = String::new(); - let mut starts_with_fenced_code_block = false; + let mut starts_with_markdown_codeblock = false; + let mut includes_start_or_end_span = false; stream.filter_map(move |chunk| { let chunk = match chunk { Ok(chunk) => chunk, @@ -292,11 +293,31 @@ fn strip_markdown_codeblock( }; buffer.push_str(&chunk); + if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") { + includes_start_or_end_span = true; + + buffer = buffer + .strip_prefix("<|S|>") + .or_else(|| buffer.strip_prefix("<|S|")) + .unwrap_or(&buffer) + .to_string(); + } else if buffer.ends_with("|E|>") { + includes_start_or_end_span = true; + } else if buffer.starts_with("<|") + || buffer.starts_with("<|S") + || buffer.starts_with("<|S|") + || buffer.ends_with("|") + || buffer.ends_with("|E") + || buffer.ends_with("|E|") + { + return future::ready(None); + } + if first_line { if buffer == "" || buffer == "`" || buffer == "``" { return future::ready(None); } else if buffer.starts_with("```") { - starts_with_fenced_code_block = true; + starts_with_markdown_codeblock = true; if let Some(newline_ix) = buffer.find('\n') { buffer.replace_range(..newline_ix + 1, ""); first_line = false; @@ -306,16 +327,26 @@ fn strip_markdown_codeblock( } } - let text = if starts_with_fenced_code_block { - buffer + let mut text = buffer.to_string(); + if starts_with_markdown_codeblock { + text = text .strip_suffix("\n```\n") - .or_else(|| buffer.strip_suffix("\n```")) - .or_else(|| buffer.strip_suffix("\n``")) - .or_else(|| buffer.strip_suffix("\n`")) - .or_else(|| buffer.strip_suffix('\n')) - .unwrap_or(&buffer) - } else { - &buffer + .or_else(|| text.strip_suffix("\n```")) + .or_else(|| text.strip_suffix("\n``")) + .or_else(|| text.strip_suffix("\n`")) + .or_else(|| text.strip_suffix('\n')) + .unwrap_or(&text) + .to_string(); + } + + if includes_start_or_end_span { + text = text + .strip_suffix("|E|>") + .or_else(|| text.strip_suffix("E|>")) + .or_else(|| text.strip_prefix("|>")) + .or_else(|| text.strip_prefix(">")) + .unwrap_or(&text) + .to_string(); }; if text.contains('\n') { @@ -328,6 +359,7 @@ fn strip_markdown_codeblock( } else { Some(Ok(buffer.clone())) }; + buffer = remainder; future::ready(result) }) @@ -335,6 +367,8 @@ fn strip_markdown_codeblock( #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; @@ -405,6 +439,7 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); + println!("CHUNK: {:?}", &chunk); provider.send_completion(chunk); new_text = suffix; deterministic.run_until_parked(); @@ -537,6 +572,7 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); + println!("{:?}", &chunk); provider.send_completion(chunk); new_text = suffix; deterministic.run_until_parked(); @@ -558,50 +594,82 @@ mod tests { } #[gpui::test] - async fn test_strip_markdown_codeblock() { + async fn test_strip_invalid_spans_from_codeblock() { assert_eq!( - strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2)) .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2)) .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( - strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, + strip_invalid_spans_from_codeblock(chunks( + "```html\n```js\nLorem ipsum dolor\n```\n```", + 2 + )) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, "```js\nLorem ipsum dolor\n```" ); assert_eq!( - strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) .map(|chunk| chunk.unwrap()) .collect::() .await, "``\nLorem ipsum dolor\n```" ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); fn chunks(text: &str, size: usize) -> impl Stream> { stream::iter( text.chars() diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 25af023c40..b678c6fe3b 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -80,12 +80,12 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> S if !flushed_selection { // The collapsed node ends after the selection starts, so we'll flush the selection first. summary.extend(buffer.text_for_range(offset..selected_range.start)); - summary.push_str("<|START|"); + summary.push_str("<|S|"); if selected_range.end == selected_range.start { summary.push_str(">"); } else { summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|END|>"); + summary.push_str("|E|>"); } offset = selected_range.end; flushed_selection = true; @@ -107,12 +107,12 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> S // Flush selection if we haven't already done so. if !flushed_selection && offset <= selected_range.start { summary.extend(buffer.text_for_range(offset..selected_range.start)); - summary.push_str("<|START|"); + summary.push_str("<|S|"); if selected_range.end == selected_range.start { summary.push_str(">"); } else { summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|END|>"); + summary.push_str("|E|>"); } offset = selected_range.end; } @@ -260,7 +260,7 @@ pub(crate) mod tests { summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), indoc! {" struct X { - <|START|>a: usize, + <|S|>a: usize, b: usize, } @@ -286,7 +286,7 @@ pub(crate) mod tests { impl X { fn new() -> Self { - let <|START|a |END|>= 1; + let <|S|a |E|>= 1; let b = 2; Self { a, b } } @@ -307,7 +307,7 @@ pub(crate) mod tests { } impl X { - <|START|> + <|S|> fn new() -> Self {} pub fn a(&self, param: bool) -> usize {} @@ -333,7 +333,7 @@ pub(crate) mod tests { pub fn b(&self) -> usize {} } - <|START|>"} + <|S|>"} ); // Ensure nested functions get collapsed properly. @@ -369,7 +369,7 @@ pub(crate) mod tests { assert_eq!( summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), indoc! {" - <|START|>struct X { + <|S|>struct X { a: usize, b: usize, } diff --git a/crates/audio2/Cargo.toml b/crates/audio2/Cargo.toml index 298142dbef..3688f108f4 100644 --- a/crates/audio2/Cargo.toml +++ b/crates/audio2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/audio2.rs" doctest = false [dependencies] -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } collections = { path = "../collections" } util = { path = "../util" } diff --git a/crates/audio2/src/assets.rs b/crates/audio2/src/assets.rs index 66e0bf5aa5..b58e1f6aee 100644 --- a/crates/audio2/src/assets.rs +++ b/crates/audio2/src/assets.rs @@ -2,7 +2,7 @@ use std::{io::Cursor, sync::Arc}; use anyhow::Result; use collections::HashMap; -use gpui2::{AppContext, AssetSource}; +use gpui::{AppContext, AssetSource}; use rodio::{ source::{Buffered, SamplesConverter}, Decoder, Source, diff --git a/crates/audio2/src/audio2.rs b/crates/audio2/src/audio2.rs index d04587d74e..9264ed25d6 100644 --- a/crates/audio2/src/audio2.rs +++ b/crates/audio2/src/audio2.rs @@ -1,14 +1,13 @@ use assets::SoundRegistry; -use futures::{channel::mpsc, StreamExt}; -use gpui2::{AppContext, AssetSource, Executor}; +use gpui::{AppContext, AssetSource}; use rodio::{OutputStream, OutputStreamHandle}; use util::ResultExt; mod assets; pub fn init(source: impl AssetSource, cx: &mut AppContext) { - cx.set_global(Audio::new(cx.executor())); cx.set_global(SoundRegistry::new(source)); + cx.set_global(Audio::new()); } pub enum Sound { @@ -34,15 +33,18 @@ impl Sound { } pub struct Audio { - tx: mpsc::UnboundedSender>, -} - -struct AudioState { _output_stream: Option, output_handle: Option, } -impl AudioState { +impl Audio { + pub fn new() -> Self { + Self { + _output_stream: None, + output_handle: None, + } + } + fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { if self.output_handle.is_none() { let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); @@ -53,59 +55,27 @@ impl AudioState { self.output_handle.as_ref() } - fn take(&mut self) { - self._output_stream.take(); - self.output_handle.take(); - } -} - -impl Audio { - pub fn new(executor: &Executor) -> Self { - let (tx, mut rx) = mpsc::unbounded::>(); - executor - .spawn_on_main(|| async move { - let mut audio = AudioState { - _output_stream: None, - output_handle: None, - }; - - while let Some(f) = rx.next().await { - (f)(&mut audio); - } - }) - .detach(); - - Self { tx } - } - pub fn play_sound(sound: Sound, cx: &mut AppContext) { if !cx.has_global::() { return; } - let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else { - return; - }; - - let this = cx.global::(); - this.tx - .unbounded_send(Box::new(move |state| { - if let Some(output_handle) = state.ensure_output_exists() { - output_handle.play_raw(source).log_err(); - } - })) - .ok(); + cx.update_global::(|this, cx| { + let output_handle = this.ensure_output_exists()?; + let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; + output_handle.play_raw(source).log_err()?; + Some(()) + }); } - pub fn end_call(cx: &AppContext) { + pub fn end_call(cx: &mut AppContext) { if !cx.has_global::() { return; } - let this = cx.global::(); - - this.tx - .unbounded_send(Box::new(move |state| state.take())) - .ok(); + cx.update_global::(|this, _| { + this._output_stream.take(); + this.output_handle.take(); + }); } } diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index f0e47832ed..9e13463680 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -10,26 +10,26 @@ doctest = false [features] test-support = [ - "client2/test-support", + "client/test-support", "collections/test-support", - "gpui2/test-support", + "gpui/test-support", "live_kit_client/test-support", - "project2/test-support", + "project/test-support", "util/test-support" ] [dependencies] -audio2 = { path = "../audio2" } -client2 = { path = "../client2" } +audio = { package = "audio2", path = "../audio2" } +client = { package = "client2", path = "../client2" } collections = { path = "../collections" } -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } log.workspace = true -live_kit_client = { path = "../live_kit_client" } -fs2 = { path = "../fs2" } -language2 = { path = "../language2" } +live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2" } +fs = { package = "fs2", path = "../fs2" } +language = { package = "language2", path = "../language2" } media = { path = "../media" } -project2 = { path = "../project2" } -settings2 = { path = "../settings2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } anyhow.workspace = true @@ -42,11 +42,11 @@ serde_json.workspace = true serde_derive.workspace = true [dev-dependencies] -client2 = { path = "../client2", features = ["test-support"] } -fs2 = { path = "../fs2", features = ["test-support"] } -language2 = { path = "../language2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -live_kit_client = { path = "../live_kit_client", features = ["test-support"] } -project2 = { path = "../project2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index fd09dc3180..477931919d 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -3,21 +3,21 @@ pub mod participant; pub mod room; use anyhow::{anyhow, Result}; -use audio2::Audio; +use audio::Audio; use call_settings::CallSettings; -use client2::{ +use client::{ proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, }; use collections::HashSet; use futures::{future::Shared, FutureExt}; -use gpui2::{ +use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel, }; use postage::watch; -use project2::Project; -use settings2::Settings; +use project::Project; +use settings::Settings; use std::sync::Arc; pub use participant::ParticipantLocation; @@ -50,7 +50,7 @@ pub struct ActiveCall { ), client: Arc, user_store: Model, - _subscriptions: Vec, + _subscriptions: Vec, } impl EventEmitter for ActiveCall { @@ -196,7 +196,7 @@ impl ActiveCall { }) .shared(); self.pending_room_creation = Some(room.clone()); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { room.await.map_err(|err| anyhow!("{:?}", err))?; anyhow::Ok(()) }) @@ -230,7 +230,7 @@ impl ActiveCall { }; let client = self.client.clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { client .request(proto::CancelCall { room_id, diff --git a/crates/call2/src/call_settings.rs b/crates/call2/src/call_settings.rs index c83ed73980..9375feedf0 100644 --- a/crates/call2/src/call_settings.rs +++ b/crates/call2/src/call_settings.rs @@ -1,8 +1,8 @@ use anyhow::Result; -use gpui2::AppContext; +use gpui::AppContext; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; #[derive(Deserialize, Debug)] pub struct CallSettings { diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index 7f3e91dbba..f62d103f17 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, Result}; -use client2::ParticipantIndex; -use client2::{proto, User}; -use gpui2::WeakModel; +use client::ParticipantIndex; +use client::{proto, User}; +use collections::HashMap; +use gpui::WeakModel; pub use live_kit_client::Frame; -use project2::Project; -use std::{fmt, sync::Arc}; +use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +use project::Project; +use std::sync::Arc; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { @@ -45,27 +47,6 @@ pub struct RemoteParticipant { pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, - // pub video_tracks: HashMap>, - // pub audio_tracks: HashMap>, -} - -#[derive(Clone)] -pub struct RemoteVideoTrack { - pub(crate) live_kit_track: Arc, -} - -unsafe impl Send for RemoteVideoTrack {} -// todo!("remove this sync because it's not legit") -unsafe impl Sync for RemoteVideoTrack {} - -impl fmt::Debug for RemoteVideoTrack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RemoteVideoTrack").finish() - } -} - -impl RemoteVideoTrack { - pub fn frames(&self) -> async_broadcast::Receiver { - self.live_kit_track.frames() - } + pub video_tracks: HashMap>, + pub audio_tracks: HashMap>, } diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index b7bac52a8b..a46269a508 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1,30 +1,30 @@ -#![allow(dead_code, unused)] -// todo!() - use crate::{ call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, IncomingCall, }; use anyhow::{anyhow, Result}; -use audio2::{Audio, Sound}; -use client2::{ +use audio::{Audio, Sound}; +use client::{ proto::{self, PeerId}, Client, ParticipantIndex, TypedEnvelope, User, UserStore, }; use collections::{BTreeMap, HashMap, HashSet}; -use fs2::Fs; +use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui2::{ +use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, }; -use language2::LanguageRegistry; -use live_kit_client::{LocalTrackPublication, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate}; +use language::LanguageRegistry; +use live_kit_client::{ + LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, + RemoteVideoTrackUpdate, +}; use postage::{sink::Sink, stream::Stream, watch}; -use project2::Project; -use settings2::Settings; -use std::{future::Future, sync::Arc, time::Duration}; -use util::{ResultExt, TryFutureExt}; +use project::Project; +use settings::Settings; +use std::{future::Future, mem, sync::Arc, time::Duration}; +use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -59,7 +59,7 @@ pub enum Event { pub struct Room { id: u64, channel_id: Option, - // live_kit: Option, + live_kit: Option, status: RoomStatus, shared_projects: HashSet>, joined_projects: HashSet>, @@ -72,8 +72,8 @@ pub struct Room { client: Arc, user_store: Model, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, - client_subscriptions: Vec, - _subscriptions: Vec, + client_subscriptions: Vec, + _subscriptions: Vec, room_update_completed_tx: watch::Sender>, room_update_completed_rx: watch::Receiver>, pending_room_update: Option>, @@ -95,15 +95,14 @@ impl Room { #[cfg(any(test, feature = "test-support"))] pub fn is_connected(&self) -> bool { - false - // if let Some(live_kit) = self.live_kit.as_ref() { - // matches!( - // *live_kit.room.status().borrow(), - // live_kit_client::ConnectionState::Connected { .. } - // ) - // } else { - // false - // } + if let Some(live_kit) = self.live_kit.as_ref() { + matches!( + *live_kit.room.status().borrow(), + live_kit_client::ConnectionState::Connected { .. } + ) + } else { + false + } } fn new( @@ -114,125 +113,130 @@ impl Room { user_store: Model, cx: &mut ModelContext, ) -> Self { - todo!() - // let _live_kit_room = if let Some(connection_info) = live_kit_connection_info { - // let room = live_kit_client::Room::new(); - // let mut status = room.status(); - // // Consume the initial status of the room. - // let _ = status.try_recv(); - // let _maintain_room = cx.spawn(|this, mut cx| async move { - // while let Some(status) = status.next().await { - // let this = if let Some(this) = this.upgrade() { - // this - // } else { - // break; - // }; + let live_kit_room = if let Some(connection_info) = live_kit_connection_info { + let room = live_kit_client::Room::new(); + let mut status = room.status(); + // Consume the initial status of the room. + let _ = status.try_recv(); + let _maintain_room = cx.spawn(|this, mut cx| async move { + while let Some(status) = status.next().await { + let this = if let Some(this) = this.upgrade() { + this + } else { + break; + }; - // if status == live_kit_client::ConnectionState::Disconnected { - // this.update(&mut cx, |this, cx| this.leave(cx).log_err()) - // .ok(); - // break; - // } - // } - // }); + if status == live_kit_client::ConnectionState::Disconnected { + this.update(&mut cx, |this, cx| this.leave(cx).log_err()) + .ok(); + break; + } + } + }); - // let mut track_video_changes = room.remote_video_track_updates(); - // let _maintain_video_tracks = cx.spawn(|this, mut cx| async move { - // while let Some(track_change) = track_video_changes.next().await { - // let this = if let Some(this) = this.upgrade() { - // this - // } else { - // break; - // }; + let _maintain_video_tracks = cx.spawn({ + let room = room.clone(); + move |this, mut cx| async move { + let mut track_video_changes = room.remote_video_track_updates(); + while let Some(track_change) = track_video_changes.next().await { + let this = if let Some(this) = this.upgrade() { + this + } else { + break; + }; - // this.update(&mut cx, |this, cx| { - // this.remote_video_track_updated(track_change, cx).log_err() - // }) - // .ok(); - // } - // }); + this.update(&mut cx, |this, cx| { + this.remote_video_track_updated(track_change, cx).log_err() + }) + .ok(); + } + } + }); - // let mut track_audio_changes = room.remote_audio_track_updates(); - // let _maintain_audio_tracks = cx.spawn(|this, mut cx| async move { - // while let Some(track_change) = track_audio_changes.next().await { - // let this = if let Some(this) = this.upgrade() { - // this - // } else { - // break; - // }; + let _maintain_audio_tracks = cx.spawn({ + let room = room.clone(); + |this, mut cx| async move { + let mut track_audio_changes = room.remote_audio_track_updates(); + while let Some(track_change) = track_audio_changes.next().await { + let this = if let Some(this) = this.upgrade() { + this + } else { + break; + }; - // this.update(&mut cx, |this, cx| { - // this.remote_audio_track_updated(track_change, cx).log_err() - // }) - // .ok(); - // } - // }); + this.update(&mut cx, |this, cx| { + this.remote_audio_track_updated(track_change, cx).log_err() + }) + .ok(); + } + } + }); - // let connect = room.connect(&connection_info.server_url, &connection_info.token); - // cx.spawn(|this, mut cx| async move { - // connect.await?; + let connect = room.connect(&connection_info.server_url, &connection_info.token); + cx.spawn(|this, mut cx| async move { + connect.await?; - // if !cx.update(|cx| Self::mute_on_join(cx))? { - // this.update(&mut cx, |this, cx| this.share_microphone(cx))? - // .await?; - // } + if !cx.update(|cx| Self::mute_on_join(cx))? { + this.update(&mut cx, |this, cx| this.share_microphone(cx))? + .await?; + } - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); - // Some(LiveKitRoom { - // room, - // screen_track: LocalTrack::None, - // microphone_track: LocalTrack::None, - // next_publish_id: 0, - // muted_by_user: false, - // deafened: false, - // speaking: false, - // _maintain_room, - // _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks], - // }) - // } else { - // None - // }; + Some(LiveKitRoom { + room, + screen_track: LocalTrack::None, + microphone_track: LocalTrack::None, + next_publish_id: 0, + muted_by_user: false, + deafened: false, + speaking: false, + _maintain_room, + _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks], + }) + } else { + None + }; - // let maintain_connection = cx.spawn({ - // let client = client.clone(); - // move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() - // }); + let maintain_connection = cx.spawn({ + let client = client.clone(); + move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() + }); - // Audio::play_sound(Sound::Joined, cx); + Audio::play_sound(Sound::Joined, cx); - // let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); + let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); - // Self { - // id, - // channel_id, - // // live_kit: live_kit_room, - // status: RoomStatus::Online, - // shared_projects: Default::default(), - // joined_projects: Default::default(), - // participant_user_ids: Default::default(), - // local_participant: Default::default(), - // remote_participants: Default::default(), - // pending_participants: Default::default(), - // pending_call_count: 0, - // client_subscriptions: vec![ - // client.add_message_handler(cx.weak_handle(), Self::handle_room_updated) - // ], - // _subscriptions: vec![ - // cx.on_release(Self::released), - // cx.on_app_quit(Self::app_will_quit), - // ], - // leave_when_empty: false, - // pending_room_update: None, - // client, - // user_store, - // follows_by_leader_id_project_id: Default::default(), - // maintain_connection: Some(maintain_connection), - // room_update_completed_tx, - // room_update_completed_rx, - // } + Self { + id, + channel_id, + live_kit: live_kit_room, + status: RoomStatus::Online, + shared_projects: Default::default(), + joined_projects: Default::default(), + participant_user_ids: Default::default(), + local_participant: Default::default(), + remote_participants: Default::default(), + pending_participants: Default::default(), + pending_call_count: 0, + client_subscriptions: vec![ + client.add_message_handler(cx.weak_model(), Self::handle_room_updated) + ], + _subscriptions: vec![ + cx.on_release(Self::released), + cx.on_app_quit(Self::app_will_quit), + ], + leave_when_empty: false, + pending_room_update: None, + client, + user_store, + follows_by_leader_id_project_id: Default::default(), + maintain_connection: Some(maintain_connection), + room_update_completed_tx, + room_update_completed_rx, + } } pub(crate) fn create( @@ -322,7 +326,7 @@ impl Room { fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { let task = if self.status.is_online() { let leave = self.leave_internal(cx); - Some(cx.executor().spawn(async move { + Some(cx.background_executor().spawn(async move { leave.await.log_err(); })) } else { @@ -337,7 +341,7 @@ impl Room { } pub fn mute_on_join(cx: &AppContext) -> bool { - CallSettings::get_global(cx).mute_on_join || client2::IMPERSONATE_LOGIN.is_some() + CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( @@ -390,7 +394,7 @@ impl Room { self.clear_state(cx); let leave_room = self.client.request(proto::LeaveRoom {}); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { leave_room.await?; anyhow::Ok(()) }) @@ -418,7 +422,7 @@ impl Room { self.pending_participants.clear(); self.participant_user_ids.clear(); self.client_subscriptions.clear(); - // self.live_kit.take(); + self.live_kit.take(); self.pending_room_update.take(); self.maintain_connection.take(); } @@ -445,7 +449,8 @@ impl Room { // Wait for client to re-establish a connection to the server. { - let mut reconnection_timeout = cx.executor().timer(RECONNECT_TIMEOUT).fuse(); + let mut reconnection_timeout = + cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); let client_reconnection = async { let mut remaining_attempts = 3; while remaining_attempts > 0 { @@ -794,43 +799,43 @@ impl Room { location, muted: true, speaking: false, - // video_tracks: Default::default(), - // audio_tracks: Default::default(), + video_tracks: Default::default(), + audio_tracks: Default::default(), }, ); Audio::play_sound(Sound::Joined, cx); - // if let Some(live_kit) = this.live_kit.as_ref() { - // let video_tracks = - // live_kit.room.remote_video_tracks(&user.id.to_string()); - // let audio_tracks = - // live_kit.room.remote_audio_tracks(&user.id.to_string()); - // let publications = live_kit - // .room - // .remote_audio_track_publications(&user.id.to_string()); + if let Some(live_kit) = this.live_kit.as_ref() { + let video_tracks = + live_kit.room.remote_video_tracks(&user.id.to_string()); + let audio_tracks = + live_kit.room.remote_audio_tracks(&user.id.to_string()); + let publications = live_kit + .room + .remote_audio_track_publications(&user.id.to_string()); - // for track in video_tracks { - // this.remote_video_track_updated( - // RemoteVideoTrackUpdate::Subscribed(track), - // cx, - // ) - // .log_err(); - // } + for track in video_tracks { + this.remote_video_track_updated( + RemoteVideoTrackUpdate::Subscribed(track), + cx, + ) + .log_err(); + } - // for (track, publication) in - // audio_tracks.iter().zip(publications.iter()) - // { - // this.remote_audio_track_updated( - // RemoteAudioTrackUpdate::Subscribed( - // track.clone(), - // publication.clone(), - // ), - // cx, - // ) - // .log_err(); - // } - // } + for (track, publication) in + audio_tracks.iter().zip(publications.iter()) + { + this.remote_audio_track_updated( + RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + ), + cx, + ) + .log_err(); + } + } } } @@ -918,7 +923,6 @@ impl Room { change: RemoteVideoTrackUpdate, cx: &mut ModelContext, ) -> Result<()> { - todo!(); match change { RemoteVideoTrackUpdate::Subscribed(track) => { let user_id = track.publisher_id().parse()?; @@ -927,12 +931,7 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - // participant.video_tracks.insert( - // track_id.clone(), - // Arc::new(RemoteVideoTrack { - // live_kit_track: track, - // }), - // ); + participant.video_tracks.insert(track_id.clone(), track); cx.emit(Event::RemoteVideoTracksChanged { participant_id: participant.peer_id, }); @@ -946,7 +945,7 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; - // participant.video_tracks.remove(&track_id); + participant.video_tracks.remove(&track_id); cx.emit(Event::RemoteVideoTracksChanged { participant_id: participant.peer_id, }); @@ -976,65 +975,61 @@ impl Room { participant.speaking = false; } } - // todo!() - // if let Some(id) = self.client.user_id() { - // if let Some(room) = &mut self.live_kit { - // if let Ok(_) = speaker_ids.binary_search(&id) { - // room.speaking = true; - // } else { - // room.speaking = false; - // } - // } - // } + if let Some(id) = self.client.user_id() { + if let Some(room) = &mut self.live_kit { + if let Ok(_) = speaker_ids.binary_search(&id) { + room.speaking = true; + } else { + room.speaking = false; + } + } + } cx.notify(); } RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => { - // todo!() - // let mut found = false; - // for participant in &mut self.remote_participants.values_mut() { - // for track in participant.audio_tracks.values() { - // if track.sid() == track_id { - // found = true; - // break; - // } - // } - // if found { - // participant.muted = muted; - // break; - // } - // } + let mut found = false; + for participant in &mut self.remote_participants.values_mut() { + for track in participant.audio_tracks.values() { + if track.sid() == track_id { + found = true; + break; + } + } + if found { + participant.muted = muted; + break; + } + } cx.notify(); } RemoteAudioTrackUpdate::Subscribed(track, publication) => { - // todo!() - // let user_id = track.publisher_id().parse()?; - // let track_id = track.sid().to_string(); - // let participant = self - // .remote_participants - // .get_mut(&user_id) - // .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - // // participant.audio_tracks.insert(track_id.clone(), track); - // participant.muted = publication.is_muted(); + let user_id = track.publisher_id().parse()?; + let track_id = track.sid().to_string(); + let participant = self + .remote_participants + .get_mut(&user_id) + .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; + participant.audio_tracks.insert(track_id.clone(), track); + participant.muted = publication.is_muted(); - // cx.emit(Event::RemoteAudioTracksChanged { - // participant_id: participant.peer_id, - // }); + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); } RemoteAudioTrackUpdate::Unsubscribed { publisher_id, track_id, } => { - // todo!() - // let user_id = publisher_id.parse()?; - // let participant = self - // .remote_participants - // .get_mut(&user_id) - // .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; - // participant.audio_tracks.remove(&track_id); - // cx.emit(Event::RemoteAudioTracksChanged { - // participant_id: participant.peer_id, - // }); + let user_id = publisher_id.parse()?; + let participant = self + .remote_participants + .get_mut(&user_id) + .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; + participant.audio_tracks.remove(&track_id); + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); } } @@ -1201,7 +1196,7 @@ impl Room { }; cx.notify(); - cx.executor().spawn_on_main(move || async move { + cx.background_executor().spawn(async move { client .request(proto::UpdateParticipantLocation { room_id, @@ -1215,278 +1210,270 @@ impl Room { } pub fn is_screen_sharing(&self) -> bool { - todo!() - // self.live_kit.as_ref().map_or(false, |live_kit| { - // !matches!(live_kit.screen_track, LocalTrack::None) - // }) + self.live_kit.as_ref().map_or(false, |live_kit| { + !matches!(live_kit.screen_track, LocalTrack::None) + }) } pub fn is_sharing_mic(&self) -> bool { - todo!() - // self.live_kit.as_ref().map_or(false, |live_kit| { - // !matches!(live_kit.microphone_track, LocalTrack::None) - // }) + self.live_kit.as_ref().map_or(false, |live_kit| { + !matches!(live_kit.microphone_track, LocalTrack::None) + }) } pub fn is_muted(&self, cx: &AppContext) -> bool { - todo!() - // self.live_kit - // .as_ref() - // .and_then(|live_kit| match &live_kit.microphone_track { - // LocalTrack::None => Some(Self::mute_on_join(cx)), - // LocalTrack::Pending { muted, .. } => Some(*muted), - // LocalTrack::Published { muted, .. } => Some(*muted), - // }) - // .unwrap_or(false) + self.live_kit + .as_ref() + .and_then(|live_kit| match &live_kit.microphone_track { + LocalTrack::None => Some(Self::mute_on_join(cx)), + LocalTrack::Pending { muted, .. } => Some(*muted), + LocalTrack::Published { muted, .. } => Some(*muted), + }) + .unwrap_or(false) } pub fn is_speaking(&self) -> bool { - todo!() - // self.live_kit - // .as_ref() - // .map_or(false, |live_kit| live_kit.speaking) + self.live_kit + .as_ref() + .map_or(false, |live_kit| live_kit.speaking) } pub fn is_deafened(&self) -> Option { - // self.live_kit.as_ref().map(|live_kit| live_kit.deafened) - todo!() + self.live_kit.as_ref().map(|live_kit| live_kit.deafened) } #[track_caller] pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { - todo!() - // if self.status.is_offline() { - // return Task::ready(Err(anyhow!("room is offline"))); - // } else if self.is_sharing_mic() { - // return Task::ready(Err(anyhow!("microphone was already shared"))); - // } + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } else if self.is_sharing_mic() { + return Task::ready(Err(anyhow!("microphone was already shared"))); + } - // let publish_id = if let Some(live_kit) = self.live_kit.as_mut() { - // let publish_id = post_inc(&mut live_kit.next_publish_id); - // live_kit.microphone_track = LocalTrack::Pending { - // publish_id, - // muted: false, - // }; - // cx.notify(); - // publish_id - // } else { - // return Task::ready(Err(anyhow!("live-kit was not initialized"))); - // }; + let publish_id = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.microphone_track = LocalTrack::Pending { + publish_id, + muted: false, + }; + cx.notify(); + publish_id + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; - // cx.spawn(move |this, mut cx| async move { - // let publish_track = async { - // let track = LocalAudioTrack::create(); - // this.upgrade() - // .ok_or_else(|| anyhow!("room was dropped"))? - // .update(&mut cx, |this, _| { - // this.live_kit - // .as_ref() - // .map(|live_kit| live_kit.room.publish_audio_track(track)) - // })? - // .ok_or_else(|| anyhow!("live-kit was not initialized"))? - // .await - // }; + cx.spawn(move |this, mut cx| async move { + let publish_track = async { + let track = LocalAudioTrack::create(); + this.upgrade() + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, _| { + this.live_kit + .as_ref() + .map(|live_kit| live_kit.room.publish_audio_track(track)) + })? + .ok_or_else(|| anyhow!("live-kit was not initialized"))? + .await + }; - // let publication = publish_track.await; - // this.upgrade() - // .ok_or_else(|| anyhow!("room was dropped"))? - // .update(&mut cx, |this, cx| { - // let live_kit = this - // .live_kit - // .as_mut() - // .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + let publication = publish_track.await; + this.upgrade() + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - // let (canceled, muted) = if let LocalTrack::Pending { - // publish_id: cur_publish_id, - // muted, - // } = &live_kit.microphone_track - // { - // (*cur_publish_id != publish_id, *muted) - // } else { - // (true, false) - // }; + let (canceled, muted) = if let LocalTrack::Pending { + publish_id: cur_publish_id, + muted, + } = &live_kit.microphone_track + { + (*cur_publish_id != publish_id, *muted) + } else { + (true, false) + }; - // match publication { - // Ok(publication) => { - // if canceled { - // live_kit.room.unpublish_track(publication); - // } else { - // if muted { - // cx.executor().spawn(publication.set_mute(muted)).detach(); - // } - // live_kit.microphone_track = LocalTrack::Published { - // track_publication: publication, - // muted, - // }; - // cx.notify(); - // } - // Ok(()) - // } - // Err(error) => { - // if canceled { - // Ok(()) - // } else { - // live_kit.microphone_track = LocalTrack::None; - // cx.notify(); - // Err(error) - // } - // } - // } - // })? - // }) + match publication { + Ok(publication) => { + if canceled { + live_kit.room.unpublish_track(publication); + } else { + if muted { + cx.background_executor() + .spawn(publication.set_mute(muted)) + .detach(); + } + live_kit.microphone_track = LocalTrack::Published { + track_publication: publication, + muted, + }; + cx.notify(); + } + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.microphone_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + })? + }) } pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { - todo!() - // if self.status.is_offline() { - // return Task::ready(Err(anyhow!("room is offline"))); - // } else if self.is_screen_sharing() { - // return Task::ready(Err(anyhow!("screen was already shared"))); - // } + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } else if self.is_screen_sharing() { + return Task::ready(Err(anyhow!("screen was already shared"))); + } - // let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { - // let publish_id = post_inc(&mut live_kit.next_publish_id); - // live_kit.screen_track = LocalTrack::Pending { - // publish_id, - // muted: false, - // }; - // cx.notify(); - // (live_kit.room.display_sources(), publish_id) - // } else { - // return Task::ready(Err(anyhow!("live-kit was not initialized"))); - // }; + let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.screen_track = LocalTrack::Pending { + publish_id, + muted: false, + }; + cx.notify(); + (live_kit.room.display_sources(), publish_id) + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; - // cx.spawn(move |this, mut cx| async move { - // let publish_track = async { - // let displays = displays.await?; - // let display = displays - // .first() - // .ok_or_else(|| anyhow!("no display found"))?; - // let track = LocalVideoTrack::screen_share_for_display(&display); - // this.upgrade() - // .ok_or_else(|| anyhow!("room was dropped"))? - // .update(&mut cx, |this, _| { - // this.live_kit - // .as_ref() - // .map(|live_kit| live_kit.room.publish_video_track(track)) - // })? - // .ok_or_else(|| anyhow!("live-kit was not initialized"))? - // .await - // }; + cx.spawn(move |this, mut cx| async move { + let publish_track = async { + let displays = displays.await?; + let display = displays + .first() + .ok_or_else(|| anyhow!("no display found"))?; + let track = LocalVideoTrack::screen_share_for_display(&display); + this.upgrade() + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, _| { + this.live_kit + .as_ref() + .map(|live_kit| live_kit.room.publish_video_track(track)) + })? + .ok_or_else(|| anyhow!("live-kit was not initialized"))? + .await + }; - // let publication = publish_track.await; - // this.upgrade() - // .ok_or_else(|| anyhow!("room was dropped"))? - // .update(&mut cx, |this, cx| { - // let live_kit = this - // .live_kit - // .as_mut() - // .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + let publication = publish_track.await; + this.upgrade() + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - // let (canceled, muted) = if let LocalTrack::Pending { - // publish_id: cur_publish_id, - // muted, - // } = &live_kit.screen_track - // { - // (*cur_publish_id != publish_id, *muted) - // } else { - // (true, false) - // }; + let (canceled, muted) = if let LocalTrack::Pending { + publish_id: cur_publish_id, + muted, + } = &live_kit.screen_track + { + (*cur_publish_id != publish_id, *muted) + } else { + (true, false) + }; - // match publication { - // Ok(publication) => { - // if canceled { - // live_kit.room.unpublish_track(publication); - // } else { - // if muted { - // cx.executor().spawn(publication.set_mute(muted)).detach(); - // } - // live_kit.screen_track = LocalTrack::Published { - // track_publication: publication, - // muted, - // }; - // cx.notify(); - // } + match publication { + Ok(publication) => { + if canceled { + live_kit.room.unpublish_track(publication); + } else { + if muted { + cx.background_executor() + .spawn(publication.set_mute(muted)) + .detach(); + } + live_kit.screen_track = LocalTrack::Published { + track_publication: publication, + muted, + }; + cx.notify(); + } - // Audio::play_sound(Sound::StartScreenshare, cx); + Audio::play_sound(Sound::StartScreenshare, cx); - // Ok(()) - // } - // Err(error) => { - // if canceled { - // Ok(()) - // } else { - // live_kit.screen_track = LocalTrack::None; - // cx.notify(); - // Err(error) - // } - // } - // } - // })? - // }) + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.screen_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + })? + }) } pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { - todo!() - // let should_mute = !self.is_muted(cx); - // if let Some(live_kit) = self.live_kit.as_mut() { - // if matches!(live_kit.microphone_track, LocalTrack::None) { - // return Ok(self.share_microphone(cx)); - // } + let should_mute = !self.is_muted(cx); + if let Some(live_kit) = self.live_kit.as_mut() { + if matches!(live_kit.microphone_track, LocalTrack::None) { + return Ok(self.share_microphone(cx)); + } - // let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; - // live_kit.muted_by_user = should_mute; + let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; + live_kit.muted_by_user = should_mute; - // if old_muted == true && live_kit.deafened == true { - // if let Some(task) = self.toggle_deafen(cx).ok() { - // task.detach(); - // } - // } + if old_muted == true && live_kit.deafened == true { + if let Some(task) = self.toggle_deafen(cx).ok() { + task.detach(); + } + } - // Ok(ret_task) - // } else { - // Err(anyhow!("LiveKit not started")) - // } + Ok(ret_task) + } else { + Err(anyhow!("LiveKit not started")) + } } pub fn toggle_deafen(&mut self, cx: &mut ModelContext) -> Result>> { - todo!() - // if let Some(live_kit) = self.live_kit.as_mut() { - // (*live_kit).deafened = !live_kit.deafened; + if let Some(live_kit) = self.live_kit.as_mut() { + (*live_kit).deafened = !live_kit.deafened; - // let mut tasks = Vec::with_capacity(self.remote_participants.len()); - // // Context notification is sent within set_mute itself. - // let mut mute_task = None; - // // When deafening, mute user's mic as well. - // // When undeafening, unmute user's mic unless it was manually muted prior to deafening. - // if live_kit.deafened || !live_kit.muted_by_user { - // mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0); - // }; - // for participant in self.remote_participants.values() { - // for track in live_kit - // .room - // .remote_audio_track_publications(&participant.user.id.to_string()) - // { - // let deafened = live_kit.deafened; - // tasks.push( - // cx.executor() - // .spawn_on_main(move || track.set_enabled(!deafened)), - // ); - // } - // } + let mut tasks = Vec::with_capacity(self.remote_participants.len()); + // Context notification is sent within set_mute itself. + let mut mute_task = None; + // When deafening, mute user's mic as well. + // When undeafening, unmute user's mic unless it was manually muted prior to deafening. + if live_kit.deafened || !live_kit.muted_by_user { + mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0); + }; + for participant in self.remote_participants.values() { + for track in live_kit + .room + .remote_audio_track_publications(&participant.user.id.to_string()) + { + let deafened = live_kit.deafened; + tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened))); + } + } - // Ok(cx.executor().spawn_on_main(|| async { - // if let Some(mute_task) = mute_task { - // mute_task.await?; - // } - // for task in tasks { - // task.await?; - // } - // Ok(()) - // })) - // } else { - // Err(anyhow!("LiveKit not started")) - // } + Ok(cx.foreground_executor().spawn(async move { + if let Some(mute_task) = mute_task { + mute_task.await?; + } + for task in tasks { + task.await?; + } + Ok(()) + })) + } else { + Err(anyhow!("LiveKit not started")) + } } pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { @@ -1494,37 +1481,35 @@ impl Room { return Err(anyhow!("room is offline")); } - todo!() - // let live_kit = self - // .live_kit - // .as_mut() - // .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - // match mem::take(&mut live_kit.screen_track) { - // LocalTrack::None => Err(anyhow!("screen was not shared")), - // LocalTrack::Pending { .. } => { - // cx.notify(); - // Ok(()) - // } - // LocalTrack::Published { - // track_publication, .. - // } => { - // live_kit.room.unpublish_track(track_publication); - // cx.notify(); + let live_kit = self + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + match mem::take(&mut live_kit.screen_track) { + LocalTrack::None => Err(anyhow!("screen was not shared")), + LocalTrack::Pending { .. } => { + cx.notify(); + Ok(()) + } + LocalTrack::Published { + track_publication, .. + } => { + live_kit.room.unpublish_track(track_publication); + cx.notify(); - // Audio::play_sound(Sound::StopScreenshare, cx); - // Ok(()) - // } - // } + Audio::play_sound(Sound::StopScreenshare, cx); + Ok(()) + } + } } #[cfg(any(test, feature = "test-support"))] pub fn set_display_sources(&self, sources: Vec) { - todo!() - // self.live_kit - // .as_ref() - // .unwrap() - // .room - // .set_display_sources(sources); + self.live_kit + .as_ref() + .unwrap() + .room + .set_display_sources(sources); } } @@ -1568,7 +1553,8 @@ impl LiveKitRoom { *muted = should_mute; cx.notify(); Ok(( - cx.executor().spawn(track_publication.set_mute(*muted)), + cx.background_executor() + .spawn(track_publication.set_mute(*muted)), old_muted, )) } diff --git a/crates/client2/Cargo.toml b/crates/client2/Cargo.toml index 8a6edbb428..ace229bc21 100644 --- a/crates/client2/Cargo.toml +++ b/crates/client2/Cargo.toml @@ -9,17 +9,17 @@ path = "src/client2.rs" doctest = false [features] -test-support = ["collections/test-support", "gpui2/test-support", "rpc2/test-support"] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] [dependencies] collections = { path = "../collections" } -db2 = { path = "../db2" } -gpui2 = { path = "../gpui2" } +db = { package = "db2", path = "../db2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } -rpc2 = { path = "../rpc2" } -text = { path = "../text" } -settings2 = { path = "../settings2" } -feature_flags2 = { path = "../feature_flags2" } +rpc = { package = "rpc2", path = "../rpc2" } +text = { package = "text2", path = "../text2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true @@ -46,7 +46,7 @@ url = "2.2" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -rpc2 = { path = "../rpc2", features = ["test-support"] } -settings = { path = "../settings", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index 19e8685c28..6494e0350b 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -11,9 +11,10 @@ use async_tungstenite::tungstenite::{ http::{Request, StatusCode}, }; use futures::{ - future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, TryStreamExt, + future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, + TryStreamExt, }; -use gpui2::{ +use gpui::{ serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task, WeakModel, }; @@ -21,10 +22,10 @@ use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; use rand::prelude::*; -use rpc2::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; +use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use std::{ any::TypeId, collections::HashMap, @@ -43,7 +44,7 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use rpc2::*; +pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; @@ -240,7 +241,7 @@ struct ClientState { Box, &Arc, AsyncAppContext, - ) -> BoxFuture<'static, Result<()>>, + ) -> LocalBoxFuture<'static, Result<()>>, >, >, } @@ -310,10 +311,7 @@ pub struct PendingEntitySubscription { consumed: bool, } -impl PendingEntitySubscription -where - T: 'static + Send, -{ +impl PendingEntitySubscription { pub fn set_model(mut self, model: &Model, cx: &mut AsyncAppContext) -> Subscription { self.consumed = true; let mut state = self.client.state.write(); @@ -341,10 +339,7 @@ where } } -impl Drop for PendingEntitySubscription -where - T: 'static, -{ +impl Drop for PendingEntitySubscription { fn drop(&mut self) { if !self.consumed { let mut state = self.client.state.write(); @@ -372,7 +367,7 @@ pub struct TelemetrySettingsContent { pub metrics: Option, } -impl settings2::Settings for TelemetrySettings { +impl settings::Settings for TelemetrySettings { const KEY: Option<&'static str> = Some("telemetry"); type FileContent = TelemetrySettingsContent; @@ -505,7 +500,7 @@ impl Client { }, &cx, ); - cx.executor().timer(delay).await; + cx.background_executor().timer(delay).await; delay = delay .mul_f32(rng.gen_range(1.0..=2.0)) .min(reconnect_interval); @@ -529,7 +524,7 @@ impl Client { remote_id: u64, ) -> Result> where - T: 'static + Send, + T: 'static, { let id = (TypeId::of::(), remote_id); @@ -557,9 +552,13 @@ impl Client { ) -> Subscription where M: EnvelopedMessage, - E: 'static + Send, - H: 'static + Send + Sync + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future> + Send, + E: 'static, + H: 'static + + Sync + + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + + Send + + Sync, + F: 'static + Future>, { let message_type_id = TypeId::of::(); @@ -573,7 +572,7 @@ impl Client { Arc::new(move |subscriber, envelope, client, cx| { let subscriber = subscriber.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - handler(subscriber, *envelope, client.clone(), cx).boxed() + handler(subscriber, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -599,9 +598,13 @@ impl Client { ) -> Subscription where M: RequestMessage, - E: 'static + Send, - H: 'static + Send + Sync + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future> + Send, + E: 'static, + H: 'static + + Sync + + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + + Send + + Sync, + F: 'static + Future>, { self.add_message_handler(model, move |handle, envelope, this, cx| { Self::respond_to_request( @@ -615,9 +618,9 @@ impl Client { pub fn add_model_message_handler(self: &Arc, handler: H) where M: EntityMessage, - E: 'static + Send, - H: 'static + Send + Sync + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future> + Send, + E: 'static, + H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, + F: 'static + Future>, { self.add_entity_message_handler::(move |subscriber, message, client, cx| { handler(subscriber.downcast::().unwrap(), message, client, cx) @@ -627,9 +630,9 @@ impl Client { fn add_entity_message_handler(self: &Arc, handler: H) where M: EntityMessage, - E: 'static + Send, - H: 'static + Send + Sync + Fn(AnyModel, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future> + Send, + E: 'static, + H: 'static + Fn(AnyModel, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, + F: 'static + Future>, { let model_type_id = TypeId::of::(); let message_type_id = TypeId::of::(); @@ -655,7 +658,7 @@ impl Client { message_type_id, Arc::new(move |handle, envelope, client, cx| { let envelope = envelope.into_any().downcast::>().unwrap(); - handler(handle, *envelope, client.clone(), cx).boxed() + handler(handle, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -666,9 +669,9 @@ impl Client { pub fn add_model_request_handler(self: &Arc, handler: H) where M: EntityMessage + RequestMessage, - E: 'static + Send, - H: 'static + Send + Sync + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future> + Send, + E: 'static, + H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, + F: 'static + Future>, { self.add_model_message_handler(move |entity, envelope, client, cx| { Self::respond_to_request::( @@ -705,7 +708,7 @@ impl Client { read_credentials_from_keychain(cx).await.is_some() } - #[async_recursion] + #[async_recursion(?Send)] pub async fn authenticate_and_connect( self: &Arc, try_keychain: bool, @@ -763,7 +766,8 @@ impl Client { self.set_status(Status::Reconnecting, cx); } - let mut timeout = futures::FutureExt::fuse(cx.executor().timer(CONNECTION_TIMEOUT)); + let mut timeout = + futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT)); futures::select_biased! { connection = self.establish_connection(&credentials, cx).fuse() => { match connection { @@ -814,7 +818,7 @@ impl Client { conn: Connection, cx: &AsyncAppContext, ) -> Result<()> { - let executor = cx.executor(); + let executor = cx.background_executor(); log::info!("add connection to peer"); let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn, { let executor = executor.clone(); @@ -975,10 +979,10 @@ impl Client { "Authorization", format!("{} {}", credentials.user_id, credentials.access_token), ) - .header("x-zed-protocol-version", rpc2::PROTOCOL_VERSION); + .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION); let http = self.http.clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?; let rpc_host = rpc_url .host_str() @@ -1025,7 +1029,7 @@ impl Client { // zed server to encrypt the user's access token, so that it can'be intercepted by // any other app running on the user's device. let (public_key, private_key) = - rpc2::auth::keypair().expect("failed to generate keypair for auth"); + rpc::auth::keypair().expect("failed to generate keypair for auth"); let public_key_string = String::try_from(public_key).expect("failed to serialize public key for auth"); @@ -1049,7 +1053,7 @@ impl Client { write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); } - cx.run_on_main(move |cx| cx.open_url(&url))?.await; + cx.update(|cx| cx.open_url(&url))?; // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted // access token from the query params. @@ -1100,7 +1104,7 @@ impl Client { let access_token = private_key .decrypt_string(&access_token) .context("failed to decrypt access token")?; - cx.run_on_main(|cx| cx.activate(true))?.await; + cx.update(|cx| cx.activate(true))?; Ok(Credentials { user_id: user_id.parse()?, @@ -1292,7 +1296,7 @@ impl Client { sender_id, type_name ); - cx.spawn_on_main(move |_| async move { + cx.spawn(move |_| async move { match future.await { Ok(()) => { log::debug!( @@ -1331,9 +1335,8 @@ async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option Result<()> { - cx.run_on_main(move |cx| { + cx.update(move |cx| { cx.write_credentials( &ZED_SERVER_URL, &credentials.user_id.to_string(), credentials.access_token.as_bytes(), ) })? - .await } async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> { - cx.run_on_main(move |cx| cx.delete_credentials(&ZED_SERVER_URL))? - .await + cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))? } const WORKTREE_URL_PREFIX: &str = "zed://worktrees/"; @@ -1382,12 +1383,12 @@ mod tests { use super::*; use crate::test::FakeServer; - use gpui2::{Context, Executor, TestAppContext}; + use gpui::{BackgroundExecutor, Context, TestAppContext}; use parking_lot::Mutex; use std::future; use util::http::FakeHttpClient; - #[gpui2::test(iterations = 10)] + #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1421,15 +1422,15 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } - #[gpui2::test(iterations = 10)] - async fn test_connection_timeout(executor: Executor, cx: &mut TestAppContext) { + #[gpui::test(iterations = 10)] + async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let mut status = client.status(); // Time out when client tries to connect. client.override_authenticate(move |cx| { - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { Ok(Credentials { user_id, access_token: "token".into(), @@ -1437,7 +1438,7 @@ mod tests { }) }); client.override_establish_connection(|_, cx| { - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { future::pending::<()>().await; unreachable!() }) @@ -1471,7 +1472,7 @@ mod tests { // Time out when re-establishing the connection. server.allow_connections(); client.override_establish_connection(|_, cx| { - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { future::pending::<()>().await; unreachable!() }) @@ -1489,8 +1490,11 @@ mod tests { )); } - #[gpui2::test(iterations = 10)] - async fn test_authenticating_more_than_once(cx: &mut TestAppContext, executor: Executor) { + #[gpui::test(iterations = 10)] + async fn test_authenticating_more_than_once( + cx: &mut TestAppContext, + executor: BackgroundExecutor, + ) { let auth_count = Arc::new(Mutex::new(0)); let dropped_auth_count = Arc::new(Mutex::new(0)); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1500,7 +1504,7 @@ mod tests { move |cx| { let auth_count = auth_count.clone(); let dropped_auth_count = dropped_auth_count.clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { *auth_count.lock() += 1; let _drop = util::defer(move || *dropped_auth_count.lock() += 1); future::pending::<()>().await; @@ -1537,7 +1541,7 @@ mod tests { assert_eq!(decode_worktree_url("not://the-right-format"), None); } - #[gpui2::test] + #[gpui::test] async fn test_subscribing_to_entity(cx: &mut TestAppContext) { let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1590,7 +1594,7 @@ mod tests { done_rx2.next().await.unwrap(); } - #[gpui2::test] + #[gpui::test] async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1618,7 +1622,7 @@ mod tests { done_rx2.next().await.unwrap(); } - #[gpui2::test] + #[gpui::test] async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); diff --git a/crates/client2/src/telemetry.rs b/crates/client2/src/telemetry.rs index 47d1c143e1..3723f7b906 100644 --- a/crates/client2/src/telemetry.rs +++ b/crates/client2/src/telemetry.rs @@ -1,9 +1,9 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; -use gpui2::{serde_json, AppContext, AppMetadata, Executor, Task}; +use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; -use settings2::Settings; +use settings::Settings; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, @@ -14,7 +14,7 @@ use util::{channel::ReleaseChannel, TryFutureExt}; pub struct Telemetry { http_client: Arc, - executor: Executor, + executor: BackgroundExecutor, state: Mutex, } @@ -123,7 +123,7 @@ impl Telemetry { // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, - executor: cx.executor().clone(), + executor: cx.background_executor().clone(), state: Mutex::new(TelemetryState { app_metadata: cx.app_metadata(), architecture: env::consts::ARCH, diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs index f30547dcfc..5462799103 100644 --- a/crates/client2/src/test.rs +++ b/crates/client2/src/test.rs @@ -1,9 +1,9 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{anyhow, Result}; use futures::{stream::BoxStream, StreamExt}; -use gpui2::{Context, Executor, Model, TestAppContext}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext}; use parking_lot::Mutex; -use rpc2::{ +use rpc::{ proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -14,7 +14,7 @@ pub struct FakeServer { peer: Arc, state: Arc>, user_id: u64, - executor: Executor, + executor: BackgroundExecutor, } #[derive(Default)] @@ -79,10 +79,10 @@ impl FakeServer { } let (client_conn, server_conn, _) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.background_executor().clone()); let (connection_id, io, incoming) = - peer.add_test_connection(server_conn, cx.executor().clone()); - cx.executor().spawn(io).detach(); + peer.add_test_connection(server_conn, cx.background_executor().clone()); + cx.background_executor().spawn(io).detach(); { let mut state = state.lock(); state.connection_id = Some(connection_id); diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index 2a8cf34af4..baf3a19dad 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -1,11 +1,11 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; -use feature_flags2::FeatureFlagAppExt; +use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; -use gpui2::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task}; +use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task}; use postage::{sink::Sink, watch}; -use rpc2::proto::{RequestMessage, UsersResponse}; +use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use text::ReplicaId; use util::http::HttpClient; diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 987c295407..dea6e09245 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.27.0" +version = "0.28.0" publish = false [[bin]] diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index f83824d808..2021194607 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -11,21 +11,21 @@ doctest = false [features] test-support = [ "collections/test-support", - "gpui2/test-support", - "language2/test-support", - "lsp2/test-support", - "settings2/test-support", + "gpui/test-support", + "language/test-support", + "lsp/test-support", + "settings/test-support", "util/test-support", ] [dependencies] collections = { path = "../collections" } context_menu = { path = "../context_menu" } -gpui2 = { path = "../gpui2" } -language2 = { path = "../language2" } -settings2 = { path = "../settings2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +settings = { package = "settings2", path = "../settings2" } theme = { path = "../theme" } -lsp2 = { path = "../lsp2" } +lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } @@ -42,9 +42,9 @@ parking_lot.workspace = true clock = { path = "../clock" } collections = { path = "../collections", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -language2 = { path = "../language2", features = ["test-support"] } -lsp2 = { path = "../lsp2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } -settings2 = { path = "../settings2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 083c491656..6b1190a5bf 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -6,20 +6,20 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use collections::{HashMap, HashSet}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; -use gpui2::{ +use gpui::{ AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, ModelContext, Task, WeakModel, }; -use language2::{ +use language::{ language_settings::{all_language_settings, language_settings}, point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, LanguageServerName, PointUtf16, ToPointUtf16, }; -use lsp2::{LanguageServer, LanguageServerBinary, LanguageServerId}; +use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId}; use node_runtime::NodeRuntime; use parking_lot::Mutex; use request::StatusNotification; -use settings2::SettingsStore; +use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ ffi::OsString, @@ -172,11 +172,11 @@ impl Status { } struct RegisteredBuffer { - uri: lsp2::Url, + uri: lsp::Url, language_id: String, snapshot: BufferSnapshot, snapshot_version: i32, - _subscriptions: [gpui2::Subscription; 2], + _subscriptions: [gpui::Subscription; 2], pending_buffer_change: Task>, } @@ -208,7 +208,7 @@ impl RegisteredBuffer { let new_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot()).ok()?; let content_changes = cx - .executor() + .background_executor() .spawn({ let new_snapshot = new_snapshot.clone(); async move { @@ -220,8 +220,8 @@ impl RegisteredBuffer { let new_text = new_snapshot .text_for_range(edit.new.start.1..edit.new.end.1) .collect(); - lsp2::TextDocumentContentChangeEvent { - range: Some(lsp2::Range::new( + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( point_to_lsp(edit_start), point_to_lsp(edit_end), )), @@ -243,9 +243,9 @@ impl RegisteredBuffer { buffer.snapshot = new_snapshot; server .lsp - .notify::( - lsp2::DidChangeTextDocumentParams { - text_document: lsp2::VersionedTextDocumentIdentifier::new( + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( buffer.uri.clone(), buffer.snapshot_version, ), @@ -280,7 +280,7 @@ pub struct Copilot { server: CopilotServer, buffers: HashSet>, server_id: LanguageServerId, - _subscription: gpui2::Subscription, + _subscription: gpui::Subscription, } pub enum Event { @@ -535,7 +535,7 @@ impl Copilot { } }; - cx.executor() + cx.background_executor() .spawn(task.map_err(|err| anyhow!("{:?}", err))) } else { // If we're downloading, wait until download is finished @@ -549,7 +549,7 @@ impl Copilot { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server { let server = server.clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { server .request::(request::SignOutParams {}) .await?; @@ -579,7 +579,7 @@ impl Copilot { cx.notify(); - cx.executor().spawn(start_task) + cx.background_executor().spawn(start_task) } pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc)> { @@ -608,13 +608,13 @@ impl Copilot { registered_buffers .entry(buffer.entity_id()) .or_insert_with(|| { - let uri: lsp2::Url = uri_for_buffer(buffer, cx); + let uri: lsp::Url = uri_for_buffer(buffer, cx); let language_id = id_for_language(buffer.read(cx).language()); let snapshot = buffer.read(cx).snapshot(); server - .notify::( - lsp2::DidOpenTextDocumentParams { - text_document: lsp2::TextDocumentItem { + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem { uri: uri.clone(), language_id: language_id.clone(), version: 0, @@ -647,29 +647,29 @@ impl Copilot { fn handle_buffer_event( &mut self, buffer: Model, - event: &language2::Event, + event: &language::Event, cx: &mut ModelContext, ) -> Result<()> { if let Ok(server) = self.server.as_running() { if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) { match event { - language2::Event::Edited => { + language::Event::Edited => { let _ = registered_buffer.report_changes(&buffer, cx); } - language2::Event::Saved => { + language::Event::Saved => { server .lsp - .notify::( - lsp2::DidSaveTextDocumentParams { - text_document: lsp2::TextDocumentIdentifier::new( + .notify::( + lsp::DidSaveTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( registered_buffer.uri.clone(), ), text: None, }, )?; } - language2::Event::FileHandleChanged | language2::Event::LanguageChanged => { + language::Event::FileHandleChanged | language::Event::LanguageChanged => { let new_language_id = id_for_language(buffer.read(cx).language()); let new_uri = uri_for_buffer(&buffer, cx); if new_uri != registered_buffer.uri @@ -679,16 +679,16 @@ impl Copilot { registered_buffer.language_id = new_language_id; server .lsp - .notify::( - lsp2::DidCloseTextDocumentParams { - text_document: lsp2::TextDocumentIdentifier::new(old_uri), + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), }, )?; server .lsp - .notify::( - lsp2::DidOpenTextDocumentParams { - text_document: lsp2::TextDocumentItem::new( + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( registered_buffer.uri.clone(), registered_buffer.language_id.clone(), registered_buffer.snapshot_version, @@ -711,9 +711,9 @@ impl Copilot { if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { server .lsp - .notify::( - lsp2::DidCloseTextDocumentParams { - text_document: lsp2::TextDocumentIdentifier::new(buffer.uri), + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer.uri), }, ) .log_err(); @@ -760,7 +760,7 @@ impl Copilot { .request::(request::NotifyAcceptedParams { uuid: completion.uuid.clone(), }); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { request.await?; Ok(()) }) @@ -784,7 +784,7 @@ impl Copilot { .map(|completion| completion.uuid.clone()) .collect(), }); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { request.await?; Ok(()) }) @@ -798,7 +798,7 @@ impl Copilot { ) -> Task>> where R: 'static - + lsp2::request::Request< + + lsp::request::Request< Params = request::GetCompletionsParams, Result = request::GetCompletionsResult, >, @@ -827,7 +827,7 @@ impl Copilot { .map(|file| file.path().to_path_buf()) .unwrap_or_default(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let (version, snapshot) = snapshot.await?; let result = lsp .request::(request::GetCompletionsParams { @@ -926,9 +926,9 @@ fn id_for_language(language: Option<&Arc>) -> String { } } -fn uri_for_buffer(buffer: &Model, cx: &AppContext) -> lsp2::Url { +fn uri_for_buffer(buffer: &Model, cx: &AppContext) -> lsp::Url { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { - lsp2::Url::from_file_path(file.abs_path(cx)).unwrap() + lsp::Url::from_file_path(file.abs_path(cx)).unwrap() } else { format!("buffer://{}", buffer.entity_id()).parse().unwrap() } diff --git a/crates/copilot2/src/request.rs b/crates/copilot2/src/request.rs index fee92051dc..0f9a478b91 100644 --- a/crates/copilot2/src/request.rs +++ b/crates/copilot2/src/request.rs @@ -8,7 +8,7 @@ pub struct CheckStatusParams { pub local_checks_only: bool, } -impl lsp2::request::Request for CheckStatus { +impl lsp::request::Request for CheckStatus { type Params = CheckStatusParams; type Result = SignInStatus; const METHOD: &'static str = "checkStatus"; @@ -33,7 +33,7 @@ pub struct PromptUserDeviceFlow { pub verification_uri: String, } -impl lsp2::request::Request for SignInInitiate { +impl lsp::request::Request for SignInInitiate { type Params = SignInInitiateParams; type Result = SignInInitiateResult; const METHOD: &'static str = "signInInitiate"; @@ -66,7 +66,7 @@ pub enum SignInStatus { NotSignedIn, } -impl lsp2::request::Request for SignInConfirm { +impl lsp::request::Request for SignInConfirm { type Params = SignInConfirmParams; type Result = SignInStatus; const METHOD: &'static str = "signInConfirm"; @@ -82,7 +82,7 @@ pub struct SignOutParams {} #[serde(rename_all = "camelCase")] pub struct SignOutResult {} -impl lsp2::request::Request for SignOut { +impl lsp::request::Request for SignOut { type Params = SignOutParams; type Result = SignOutResult; const METHOD: &'static str = "signOut"; @@ -102,9 +102,9 @@ pub struct GetCompletionsDocument { pub tab_size: u32, pub indent_size: u32, pub insert_spaces: bool, - pub uri: lsp2::Url, + pub uri: lsp::Url, pub relative_path: String, - pub position: lsp2::Position, + pub position: lsp::Position, pub version: usize, } @@ -118,13 +118,13 @@ pub struct GetCompletionsResult { #[serde(rename_all = "camelCase")] pub struct Completion { pub text: String, - pub position: lsp2::Position, + pub position: lsp::Position, pub uuid: String, - pub range: lsp2::Range, + pub range: lsp::Range, pub display_text: String, } -impl lsp2::request::Request for GetCompletions { +impl lsp::request::Request for GetCompletions { type Params = GetCompletionsParams; type Result = GetCompletionsResult; const METHOD: &'static str = "getCompletions"; @@ -132,7 +132,7 @@ impl lsp2::request::Request for GetCompletions { pub enum GetCompletionsCycling {} -impl lsp2::request::Request for GetCompletionsCycling { +impl lsp::request::Request for GetCompletionsCycling { type Params = GetCompletionsParams; type Result = GetCompletionsResult; const METHOD: &'static str = "getCompletionsCycling"; @@ -149,7 +149,7 @@ pub struct LogMessageParams { pub extra: Vec, } -impl lsp2::notification::Notification for LogMessage { +impl lsp::notification::Notification for LogMessage { type Params = LogMessageParams; const METHOD: &'static str = "LogMessage"; } @@ -162,7 +162,7 @@ pub struct StatusNotificationParams { pub status: String, // One of Normal/InProgress } -impl lsp2::notification::Notification for StatusNotification { +impl lsp::notification::Notification for StatusNotification { type Params = StatusNotificationParams; const METHOD: &'static str = "statusNotification"; } @@ -176,7 +176,7 @@ pub struct SetEditorInfoParams { pub editor_plugin_info: EditorPluginInfo, } -impl lsp2::request::Request for SetEditorInfo { +impl lsp::request::Request for SetEditorInfo { type Params = SetEditorInfoParams; type Result = String; const METHOD: &'static str = "setEditorInfo"; @@ -204,7 +204,7 @@ pub struct NotifyAcceptedParams { pub uuid: String, } -impl lsp2::request::Request for NotifyAccepted { +impl lsp::request::Request for NotifyAccepted { type Params = NotifyAcceptedParams; type Result = String; const METHOD: &'static str = "notifyAccepted"; @@ -218,7 +218,7 @@ pub struct NotifyRejectedParams { pub uuids: Vec, } -impl lsp2::request::Request for NotifyRejected { +impl lsp::request::Request for NotifyRejected { type Params = NotifyRejectedParams; type Result = String; const METHOD: &'static str = "notifyRejected"; diff --git a/crates/db2/Cargo.toml b/crates/db2/Cargo.toml index 6ef8ec0874..c73e6314c5 100644 --- a/crates/db2/Cargo.toml +++ b/crates/db2/Cargo.toml @@ -13,7 +13,7 @@ test-support = [] [dependencies] collections = { path = "../collections" } -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } sqlez = { path = "../sqlez" } sqlez_macros = { path = "../sqlez_macros" } util = { path = "../util" } @@ -28,6 +28,6 @@ serde_derive.workspace = true smol.workspace = true [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } env_logger.workspace = true tempdir.workspace = true diff --git a/crates/db2/src/db2.rs b/crates/db2/src/db2.rs index e2e1ae9eaa..573845aa2e 100644 --- a/crates/db2/src/db2.rs +++ b/crates/db2/src/db2.rs @@ -4,7 +4,7 @@ pub mod query; // Re-export pub use anyhow; use anyhow::Context; -use gpui2::AppContext; +use gpui::AppContext; pub use indoc::indoc; pub use lazy_static; pub use smol; @@ -185,143 +185,147 @@ pub fn write_and_log(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send where F: Future> + Send, { - cx.executor() + cx.background_executor() .spawn(async move { db_write().await.log_err() }) .detach() } -// #[cfg(test)] -// mod tests { -// use std::thread; +#[cfg(test)] +mod tests { + use std::thread; -// use sqlez::domain::Domain; -// use sqlez_macros::sql; -// use tempdir::TempDir; + use sqlez::domain::Domain; + use sqlez_macros::sql; + use tempdir::TempDir; -// use crate::open_db; + use crate::open_db; -// // Test bad migration panics -// #[gpui::test] -// #[should_panic] -// async fn test_bad_migration_panics() { -// enum BadDB {} + // Test bad migration panics + #[gpui::test] + #[should_panic] + async fn test_bad_migration_panics() { + enum BadDB {} -// impl Domain for BadDB { -// fn name() -> &'static str { -// "db_tests" -// } + impl Domain for BadDB { + fn name() -> &'static str { + "db_tests" + } -// fn migrations() -> &'static [&'static str] { -// &[ -// sql!(CREATE TABLE test(value);), -// // failure because test already exists -// sql!(CREATE TABLE test(value);), -// ] -// } -// } + fn migrations() -> &'static [&'static str] { + &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ] + } + } -// let tempdir = TempDir::new("DbTests").unwrap(); -// let _bad_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// } + let tempdir = TempDir::new("DbTests").unwrap(); + let _bad_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + } -// /// Test that DB exists but corrupted (causing recreate) -// #[gpui::test] -// async fn test_db_corruption() { -// enum CorruptedDB {} + /// Test that DB exists but corrupted (causing recreate) + #[gpui::test] + async fn test_db_corruption(cx: &mut gpui::TestAppContext) { + cx.executor().allow_parking(); -// impl Domain for CorruptedDB { -// fn name() -> &'static str { -// "db_tests" -// } + enum CorruptedDB {} -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test(value);)] -// } -// } + impl Domain for CorruptedDB { + fn name() -> &'static str { + "db_tests" + } -// enum GoodDB {} + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } + } -// impl Domain for GoodDB { -// fn name() -> &'static str { -// "db_tests" //Notice same name -// } + enum GoodDB {} -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test2(value);)] //But different migration -// } -// } + impl Domain for GoodDB { + fn name() -> &'static str { + "db_tests" //Notice same name + } -// let tempdir = TempDir::new("DbTests").unwrap(); -// { -// let corrupt_db = -// open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!(corrupt_db.persistent()); -// } + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } + } -// let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!( -// good_db.select_row::("SELECT * FROM test2").unwrap()() -// .unwrap() -// .is_none() -// ); -// } + let tempdir = TempDir::new("DbTests").unwrap(); + { + let corrupt_db = + open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(corrupt_db.persistent()); + } -// /// Test that DB exists but corrupted (causing recreate) -// #[gpui::test(iterations = 30)] -// async fn test_simultaneous_db_corruption() { -// enum CorruptedDB {} + let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!( + good_db.select_row::("SELECT * FROM test2").unwrap()() + .unwrap() + .is_none() + ); + } -// impl Domain for CorruptedDB { -// fn name() -> &'static str { -// "db_tests" -// } + /// Test that DB exists but corrupted (causing recreate) + #[gpui::test(iterations = 30)] + async fn test_simultaneous_db_corruption(cx: &mut gpui::TestAppContext) { + cx.executor().allow_parking(); -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test(value);)] -// } -// } + enum CorruptedDB {} -// enum GoodDB {} + impl Domain for CorruptedDB { + fn name() -> &'static str { + "db_tests" + } -// impl Domain for GoodDB { -// fn name() -> &'static str { -// "db_tests" //Notice same name -// } + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } + } -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test2(value);)] //But different migration -// } -// } + enum GoodDB {} -// let tempdir = TempDir::new("DbTests").unwrap(); -// { -// // Setup the bad database -// let corrupt_db = -// open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!(corrupt_db.persistent()); -// } + impl Domain for GoodDB { + fn name() -> &'static str { + "db_tests" //Notice same name + } -// // Try to connect to it a bunch of times at once -// let mut guards = vec![]; -// for _ in 0..10 { -// let tmp_path = tempdir.path().to_path_buf(); -// let guard = thread::spawn(move || { -// let good_db = smol::block_on(open_db::( -// tmp_path.as_path(), -// &util::channel::ReleaseChannel::Dev, -// )); -// assert!( -// good_db.select_row::("SELECT * FROM test2").unwrap()() -// .unwrap() -// .is_none() -// ); -// }); + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } + } -// guards.push(guard); -// } + let tempdir = TempDir::new("DbTests").unwrap(); + { + // Setup the bad database + let corrupt_db = + open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(corrupt_db.persistent()); + } -// for guard in guards.into_iter() { -// assert!(guard.join().is_ok()); -// } -// } -// } + // Try to connect to it a bunch of times at once + let mut guards = vec![]; + for _ in 0..10 { + let tmp_path = tempdir.path().to_path_buf(); + let guard = thread::spawn(move || { + let good_db = smol::block_on(open_db::( + tmp_path.as_path(), + &util::channel::ReleaseChannel::Dev, + )); + assert!( + good_db.select_row::("SELECT * FROM test2").unwrap()() + .unwrap() + .is_none() + ); + }); + + guards.push(guard); + } + + for guard in guards.into_iter() { + assert!(guard.join().is_ok()); + } + } +} diff --git a/crates/db2/src/kvp.rs b/crates/db2/src/kvp.rs index 254d91689d..0b0cdd9aa1 100644 --- a/crates/db2/src/kvp.rs +++ b/crates/db2/src/kvp.rs @@ -31,32 +31,32 @@ impl KeyValueStore { } } -// #[cfg(test)] -// mod tests { -// use crate::kvp::KeyValueStore; +#[cfg(test)] +mod tests { + use crate::kvp::KeyValueStore; -// #[gpui::test] -// async fn test_kvp() { -// let db = KeyValueStore(crate::open_test_db("test_kvp").await); + #[gpui::test] + async fn test_kvp() { + let db = KeyValueStore(crate::open_test_db("test_kvp").await); -// assert_eq!(db.read_kvp("key-1").unwrap(), None); + assert_eq!(db.read_kvp("key-1").unwrap(), None); -// db.write_kvp("key-1".to_string(), "one".to_string()) -// .await -// .unwrap(); -// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string())); + db.write_kvp("key-1".to_string(), "one".to_string()) + .await + .unwrap(); + assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string())); -// db.write_kvp("key-1".to_string(), "one-2".to_string()) -// .await -// .unwrap(); -// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string())); + db.write_kvp("key-1".to_string(), "one-2".to_string()) + .await + .unwrap(); + assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string())); -// db.write_kvp("key-2".to_string(), "two".to_string()) -// .await -// .unwrap(); -// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string())); + db.write_kvp("key-2".to_string(), "two".to_string()) + .await + .unwrap(); + assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string())); -// db.delete_kvp("key-1".to_string()).await.unwrap(); -// assert_eq!(db.read_kvp("key-1").unwrap(), None); -// } -// } + db.delete_kvp("key-1".to_string()).await.unwrap(); + assert_eq!(db.read_kvp("key-1").unwrap(), None); + } +} diff --git a/crates/feature_flags2/Cargo.toml b/crates/feature_flags2/Cargo.toml index ad77330ac3..8ae39b31db 100644 --- a/crates/feature_flags2/Cargo.toml +++ b/crates/feature_flags2/Cargo.toml @@ -8,5 +8,5 @@ publish = false path = "src/feature_flags2.rs" [dependencies] -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } anyhow.workspace = true diff --git a/crates/feature_flags2/src/feature_flags2.rs b/crates/feature_flags2/src/feature_flags2.rs index 7b1c0dd4d7..23167796ec 100644 --- a/crates/feature_flags2/src/feature_flags2.rs +++ b/crates/feature_flags2/src/feature_flags2.rs @@ -1,4 +1,4 @@ -use gpui2::{AppContext, Subscription, ViewContext}; +use gpui::{AppContext, Subscription, ViewContext}; #[derive(Default)] struct FeatureFlags { @@ -28,7 +28,7 @@ pub trait FeatureFlagViewExt { F: Fn(bool, &mut V, &mut ViewContext) + Send + Sync + 'static; } -impl FeatureFlagViewExt for ViewContext<'_, '_, V> +impl FeatureFlagViewExt for ViewContext<'_, V> where V: 'static + Send + Sync, { diff --git a/crates/fs2/Cargo.toml b/crates/fs2/Cargo.toml index 36f4e9c9c9..ca525afe5f 100644 --- a/crates/fs2/Cargo.toml +++ b/crates/fs2/Cargo.toml @@ -10,7 +10,7 @@ path = "src/fs2.rs" [dependencies] collections = { path = "../collections" } rope = { path = "../rope" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } @@ -31,10 +31,10 @@ log.workspace = true libc = "0.2" time.workspace = true -gpui2 = { path = "../gpui2", optional = true} +gpui = { package = "gpui2", path = "../gpui2", optional = true} [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } [features] -test-support = ["gpui2/test-support"] +test-support = ["gpui/test-support"] diff --git a/crates/fs2/src/fs2.rs b/crates/fs2/src/fs2.rs index 6ff8676473..350a33b208 100644 --- a/crates/fs2/src/fs2.rs +++ b/crates/fs2/src/fs2.rs @@ -288,7 +288,7 @@ impl Fs for RealFs { pub struct FakeFs { // Use an unfair lock to ensure tests are deterministic. state: Mutex, - executor: gpui2::Executor, + executor: gpui::BackgroundExecutor, } #[cfg(any(test, feature = "test-support"))] @@ -434,7 +434,7 @@ lazy_static::lazy_static! { #[cfg(any(test, feature = "test-support"))] impl FakeFs { - pub fn new(executor: gpui2::Executor) -> Arc { + pub fn new(executor: gpui::BackgroundExecutor) -> Arc { Arc::new(Self { executor, state: Mutex::new(FakeFsState { @@ -1222,11 +1222,11 @@ pub fn copy_recursive<'a>( #[cfg(test)] mod tests { use super::*; - use gpui2::Executor; + use gpui::BackgroundExecutor; use serde_json::json; - #[gpui2::test] - async fn test_fake_fs(executor: Executor) { + #[gpui::test] + async fn test_fake_fs(executor: BackgroundExecutor) { let fs = FakeFs::new(executor.clone()); fs.insert_tree( "/root", diff --git a/crates/fuzzy2/Cargo.toml b/crates/fuzzy2/Cargo.toml index 5b92a27a27..a112554a39 100644 --- a/crates/fuzzy2/Cargo.toml +++ b/crates/fuzzy2/Cargo.toml @@ -9,5 +9,5 @@ path = "src/fuzzy2.rs" doctest = false [dependencies] -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } diff --git a/crates/fuzzy2/src/paths.rs b/crates/fuzzy2/src/paths.rs index f6c5fba6c9..e982195158 100644 --- a/crates/fuzzy2/src/paths.rs +++ b/crates/fuzzy2/src/paths.rs @@ -1,4 +1,4 @@ -use gpui2::Executor; +use gpui::BackgroundExecutor; use std::{ borrow::Cow, cmp::{self, Ordering}, @@ -134,7 +134,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( smart_case: bool, max_results: usize, cancel_flag: &AtomicBool, - executor: Executor, + executor: BackgroundExecutor, ) -> Vec { let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum(); if path_count == 0 { diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs index 6f7533ddd0..085362dd2c 100644 --- a/crates/fuzzy2/src/strings.rs +++ b/crates/fuzzy2/src/strings.rs @@ -2,7 +2,7 @@ use crate::{ matcher::{Match, MatchCandidate, Matcher}, CharBag, }; -use gpui2::Executor; +use gpui::BackgroundExecutor; use std::{ borrow::Cow, cmp::{self, Ordering}, @@ -83,7 +83,7 @@ pub async fn match_strings( smart_case: bool, max_results: usize, cancel_flag: &AtomicBool, - executor: Executor, + executor: BackgroundExecutor, ) -> Vec { if candidates.is_empty() || max_results == 0 { return Default::default(); diff --git a/crates/git3/Cargo.toml b/crates/git3/Cargo.toml new file mode 100644 index 0000000000..e88fa6574d --- /dev/null +++ b/crates/git3/Cargo.toml @@ -0,0 +1,30 @@ +[package] +# git2 was already taken. +name = "git3" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/git.rs" + +[dependencies] +anyhow.workspace = true +clock = { path = "../clock" } +lazy_static.workspace = true +sum_tree = { path = "../sum_tree" } +text = { package = "text2", path = "../text2" } +collections = { path = "../collections" } +util = { path = "../util" } +log.workspace = true +smol.workspace = true +parking_lot.workspace = true +async-trait.workspace = true +futures.workspace = true +git2.workspace = true + +[dev-dependencies] +unindent.workspace = true + +[features] +test-support = [] diff --git a/crates/git3/src/diff.rs b/crates/git3/src/diff.rs new file mode 100644 index 0000000000..39383cfc78 --- /dev/null +++ b/crates/git3/src/diff.rs @@ -0,0 +1,412 @@ +use std::{iter, ops::Range}; +use sum_tree::SumTree; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; + +pub use git2 as libgit; +use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiffHunkStatus { + Added, + Modified, + Removed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiffHunk { + pub buffer_range: Range, + pub diff_base_byte_range: Range, +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added + } else if self.buffer_range.is_empty() { + DiffHunkStatus::Removed + } else { + DiffHunkStatus::Modified + } + } +} + +impl sum_tree::Item for DiffHunk { + type Summary = DiffHunkSummary; + + fn summary(&self) -> Self::Summary { + DiffHunkSummary { + buffer_range: self.buffer_range.clone(), + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct DiffHunkSummary { + buffer_range: Range, +} + +impl sum_tree::Summary for DiffHunkSummary { + type Context = text::BufferSnapshot; + + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { + self.buffer_range.start = self + .buffer_range + .start + .min(&other.buffer_range.start, buffer); + self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer); + } +} + +#[derive(Clone)] +pub struct BufferDiff { + last_buffer_version: Option, + tree: SumTree>, +} + +impl BufferDiff { + pub fn new() -> BufferDiff { + BufferDiff { + last_buffer_version: None, + tree: SumTree::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.tree.is_empty() + } + + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let start = buffer.anchor_before(Point::new(range.start, 0)); + let end = buffer.anchor_after(Point::new(range.end, 0)); + + self.hunks_intersecting_range(start..end, buffer) + } + + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + let anchor_iter = std::iter::from_fn(move || { + cursor.next(buffer); + cursor.item() + }) + .flat_map(move |hunk| { + [ + (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), + (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ] + .into_iter() + }); + + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); + iter::from_fn(move || { + let (start_point, start_base) = summaries.next()?; + let (end_point, end_base) = summaries.next()?; + + let end_row = if end_point.column > 0 { + end_point.row + 1 + } else { + end_point.row + }; + + Some(DiffHunk { + buffer_range: start_point.row..end_row, + diff_base_byte_range: start_base..end_base, + }) + }) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + std::iter::from_fn(move || { + cursor.prev(buffer); + + let hunk = cursor.item()?; + let range = hunk.buffer_range.to_point(buffer); + let end_row = if range.end.column > 0 { + range.end.row + 1 + } else { + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }) + } + + pub fn clear(&mut self, buffer: &text::BufferSnapshot) { + self.last_buffer_version = Some(buffer.version().clone()); + self.tree = SumTree::new(); + } + + pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) { + let mut tree = SumTree::new(); + + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(&diff_base, &buffer_text); + + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); + tree.push(hunk, buffer); + } + } + + self.tree = tree; + self.last_buffer_version = Some(buffer.version().clone()); + } + + #[cfg(test)] + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + let start = text.anchor_before(Point::new(0, 0)); + let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); + self.hunks_intersecting_range(start..end, text) + } + + fn diff<'a>(head: &'a str, current: &'a str) -> Option> { + let mut options = GitOptions::default(); + options.context_lines(0); + + let patch = GitPatch::from_buffers( + head.as_bytes(), + None, + current.as_bytes(), + None, + Some(&mut options), + ); + + match patch { + Ok(patch) => Some(patch), + + Err(err) => { + log::error!("`GitPatch::from_buffers` failed: {}", err); + None + } + } + } + + fn process_patch_hunk<'a>( + patch: &GitPatch<'a>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, + ) -> DiffHunk { + let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); + assert!(line_item_count > 0); + + let mut first_deletion_buffer_row: Option = None; + let mut buffer_row_range: Option> = None; + let mut diff_base_byte_range: Option> = None; + + for line_index in 0..line_item_count { + let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + let kind = line.origin_value(); + let content_offset = line.content_offset() as isize; + let content_len = line.content().len() as isize; + + if kind == GitDiffLineType::Addition { + *buffer_row_divergence += 1; + let row = line.new_lineno().unwrap().saturating_sub(1); + + match &mut buffer_row_range { + Some(buffer_row_range) => buffer_row_range.end = row + 1, + None => buffer_row_range = Some(row..row + 1), + } + } + + if kind == GitDiffLineType::Deletion { + let end = content_offset + content_len; + + match &mut diff_base_byte_range { + Some(head_byte_range) => head_byte_range.end = end as usize, + None => diff_base_byte_range = Some(content_offset as usize..end as usize), + } + + if first_deletion_buffer_row.is_none() { + let old_row = line.old_lineno().unwrap().saturating_sub(1); + let row = old_row as i64 + *buffer_row_divergence; + first_deletion_buffer_row = Some(row as u32); + } + + *buffer_row_divergence -= 1; + } + } + + //unwrap_or deletion without addition + let buffer_row_range = buffer_row_range.unwrap_or_else(|| { + //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk + let row = first_deletion_buffer_row.unwrap(); + row..row + }); + + //unwrap_or addition without deletion + let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0); + + let start = Point::new(buffer_row_range.start, 0); + let end = Point::new(buffer_row_range.end, 0); + let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); + DiffHunk { + buffer_range, + diff_base_byte_range, + } + } +} + +/// Range (crossing new lines), old, new +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn assert_hunks( + diff_hunks: Iter, + buffer: &BufferSnapshot, + diff_base: &str, + expected_hunks: &[(Range, &str, &str)], +) where + Iter: Iterator>, +{ + let actual_hunks = diff_hunks + .map(|hunk| { + ( + hunk.buffer_range.clone(), + &diff_base[hunk.diff_base_byte_range], + buffer + .text_for_range( + Point::new(hunk.buffer_range.start, 0) + ..Point::new(hunk.buffer_range.end, 0), + ) + .collect::(), + ) + }) + .collect::>(); + + let expected_hunks: Vec<_> = expected_hunks + .iter() + .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .collect(); + + assert_eq!(actual_hunks, expected_hunks); +} + +#[cfg(test)] +mod tests { + use std::assert_eq; + + use super::*; + use text::Buffer; + use unindent::Unindent as _; + + #[test] + fn test_buffer_diff_simple() { + let diff_base = " + one + two + three + " + .unindent(); + + let buffer_text = " + one + HELLO + three + " + .unindent(); + + let mut buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(1..2, "two\n", "HELLO\n")], + ); + + buffer.edit([(0..0, "point five\n")]); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + ); + + diff.clear(&buffer); + assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]); + } + + #[test] + fn test_buffer_diff_range() { + let diff_base = " + one + two + three + four + five + six + seven + eight + nine + ten + " + .unindent(); + + let buffer_text = " + A + one + B + two + C + three + HELLO + four + five + SIXTEEN + seven + eight + WORLD + nine + + ten + + " + .unindent(); + + let buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_eq!(diff.hunks(&buffer).count(), 8); + + assert_hunks( + diff.hunks_in_row_range(7..12, &buffer), + &buffer, + &diff_base, + &[ + (6..7, "", "HELLO\n"), + (9..10, "six\n", "SIXTEEN\n"), + (12..13, "", "WORLD\n"), + ], + ); + } +} diff --git a/crates/git3/src/git.rs b/crates/git3/src/git.rs new file mode 100644 index 0000000000..b1b885eca2 --- /dev/null +++ b/crates/git3/src/git.rs @@ -0,0 +1,11 @@ +use std::ffi::OsStr; + +pub use git2 as libgit; +pub use lazy_static::lazy_static; + +pub mod diff; + +lazy_static! { + pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); + pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 474ea8364f..a7d81e5a4d 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -84,7 +84,7 @@ struct DeterministicState { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ExecutorEvent { PollRunnable { id: usize }, - EnqueuRunnable { id: usize }, + EnqueueRunnable { id: usize }, } #[cfg(any(test, feature = "test-support"))] @@ -199,7 +199,7 @@ impl Deterministic { let unparker = self.parker.lock().unparker(); let (runnable, task) = async_task::spawn_local(future, move |runnable| { let mut state = state.lock(); - state.push_to_history(ExecutorEvent::EnqueuRunnable { id }); + state.push_to_history(ExecutorEvent::EnqueueRunnable { id }); state .scheduled_from_foreground .entry(cx_id) @@ -229,7 +229,7 @@ impl Deterministic { let mut state = state.lock(); state .poll_history - .push(ExecutorEvent::EnqueuRunnable { id }); + .push(ExecutorEvent::EnqueueRunnable { id }); state .scheduled_from_background .push(BackgroundRunnable { id, runnable }); @@ -616,7 +616,7 @@ impl ExecutorEvent { pub fn id(&self) -> usize { match self { ExecutorEvent::PollRunnable { id } => *id, - ExecutorEvent::EnqueuRunnable { id } => *id, + ExecutorEvent::EnqueueRunnable { id } => *id, } } } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 638e5c6ca3..84843c9876 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -4,7 +4,7 @@ use collections::{HashMap, HashSet}; use serde::Deserialize; use std::any::{type_name, Any}; -pub trait Action: Any + Send { +pub trait Action: 'static { fn qualified_name() -> SharedString where Self: Sized; @@ -19,7 +19,7 @@ pub trait Action: Any + Send { impl Action for A where - A: for<'a> Deserialize<'a> + PartialEq + Any + Send + Clone + Default, + A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + 'static, { fn qualified_name() -> SharedString { type_name::().into() diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index a49b2aaa1c..da4d59daed 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -5,6 +5,7 @@ mod model_context; mod test_context; pub use async_context::*; +use derive_more::{Deref, DerefMut}; pub use entity_map::*; pub use model_context::*; use refineable::Refineable; @@ -13,30 +14,56 @@ use smallvec::SmallVec; pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AppMetadata, AssetSource, - ClipboardItem, Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, - KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, Pixels, Platform, Point, Render, - SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, - TextSystem, View, Window, WindowContext, WindowHandle, WindowId, + current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle, + AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, + Entity, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, LayoutId, + PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SharedString, + SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, + View, Window, WindowContext, WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; -use futures::{future::BoxFuture, Future}; +use futures::{channel::oneshot, future::LocalBoxFuture, Future}; use parking_lot::Mutex; use slotmap::SlotMap; use std::{ any::{type_name, Any, TypeId}, - borrow::Borrow, + cell::{Ref, RefCell, RefMut}, marker::PhantomData, mem, ops::{Deref, DerefMut}, - path::PathBuf, - sync::{atomic::Ordering::SeqCst, Arc, Weak}, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{atomic::Ordering::SeqCst, Arc}, time::Duration, }; use util::http::{self, HttpClient}; -pub struct App(Arc>); +/// Temporary(?) wrapper around RefCell to help us debug any double borrows. +/// Strongly consider removing after stabilization. +pub struct AppCell { + app: RefCell, +} + +impl AppCell { + pub fn borrow(&self) -> AppRef { + AppRef(self.app.borrow()) + } + + pub fn borrow_mut(&self) -> AppRefMut { + // let thread_id = std::thread::current().id(); + // dbg!("borrowed {thread_id:?}"); + AppRefMut(self.app.borrow_mut()) + } +} + +#[derive(Deref, DerefMut)] +pub struct AppRef<'a>(Ref<'a, AppContext>); + +#[derive(Deref, DerefMut)] +pub struct AppRefMut<'a>(RefMut<'a, AppContext>); + +pub struct App(Rc); /// Represents an application before it is fully launched. Once your app is /// configured, you'll start the app with `App::run`. @@ -54,13 +81,12 @@ impl App { /// app is fully launched. pub fn run(self, on_finish_launching: F) where - F: 'static + FnOnce(&mut MainThread), + F: 'static + FnOnce(&mut AppContext), { let this = self.0.clone(); - let platform = self.0.lock().platform.clone(); - platform.borrow_on_main_thread().run(Box::new(move || { - let cx = &mut *this.lock(); - let cx = unsafe { mem::transmute::<&mut AppContext, &mut MainThread>(cx) }; + let platform = self.0.borrow().platform.clone(); + platform.run(Box::new(move || { + let cx = &mut *this.borrow_mut(); on_finish_launching(cx); })); } @@ -71,16 +97,12 @@ impl App { where F: 'static + FnMut(Vec, &mut AppContext), { - let this = Arc::downgrade(&self.0); - self.0 - .lock() - .platform - .borrow_on_main_thread() - .on_open_urls(Box::new(move |urls| { - if let Some(app) = this.upgrade() { - callback(urls, &mut app.lock()); - } - })); + let this = Rc::downgrade(&self.0); + self.0.borrow().platform.on_open_urls(Box::new(move |urls| { + if let Some(app) = this.upgrade() { + callback(urls, &mut *app.borrow_mut()); + } + })); self } @@ -88,49 +110,57 @@ impl App { where F: 'static + FnMut(&mut AppContext), { - let this = Arc::downgrade(&self.0); - self.0 - .lock() - .platform - .borrow_on_main_thread() - .on_reopen(Box::new(move || { - if let Some(app) = this.upgrade() { - callback(&mut app.lock()); - } - })); + let this = Rc::downgrade(&self.0); + self.0.borrow_mut().platform.on_reopen(Box::new(move || { + if let Some(app) = this.upgrade() { + callback(&mut app.borrow_mut()); + } + })); self } pub fn metadata(&self) -> AppMetadata { - self.0.lock().app_metadata.clone() + self.0.borrow().app_metadata.clone() } - pub fn executor(&self) -> Executor { - self.0.lock().executor.clone() + pub fn background_executor(&self) -> BackgroundExecutor { + self.0.borrow().background_executor.clone() + } + + pub fn foreground_executor(&self) -> ForegroundExecutor { + self.0.borrow().foreground_executor.clone() } pub fn text_system(&self) -> Arc { - self.0.lock().text_system.clone() + self.0.borrow().text_system.clone() } } type ActionBuilder = fn(json: Option) -> anyhow::Result>; -type FrameCallback = Box; -type Handler = Box bool + Send + 'static>; -type Listener = Box bool + Send + 'static>; -type QuitHandler = Box BoxFuture<'static, ()> + Send + 'static>; -type ReleaseListener = Box; +pub(crate) type FrameCallback = Box; +type Handler = Box bool + 'static>; +type Listener = Box bool + 'static>; +type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; +type ReleaseListener = Box; + +// struct FrameConsumer { +// next_frame_callbacks: Vec, +// task: Task<()>, +// display_linker +// } pub struct AppContext { - this: Weak>, - pub(crate) platform: MainThreadOnly, + this: Weak, + pub(crate) platform: Rc, app_metadata: AppMetadata, text_system: Arc, flushing_effects: bool, pending_updates: usize, pub(crate) active_drag: Option, pub(crate) next_frame_callbacks: HashMap>, - pub(crate) executor: Executor, + pub(crate) frame_consumers: HashMap>, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, pub(crate) svg_renderer: SvgRenderer, asset_source: Arc, pub(crate) image_cache: ImageCache, @@ -140,7 +170,7 @@ pub struct AppContext { pub(crate) windows: SlotMap>, pub(crate) keymap: Arc>, pub(crate) global_action_listeners: - HashMap>>, + HashMap>>, action_builders: HashMap, pending_effects: VecDeque, pub(crate) pending_notifications: HashSet, @@ -156,11 +186,12 @@ pub struct AppContext { impl AppContext { pub(crate) fn new( - platform: Arc, + platform: Rc, asset_source: Arc, http_client: Arc, - ) -> Arc> { - let executor = platform.executor(); + ) -> Rc { + let executor = platform.background_executor(); + let foreground_executor = platform.foreground_executor(); assert!( executor.is_main_thread(), "must construct App on main thread" @@ -175,16 +206,19 @@ impl AppContext { app_version: platform.app_version().ok(), }; - Arc::new_cyclic(|this| { - Mutex::new(AppContext { + Rc::new_cyclic(|this| AppCell { + app: RefCell::new(AppContext { this: this.clone(), - text_system, - platform: MainThreadOnly::new(platform, executor.clone()), + platform, app_metadata, + text_system, flushing_effects: false, pending_updates: 0, - next_frame_callbacks: Default::default(), - executor, + active_drag: None, + next_frame_callbacks: HashMap::default(), + frame_consumers: HashMap::default(), + background_executor: executor, + foreground_executor, svg_renderer: SvgRenderer::new(asset_source.clone()), asset_source, image_cache: ImageCache::new(http_client), @@ -205,8 +239,7 @@ impl AppContext { quit_observers: SubscriberSet::new(), layout_id_buffer: Default::default(), propagate_event: true, - active_drag: None, - }) + }), }) } @@ -215,17 +248,16 @@ impl AppContext { pub fn quit(&mut self) { let mut futures = Vec::new(); - self.quit_observers.clone().retain(&(), |observer| { + for observer in self.quit_observers.remove(&()) { futures.push(observer(self)); - true - }); + } self.windows.clear(); self.flush_effects(); let futures = futures::future::join_all(futures); if self - .executor + .background_executor .block_with_timeout(Duration::from_millis(100), futures) .is_err() { @@ -244,7 +276,6 @@ 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); @@ -257,44 +288,91 @@ impl AppContext { result } - pub(crate) fn read_window( - &mut self, - id: WindowId, - read: impl FnOnce(&WindowContext) -> R, - ) -> Result { - let window = self - .windows - .get(id) - .ok_or_else(|| anyhow!("window not found"))? - .as_ref() - .unwrap(); - Ok(read(&WindowContext::immutable(self, &window))) + pub fn windows(&self) -> Vec { + self.windows + .values() + .filter_map(|window| Some(window.as_ref()?.handle.clone())) + .collect() } - pub(crate) fn update_window( + /// Opens a new window with the given option and the root view returned by the given function. + /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific + /// functionality. + pub fn open_window( &mut self, - id: WindowId, - update: impl FnOnce(&mut WindowContext) -> R, - ) -> Result { + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut WindowContext) -> View, + ) -> WindowHandle { self.update(|cx| { - let mut window = cx - .windows - .get_mut(id) - .ok_or_else(|| anyhow!("window not found"))? - .take() - .unwrap(); - - let result = update(&mut WindowContext::mutable(cx, &mut window)); - - cx.windows - .get_mut(id) - .ok_or_else(|| anyhow!("window not found"))? - .replace(window); - - Ok(result) + let id = cx.windows.insert(None); + let handle = WindowHandle::new(id); + let mut window = Window::new(handle.into(), options, cx); + let root_view = build_root_view(&mut WindowContext::new(cx, &mut window)); + window.root_view.replace(root_view.into()); + cx.windows.get_mut(id).unwrap().replace(window); + handle }) } + /// Instructs the platform to activate the application by bringing it to the foreground. + pub fn activate(&self, ignoring_other_apps: bool) { + self.platform.activate(ignoring_other_apps); + } + + /// Returns the list of currently active displays. + pub fn displays(&self) -> Vec> { + self.platform.displays() + } + + /// Writes data to the platform clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item) + } + + /// Reads data from the platform clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Writes credentials to the platform keychain. + pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { + self.platform.write_credentials(url, username, password) + } + + /// Reads credentials from the platform keychain. + pub fn read_credentials(&self, url: &str) -> Result)>> { + self.platform.read_credentials(url) + } + + /// Deletes credentials from the platform keychain. + pub fn delete_credentials(&self, url: &str) -> Result<()> { + self.platform.delete_credentials(url) + } + + /// Directs the platform's default browser to open the given URL. + pub fn open_url(&self, url: &str) { + self.platform.open_url(url); + } + + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.platform.path_for_auxiliary_executable(name) + } + + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + self.platform.prompt_for_paths(options) + } + + pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + self.platform.prompt_for_new_path(directory) + } + + pub fn reveal_path(&self, path: &Path) { + self.platform.reveal_path(path) + } + pub(crate) fn push_effect(&mut self, effect: Effect) { match &effect { Effect::Notify { emitter } => { @@ -326,8 +404,11 @@ impl AppContext { self.apply_notify_effect(emitter); } Effect::Emit { emitter, event } => self.apply_emit_effect(emitter, event), - Effect::FocusChanged { window_id, focused } => { - self.apply_focus_changed_effect(window_id, focused); + Effect::FocusChanged { + window_handle, + focused, + } => { + self.apply_focus_changed_effect(window_handle, focused); } Effect::Refresh => { self.apply_refresh_effect(); @@ -347,18 +428,18 @@ impl AppContext { let dirty_window_ids = self .windows .iter() - .filter_map(|(window_id, window)| { + .filter_map(|(_, window)| { let window = window.as_ref().unwrap(); if window.dirty { - Some(window_id) + Some(window.handle.clone()) } else { None } }) .collect::>(); - for dirty_window_id in dirty_window_ids { - self.update_window(dirty_window_id, |cx| cx.draw()).unwrap(); + for dirty_window_handle in dirty_window_ids { + dirty_window_handle.update(self, |_, cx| cx.draw()).unwrap(); } } @@ -375,8 +456,8 @@ impl AppContext { for (entity_id, mut entity) in dropped { self.observers.remove(&entity_id); self.event_listeners.remove(&entity_id); - for mut release_callback in self.release_listeners.remove(&entity_id) { - release_callback(&mut entity, self); + for release_callback in self.release_listeners.remove(&entity_id) { + release_callback(entity.as_mut(), self); } } } @@ -386,27 +467,27 @@ impl AppContext { /// For now, we simply blur the window if this happens, but we may want to support invoking /// a window blur handler to restore focus to some logical element. fn release_dropped_focus_handles(&mut self) { - let window_ids = self.windows.keys().collect::>(); - for window_id in window_ids { - self.update_window(window_id, |cx| { - let mut blur_window = false; - let focus = cx.window.focus; - cx.window.focus_handles.write().retain(|handle_id, count| { - if count.load(SeqCst) == 0 { - if focus == Some(handle_id) { - blur_window = true; + for window_handle in self.windows() { + window_handle + .update(self, |_, cx| { + let mut blur_window = false; + let focus = cx.window.focus; + cx.window.focus_handles.write().retain(|handle_id, count| { + if count.load(SeqCst) == 0 { + if focus == Some(handle_id) { + blur_window = true; + } + false + } else { + true } - false - } else { - true - } - }); + }); - if blur_window { - cx.blur(); - } - }) - .unwrap(); + if blur_window { + cx.blur(); + } + }) + .unwrap(); } } @@ -423,30 +504,35 @@ impl AppContext { .retain(&emitter, |handler| handler(event.as_ref(), self)); } - fn apply_focus_changed_effect(&mut self, window_id: WindowId, focused: Option) { - self.update_window(window_id, |cx| { - if cx.window.focus == focused { - let mut listeners = mem::take(&mut cx.window.focus_listeners); - let focused = - focused.map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); - let blurred = cx - .window - .last_blur - .take() - .unwrap() - .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); - if focused.is_some() || blurred.is_some() { - let event = FocusEvent { focused, blurred }; - for listener in &listeners { - listener(&event, cx); + fn apply_focus_changed_effect( + &mut self, + window_handle: AnyWindowHandle, + focused: Option, + ) { + window_handle + .update(self, |_, cx| { + if cx.window.focus == focused { + let mut listeners = mem::take(&mut cx.window.focus_listeners); + let focused = focused + .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); + let blurred = cx + .window + .last_blur + .take() + .unwrap() + .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); + if focused.is_some() || blurred.is_some() { + let event = FocusEvent { focused, blurred }; + for listener in &listeners { + listener(&event, cx); + } } - } - listeners.extend(cx.window.focus_listeners.drain(..)); - cx.window.focus_listeners = listeners; - } - }) - .ok(); + listeners.extend(cx.window.focus_listeners.drain(..)); + cx.window.focus_listeners = listeners; + } + }) + .ok(); } fn apply_refresh_effect(&mut self) { @@ -464,7 +550,7 @@ impl AppContext { .retain(&type_id, |observer| observer(self)); } - fn apply_defer_effect(&mut self, callback: Box) { + fn apply_defer_effect(&mut self, callback: Box) { callback(self); } @@ -473,72 +559,34 @@ impl AppContext { pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext { app: unsafe { mem::transmute(self.this.clone()) }, - executor: self.executor.clone(), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), } } /// Obtains a reference to the executor, which can be used to spawn futures. - pub fn executor(&self) -> &Executor { - &self.executor + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor } - /// Runs the given closure on the main thread, where interaction with the platform - /// is possible. The given closure will be invoked with a `MainThread`, which - /// has platform-specific methods that aren't present on `AppContext`. - pub fn run_on_main( - &mut self, - f: impl FnOnce(&mut MainThread) -> R + Send + 'static, - ) -> Task - where - R: Send + 'static, - { - if self.executor.is_main_thread() { - Task::ready(f(unsafe { - mem::transmute::<&mut AppContext, &mut MainThread>(self) - })) - } else { - let this = self.this.upgrade().unwrap(); - self.executor.run_on_main(move || { - let cx = &mut *this.lock(); - cx.update(|cx| f(unsafe { mem::transmute::<&mut Self, &mut MainThread>(cx) })) - }) - } - } - - /// Spawns the future returned by the given function on the main thread, where interaction with - /// the platform is possible. The given closure will be invoked with a `MainThread`, - /// which has platform-specific methods that aren't present on `AsyncAppContext`. The future will be - /// polled exclusively on the main thread. - // todo!("I think we need somehow to prevent the MainThread from implementing Send") - pub fn spawn_on_main( - &self, - f: impl FnOnce(MainThread) -> F + Send + 'static, - ) -> Task - where - F: Future + 'static, - R: Send + 'static, - { - let cx = self.to_async(); - self.executor.spawn_on_main(move || f(MainThread(cx))) + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor } /// Spawns the future returned by the given function on the thread pool. The closure will be invoked /// with AsyncAppContext, which allows the application state to be accessed across await points. - pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where - Fut: Future + Send + 'static, - R: Send + 'static, + Fut: Future + 'static, + R: 'static, { - let cx = self.to_async(); - self.executor.spawn(async move { - let future = f(cx); - future.await - }) + self.foreground_executor.spawn(f(self.to_async())) } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. - pub fn defer(&mut self, f: impl FnOnce(&mut AppContext) + 'static + Send) { + pub fn defer(&mut self, f: impl FnOnce(&mut AppContext) + 'static) { self.push_effect(Effect::Defer { callback: Box::new(f), }); @@ -597,7 +645,7 @@ impl AppContext { /// Access the global of the given type mutably. A default value is assigned if a global of this type has not /// yet been assigned. - pub fn default_global(&mut self) -> &mut G { + pub fn default_global(&mut self) -> &mut G { let global_type = TypeId::of::(); self.push_effect(Effect::NotifyGlobalObservers { global_type }); self.globals_by_type @@ -608,7 +656,7 @@ impl AppContext { } /// Set the value of the global of the given type. - pub fn set_global(&mut self, global: G) { + pub fn set_global(&mut self, global: G) { let global_type = TypeId::of::(); self.push_effect(Effect::NotifyGlobalObservers { global_type }); self.globals_by_type.insert(global_type, Box::new(global)); @@ -626,7 +674,7 @@ impl AppContext { /// Register a callback to be invoked when a global of the given type is updated. pub fn observe_global( &mut self, - mut f: impl FnMut(&mut Self) + Send + 'static, + mut f: impl FnMut(&mut Self) + 'static, ) -> Subscription { self.global_observers.insert( TypeId::of::(), @@ -658,6 +706,24 @@ impl AppContext { self.globals_by_type.insert(global_type, lease.global); } + pub fn observe_release( + &mut self, + handle: &E, + on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, + ) -> Subscription + where + E: Entity, + T: 'static, + { + self.release_listeners.insert( + handle.entity_id(), + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + on_release(entity, cx) + }), + ) + } + pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) { self.text_style_stack.push(text_style); } @@ -673,7 +739,7 @@ impl AppContext { } /// Register a global listener for actions invoked via the keyboard. - pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + Send + 'static) { + pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { self.global_action_listeners .entry(TypeId::of::()) .or_default() @@ -711,19 +777,18 @@ impl AppContext { } impl Context for AppContext { - type ModelContext<'a, T> = ModelContext<'a, T>; type Result = T; /// Build an entity that is owned by the application. The given function will be invoked with /// a `ModelContext` and must return an object representing the entity. A `Model` will be returned /// which can be used to access the entity in a context. - fn build_model( + fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Model { self.update(|cx| { let slot = cx.entities.reserve(); - let entity = build_model(&mut ModelContext::mutable(cx, slot.downgrade())); + let entity = build_model(&mut ModelContext::new(cx, slot.downgrade())); cx.entities.insert(slot, entity) }) } @@ -733,117 +798,39 @@ impl Context for AppContext { fn update_model( &mut self, model: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> R { self.update(|cx| { let mut entity = cx.entities.lease(model); - let result = update( - &mut entity, - &mut ModelContext::mutable(cx, model.downgrade()), - ); + let result = update(&mut entity, &mut ModelContext::new(cx, model.downgrade())); cx.entities.end_lease(entity); result }) } -} -impl MainThread -where - C: Borrow, -{ - pub(crate) fn platform(&self) -> &dyn Platform { - self.0.borrow().platform.borrow_on_main_thread() - } - - /// Instructs the platform to activate the application by bringing it to the foreground. - pub fn activate(&self, ignoring_other_apps: bool) { - self.platform().activate(ignoring_other_apps); - } - - /// Writes data to the platform clipboard. - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.platform().write_to_clipboard(item) - } - - /// Reads data from the platform clipboard. - pub fn read_from_clipboard(&self) -> Option { - self.platform().read_from_clipboard() - } - - /// Writes credentials to the platform keychain. - pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { - self.platform().write_credentials(url, username, password) - } - - /// Reads credentials from the platform keychain. - pub fn read_credentials(&self, url: &str) -> Result)>> { - self.platform().read_credentials(url) - } - - /// Deletes credentials from the platform keychain. - pub fn delete_credentials(&self, url: &str) -> Result<()> { - self.platform().delete_credentials(url) - } - - /// Directs the platform's default browser to open the given URL. - pub fn open_url(&self, url: &str) { - self.platform().open_url(url); - } - - pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { - self.platform().path_for_auxiliary_executable(name) - } -} - -impl MainThread { - fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { - self.0.update(|cx| { - update(unsafe { - std::mem::transmute::<&mut AppContext, &mut MainThread>(cx) - }) - }) - } - - pub(crate) fn update_window( - &mut self, - id: WindowId, - update: impl FnOnce(&mut MainThread) -> R, - ) -> Result { - self.0.update_window(id, |cx| { - update(unsafe { - std::mem::transmute::<&mut WindowContext, &mut MainThread>(cx) - }) - }) - } - - /// Opens a new window with the given option and the root view returned by the given function. - /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific - /// functionality. - pub fn open_window( - &mut self, - options: crate::WindowOptions, - build_root_view: impl FnOnce(&mut WindowContext) -> View + Send + 'static, - ) -> WindowHandle { + fn update_window(&mut self, handle: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { self.update(|cx| { - let id = cx.windows.insert(None); - let handle = WindowHandle::new(id); - let mut window = Window::new(handle.into(), options, cx); - let root_view = build_root_view(&mut WindowContext::mutable(cx, &mut window)); - window.root_view.replace(root_view.into()); - cx.windows.get_mut(id).unwrap().replace(window); - handle - }) - } + let mut window = cx + .windows + .get_mut(handle.id) + .ok_or_else(|| anyhow!("window not found"))? + .take() + .unwrap(); - /// Update the global of the given type with a closure. Unlike `global_mut`, this method provides - /// your closure with mutable access to the `MainThread` and the global simultaneously. - pub fn update_global( - &mut self, - update: impl FnOnce(&mut G, &mut MainThread) -> R, - ) -> R { - self.0.update_global(|global, cx| { - let cx = unsafe { mem::transmute::<&mut AppContext, &mut MainThread>(cx) }; - update(global, cx) + let root_view = window.root_view.clone().unwrap(); + let result = update(root_view, &mut WindowContext::new(cx, &mut window)); + + if !window.removed { + cx.windows + .get_mut(handle.id) + .ok_or_else(|| anyhow!("window not found"))? + .replace(window); + } + + Ok(result) }) } } @@ -855,10 +842,10 @@ pub(crate) enum Effect { }, Emit { emitter: EntityId, - event: Box, + event: Box, }, FocusChanged { - window_id: WindowId, + window_handle: AnyWindowHandle, focused: Option, }, Refresh, @@ -866,7 +853,7 @@ pub(crate) enum Effect { global_type: TypeId, }, Defer { - callback: Box, + callback: Box, }, } @@ -905,15 +892,3 @@ pub(crate) struct AnyDrag { pub view: AnyView, pub cursor_offset: Point, } - -#[cfg(test)] -mod tests { - use super::AppContext; - - #[test] - fn test_app_context_send_sync() { - // This will not compile if `AppContext` does not implement `Send` - fn assert_send() {} - assert_send::(); - } -} diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 042a75848e..e3ae78d78f 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -1,48 +1,57 @@ use crate::{ - AnyWindowHandle, AppContext, Context, Executor, MainThread, Model, ModelContext, Result, Task, - WindowContext, + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor, + Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext, + WindowHandle, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context as _}; use derive_more::{Deref, DerefMut}; -use parking_lot::Mutex; -use std::{future::Future, sync::Weak}; +use std::{future::Future, rc::Weak}; #[derive(Clone)] pub struct AsyncAppContext { - pub(crate) app: Weak>, - pub(crate) executor: Executor, + pub(crate) app: Weak, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, } impl Context for AsyncAppContext { - type ModelContext<'a, T> = ModelContext<'a, T>; type Result = Result; fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Self::Result> where - T: 'static + Send, + T: 'static, { let app = self .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Need this to compile - Ok(lock.build_model(build_model)) + let mut app = app.borrow_mut(); + Ok(app.build_model(build_model)) } fn update_model( &mut self, handle: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Self::Result { let app = self .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Need this to compile - Ok(lock.update_model(handle, update)) + let mut app = app.borrow_mut(); + Ok(app.update_model(handle, update)) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.borrow_mut(); + lock.update_window(window, f) } } @@ -52,13 +61,17 @@ impl AsyncAppContext { .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Need this to compile + let mut lock = app.borrow_mut(); lock.refresh(); Ok(()) } - pub fn executor(&self) -> &Executor { - &self.executor + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor } pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result { @@ -66,70 +79,32 @@ impl AsyncAppContext { .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); + let mut lock = app.borrow_mut(); Ok(f(&mut *lock)) } - pub fn read_window( + pub fn open_window( &self, - handle: AnyWindowHandle, - update: impl FnOnce(&WindowContext) -> R, - ) -> Result { - let app = self - .app - .upgrade() - .ok_or_else(|| anyhow!("app was released"))?; - let mut app_context = app.lock(); - app_context.read_window(handle.id, update) - } - - pub fn update_window( - &self, - handle: AnyWindowHandle, - update: impl FnOnce(&mut WindowContext) -> R, - ) -> Result { - let app = self - .app - .upgrade() - .ok_or_else(|| anyhow!("app was released"))?; - let mut app_context = app.lock(); - app_context.update_window(handle.id, update) - } - - pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut WindowContext) -> View, + ) -> Result> where - Fut: Future + Send + 'static, - R: Send + 'static, + V: Render, { - let this = self.clone(); - self.executor.spawn(async move { f(this).await }) + let app = self + .app + .upgrade() + .ok_or_else(|| anyhow!("app was released"))?; + let mut lock = app.borrow_mut(); + Ok(lock.open_window(options, build_root_view)) } - pub fn spawn_on_main( - &self, - f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static, - ) -> Task + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, - R: Send + 'static, + R: 'static, { - let this = self.clone(); - self.executor.spawn_on_main(|| f(this)) - } - - pub fn run_on_main( - &self, - f: impl FnOnce(&mut MainThread) -> R + Send + 'static, - ) -> Result> - where - R: Send + 'static, - { - let app = self - .app - .upgrade() - .ok_or_else(|| anyhow!("app was released"))?; - let mut app_context = app.lock(); - Ok(app_context.run_on_main(f)) + self.foreground_executor.spawn(f(self.clone())) } pub fn has_global(&self) -> Result { @@ -137,8 +112,8 @@ impl AsyncAppContext { .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let lock = app.lock(); // Need this to compile - Ok(lock.has_global::()) + let app = app.borrow_mut(); + Ok(app.has_global::()) } pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result { @@ -146,8 +121,8 @@ impl AsyncAppContext { .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let lock = app.lock(); // Need this to compile - Ok(read(lock.global(), &lock)) + let app = app.borrow_mut(); + Ok(read(app.global(), &app)) } pub fn try_read_global( @@ -155,8 +130,8 @@ impl AsyncAppContext { read: impl FnOnce(&G, &AppContext) -> R, ) -> Option { let app = self.app.upgrade()?; - let lock = app.lock(); // Need this to compile - Some(read(lock.try_global()?, &lock)) + let app = app.borrow_mut(); + Some(read(app.try_global()?, &app)) } pub fn update_global( @@ -167,8 +142,8 @@ impl AsyncAppContext { .app .upgrade() .ok_or_else(|| anyhow!("app was released"))?; - let mut lock = app.lock(); // Need this to compile - Ok(lock.update_global(update)) + let mut app = app.borrow_mut(); + Ok(app.update_global(update)) } } @@ -185,22 +160,22 @@ impl AsyncWindowContext { Self { app, window } } - pub fn update(&self, update: impl FnOnce(&mut WindowContext) -> R) -> Result { + pub fn update( + &mut self, + update: impl FnOnce(AnyView, &mut WindowContext) -> R, + ) -> Result { self.app.update_window(self.window, update) } - pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + Send + 'static) { - self.app - .update_window(self.window, |cx| cx.on_next_frame(f)) - .ok(); + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { + self.window.update(self, |_, cx| cx.on_next_frame(f)).ok(); } pub fn read_global( - &self, + &mut self, read: impl FnOnce(&G, &WindowContext) -> R, ) -> Result { - self.app - .read_window(self.window, |cx| read(cx.global(), cx)) + self.window.update(self, |_, cx| read(cx.global(), cx)) } pub fn update_global( @@ -210,43 +185,78 @@ impl AsyncWindowContext { where G: 'static, { - self.app - .update_window(self.window, |cx| cx.update_global(update)) + self.window.update(self, |_, cx| cx.update_global(update)) + } + + pub fn spawn(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.clone())) } } impl Context for AsyncWindowContext { - type ModelContext<'a, T> = ModelContext<'a, T>; type Result = Result; fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Result> where - T: 'static + Send, + T: 'static, { - self.app - .update_window(self.window, |cx| cx.build_model(build_model)) + self.window + .update(self, |_, cx| cx.build_model(build_model)) } fn update_model( &mut self, handle: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Result { - self.app - .update_window(self.window, |cx| cx.update_model(handle, update)) + self.window + .update(self, |_, cx| cx.update_model(handle, update)) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.app.update_window(window, update) } } -#[cfg(test)] -mod tests { - use super::*; +impl VisualContext for AsyncWindowContext { + fn build_view( + &mut self, + build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static, + { + self.window + .update(self, |_, cx| cx.build_view(build_view_state)) + } - #[test] - fn test_async_app_context_send_sync() { - fn assert_send_sync() {} - assert_send_sync::(); + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.window + .update(self, |_, cx| cx.update_view(view, update)) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: Render, + { + self.window + .update(self, |_, cx| cx.replace_root_view(build_view)) } } diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index bbeabd3e4f..e626f8c409 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -1,4 +1,4 @@ -use crate::{private::Sealed, AnyBox, AppContext, Context, Entity}; +use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; @@ -59,7 +59,7 @@ impl EntityMap { /// Insert an entity into a slot obtained by calling `reserve`. pub fn insert(&mut self, slot: Slot, entity: T) -> Model where - T: 'static + Send, + T: 'static, { let model = slot.0; self.entities.insert(model.entity_id, Box::new(entity)); @@ -106,7 +106,12 @@ impl EntityMap { dropped_entity_ids .into_iter() .map(|entity_id| { - ref_counts.counts.remove(entity_id); + let count = ref_counts.counts.remove(entity_id).unwrap(); + debug_assert_eq!( + count.load(SeqCst), + 0, + "dropped an entity that was referenced" + ); (entity_id, self.entities.remove(entity_id).unwrap()) }) .collect() @@ -164,6 +169,10 @@ impl AnyModel { self.entity_id } + pub fn entity_type(&self) -> TypeId { + self.entity_type + } + pub fn downgrade(&self) -> AnyWeakModel { AnyWeakModel { entity_id: self.entity_id, @@ -211,7 +220,7 @@ impl Drop for AnyModel { let count = entity_map .counts .get(self.entity_id) - .expect("Detected over-release of a model."); + .expect("detected over-release of a handle."); let prev_count = count.fetch_sub(1, SeqCst); assert_ne!(prev_count, 0, "Detected over-release of a model."); if prev_count == 1 { @@ -324,7 +333,7 @@ impl Model { pub fn update( &self, cx: &mut C, - update: impl FnOnce(&mut T, &mut C::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> C::Result where C: Context, @@ -395,12 +404,16 @@ impl AnyWeakModel { } pub fn upgrade(&self) -> Option { - let entity_map = self.entity_ref_counts.upgrade()?; - entity_map - .read() - .counts - .get(self.entity_id)? - .fetch_add(1, SeqCst); + let ref_counts = &self.entity_ref_counts.upgrade()?; + let ref_counts = ref_counts.read(); + let ref_count = ref_counts.counts.get(self.entity_id)?; + + // entity_id is in dropped_entity_ids + if ref_count.load(SeqCst) == 0 { + return None; + } + ref_count.fetch_add(1, SeqCst); + Some(AnyModel { entity_id: self.entity_id, entity_type: self.entity_type, @@ -466,7 +479,7 @@ impl WeakModel { pub fn update( &self, cx: &mut C, - update: impl FnOnce(&mut T, &mut C::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Result where C: Context, @@ -499,3 +512,60 @@ impl PartialEq> for WeakModel { self.entity_id() == other.any_model.entity_id() } } + +#[cfg(test)] +mod test { + use crate::EntityMap; + + struct TestEntity { + pub i: i32, + } + + #[test] + fn test_entity_map_slot_assignment_before_cleanup() { + // Tests that slots are not re-used before take_dropped. + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 1 }); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 2 }); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 2); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1, 2], + ); + } + + #[test] + fn test_entity_map_weak_upgrade_before_cleanup() { + // Tests that weak handles are not upgraded before take_dropped + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let handle = entity_map.insert(slot, TestEntity { i: 1 }); + let weak = handle.downgrade(); + drop(handle); + + let strong = weak.upgrade(); + assert_eq!(strong, None); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 1); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1], + ); + } +} diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 8a4576c052..cb25adfb63 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -1,7 +1,8 @@ use crate::{ - AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, EventEmitter, MainThread, - Model, Reference, Subscription, Task, WeakModel, + AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, + EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, }; +use anyhow::Result; use derive_more::{Deref, DerefMut}; use futures::FutureExt; use std::{ @@ -14,16 +15,13 @@ use std::{ pub struct ModelContext<'a, T> { #[deref] #[deref_mut] - app: Reference<'a, AppContext>, + app: &'a mut AppContext, model_state: WeakModel, } impl<'a, T: 'static> ModelContext<'a, T> { - pub(crate) fn mutable(app: &'a mut AppContext, model_state: WeakModel) -> Self { - Self { - app: Reference::Mutable(app), - model_state, - } + pub(crate) fn new(app: &'a mut AppContext, model_state: WeakModel) -> Self { + Self { app, model_state } } pub fn entity_id(&self) -> EntityId { @@ -40,15 +38,15 @@ impl<'a, T: 'static> ModelContext<'a, T> { self.model_state.clone() } - pub fn observe( + pub fn observe( &mut self, entity: &E, - mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + Send + 'static, + mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + 'static, ) -> Subscription where - T: 'static + Send, - T2: 'static, - E: Entity, + T: 'static, + W: 'static, + E: Entity, { let this = self.weak_model(); let entity_id = entity.entity_id(); @@ -69,10 +67,10 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + Send + 'static, + mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + 'static, ) -> Subscription where - T: 'static + Send, + T: 'static, T2: 'static + EventEmitter, E: Entity, { @@ -95,7 +93,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn on_release( &mut self, - mut on_release: impl FnMut(&mut T, &mut AppContext) + Send + 'static, + on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, ) -> Subscription where T: 'static, @@ -112,10 +110,10 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn observe_release( &mut self, entity: &E, - mut on_release: impl FnMut(&mut T, &mut T2, &mut ModelContext<'_, T>) + Send + 'static, + on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static, ) -> Subscription where - T: Any + Send, + T: Any, T2: 'static, E: Entity, { @@ -134,10 +132,10 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn observe_global( &mut self, - mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + Send + 'static, + mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static, ) -> Subscription where - T: 'static + Send, + T: 'static, { let handle = self.weak_model(); self.global_observers.insert( @@ -148,11 +146,11 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn on_app_quit( &mut self, - mut on_quit: impl FnMut(&mut T, &mut ModelContext) -> Fut + Send + 'static, + mut on_quit: impl FnMut(&mut T, &mut ModelContext) -> Fut + 'static, ) -> Subscription where - Fut: 'static + Future + Send, - T: 'static + Send, + Fut: 'static + Future, + T: 'static, { let handle = self.weak_model(); self.app.quit_observers.insert( @@ -164,7 +162,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { future.await; } } - .boxed() + .boxed_local() }), ) } @@ -183,7 +181,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where - G: 'static + Send, + G: 'static, { let mut global = self.app.lease_global::(); let result = f(&mut global, self); @@ -191,36 +189,20 @@ impl<'a, T: 'static> ModelContext<'a, T> { result } - pub fn spawn( - &self, - f: impl FnOnce(WeakModel, AsyncAppContext) -> Fut + Send + 'static, - ) -> Task + pub fn spawn(&self, f: impl FnOnce(WeakModel, AsyncAppContext) -> Fut) -> Task where T: 'static, - Fut: Future + Send + 'static, - R: Send + 'static, + Fut: Future + 'static, + R: 'static, { let this = self.weak_model(); self.app.spawn(|cx| f(this, cx)) } - - pub fn spawn_on_main( - &self, - f: impl FnOnce(WeakModel, MainThread) -> Fut + Send + 'static, - ) -> Task - where - Fut: Future + 'static, - R: Send + 'static, - { - let this = self.weak_model(); - self.app.spawn_on_main(|cx| f(this, cx)) - } } impl<'a, T> ModelContext<'a, T> where T: EventEmitter, - T::Event: Send, { pub fn emit(&mut self, event: T::Event) { self.app.pending_effects.push_back(Effect::Emit { @@ -231,26 +213,29 @@ where } impl<'a, T> Context for ModelContext<'a, T> { - type ModelContext<'b, U> = ModelContext<'b, U>; type Result = U; - fn build_model( + fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, U>) -> U, - ) -> Model - where - U: 'static + Send, - { + build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U, + ) -> Model { self.app.build_model(build_model) } fn update_model( &mut self, handle: &Model, - update: impl FnOnce(&mut U, &mut Self::ModelContext<'_, U>) -> R, + update: impl FnOnce(&mut U, &mut ModelContext<'_, U>) -> R, ) -> R { self.app.update_model(handle, update) } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> R, + { + self.app.update_window(window, update) + } } impl Borrow for ModelContext<'_, T> { diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2b09a95a34..e731dccc6e 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,137 +1,115 @@ use crate::{ - AnyWindowHandle, AppContext, AsyncAppContext, Context, Executor, MainThread, Model, - ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext, + AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, + EventEmitter, ForegroundExecutor, Model, ModelContext, Result, Task, TestDispatcher, + TestPlatform, WindowContext, }; -use parking_lot::Mutex; -use std::{future::Future, sync::Arc}; +use anyhow::{anyhow, bail}; +use futures::{Stream, StreamExt}; +use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { - pub app: Arc>, - pub executor: Executor, + pub app: Rc, + pub background_executor: BackgroundExecutor, + pub foreground_executor: ForegroundExecutor, } impl Context for TestAppContext { - type ModelContext<'a, T> = ModelContext<'a, T>; type Result = T; fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Self::Result> where - T: 'static + Send, + T: 'static, { - let mut lock = self.app.lock(); - lock.build_model(build_model) + let mut app = self.app.borrow_mut(); + app.build_model(build_model) } fn update_model( &mut self, handle: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Self::Result { - let mut lock = self.app.lock(); - lock.update_model(handle, update) + let mut app = self.app.borrow_mut(); + app.update_model(handle, update) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) } } impl TestAppContext { pub fn new(dispatcher: TestDispatcher) -> Self { - let executor = Executor::new(Arc::new(dispatcher)); - let platform = Arc::new(TestPlatform::new(executor.clone())); + let dispatcher = Arc::new(dispatcher); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let platform = Rc::new(TestPlatform::new( + background_executor.clone(), + foreground_executor.clone(), + )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); Self { app: AppContext::new(platform, asset_source, http_client), - executor, + background_executor, + foreground_executor, } } pub fn quit(&self) { - self.app.lock().quit(); + self.app.borrow_mut().quit(); } pub fn refresh(&mut self) -> Result<()> { - let mut lock = self.app.lock(); - lock.refresh(); + let mut app = self.app.borrow_mut(); + app.refresh(); Ok(()) } - pub fn executor(&self) -> &Executor { - &self.executor + pub fn executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor } pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> R { - let mut lock = self.app.lock(); - f(&mut *lock) + let mut cx = self.app.borrow_mut(); + cx.update(f) } - pub fn read_window( - &self, - handle: AnyWindowHandle, - read: impl FnOnce(&WindowContext) -> R, - ) -> R { - let mut app_context = self.app.lock(); - app_context.read_window(handle.id, read).unwrap() - } - - pub fn update_window( - &self, - handle: AnyWindowHandle, - update: impl FnOnce(&mut WindowContext) -> R, - ) -> R { - let mut app = self.app.lock(); - app.update_window(handle.id, update).unwrap() - } - - pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task - where - Fut: Future + Send + 'static, - R: Send + 'static, - { - let cx = self.to_async(); - self.executor.spawn(async move { f(cx).await }) - } - - pub fn spawn_on_main( - &self, - f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static, - ) -> Task + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, - R: Send + 'static, + R: 'static, { - let cx = self.to_async(); - self.executor.spawn_on_main(|| f(cx)) - } - - pub fn run_on_main( - &self, - f: impl FnOnce(&mut MainThread) -> R + Send + 'static, - ) -> Task - where - R: Send + 'static, - { - let mut app_context = self.app.lock(); - app_context.run_on_main(f) + self.foreground_executor.spawn(f(self.to_async())) } pub fn has_global(&self) -> bool { - let lock = self.app.lock(); - lock.has_global::() + let app = self.app.borrow(); + app.has_global::() } pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R { - let lock = self.app.lock(); - read(lock.global(), &lock) + let app = self.app.borrow(); + read(app.global(), &app) } pub fn try_read_global( &self, read: impl FnOnce(&G, &AppContext) -> R, ) -> Option { - let lock = self.app.lock(); + let lock = self.app.borrow(); Some(read(lock.try_global()?, &lock)) } @@ -139,14 +117,75 @@ impl TestAppContext { &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, ) -> R { - let mut lock = self.app.lock(); + let mut lock = self.app.borrow_mut(); lock.update_global(update) } pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext { - app: Arc::downgrade(&self.app), - executor: self.executor.clone(), + app: Rc::downgrade(&self.app), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), } } + + pub fn notifications(&mut self, entity: &Model) -> impl Stream { + let (tx, rx) = futures::channel::mpsc::unbounded(); + + entity.update(self, move |_, cx: &mut ModelContext| { + cx.observe(entity, { + let tx = tx.clone(); + move |_, _, _| { + let _ = tx.unbounded_send(()); + } + }) + .detach(); + + cx.on_release(move |_, _| tx.close_channel()).detach(); + }); + + rx + } + + pub fn events( + &mut self, + entity: &Model, + ) -> futures::channel::mpsc::UnboundedReceiver + where + T::Event: 'static + Clone, + { + let (tx, rx) = futures::channel::mpsc::unbounded(); + entity + .update(self, |_, cx: &mut ModelContext| { + cx.subscribe(entity, move |_model, _handle, event, _cx| { + let _ = tx.unbounded_send(event.clone()); + }) + }) + .detach(); + rx + } + + pub async fn condition( + &mut self, + model: &Model, + mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool, + ) { + let timer = self.executor().timer(Duration::from_secs(3)); + let mut notifications = self.notifications(model); + + use futures::FutureExt as _; + use smol::future::FutureExt as _; + + async { + while notifications.next().await.is_some() { + if model.update(self, &mut predicate) { + return Ok(()); + } + } + bail!("model dropped") + } + .race(timer.map(|_| Err(anyhow!("condition timed out")))) + .await + .unwrap(); + } } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index a715ed30ee..a92dbd6ff9 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -4,7 +4,7 @@ pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; pub trait Element { - type ElementState: 'static + Send; + type ElementState: 'static; fn id(&self) -> Option; @@ -97,7 +97,7 @@ impl> RenderedElement { impl ElementObject for RenderedElement where E: Element, - E::ElementState: 'static + Send, + E::ElementState: 'static, { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { let frame_state = if let Some(id) = self.element.id() { @@ -170,16 +170,14 @@ where } } -pub struct AnyElement(Box + Send>); - -unsafe impl Send for AnyElement {} +pub struct AnyElement(Box>); impl AnyElement { pub fn new(element: E) -> Self where V: 'static, - E: 'static + Element + Send, - E::ElementState: Any + Send, + E: 'static + Element, + E::ElementState: Any, { AnyElement(Box::new(RenderedElement::new(element))) } @@ -200,14 +198,19 @@ impl AnyElement { pub trait Component { fn render(self) -> AnyElement; - fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + U: Component, + { + f(self) + } + + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self where Self: Sized, { - if condition { - self = then(self); - } - self + self.map(|this| if condition { then(this) } else { this }) } } @@ -220,8 +223,8 @@ impl Component for AnyElement { impl Element for Option where V: 'static, - E: 'static + Component + Send, - F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static, + E: 'static + Component, + F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, { type ElementState = AnyElement; @@ -264,8 +267,8 @@ where impl Component for Option where V: 'static, - E: 'static + Component + Send, - F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static, + E: 'static + Component, + F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -275,8 +278,8 @@ where impl Component for F where V: 'static, - E: 'static + Component + Send, - F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static, + E: 'static + Component, + F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, { fn render(self) -> AnyElement { AnyElement::new(Some(self)) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 6fe10d94a3..56940efce4 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -305,7 +305,6 @@ where impl Component for Div where - // V: Any + Send + Sync, I: ElementInteraction, F: ElementFocus, { diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 747e573ea5..a35436d74e 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -109,7 +109,9 @@ where let corner_radii = style.corner_radii; if let Some(uri) = self.uri.clone() { - let image_future = cx.image_cache.get(uri); + // eprintln!(">>> image_cache.get({uri}"); + let image_future = cx.image_cache.get(uri.clone()); + // eprintln!("<<< image_cache.get({uri}"); if let Some(data) = image_future .clone() .now_or_never() diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 3aff568c4c..4bc3705490 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -44,9 +44,6 @@ pub struct Text { state_type: PhantomData, } -unsafe impl Send for Text {} -unsafe impl Sync for Text {} - impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index b2ba710fe7..25e88068c3 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -6,7 +6,11 @@ use std::{ marker::PhantomData, mem, pin::Pin, - sync::Arc, + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, task::{Context, Poll}, time::Duration, }; @@ -14,10 +18,16 @@ use util::TryFutureExt; use waker_fn::waker_fn; #[derive(Clone)] -pub struct Executor { +pub struct BackgroundExecutor { dispatcher: Arc, } +#[derive(Clone)] +pub struct ForegroundExecutor { + dispatcher: Arc, + not_send: PhantomData>, +} + #[must_use] pub enum Task { Ready(Option), @@ -43,7 +53,7 @@ where E: 'static + Send + Debug, { pub fn detach_and_log_err(self, cx: &mut AppContext) { - cx.executor().spawn(self.log_err()).detach(); + cx.background_executor().spawn(self.log_err()).detach(); } } @@ -58,7 +68,7 @@ impl Future for Task { } } -impl Executor { +impl BackgroundExecutor { pub fn new(dispatcher: Arc) -> Self { Self { dispatcher } } @@ -76,68 +86,30 @@ impl Executor { Task::Spawned(task) } - /// Enqueues the given closure to run on the application's event loop. - /// Returns the result asynchronously. - pub fn run_on_main(&self, func: F) -> Task - where - F: FnOnce() -> R + Send + 'static, - R: Send + 'static, - { - if self.dispatcher.is_main_thread() { - Task::ready(func()) - } else { - self.spawn_on_main(move || async move { func() }) - } - } - - /// Enqueues the given closure to be run on the application's event loop. The - /// closure returns a future which will be run to completion on the main thread. - pub fn spawn_on_main(&self, func: impl FnOnce() -> F + Send + 'static) -> Task - where - F: Future + 'static, - R: Send + 'static, - { - let (runnable, task) = async_task::spawn( - { - let this = self.clone(); - async move { - let task = this.spawn_on_main_local(func()); - task.await - } - }, - { - let dispatcher = self.dispatcher.clone(); - move |runnable| dispatcher.dispatch_on_main_thread(runnable) - }, - ); - runnable.schedule(); - Task::Spawned(task) - } - - /// Enqueues the given closure to be run on the application's event loop. Must - /// be called on the main thread. - pub fn spawn_on_main_local(&self, future: impl Future + 'static) -> Task - where - R: 'static, - { - assert!( - self.dispatcher.is_main_thread(), - "must be called on main thread" - ); - - let dispatcher = self.dispatcher.clone(); - let (runnable, task) = async_task::spawn_local(future, move |runnable| { - dispatcher.dispatch_on_main_thread(runnable) - }); - runnable.schedule(); - Task::Spawned(task) + #[cfg(any(test, feature = "test-support"))] + pub fn block_test(&self, future: impl Future) -> R { + self.block_internal(false, future) } pub fn block(&self, future: impl Future) -> R { + self.block_internal(true, future) + } + + pub(crate) fn block_internal( + &self, + background_only: bool, + future: impl Future, + ) -> R { pin_mut!(future); - let (parker, unparker) = parking::pair(); - let waker = waker_fn(move || { - unparker.unpark(); + let unparker = self.dispatcher.unparker(); + let awoken = Arc::new(AtomicBool::new(false)); + + let waker = waker_fn({ + let awoken = awoken.clone(); + move || { + awoken.store(true, SeqCst); + unparker.unpark(); + } }); let mut cx = std::task::Context::from_waker(&waker); @@ -145,12 +117,24 @@ impl Executor { match future.as_mut().poll(&mut cx) { Poll::Ready(result) => return result, Poll::Pending => { - if !self.dispatcher.poll() { - #[cfg(any(test, feature = "test-support"))] - if let Some(_) = self.dispatcher.as_test() { - panic!("blocked with nothing left to run") + if !self.dispatcher.poll(background_only) { + if awoken.swap(false, SeqCst) { + continue; } - parker.park(); + + #[cfg(any(test, feature = "test-support"))] + if let Some(test) = self.dispatcher.as_test() { + if !test.parking_allowed() { + let mut backtrace_message = String::new(); + if let Some(backtrace) = test.waiting_backtrace() { + backtrace_message = + format!("\nbacktrace of waiting future:\n{:?}", backtrace); + } + panic!("parked with nothing left to run\n{:?}", backtrace_message) + } + } + + self.dispatcher.park(); } } } @@ -206,17 +190,17 @@ impl Executor { #[cfg(any(test, feature = "test-support"))] pub fn start_waiting(&self) { - todo!("start_waiting") + self.dispatcher.as_test().unwrap().start_waiting(); } #[cfg(any(test, feature = "test-support"))] pub fn finish_waiting(&self) { - todo!("finish_waiting") + self.dispatcher.as_test().unwrap().finish_waiting(); } #[cfg(any(test, feature = "test-support"))] pub fn simulate_random_delay(&self) -> impl Future { - self.spawn(self.dispatcher.as_test().unwrap().simulate_random_delay()) + self.dispatcher.as_test().unwrap().simulate_random_delay() } #[cfg(any(test, feature = "test-support"))] @@ -229,6 +213,11 @@ impl Executor { self.dispatcher.as_test().unwrap().run_until_parked() } + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + self.dispatcher.as_test().unwrap().allow_parking(); + } + pub fn num_cpus(&self) -> usize { num_cpus::get() } @@ -238,8 +227,31 @@ impl Executor { } } +impl ForegroundExecutor { + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher, + not_send: PhantomData, + } + } + + /// Enqueues the given closure to be run on any thread. The closure returns + /// a future which will be run to completion on any available thread. + pub fn spawn(&self, future: impl Future + 'static) -> Task + where + R: 'static, + { + let dispatcher = self.dispatcher.clone(); + let (runnable, task) = async_task::spawn_local(future, move |runnable| { + dispatcher.dispatch_on_main_thread(runnable) + }); + runnable.schedule(); + Task::Spawned(task) + } +} + pub struct Scope<'a> { - executor: Executor, + executor: BackgroundExecutor, futures: Vec + Send + 'static>>>, tx: Option>, rx: mpsc::Receiver<()>, @@ -247,7 +259,7 @@ pub struct Scope<'a> { } impl<'a> Scope<'a> { - fn new(executor: Executor) -> Self { + fn new(executor: BackgroundExecutor) -> Self { let (tx, rx) = mpsc::channel(1); Self { executor, diff --git a/crates/gpui2/src/focusable.rs b/crates/gpui2/src/focusable.rs index d7cfc5fe8f..99f8bb1dd6 100644 --- a/crates/gpui2/src/focusable.rs +++ b/crates/gpui2/src/focusable.rs @@ -8,7 +8,7 @@ use smallvec::SmallVec; pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = - Box) + Send + 'static>; + Box) + 'static>; pub trait Focusable: Element { fn focus_listeners(&mut self) -> &mut FocusListeners; @@ -42,7 +42,7 @@ pub trait Focusable: Element { fn on_focus( mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -58,7 +58,7 @@ pub trait Focusable: Element { fn on_blur( mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -74,7 +74,7 @@ pub trait Focusable: Element { fn on_focus_in( mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -99,7 +99,7 @@ pub trait Focusable: Element { fn on_focus_out( mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -122,7 +122,7 @@ pub trait Focusable: Element { } } -pub trait ElementFocus: 'static + Send { +pub trait ElementFocus: 'static { fn as_focusable(&self) -> Option<&FocusEnabled>; fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled>; diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index eedf8bbb2c..7d4073144c 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -931,6 +931,18 @@ impl From for GlobalPixels { } } +impl sqlez::bindable::StaticColumnCount for GlobalPixels {} + +impl sqlez::bindable::Bind for GlobalPixels { + fn bind( + &self, + statement: &sqlez::statement::Statement, + start_index: i32, + ) -> anyhow::Result { + self.0.bind(statement, start_index) + } +} + #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] pub struct Rems(f32); diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 8625866a44..49cc3cebc2 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -68,51 +68,56 @@ use derive_more::{Deref, DerefMut}; use std::{ any::{Any, TypeId}, borrow::{Borrow, BorrowMut}, - mem, - ops::{Deref, DerefMut}, - sync::Arc, }; use taffy::TaffyLayoutEngine; -type AnyBox = Box; +type AnyBox = Box; pub trait Context { - type ModelContext<'a, T>; type Result; - fn build_model( + fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, - ) -> Self::Result> - where - T: 'static + Send; + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result>; - fn update_model( + fn update_model( &mut self, handle: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, - ) -> Self::Result; + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static; + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; } pub trait VisualContext: Context { - type ViewContext<'a, 'w, V>; - fn build_view( &mut self, - build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Send; + V: 'static; fn update_view( &mut self, view: &View, - update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Self::Result; + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: Render; } pub trait Entity: Sealed { - type Weak: 'static + Send; + type Weak: 'static; fn entity_id(&self) -> EntityId; fn downgrade(&self) -> Self::Weak; @@ -127,106 +132,12 @@ pub enum GlobalKey { Type(TypeId), } -#[repr(transparent)] -pub struct MainThread(T); - -impl Deref for MainThread { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MainThread { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Context for MainThread { - type ModelContext<'a, T> = MainThread>; - type Result = C::Result; - - fn build_model( - &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, - ) -> Self::Result> - where - T: 'static + Send, - { - self.0.build_model(|cx| { - let cx = unsafe { - mem::transmute::< - &mut C::ModelContext<'_, T>, - &mut MainThread>, - >(cx) - }; - build_model(cx) - }) - } - - fn update_model( - &mut self, - handle: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, - ) -> Self::Result { - self.0.update_model(handle, |entity, cx| { - let cx = unsafe { - mem::transmute::< - &mut C::ModelContext<'_, T>, - &mut MainThread>, - >(cx) - }; - update(entity, cx) - }) - } -} - -impl VisualContext for MainThread { - type ViewContext<'a, 'w, V> = MainThread>; - - fn build_view( - &mut self, - build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V, - ) -> Self::Result> - where - V: 'static + Send, - { - self.0.build_view(|cx| { - let cx = unsafe { - mem::transmute::< - &mut C::ViewContext<'_, '_, V>, - &mut MainThread>, - >(cx) - }; - build_view_state(cx) - }) - } - - fn update_view( - &mut self, - view: &View, - update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R, - ) -> Self::Result { - self.0.update_view(view, |view_state, cx| { - let cx = unsafe { - mem::transmute::< - &mut C::ViewContext<'_, '_, V>, - &mut MainThread>, - >(cx) - }; - update(view_state, cx) - }) - } -} - pub trait BorrowAppContext { fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R where F: FnOnce(&mut Self) -> R; - fn set_global(&mut self, global: T); + fn set_global(&mut self, global: T); } impl BorrowAppContext for C @@ -243,7 +154,7 @@ where result } - fn set_global(&mut self, global: G) { + fn set_global(&mut self, global: G) { self.borrow_mut().set_global(global) } } @@ -306,59 +217,3 @@ impl>> From for SharedString { Self(value.into()) } } - -pub enum Reference<'a, T> { - Immutable(&'a T), - Mutable(&'a mut T), -} - -impl<'a, T> Deref for Reference<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - match self { - Reference::Immutable(target) => target, - Reference::Mutable(target) => target, - } - } -} - -impl<'a, T> DerefMut for Reference<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - match self { - Reference::Immutable(_) => { - panic!("cannot mutably deref an immutable reference. this is a bug in GPUI."); - } - Reference::Mutable(target) => target, - } - } -} - -pub(crate) struct MainThreadOnly { - executor: Executor, - value: Arc, -} - -impl Clone for MainThreadOnly { - fn clone(&self) -> Self { - Self { - executor: self.executor.clone(), - value: self.value.clone(), - } - } -} - -/// Allows a value to be accessed only on the main thread, allowing a non-`Send` type -/// to become `Send`. -impl MainThreadOnly { - pub(crate) fn new(value: Arc, executor: Executor) -> Self { - Self { executor, value } - } - - pub(crate) fn borrow_on_main_thread(&self) -> &T { - assert!(self.executor.is_main_thread()); - &self.value - } -} - -unsafe impl Send for MainThreadOnly {} diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 5a37c3ee7a..020cb82cd2 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -50,7 +50,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_down( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -71,7 +71,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_up( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -92,7 +92,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_down_out( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -113,7 +113,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_up_out( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -133,7 +133,7 @@ pub trait StatelessInteractive: Element { fn on_mouse_move( mut self, - handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -150,7 +150,7 @@ pub trait StatelessInteractive: Element { fn on_scroll_wheel( mut self, - handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -178,7 +178,7 @@ pub trait StatelessInteractive: Element { fn on_action( mut self, - listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -196,7 +196,7 @@ pub trait StatelessInteractive: Element { fn on_key_down( mut self, - listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -214,7 +214,7 @@ pub trait StatelessInteractive: Element { fn on_key_up( mut self, - listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -258,9 +258,9 @@ pub trait StatelessInteractive: Element { self } - fn on_drop( + fn on_drop( mut self, - listener: impl Fn(&mut V, View, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -303,7 +303,7 @@ pub trait StatefulInteractive: StatelessInteractive { fn on_click( mut self, - listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, ) -> Self where Self: Sized, @@ -316,11 +316,11 @@ pub trait StatefulInteractive: StatelessInteractive { fn on_drag( mut self, - listener: impl Fn(&mut V, &mut ViewContext) -> View + Send + 'static, + listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, ) -> Self where Self: Sized, - W: 'static + Send + Render, + W: 'static + Render, { debug_assert!( self.stateful_interaction().drag_listener.is_none(), @@ -335,7 +335,7 @@ pub trait StatefulInteractive: StatelessInteractive { } } -pub trait ElementInteraction: 'static + Send { +pub trait ElementInteraction: 'static { fn as_stateless(&self) -> &StatelessInteraction; fn as_stateless_mut(&mut self) -> &mut StatelessInteraction; fn as_stateful(&self) -> Option<&StatefulInteraction>; @@ -672,7 +672,7 @@ impl From for StatefulInteraction { } } -type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static + Send; +type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteraction { pub dispatch_context: DispatchContext, @@ -1077,32 +1077,25 @@ pub struct FocusEvent { } pub type MouseDownListener = Box< - dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) - + Send - + 'static, + dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, >; pub type MouseUpListener = Box< - dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) - + Send - + 'static, + dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, >; pub type MouseMoveListener = Box< - dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) - + Send - + 'static, + dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, >; pub type ScrollWheelListener = Box< dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) - + Send + 'static, >; -pub type ClickListener = Box) + Send + 'static>; +pub type ClickListener = Box) + 'static>; pub(crate) type DragListener = - Box, &mut ViewContext) -> AnyDrag + Send + 'static>; + Box, &mut ViewContext) -> AnyDrag + 'static>; pub type KeyListener = Box< dyn Fn( @@ -1112,6 +1105,5 @@ pub type KeyListener = Box< DispatchPhase, &mut ViewContext, ) -> Option> - + Send + 'static, >; diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index cacb1922f6..a4be21ddf3 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -5,15 +5,19 @@ mod mac; mod test; use crate::{ - AnyWindowHandle, Bounds, DevicePixels, Executor, Font, FontId, FontMetrics, FontRun, - GlobalPixels, GlyphId, InputEvent, LineLayout, Pixels, Point, RenderGlyphParams, - RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, + point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, + FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, + Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, + SharedString, Size, }; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use async_task::Runnable; use futures::channel::oneshot; +use parking::Unparker; use seahash::SeaHasher; use serde::{Deserialize, Serialize}; +use sqlez::bindable::{Bind, Column, StaticColumnCount}; +use sqlez::statement::Statement; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::time::Duration; @@ -26,6 +30,7 @@ use std::{ str::FromStr, sync::Arc, }; +use uuid::Uuid; pub use keystroke::*; #[cfg(target_os = "macos")] @@ -35,12 +40,13 @@ pub use test::*; pub use time::UtcOffset; #[cfg(target_os = "macos")] -pub(crate) fn current_platform() -> Arc { - Arc::new(MacPlatform::new()) +pub(crate) fn current_platform() -> Rc { + Rc::new(MacPlatform::new()) } pub(crate) trait Platform: 'static { - fn executor(&self) -> Executor; + fn background_executor(&self) -> BackgroundExecutor; + fn foreground_executor(&self) -> ForegroundExecutor; fn text_system(&self) -> Arc; fn run(&self, on_finish_launching: Box); @@ -63,7 +69,7 @@ pub(crate) trait Platform: 'static { fn set_display_link_output_callback( &self, display_id: DisplayId, - callback: Box, + callback: Box, ); fn start_display_link(&self, display_id: DisplayId); fn stop_display_link(&self, display_id: DisplayId); @@ -104,6 +110,9 @@ pub(crate) trait Platform: 'static { pub trait PlatformDisplay: Send + Sync + Debug { fn id(&self) -> DisplayId; + /// Returns a stable identifier for this display that can be persisted and used + /// across system restarts. + fn uuid(&self) -> Result; fn as_any(&self) -> &dyn Any; fn bounds(&self) -> Bounds; } @@ -129,12 +138,7 @@ pub(crate) trait PlatformWindow { fn mouse_position(&self) -> Point; fn as_any_mut(&mut self) -> &mut dyn Any; fn set_input_handler(&mut self, input_handler: Box); - fn prompt( - &self, - level: WindowPromptLevel, - msg: &str, - answers: &[&str], - ) -> oneshot::Receiver; + fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); fn set_edited(&mut self, edited: bool); @@ -161,7 +165,9 @@ pub trait PlatformDispatcher: Send + Sync { fn dispatch(&self, runnable: Runnable); fn dispatch_on_main_thread(&self, runnable: Runnable); fn dispatch_after(&self, duration: Duration, runnable: Runnable); - fn poll(&self) -> bool; + fn poll(&self, background_only: bool) -> bool; + fn park(&self); + fn unparker(&self) -> Unparker; #[cfg(any(test, feature = "test-support"))] fn as_test(&self) -> Option<&TestDispatcher> { @@ -368,6 +374,67 @@ pub enum WindowBounds { Fixed(Bounds), } +impl StaticColumnCount for WindowBounds { + fn column_count() -> usize { + 5 + } +} + +impl Bind for WindowBounds { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let (region, next_index) = match self { + WindowBounds::Fullscreen => { + let next_index = statement.bind(&"Fullscreen", start_index)?; + (None, next_index) + } + WindowBounds::Maximized => { + let next_index = statement.bind(&"Maximized", start_index)?; + (None, next_index) + } + WindowBounds::Fixed(region) => { + let next_index = statement.bind(&"Fixed", start_index)?; + (Some(*region), next_index) + } + }; + + statement.bind( + ®ion.map(|region| { + ( + region.origin.x, + region.origin.y, + region.size.width, + region.size.height, + ) + }), + next_index, + ) + } +} + +impl Column for WindowBounds { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (window_state, next_index) = String::column(statement, start_index)?; + let bounds = match window_state.as_str() { + "Fullscreen" => WindowBounds::Fullscreen, + "Maximized" => WindowBounds::Maximized, + "Fixed" => { + let ((x, y, width, height), _) = Column::column(statement, next_index)?; + let x: f64 = x; + let y: f64 = y; + let width: f64 = width; + let height: f64 = height; + WindowBounds::Fixed(Bounds { + origin: point(x.into(), y.into()), + size: size(width.into(), height.into()), + }) + } + _ => bail!("Window State did not have a valid string"), + }; + + Ok((bounds, next_index + 4)) + } +} + #[derive(Copy, Clone, Debug)] pub enum WindowAppearance { Light, @@ -382,14 +449,6 @@ impl Default for WindowAppearance { } } -#[derive(Copy, Clone, Debug, PartialEq, Default)] -pub enum WindowPromptLevel { - #[default] - Info, - Warning, - Critical, -} - #[derive(Copy, Clone, Debug)] pub struct PathPromptOptions { pub files: bool, diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index a4ae2cc028..f5334912c6 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/crates/gpui2/src/platform/mac/dispatcher.rs @@ -9,8 +9,11 @@ use objc::{ runtime::{BOOL, YES}, sel, sel_impl, }; +use parking::{Parker, Unparker}; +use parking_lot::Mutex; use std::{ ffi::c_void, + sync::Arc, time::{Duration, SystemTime}, }; @@ -20,7 +23,17 @@ pub fn dispatch_get_main_queue() -> dispatch_queue_t { unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t } } -pub struct MacDispatcher; +pub struct MacDispatcher { + parker: Arc>, +} + +impl MacDispatcher { + pub fn new() -> Self { + MacDispatcher { + parker: Arc::new(Mutex::new(Parker::new())), + } + } +} impl PlatformDispatcher for MacDispatcher { fn is_main_thread(&self) -> bool { @@ -68,33 +81,20 @@ impl PlatformDispatcher for MacDispatcher { } } - fn poll(&self) -> bool { + fn poll(&self, _background_only: bool) -> bool { false } + + fn park(&self) { + self.parker.lock().park() + } + + fn unparker(&self) -> Unparker { + self.parker.lock().unparker() + } } extern "C" fn trampoline(runnable: *mut c_void) { let task = unsafe { Runnable::from_raw(runnable as *mut ()) }; task.run(); } - -// #include - -// int main(void) { - -// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ -// // Do some lengthy background work here... -// printf("Background Work\n"); - -// dispatch_async(dispatch_get_main_queue(), ^{ -// // Once done, update your UI on the main queue here. -// printf("UI Updated\n"); - -// }); -// }); - -// sleep(3); // prevent the program from terminating immediately - -// return 0; -// } -// ``` diff --git a/crates/gpui2/src/platform/mac/display.rs b/crates/gpui2/src/platform/mac/display.rs index dc064293f3..b326eaa66d 100644 --- a/crates/gpui2/src/platform/mac/display.rs +++ b/crates/gpui2/src/platform/mac/display.rs @@ -1,9 +1,12 @@ use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay}; +use anyhow::Result; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; use core_graphics::{ display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}, geometry::{CGPoint, CGRect, CGSize}, }; use std::any::Any; +use uuid::Uuid; #[derive(Debug)] pub struct MacDisplay(pub(crate) CGDirectDisplayID); @@ -11,17 +14,23 @@ pub struct MacDisplay(pub(crate) CGDirectDisplayID); unsafe impl Send for MacDisplay {} impl MacDisplay { - /// Get the screen with the given UUID. + /// Get the screen with the given [DisplayId]. pub fn find_by_id(id: DisplayId) -> Option { Self::all().find(|screen| screen.id() == id) } + /// Get the screen with the given persistent [Uuid]. + pub fn find_by_uuid(uuid: Uuid) -> Option { + Self::all().find(|screen| screen.uuid().ok() == Some(uuid)) + } + /// Get the primary screen - the one with the menu bar, and whose bottom left /// corner is at the origin of the AppKit coordinate system. pub fn primary() -> Self { Self::all().next().unwrap() } + /// Obtains an iterator over all currently active system displays. pub fn all() -> impl Iterator { unsafe { let mut display_count: u32 = 0; @@ -40,6 +49,11 @@ impl MacDisplay { } } +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + /// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space. /// /// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen, @@ -88,6 +102,34 @@ impl PlatformDisplay for MacDisplay { DisplayId(self.0) } + fn uuid(&self) -> Result { + let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) }; + anyhow::ensure!( + !cfuuid.is_null(), + "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID" + ); + + let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) }; + Ok(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } + fn as_any(&self) -> &dyn Any { self } diff --git a/crates/gpui2/src/platform/mac/display_linker.rs b/crates/gpui2/src/platform/mac/display_linker.rs index 6d8235a381..b63cf24e26 100644 --- a/crates/gpui2/src/platform/mac/display_linker.rs +++ b/crates/gpui2/src/platform/mac/display_linker.rs @@ -26,13 +26,13 @@ impl MacDisplayLinker { } } -type OutputCallback = Mutex>; +type OutputCallback = Mutex>; impl MacDisplayLinker { pub fn set_output_callback( &mut self, display_id: DisplayId, - output_callback: Box, + output_callback: Box, ) { if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } { let callback = Arc::new(Mutex::new(output_callback)); diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 881dd69bc8..7065c02e87 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,9 +1,9 @@ use super::BoolExt; use crate::{ - AnyWindowHandle, ClipboardItem, CursorStyle, DisplayId, Executor, InputEvent, MacDispatcher, - MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, PathPromptOptions, Platform, - PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, - WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, + InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, + PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -143,7 +143,8 @@ unsafe fn build_classes() { pub struct MacPlatform(Mutex); pub struct MacPlatformState { - executor: Executor, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, text_system: Arc, display_linker: MacDisplayLinker, pasteboard: id, @@ -164,8 +165,10 @@ pub struct MacPlatformState { impl MacPlatform { pub fn new() -> Self { + let dispatcher = Arc::new(MacDispatcher::new()); Self(Mutex::new(MacPlatformState { - executor: Executor::new(Arc::new(MacDispatcher)), + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), text_system: Arc::new(MacTextSystem::new()), display_linker: MacDisplayLinker::new(), pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, @@ -345,8 +348,12 @@ impl MacPlatform { } impl Platform for MacPlatform { - fn executor(&self) -> Executor { - self.0.lock().executor.clone() + fn background_executor(&self) -> BackgroundExecutor { + self.0.lock().background_executor.clone() + } + + fn foreground_executor(&self) -> crate::ForegroundExecutor { + self.0.lock().foreground_executor.clone() } fn text_system(&self) -> Arc { @@ -457,6 +464,10 @@ impl Platform for MacPlatform { } } + // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { + // Box::new(StatusItem::add(self.fonts())) + // } + fn displays(&self) -> Vec> { MacDisplay::all() .into_iter() @@ -464,10 +475,6 @@ impl Platform for MacPlatform { .collect() } - // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { - // Box::new(StatusItem::add(self.fonts())) - // } - fn display(&self, id: DisplayId) -> Option> { MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) } @@ -481,13 +488,13 @@ impl Platform for MacPlatform { handle: AnyWindowHandle, options: WindowOptions, ) -> Box { - Box::new(MacWindow::open(handle, options, self.executor())) + Box::new(MacWindow::open(handle, options, self.foreground_executor())) } fn set_display_link_output_callback( &self, display_id: DisplayId, - callback: Box, + callback: Box, ) { self.0 .lock() @@ -589,8 +596,8 @@ impl Platform for MacPlatform { let path = path.to_path_buf(); self.0 .lock() - .executor - .spawn_on_main_local(async move { + .background_executor + .spawn(async move { let full_path = ns_string(path.to_str().unwrap_or("")); let root_full_path = ns_string(""); let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; @@ -674,23 +681,6 @@ impl Platform for MacPlatform { } } - fn path_for_auxiliary_executable(&self, name: &str) -> Result { - unsafe { - let bundle: id = NSBundle::mainBundle(); - if bundle.is_null() { - Err(anyhow!("app is not running inside a bundle")) - } else { - let name = ns_string(name); - let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; - if url.is_null() { - Err(anyhow!("resource not found")) - } else { - ns_url_to_path(url) - } - } - } - } - // fn on_menu_command(&self, callback: Box) { // self.0.lock().menu_command = Some(callback); // } @@ -717,6 +707,23 @@ impl Platform for MacPlatform { // } // } + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let name = ns_string(name); + let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; + if url.is_null() { + Err(anyhow!("resource not found")) + } else { + ns_url_to_path(url) + } + } + } + } + fn set_cursor_style(&self, style: CursorStyle) { unsafe { let new_cursor: id = match style { diff --git a/crates/gpui2/src/platform/mac/shaders.metal b/crates/gpui2/src/platform/mac/shaders.metal index 444842d9b2..0a3a2b2129 100644 --- a/crates/gpui2/src/platform/mac/shaders.metal +++ b/crates/gpui2/src/platform/mac/shaders.metal @@ -98,10 +98,10 @@ fragment float4 quad_fragment(QuadVertexOutput input [[stage_in]], input.border_color.a *= 1. - saturate(0.5 - inset_distance); // Alpha-blend the border and the background. - float output_alpha = - quad.border_color.a + quad.background.a * (1. - quad.border_color.a); + float output_alpha = input.border_color.a + + input.background_color.a * (1. - input.border_color.a); float3 premultiplied_border_rgb = - input.border_color.rgb * quad.border_color.a; + input.border_color.rgb * input.border_color.a; float3 premultiplied_background_rgb = input.background_color.rgb * input.background_color.a; float3 premultiplied_output_rgb = diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index dbdb60469b..affeab57c7 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1,10 +1,10 @@ use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; use crate::{ - display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, Executor, ExternalPaths, - FileDropEvent, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, Modifiers, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Scene, Size, - Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, WindowPromptLevel, + display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, + FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + PromptLevel, Scene, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, }; use block::ConcreteBlock; use cocoa::{ @@ -315,7 +315,7 @@ struct InsertText { struct MacWindowState { handle: AnyWindowHandle, - executor: Executor, + executor: ForegroundExecutor, native_window: id, renderer: MetalRenderer, scene_to_render: Option, @@ -451,7 +451,11 @@ unsafe impl Send for MacWindowState {} pub struct MacWindow(Arc>); impl MacWindow { - pub fn open(handle: AnyWindowHandle, options: WindowOptions, executor: Executor) -> Self { + pub fn open( + handle: AnyWindowHandle, + options: WindowOptions, + executor: ForegroundExecutor, + ) -> Self { unsafe { let pool = NSAutoreleasePool::new(nil); @@ -674,11 +678,13 @@ impl MacWindow { impl Drop for MacWindow { fn drop(&mut self) { - let this = self.0.clone(); - let executor = self.0.lock().executor.clone(); - executor - .run_on_main(move || unsafe { - this.lock().native_window.close(); + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.close(); + } }) .detach(); } @@ -741,12 +747,7 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().input_handler = Some(input_handler); } - fn prompt( - &self, - level: WindowPromptLevel, - msg: &str, - answers: &[&str], - ) -> oneshot::Receiver { + fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver { // macOs applies overrides to modal window buttons after they are added. // Two most important for this logic are: // * Buttons with "Cancel" title will be displayed as the last buttons in the modal @@ -776,9 +777,9 @@ impl PlatformWindow for MacWindow { let alert: id = msg_send![class!(NSAlert), alloc]; let alert: id = msg_send![alert, init]; let alert_style = match level { - WindowPromptLevel::Info => 1, - WindowPromptLevel::Warning => 0, - WindowPromptLevel::Critical => 2, + PromptLevel::Info => 1, + PromptLevel::Warning => 0, + PromptLevel::Critical => 2, }; let _: () = msg_send![alert, setAlertStyle: alert_style]; let _: () = msg_send![alert, setMessageText: ns_string(msg)]; @@ -807,7 +808,7 @@ impl PlatformWindow for MacWindow { let native_window = self.0.lock().native_window; let executor = self.0.lock().executor.clone(); executor - .spawn_on_main_local(async move { + .spawn(async move { let _: () = msg_send![ alert, beginSheetModalForWindow: native_window @@ -824,7 +825,7 @@ impl PlatformWindow for MacWindow { let window = self.0.lock().native_window; let executor = self.0.lock().executor.clone(); executor - .spawn_on_main_local(async move { + .spawn(async move { unsafe { let _: () = msg_send![window, makeKeyAndOrderFront: nil]; } @@ -873,7 +874,7 @@ impl PlatformWindow for MacWindow { let this = self.0.lock(); let window = this.native_window; this.executor - .spawn_on_main_local(async move { + .spawn(async move { unsafe { window.zoom_(nil); } @@ -885,7 +886,7 @@ impl PlatformWindow for MacWindow { let this = self.0.lock(); let window = this.native_window; this.executor - .spawn_on_main_local(async move { + .spawn(async move { unsafe { window.toggleFullScreen_(nil); } @@ -1189,7 +1190,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { lock.synthetic_drag_counter += 1; let executor = lock.executor.clone(); executor - .spawn_on_main_local(synthetic_drag( + .spawn(synthetic_drag( weak_window_state, lock.synthetic_drag_counter, event.clone(), @@ -1317,7 +1318,7 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) let executor = lock.executor.clone(); drop(lock); executor - .spawn_on_main_local(async move { + .spawn(async move { let mut lock = window_state.as_ref().lock(); if let Some(mut callback) = lock.activate_callback.take() { drop(lock); diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 0ed5638a8b..618d8c7917 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -1,6 +1,8 @@ use crate::PlatformDispatcher; use async_task::Runnable; +use backtrace::Backtrace; use collections::{HashMap, VecDeque}; +use parking::{Parker, Unparker}; use parking_lot::Mutex; use rand::prelude::*; use std::{ @@ -18,6 +20,8 @@ struct TestDispatcherId(usize); pub struct TestDispatcher { id: TestDispatcherId, state: Arc>, + parker: Arc>, + unparker: Unparker, } struct TestDispatcherState { @@ -28,10 +32,13 @@ struct TestDispatcherState { time: Duration, is_main_thread: bool, next_id: TestDispatcherId, + allow_parking: bool, + waiting_backtrace: Option, } impl TestDispatcher { pub fn new(random: StdRng) -> Self { + let (parker, unparker) = parking::pair(); let state = TestDispatcherState { random, foreground: HashMap::default(), @@ -40,11 +47,15 @@ impl TestDispatcher { time: Duration::ZERO, is_main_thread: true, next_id: TestDispatcherId(1), + allow_parking: false, + waiting_backtrace: None, }; TestDispatcher { id: TestDispatcherId(0), state: Arc::new(Mutex::new(state)), + parker: Arc::new(Mutex::new(parker)), + unparker, } } @@ -66,7 +77,7 @@ impl TestDispatcher { self.state.lock().time = new_now; } - pub fn simulate_random_delay(&self) -> impl Future { + pub fn simulate_random_delay(&self) -> impl 'static + Send + Future { pub struct YieldNow { count: usize, } @@ -91,7 +102,30 @@ impl TestDispatcher { } pub fn run_until_parked(&self) { - while self.poll() {} + while self.poll(false) {} + } + + pub fn parking_allowed(&self) -> bool { + self.state.lock().allow_parking + } + + pub fn allow_parking(&self) { + self.state.lock().allow_parking = true + } + + pub fn start_waiting(&self) { + self.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved()); + } + + pub fn finish_waiting(&self) { + self.state.lock().waiting_backtrace.take(); + } + + pub fn waiting_backtrace(&self) -> Option { + self.state.lock().waiting_backtrace.take().map(|mut b| { + b.resolve(); + b + }) } } @@ -101,6 +135,8 @@ impl Clone for TestDispatcher { Self { id: TestDispatcherId(id), state: self.state.clone(), + parker: self.parker.clone(), + unparker: self.unparker.clone(), } } } @@ -112,6 +148,7 @@ impl PlatformDispatcher for TestDispatcher { fn dispatch(&self, runnable: Runnable) { self.state.lock().background.push(runnable); + self.unparker.unpark(); } fn dispatch_on_main_thread(&self, runnable: Runnable) { @@ -121,6 +158,7 @@ impl PlatformDispatcher for TestDispatcher { .entry(self.id) .or_default() .push_back(runnable); + self.unparker.unpark(); } fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) { @@ -132,7 +170,7 @@ impl PlatformDispatcher for TestDispatcher { state.delayed.insert(ix, (next_time, runnable)); } - fn poll(&self) -> bool { + fn poll(&self, background_only: bool) -> bool { let mut state = self.state.lock(); while let Some((deadline, _)) = state.delayed.first() { @@ -143,11 +181,15 @@ impl PlatformDispatcher for TestDispatcher { state.background.push(runnable); } - let foreground_len: usize = state - .foreground - .values() - .map(|runnables| runnables.len()) - .sum(); + let foreground_len: usize = if background_only { + 0 + } else { + state + .foreground + .values() + .map(|runnables| runnables.len()) + .sum() + }; let background_len = state.background.len(); if foreground_len == 0 && background_len == 0 { @@ -183,62 +225,15 @@ impl PlatformDispatcher for TestDispatcher { true } + fn park(&self) { + self.parker.lock().park(); + } + + fn unparker(&self) -> Unparker { + self.unparker.clone() + } + fn as_test(&self) -> Option<&TestDispatcher> { Some(self) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::Executor; - use std::sync::Arc; - - #[test] - fn test_dispatch() { - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); - let executor = Executor::new(Arc::new(dispatcher)); - - let result = executor.block(async { executor.run_on_main(|| 1).await }); - assert_eq!(result, 1); - - let result = executor.block({ - let executor = executor.clone(); - async move { - executor - .spawn_on_main({ - let executor = executor.clone(); - assert!(executor.is_main_thread()); - || async move { - assert!(executor.is_main_thread()); - let result = executor - .spawn({ - let executor = executor.clone(); - async move { - assert!(!executor.is_main_thread()); - - let result = executor - .spawn_on_main({ - let executor = executor.clone(); - || async move { - assert!(executor.is_main_thread()); - 2 - } - }) - .await; - - assert!(!executor.is_main_thread()); - result - } - }) - .await; - assert!(executor.is_main_thread()); - result - } - }) - .await - } - }); - assert_eq!(result, 2); - } -} diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 4d86c434d0..b4f3c739e6 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,21 +1,29 @@ -use crate::{DisplayId, Executor, Platform, PlatformTextSystem}; +use crate::{BackgroundExecutor, DisplayId, ForegroundExecutor, Platform, PlatformTextSystem}; use anyhow::{anyhow, Result}; use std::sync::Arc; pub struct TestPlatform { - executor: Executor, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, } impl TestPlatform { - pub fn new(executor: Executor) -> Self { - TestPlatform { executor } + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self { + TestPlatform { + background_executor: executor, + foreground_executor, + } } } // todo!("implement out what our tests needed in GPUI 1") impl Platform for TestPlatform { - fn executor(&self) -> Executor { - self.executor.clone() + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() } fn text_system(&self) -> Arc { @@ -73,7 +81,7 @@ impl Platform for TestPlatform { fn set_display_link_output_callback( &self, _display_id: DisplayId, - _callback: Box, + _callback: Box, ) { unimplemented!() } diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 3bf28792bb..64fcd74dd2 100644 --- a/crates/gpui2/src/subscription.rs +++ b/crates/gpui2/src/subscription.rs @@ -21,8 +21,8 @@ struct SubscriberSetState { impl SubscriberSet where - EmitterKey: 'static + Send + Ord + Clone + Debug, - Callback: 'static + Send, + EmitterKey: 'static + Ord + Clone + Debug, + Callback: 'static, { pub fn new() -> Self { Self(Arc::new(Mutex::new(SubscriberSetState { @@ -47,8 +47,8 @@ where subscribers.remove(&subscriber_id); if subscribers.is_empty() { lock.subscribers.remove(&emitter_key); - return; } + return; } // We didn't manage to remove the subscription, which means it was dropped @@ -96,7 +96,7 @@ where #[must_use] pub struct Subscription { - unsubscribe: Option>, + unsubscribe: Option>, } impl Subscription { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index f1d54e7ae0..d81df5b21c 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,13 +1,16 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, LayoutId, Model, Pixels, - Size, ViewContext, VisualContext, WeakModel, WindowContext, + BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, + Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; -use std::{any::TypeId, marker::PhantomData}; +use std::{ + any::{Any, TypeId}, + hash::{Hash, Hasher}, +}; pub trait Render: 'static + Sized { - type Element: Element + 'static + Send; + type Element: Element + 'static; fn render(&mut self, cx: &mut ViewContext) -> Self::Element; } @@ -49,7 +52,7 @@ impl View { pub fn update( &self, cx: &mut C, - f: impl FnOnce(&mut V, &mut C::ViewContext<'_, '_, V>) -> R, + f: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> C::Result where C: VisualContext, @@ -70,55 +73,23 @@ impl Clone for View { } } -impl Component for View { - fn render(self) -> AnyElement { - AnyElement::new(EraseViewState { - view: self, - parent_view_state_type: PhantomData, - }) +impl Hash for View { + fn hash(&self, state: &mut H) { + self.model.hash(state); } } -impl Element<()> for View -where - V: Render, -{ - type ElementState = AnyElement; - - fn id(&self) -> Option { - Some(ElementId::View(self.model.entity_id)) +impl PartialEq for View { + fn eq(&self, other: &Self) -> bool { + self.model == other.model } +} - fn initialize( - &mut self, - _: &mut (), - _: Option, - cx: &mut ViewContext<()>, - ) -> Self::ElementState { - self.update(cx, |state, cx| { - let mut any_element = AnyElement::new(state.render(cx)); - any_element.initialize(state, cx); - any_element - }) - } +impl Eq for View {} - fn layout( - &mut self, - _: &mut (), - element: &mut Self::ElementState, - cx: &mut ViewContext<()>, - ) -> LayoutId { - self.update(cx, |state, cx| element.layout(state, cx)) - } - - fn paint( - &mut self, - _: Bounds, - _: &mut (), - element: &mut Self::ElementState, - cx: &mut ViewContext<()>, - ) { - self.update(cx, |state, cx| element.paint(state, cx)) +impl Component for View { + fn render(self) -> AnyElement { + AnyElement::new(AnyView::from(self)) } } @@ -127,17 +98,25 @@ pub struct WeakView { } impl WeakView { + pub fn entity_id(&self) -> EntityId { + self.model.entity_id + } + pub fn upgrade(&self) -> Option> { Entity::upgrade_from(self) } - pub fn update( + pub fn update( &self, - cx: &mut WindowContext, - f: impl FnOnce(&mut V, &mut ViewContext) -> R, - ) -> Result { + cx: &mut C, + f: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Result + where + C: VisualContext, + Result>: Flatten, + { let view = self.upgrade().context("error upgrading view")?; - Ok(view.update(cx, f)) + Ok(view.update(cx, f)).flatten() } } @@ -149,115 +128,19 @@ impl Clone for WeakView { } } -struct EraseViewState { - view: View, - parent_view_state_type: PhantomData, -} - -unsafe impl Send for EraseViewState {} - -impl Component for EraseViewState { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl Hash for WeakView { + fn hash(&self, state: &mut H) { + self.model.hash(state); } } -impl Element for EraseViewState { - type ElementState = AnyBox; - - fn id(&self) -> Option { - Element::id(&self.view) - } - - fn initialize( - &mut self, - _: &mut ParentV, - _: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - ViewObject::initialize(&mut self.view, cx) - } - - fn layout( - &mut self, - _: &mut ParentV, - element: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { - ViewObject::layout(&mut self.view, element, cx) - } - - fn paint( - &mut self, - bounds: Bounds, - _: &mut ParentV, - element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - ViewObject::paint(&mut self.view, bounds, element, cx) +impl PartialEq for WeakView { + fn eq(&self, other: &Self) -> bool { + self.model == other.model } } -trait ViewObject: Send + Sync { - fn entity_type(&self) -> TypeId; - fn entity_id(&self) -> EntityId; - fn model(&self) -> AnyModel; - fn initialize(&self, cx: &mut WindowContext) -> AnyBox; - fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId; - fn paint(&self, bounds: Bounds, element: &mut AnyBox, cx: &mut WindowContext); - fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; -} - -impl ViewObject for View -where - V: Render, -{ - fn entity_type(&self) -> TypeId { - TypeId::of::() - } - - fn entity_id(&self) -> EntityId { - Entity::entity_id(self) - } - - fn model(&self) -> AnyModel { - self.model.clone().into_any() - } - - fn initialize(&self, cx: &mut WindowContext) -> AnyBox { - cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { - self.update(cx, |state, cx| { - let mut any_element = Box::new(AnyElement::new(state.render(cx))); - any_element.initialize(state, cx); - any_element - }) - }) - } - - fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId { - cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { - self.update(cx, |state, cx| { - let element = element.downcast_mut::>().unwrap(); - element.layout(state, cx) - }) - }) - } - - fn paint(&self, _: Bounds, element: &mut AnyBox, cx: &mut WindowContext) { - cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { - self.update(cx, |state, cx| { - let element = element.downcast_mut::>().unwrap(); - element.paint(state, cx); - }); - }); - } - - fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct(&format!("AnyView<{}>", std::any::type_name::())) - .field("entity_id", &ViewObject::entity_id(self).as_u64()) - .finish() - } -} +impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { @@ -289,7 +172,7 @@ impl AnyView { } } - pub(crate) fn entity_type(&self) -> TypeId { + pub fn entity_type(&self) -> TypeId { self.model.entity_type } @@ -343,7 +226,7 @@ impl From> for AnyView { } impl Element for AnyView { - type ElementState = AnyBox; + type ElementState = Box; fn id(&self) -> Option { Some(self.model.entity_id.into()) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3997a3197f..cebf546217 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -3,16 +3,20 @@ use crate::{ Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, - MainThread, MainThreadOnly, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, - Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakModel, WeakView, - WindowOptions, SUBPIXEL_VARIANTS, + Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use collections::HashMap; use derive_more::{Deref, DerefMut}; +use futures::{ + channel::{mpsc, oneshot}, + StreamExt, +}; use parking_lot::RwLock; use slotmap::SlotMap; use smallvec::SmallVec; @@ -21,8 +25,10 @@ use std::{ borrow::{Borrow, BorrowMut, Cow}, fmt::Debug, future::Future, + hash::{Hash, Hasher}, marker::PhantomData, mem, + rc::Rc, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -52,7 +58,8 @@ pub enum DispatchPhase { Capture, } -type AnyListener = Box; +type AnyObserver = Box bool + 'static>; +type AnyListener = Box; type AnyKeyListener = Box< dyn Fn( &dyn Any, @@ -60,10 +67,9 @@ type AnyKeyListener = Box< DispatchPhase, &mut WindowContext, ) -> Option> - + Send + 'static, >; -type AnyFocusListener = Box; +type AnyFocusListener = Box; slotmap::new_key_type! { pub struct FocusId; } @@ -158,8 +164,9 @@ impl Drop for FocusHandle { // Holds the state for a specific window. pub struct Window { - handle: AnyWindowHandle, - platform_window: MainThreadOnly>, + pub(crate) handle: AnyWindowHandle, + pub(crate) removed: bool, + platform_window: Box, display_id: DisplayId, sprite_atlas: Arc, rem_size: Pixels, @@ -184,6 +191,10 @@ pub struct Window { default_prevented: bool, mouse_position: Point, scale_factor: f32, + bounds: WindowBounds, + bounds_observers: SubscriberSet<(), AnyObserver>, + active: bool, + activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) scene_builder: SceneBuilder, pub(crate) dirty: bool, pub(crate) last_blur: Option>, @@ -194,46 +205,60 @@ impl Window { pub(crate) fn new( handle: AnyWindowHandle, options: WindowOptions, - cx: &mut MainThread, + cx: &mut AppContext, ) -> Self { - let platform_window = cx.platform().open_window(handle, options); + let platform_window = cx.platform.open_window(handle, options); let display_id = platform_window.display().id(); let sprite_atlas = platform_window.sprite_atlas(); let mouse_position = platform_window.mouse_position(); let content_size = platform_window.content_size(); let scale_factor = platform_window.scale_factor(); + let bounds = platform_window.bounds(); + platform_window.on_resize(Box::new({ - let cx = cx.to_async(); - move |content_size, scale_factor| { - cx.update_window(handle, |cx| { - cx.window.scale_factor = scale_factor; - cx.window.scene_builder = SceneBuilder::new(); - cx.window.content_size = content_size; - cx.window.display_id = cx - .window - .platform_window - .borrow_on_main_thread() - .display() - .id(); - cx.window.dirty = true; - }) - .log_err(); + let mut cx = cx.to_async(); + move |_, _| { + handle + .update(&mut cx, |_, cx| cx.window_bounds_changed()) + .log_err(); + } + })); + platform_window.on_moved(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, cx| cx.window_bounds_changed()) + .log_err(); + } + })); + platform_window.on_active_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, cx| { + cx.window.active = active; + cx.window + .activation_observers + .clone() + .retain(&(), |callback| callback(cx)); + }) + .log_err(); } })); platform_window.on_input({ - let cx = cx.to_async(); + let mut cx = cx.to_async(); Box::new(move |event| { - cx.update_window(handle, |cx| cx.dispatch_event(event)) + handle + .update(&mut cx, |_, cx| cx.dispatch_event(event)) .log_err() .unwrap_or(true) }) }); - let platform_window = MainThreadOnly::new(Arc::new(platform_window), cx.executor.clone()); - Window { handle, + removed: false, platform_window, display_id, sprite_atlas, @@ -259,6 +284,10 @@ impl Window { default_prevented: true, mouse_position, scale_factor, + bounds, + bounds_observers: SubscriberSet::new(), + active: false, + activation_observers: SubscriberSet::new(), scene_builder: SceneBuilder::new(), dirty: true, last_blur: None, @@ -305,24 +334,14 @@ impl ContentMask { /// Provides access to application state in the context of a single window. Derefs /// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes /// an `AppContext` and call any `AppContext` methods. -pub struct WindowContext<'a, 'w> { - pub(crate) app: Reference<'a, AppContext>, - pub(crate) window: Reference<'w, Window>, +pub struct WindowContext<'a> { + pub(crate) app: &'a mut AppContext, + pub(crate) window: &'a mut Window, } -impl<'a, 'w> WindowContext<'a, 'w> { - pub(crate) fn immutable(app: &'a AppContext, window: &'w Window) -> Self { - Self { - app: Reference::Immutable(app), - window: Reference::Immutable(window), - } - } - - pub(crate) fn mutable(app: &'a mut AppContext, window: &'w mut Window) -> Self { - Self { - app: Reference::Mutable(app), - window: Reference::Mutable(window), - } +impl<'a> WindowContext<'a> { + pub(crate) fn new(app: &'a mut AppContext, window: &'a mut Window) -> Self { + Self { app, window } } /// Obtain a handle to the window that belongs to this context. @@ -335,6 +354,11 @@ impl<'a, 'w> WindowContext<'a, 'w> { self.window.dirty = true; } + /// Close this window. + pub fn remove_window(&mut self) { + self.window.removed = true; + } + /// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus /// for elements rendered within this window. pub fn focus_handle(&mut self) -> FocusHandle { @@ -354,10 +378,9 @@ impl<'a, 'w> WindowContext<'a, 'w> { self.window.last_blur = Some(self.window.focus); } - let window_id = self.window.handle.id; self.window.focus = Some(handle.id); self.app.push_effect(Effect::FocusChanged { - window_id, + window_handle: self.window.handle, focused: Some(handle.id), }); self.notify(); @@ -369,33 +392,51 @@ impl<'a, 'w> WindowContext<'a, 'w> { self.window.last_blur = Some(self.window.focus); } - let window_id = self.window.handle.id; self.window.focus = None; self.app.push_effect(Effect::FocusChanged { - window_id, + window_handle: self.window.handle, focused: None, }); self.notify(); } - /// Schedule the given closure to be run on the main thread. It will be invoked with - /// a `MainThread`, which provides access to platform-specific functionality - /// of the window. - pub fn run_on_main( + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { + let handle = self.window.handle; + self.app.defer(move |cx| { + handle.update(cx, |_, cx| f(cx)).ok(); + }); + } + + pub fn subscribe( &mut self, - f: impl FnOnce(&mut MainThread>) -> R + Send + 'static, - ) -> Task> + entity: &E, + mut on_event: impl FnMut(E, &Emitter::Event, &mut WindowContext<'_>) + 'static, + ) -> Subscription where - R: Send + 'static, + Emitter: EventEmitter, + E: Entity, { - if self.executor.is_main_thread() { - Task::ready(Ok(f(unsafe { - mem::transmute::<&mut Self, &mut MainThread>(self) - }))) - } else { - let id = self.window.handle.id; - self.app.run_on_main(move |cx| cx.update_window(id, f)) - } + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); + let window_handle = self.window.handle; + self.app.event_listeners.insert( + entity_id, + Box::new(move |event, cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&entity) { + let event = event.downcast_ref().expect("invalid event type"); + on_event(handle, event, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ) } /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across @@ -405,66 +446,67 @@ impl<'a, 'w> WindowContext<'a, 'w> { } /// Schedule the given closure to be run directly after the current frame is rendered. - pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + Send + 'static) { - let f = Box::new(f); + pub fn on_next_frame(&mut self, callback: impl FnOnce(&mut WindowContext) + 'static) { + let handle = self.window.handle; let display_id = self.window.display_id; - self.run_on_main(move |cx| { - if let Some(callbacks) = cx.next_frame_callbacks.get_mut(&display_id) { - callbacks.push(f); - // If there was already a callback, it means that we already scheduled a frame. - if callbacks.len() > 1 { - return; + + if !self.frame_consumers.contains_key(&display_id) { + let (tx, mut rx) = mpsc::unbounded::<()>(); + self.platform.set_display_link_output_callback( + display_id, + Box::new(move |_current_time, _output_time| _ = tx.unbounded_send(())), + ); + + let consumer_task = self.app.spawn(|cx| async move { + while rx.next().await.is_some() { + cx.update(|cx| { + for callback in cx + .next_frame_callbacks + .get_mut(&display_id) + .unwrap() + .drain(..) + .collect::>() + { + callback(cx); + } + }) + .ok(); + + // Flush effects, then stop the display link if no new next_frame_callbacks have been added. + + cx.update(|cx| { + if cx.next_frame_callbacks.is_empty() { + cx.platform.stop_display_link(display_id); + } + }) + .ok(); } - } else { - let async_cx = cx.to_async(); - cx.next_frame_callbacks.insert(display_id, vec![f]); - cx.platform().set_display_link_output_callback( - display_id, - Box::new(move |_current_time, _output_time| { - let _ = async_cx.update(|cx| { - let callbacks = cx - .next_frame_callbacks - .get_mut(&display_id) - .unwrap() - .drain(..) - .collect::>(); - for callback in callbacks { - callback(cx); - } + }); + self.frame_consumers.insert(display_id, consumer_task); + } - cx.run_on_main(move |cx| { - if cx.next_frame_callbacks.get(&display_id).unwrap().is_empty() { - cx.platform().stop_display_link(display_id); - } - }) - .detach(); - }); - }), - ); - } + if self.next_frame_callbacks.is_empty() { + self.platform.start_display_link(display_id); + } - cx.platform().start_display_link(display_id); - }) - .detach(); + self.next_frame_callbacks + .entry(display_id) + .or_default() + .push(Box::new(move |cx: &mut AppContext| { + cx.update_window(handle, |_root_view, cx| callback(cx)).ok(); + })); } /// Spawn the future returned by the given closure on the application thread pool. /// The closure is provided a handle to the current window and an `AsyncWindowContext` for /// use within your future. - pub fn spawn( - &mut self, - f: impl FnOnce(AnyWindowHandle, AsyncWindowContext) -> Fut + Send + 'static, - ) -> Task + pub fn spawn(&mut self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task where - R: Send + 'static, - Fut: Future + Send + 'static, + R: 'static, + Fut: Future + 'static, { - let window = self.window.handle; - self.app.spawn(move |app| { - let cx = AsyncWindowContext::new(app, window); - let future = f(window, cx); - async move { future.await } - }) + self.app + .spawn(|app| f(AsyncWindowContext::new(app, self.window.handle))) } /// Update the global of the given type. The given closure is given simultaneous mutable @@ -528,6 +570,38 @@ impl<'a, 'w> WindowContext<'a, 'w> { bounds } + fn window_bounds_changed(&mut self) { + self.window.scale_factor = self.window.platform_window.scale_factor(); + self.window.content_size = self.window.platform_window.content_size(); + self.window.bounds = self.window.platform_window.bounds(); + self.window.display_id = self.window.platform_window.display().id(); + self.window.dirty = true; + + self.window + .bounds_observers + .clone() + .retain(&(), |callback| callback(self)); + } + + pub fn window_bounds(&self) -> WindowBounds { + self.window.bounds + } + + pub fn is_window_active(&self) -> bool { + self.window.active + } + + pub fn zoom_window(&self) { + self.window.platform_window.zoom(); + } + + pub fn display(&self) -> Option> { + self.platform + .displays() + .into_iter() + .find(|display| display.id() == self.window.display_id) + } + /// The scale factor of the display associated with the window. For example, it could /// return 2.0 for a "retina" display, indicating that each logical pixel should actually /// be rendered as two pixels on screen. @@ -541,6 +615,12 @@ impl<'a, 'w> WindowContext<'a, 'w> { self.window.rem_size } + /// Sets the size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn set_rem_size(&mut self, rem_size: impl Into) { + self.window.rem_size = rem_size.into(); + } + /// The line height associated with the current text style. pub fn line_height(&self) -> Pixels { let rem_size = self.rem_size(); @@ -569,7 +649,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { /// a specific need to register a global listener. pub fn on_mouse_event( &mut self, - handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + 'static, + handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { let order = self.window.z_index_stack.clone(); self.window @@ -906,14 +986,8 @@ impl<'a, 'w> WindowContext<'a, 'w> { self.window.root_view = Some(root_view); let scene = self.window.scene_builder.build(); - self.run_on_main(|cx| { - cx.window - .platform_window - .borrow_on_main_thread() - .draw(scene); - cx.window.dirty = false; - }) - .detach(); + self.window.platform_window.draw(scene); + self.window.dirty = false; } fn start_frame(&mut self) { @@ -1149,15 +1223,28 @@ impl<'a, 'w> WindowContext<'a, 'w> { /// is updated. pub fn observe_global( &mut self, - f: impl Fn(&mut WindowContext<'_, '_>) + Send + 'static, + f: impl Fn(&mut WindowContext<'_>) + 'static, ) -> Subscription { - let window_id = self.window.handle.id; + let window_handle = self.window.handle; self.global_observers.insert( TypeId::of::(), - Box::new(move |cx| cx.update_window(window_id, |cx| f(cx)).is_ok()), + Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()), ) } + pub fn activate_window(&self) { + self.window.platform_window.activate(); + } + + pub fn prompt( + &self, + level: PromptLevel, + msg: &str, + answers: &[&str], + ) -> oneshot::Receiver { + self.window.platform_window.prompt(level, msg, answers) + } + fn dispatch_action( &mut self, action: Box, @@ -1237,52 +1324,61 @@ impl<'a, 'w> WindowContext<'a, 'w> { } } -impl Context for WindowContext<'_, '_> { - type ModelContext<'a, T> = ModelContext<'a, T>; +impl Context for WindowContext<'_> { type Result = T; fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Model where - T: 'static + Send, + T: 'static, { let slot = self.app.entities.reserve(); - let model = build_model(&mut ModelContext::mutable(&mut *self.app, slot.downgrade())); + let model = build_model(&mut ModelContext::new(&mut *self.app, slot.downgrade())); self.entities.insert(slot, model) } fn update_model( &mut self, model: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> R { let mut entity = self.entities.lease(model); let result = update( &mut *entity, - &mut ModelContext::mutable(&mut *self.app, model.downgrade()), + &mut ModelContext::new(&mut *self.app, model.downgrade()), ); self.entities.end_lease(entity); result } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + if window == self.window.handle { + let root_view = self.window.root_view.clone().unwrap(); + Ok(update(root_view, self)) + } else { + window.update(self.app, update) + } + } } -impl VisualContext for WindowContext<'_, '_> { - type ViewContext<'a, 'w, V> = ViewContext<'a, 'w, V>; - +impl VisualContext for WindowContext<'_> { fn build_view( &mut self, - build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V, + build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Send, + V: 'static, { let slot = self.app.entities.reserve(); let view = View { model: slot.clone(), }; - let mut cx = ViewContext::mutable(&mut *self.app, &mut *self.window, view.downgrade()); + let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, &view); let entity = build_view_state(&mut cx); self.entities.insert(slot, entity); view @@ -1292,17 +1388,35 @@ impl VisualContext for WindowContext<'_, '_> { fn update_view( &mut self, view: &View, - update: impl FnOnce(&mut T, &mut Self::ViewContext<'_, '_, T>) -> R, + update: impl FnOnce(&mut T, &mut ViewContext<'_, T>) -> R, ) -> Self::Result { let mut lease = self.app.entities.lease(&view.model); - let mut cx = ViewContext::mutable(&mut *self.app, &mut *self.window, view.downgrade()); + let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, &view); let result = update(&mut *lease, &mut cx); cx.app.entities.end_lease(lease); result } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: Render, + { + let slot = self.app.entities.reserve(); + let view = View { + model: slot.clone(), + }; + let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, &view); + let entity = build_view(&mut cx); + self.entities.insert(slot, entity); + self.window.root_view = Some(view.clone().into()); + view + } } -impl<'a, 'w> std::ops::Deref for WindowContext<'a, 'w> { +impl<'a> std::ops::Deref for WindowContext<'a> { type Target = AppContext; fn deref(&self) -> &Self::Target { @@ -1310,19 +1424,19 @@ impl<'a, 'w> std::ops::Deref for WindowContext<'a, 'w> { } } -impl<'a, 'w> std::ops::DerefMut for WindowContext<'a, 'w> { +impl<'a> std::ops::DerefMut for WindowContext<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.app } } -impl<'a, 'w> Borrow for WindowContext<'a, 'w> { +impl<'a> Borrow for WindowContext<'a> { fn borrow(&self) -> &AppContext { &self.app } } -impl<'a, 'w> BorrowMut for WindowContext<'a, 'w> { +impl<'a> BorrowMut for WindowContext<'a> { fn borrow_mut(&mut self) -> &mut AppContext { &mut self.app } @@ -1422,7 +1536,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { f: impl FnOnce(Option, &mut Self) -> (R, S), ) -> R where - S: 'static + Send, + S: 'static, { self.with_element_id(id, |global_id, cx| { if let Some(any) = cx @@ -1460,7 +1574,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { f: impl FnOnce(Option, &mut Self) -> (R, S), ) -> R where - S: 'static + Send, + S: 'static, { if let Some(element_id) = element_id { self.with_element_state(element_id, f) @@ -1490,13 +1604,13 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } -impl Borrow for WindowContext<'_, '_> { +impl Borrow for WindowContext<'_> { fn borrow(&self) -> &Window { &self.window } } -impl BorrowMut for WindowContext<'_, '_> { +impl BorrowMut for WindowContext<'_> { fn borrow_mut(&mut self) -> &mut Window { &mut self.window } @@ -1504,52 +1618,48 @@ impl BorrowMut for WindowContext<'_, '_> { impl BorrowWindow for T where T: BorrowMut + BorrowMut {} -pub struct ViewContext<'a, 'w, V> { - window_cx: WindowContext<'a, 'w>, - view: WeakView, +pub struct ViewContext<'a, V> { + window_cx: WindowContext<'a>, + view: &'a View, } -impl Borrow for ViewContext<'_, '_, V> { +impl Borrow for ViewContext<'_, V> { fn borrow(&self) -> &AppContext { &*self.window_cx.app } } -impl BorrowMut for ViewContext<'_, '_, V> { +impl BorrowMut for ViewContext<'_, V> { fn borrow_mut(&mut self) -> &mut AppContext { &mut *self.window_cx.app } } -impl Borrow for ViewContext<'_, '_, V> { +impl Borrow for ViewContext<'_, V> { fn borrow(&self) -> &Window { &*self.window_cx.window } } -impl BorrowMut for ViewContext<'_, '_, V> { +impl BorrowMut for ViewContext<'_, V> { fn borrow_mut(&mut self) -> &mut Window { &mut *self.window_cx.window } } -impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { - pub(crate) fn mutable( - app: &'a mut AppContext, - window: &'w mut Window, - view: WeakView, - ) -> Self { +impl<'a, V: 'static> ViewContext<'a, V> { + pub(crate) fn new(app: &'a mut AppContext, window: &'a mut Window, view: &'a View) -> Self { Self { - window_cx: WindowContext::mutable(app, window), + window_cx: WindowContext::new(app, window), view, } } - pub fn view(&self) -> WeakView { + pub fn view(&self) -> View { self.view.clone() } - pub fn model(&self) -> WeakModel { + pub fn model(&self) -> Model { self.view.model.clone() } @@ -1560,40 +1670,50 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { result } - pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + Send + 'static) + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) where - V: Any + Send, + V: 'static, { - let view = self.view().upgrade().unwrap(); + let view = self.view(); self.window_cx.on_next_frame(move |cx| view.update(cx, f)); } + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) { + let view = self.view().downgrade(); + self.window_cx.defer(move |cx| { + view.update(cx, f).ok(); + }); + } + pub fn observe( &mut self, entity: &E, - mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, '_, V>) + Send + 'static, + mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, V>) + 'static, ) -> Subscription where V2: 'static, - V: Any + Send, + V: 'static, E: Entity, { - let view = self.view(); + let view = self.view().downgrade(); let entity_id = entity.entity_id(); let entity = entity.downgrade(); let window_handle = self.window.handle; self.app.observers.insert( entity_id, Box::new(move |cx| { - cx.update_window(window_handle.id, |cx| { - if let Some(handle) = E::upgrade_from(&entity) { - view.update(cx, |this, cx| on_notify(this, handle, cx)) - .is_ok() - } else { - false - } - }) - .unwrap_or(false) + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&entity) { + view.update(cx, |this, cx| on_notify(this, handle, cx)) + .is_ok() + } else { + false + } + }) + .unwrap_or(false) }), ) } @@ -1601,44 +1721,44 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(&mut V, E, &V2::Event, &mut ViewContext<'_, '_, V>) + Send + 'static, + mut on_event: impl FnMut(&mut V, E, &V2::Event, &mut ViewContext<'_, V>) + 'static, ) -> Subscription where V2: EventEmitter, E: Entity, { - let view = self.view(); + let view = self.view().downgrade(); let entity_id = entity.entity_id(); let handle = entity.downgrade(); let window_handle = self.window.handle; self.app.event_listeners.insert( entity_id, Box::new(move |event, cx| { - cx.update_window(window_handle.id, |cx| { - if let Some(handle) = E::upgrade_from(&handle) { - let event = event.downcast_ref().expect("invalid event type"); - view.update(cx, |this, cx| on_event(this, handle, event, cx)) - .is_ok() - } else { - false - } - }) - .unwrap_or(false) + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&handle) { + let event = event.downcast_ref().expect("invalid event type"); + view.update(cx, |this, cx| on_event(this, handle, event, cx)) + .is_ok() + } else { + false + } + }) + .unwrap_or(false) }), ) } pub fn on_release( &mut self, - mut on_release: impl FnMut(&mut V, &mut WindowContext) + Send + 'static, + on_release: impl FnOnce(&mut V, &mut WindowContext) + 'static, ) -> Subscription { let window_handle = self.window.handle; self.app.release_listeners.insert( self.view.model.entity_id, Box::new(move |this, cx| { let this = this.downcast_mut().expect("invalid entity type"); - // todo!("are we okay with silently swallowing the error?") - let _ = cx.update_window(window_handle.id, |cx| on_release(this, cx)); + let _ = window_handle.update(cx, |_, cx| on_release(this, cx)); }), ) } @@ -1646,21 +1766,21 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { pub fn observe_release( &mut self, entity: &E, - mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, '_, V>) + Send + 'static, + mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, V>) + 'static, ) -> Subscription where - V: Any + Send, + V: 'static, V2: 'static, E: Entity, { - let view = self.view(); + let view = self.view().downgrade(); let entity_id = entity.entity_id(); let window_handle = self.window.handle; self.app.release_listeners.insert( entity_id, Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); - let _ = cx.update_window(window_handle.id, |cx| { + let _ = window_handle.update(cx, |_, cx| { view.update(cx, |this, cx| on_release(this, entity, cx)) }); }), @@ -1674,11 +1794,33 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { }); } + pub fn observe_window_bounds( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + self.window.bounds_observers.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), + ) + } + + pub fn observe_window_activation( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + self.window.activation_observers.insert( + (), + Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), + ) + } + pub fn on_focus_changed( &mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + Send + 'static, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().downgrade(); self.window.focus_listeners.push(Box::new(move |event, cx| { handle .update(cx, |view, cx| listener(view, event, cx)) @@ -1694,12 +1836,12 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { let old_stack_len = self.window.key_dispatch_stack.len(); if !self.window.freeze_key_dispatch_stack { for (event_type, listener) in key_listeners { - let handle = self.view(); + let handle = self.view().downgrade(); let listener = Box::new( move |event: &dyn Any, context_stack: &[&DispatchContext], phase: DispatchPhase, - cx: &mut WindowContext<'_, '_>| { + cx: &mut WindowContext<'_>| { handle .update(cx, |view, cx| { listener(view, event, context_stack, phase, cx) @@ -1772,41 +1914,21 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { result } - pub fn run_on_main( - &mut self, - view: &mut V, - f: impl FnOnce(&mut V, &mut MainThread>) -> R + Send + 'static, - ) -> Task> - where - R: Send + 'static, - { - if self.executor.is_main_thread() { - let cx = unsafe { mem::transmute::<&mut Self, &mut MainThread>(self) }; - Task::ready(Ok(f(view, cx))) - } else { - let view = self.view().upgrade().unwrap(); - self.window_cx.run_on_main(move |cx| view.update(cx, f)) - } - } - pub fn spawn( &mut self, - f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut + Send + 'static, + f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, ) -> Task where - R: Send + 'static, - Fut: Future + Send + 'static, + R: 'static, + Fut: Future + 'static, { - let view = self.view(); - self.window_cx.spawn(move |_, cx| { - let result = f(view, cx); - async move { result.await } - }) + let view = self.view().downgrade(); + self.window_cx.spawn(|cx| f(view, cx)) } pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where - G: 'static + Send, + G: 'static, { let mut global = self.app.lease_global::(); let result = f(&mut global, self); @@ -1816,26 +1938,25 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { pub fn observe_global( &mut self, - f: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) + Send + 'static, + f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static, ) -> Subscription { - let window_id = self.window.handle.id; - let handle = self.view(); + let window_handle = self.window.handle; + let view = self.view().downgrade(); self.global_observers.insert( TypeId::of::(), Box::new(move |cx| { - cx.update_window(window_id, |cx| { - handle.update(cx, |view, cx| f(view, cx)).is_ok() - }) - .unwrap_or(false) + window_handle + .update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok()) + .unwrap_or(false) }), ) } pub fn on_mouse_event( &mut self, - handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + Send + 'static, + handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view().upgrade().unwrap(); + let handle = self.view(); self.window_cx.on_mouse_event(move |event, phase, cx| { handle.update(cx, |view, cx| { handler(view, event, phase, cx); @@ -1844,10 +1965,10 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { } } -impl<'a, 'w, V> ViewContext<'a, 'w, V> +impl ViewContext<'_, V> where V: EventEmitter, - V::Event: Any + Send, + V::Event: 'static, { pub fn emit(&mut self, event: V::Event) { let emitter = self.view.model.entity_id; @@ -1858,35 +1979,36 @@ where } } -impl<'a, 'w, V> Context for ViewContext<'a, 'w, V> { - type ModelContext<'b, U> = ModelContext<'b, U>; +impl Context for ViewContext<'_, V> { type Result = U; - fn build_model( + fn build_model( &mut self, - build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T, - ) -> Model - where - T: 'static + Send, - { + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Model { self.window_cx.build_model(build_model) } fn update_model( &mut self, model: &Model, - update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> R { self.window_cx.update_model(model, update) } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.window_cx.update_window(window, update) + } } -impl VisualContext for ViewContext<'_, '_, V> { - type ViewContext<'a, 'w, V2> = ViewContext<'a, 'w, V2>; - - fn build_view( +impl VisualContext for ViewContext<'_, V> { + fn build_view( &mut self, - build_view: impl FnOnce(&mut Self::ViewContext<'_, '_, W>) -> W, + build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> { self.window_cx.build_view(build_view) } @@ -1894,21 +2016,31 @@ impl VisualContext for ViewContext<'_, '_, V> { fn update_view( &mut self, view: &View, - update: impl FnOnce(&mut V2, &mut Self::ViewContext<'_, '_, V2>) -> R, + update: impl FnOnce(&mut V2, &mut ViewContext<'_, V2>) -> R, ) -> Self::Result { self.window_cx.update_view(view, update) } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, + ) -> Self::Result> + where + W: Render, + { + self.window_cx.replace_root_view(build_view) + } } -impl<'a, 'w, V> std::ops::Deref for ViewContext<'a, 'w, V> { - type Target = WindowContext<'a, 'w>; +impl<'a, V> std::ops::Deref for ViewContext<'a, V> { + type Target = WindowContext<'a>; fn deref(&self) -> &Self::Target { &self.window_cx } } -impl<'a, 'w, V> std::ops::DerefMut for ViewContext<'a, 'w, V> { +impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.window_cx } @@ -1923,42 +2055,74 @@ impl WindowId { } } -#[derive(PartialEq, Eq)] +#[derive(Deref, DerefMut)] pub struct WindowHandle { - id: WindowId, + #[deref] + #[deref_mut] + pub(crate) any_handle: AnyWindowHandle, state_type: PhantomData, } -impl Copy for WindowHandle {} - -impl Clone for WindowHandle { - fn clone(&self) -> Self { - WindowHandle { - id: self.id, - state_type: PhantomData, - } - } -} - -impl WindowHandle { +impl WindowHandle { pub fn new(id: WindowId) -> Self { WindowHandle { - id, + any_handle: AnyWindowHandle { + id, + state_type: TypeId::of::(), + }, + state_type: PhantomData, + } + } + + pub fn update( + self, + cx: &mut C, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Result + where + C: Context, + { + cx.update_window(self.any_handle, |root_view, cx| { + let view = root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + Ok(cx.update_view(&view, update)) + })? + } +} + +impl Copy for WindowHandle {} + +impl Clone for WindowHandle { + fn clone(&self) -> Self { + WindowHandle { + any_handle: self.any_handle, state_type: PhantomData, } } } -impl Into for WindowHandle { - fn into(self) -> AnyWindowHandle { - AnyWindowHandle { - id: self.id, - state_type: TypeId::of::(), - } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.any_handle == other.any_handle } } -#[derive(Copy, Clone, PartialEq, Eq)] +impl Eq for WindowHandle {} + +impl Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.any_handle.hash(state); + } +} + +impl Into for WindowHandle { + fn into(self) -> AnyWindowHandle { + self.any_handle + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] pub struct AnyWindowHandle { pub(crate) id: WindowId, state_type: TypeId, @@ -1968,6 +2132,28 @@ impl AnyWindowHandle { pub fn window_id(&self) -> WindowId { self.id } + + pub fn downcast(&self) -> Option> { + if TypeId::of::() == self.state_type { + Some(WindowHandle { + any_handle: *self, + state_type: PhantomData, + }) + } else { + None + } + } + + pub fn update( + self, + cx: &mut C, + update: impl FnOnce(AnyView, &mut WindowContext<'_>) -> R, + ) -> Result + where + C: Context, + { + cx.update_window(self, update) + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui2_macros/src/derive_component.rs b/crates/gpui2_macros/src/derive_component.rs index d1919c8bc4..a946703310 100644 --- a/crates/gpui2_macros/src/derive_component.rs +++ b/crates/gpui2_macros/src/derive_component.rs @@ -30,7 +30,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let expanded = quote! { impl #impl_generics gpui2::Component<#view_type> for #name #ty_generics #where_clause { fn render(self) -> gpui2::AnyElement<#view_type> { - (move |view_state: &mut #view_type, cx: &mut gpui2::ViewContext<'_, '_, #view_type>| self.render(view_state, cx)) + (move |view_state: &mut #view_type, cx: &mut gpui2::ViewContext<'_, #view_type>| self.render(view_state, cx)) .render() } } diff --git a/crates/gpui2_macros/src/test.rs b/crates/gpui2_macros/src/test.rs index b5a2111e19..acaaee597b 100644 --- a/crates/gpui2_macros/src/test.rs +++ b/crates/gpui2_macros/src/test.rs @@ -89,9 +89,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); continue; } - Some("Executor") => { - inner_fn_args.extend(quote!(gpui2::Executor::new( - std::sync::Arc::new(dispatcher.clone()) + Some("BackgroundExecutor") => { + inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new( + std::sync::Arc::new(dispatcher.clone()), ),)); continue; } @@ -105,7 +105,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { { let cx_varname = format_ident!("cx_{}", ix); cx_vars.extend(quote!( - let mut #cx_varname = gpui2::TestAppContext::new( + let mut #cx_varname = gpui::TestAppContext::new( dispatcher.clone() ); )); @@ -130,13 +130,13 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { fn #outer_fn_name() { #inner_fn - gpui2::run_test( + gpui::run_test( #num_iterations as u64, #max_retries, &mut |dispatcher, _seed| { - let executor = gpui2::Executor::new(std::sync::Arc::new(dispatcher.clone())); + let executor = gpui::BackgroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); #cx_vars - executor.block(#inner_fn_name(#inner_fn_args)); + executor.block_test(#inner_fn_name(#inner_fn_args)); #cx_teardowns }, #on_failure_fn_name, @@ -167,10 +167,10 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let cx_varname = format_ident!("cx_{}", ix); let cx_varname_lock = format_ident!("cx_{}_lock", ix); cx_vars.extend(quote!( - let mut #cx_varname = gpui2::TestAppContext::new( + let mut #cx_varname = gpui::TestAppContext::new( dispatcher.clone() ); - let mut #cx_varname_lock = #cx_varname.app.lock(); + let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); )); inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); cx_teardowns.extend(quote!( @@ -182,7 +182,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { Some("TestAppContext") => { let cx_varname = format_ident!("cx_{}", ix); cx_vars.extend(quote!( - let mut #cx_varname = gpui2::TestAppContext::new( + let mut #cx_varname = gpui::TestAppContext::new( dispatcher.clone() ); )); @@ -209,7 +209,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { fn #outer_fn_name() { #inner_fn - gpui2::run_test( + gpui::run_test( #num_iterations as u64, #max_retries, &mut |dispatcher, _seed| { diff --git a/crates/install_cli2/Cargo.toml b/crates/install_cli2/Cargo.toml index 0dd1b907fd..3310e7fbc8 100644 --- a/crates/install_cli2/Cargo.toml +++ b/crates/install_cli2/Cargo.toml @@ -14,5 +14,5 @@ test-support = [] smol.workspace = true anyhow.workspace = true log.workspace = true -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } diff --git a/crates/install_cli2/src/install_cli2.rs b/crates/install_cli2/src/install_cli2.rs index ecdf2a0f2a..7938d60210 100644 --- a/crates/install_cli2/src/install_cli2.rs +++ b/crates/install_cli2/src/install_cli2.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use gpui2::AsyncAppContext; +use gpui::AsyncAppContext; use std::path::Path; use util::ResultExt; @@ -7,9 +7,7 @@ use util::ResultExt; // actions!(cli, [Install]); pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { - let cli_path = cx - .run_on_main(|cx| cx.path_for_auxiliary_executable("cli"))? - .await?; + let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; let link_path = Path::new("/usr/local/bin/zed"); let bin_dir_path = link_path.parent().unwrap(); diff --git a/crates/journal2/Cargo.toml b/crates/journal2/Cargo.toml index 8da2f51a62..72da3deb69 100644 --- a/crates/journal2/Cargo.toml +++ b/crates/journal2/Cargo.toml @@ -10,9 +10,9 @@ doctest = false [dependencies] editor = { path = "../editor" } -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } -workspace = { path = "../workspace" } +workspace2 = { path = "../workspace2" } settings2 = { path = "../settings2" } anyhow.workspace = true diff --git a/crates/journal2/src/journal2.rs b/crates/journal2/src/journal2.rs index 6268548530..20d520e36e 100644 --- a/crates/journal2/src/journal2.rs +++ b/crates/journal2/src/journal2.rs @@ -1,6 +1,6 @@ use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; -use gpui2::AppContext; +use gpui::AppContext; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings2::Settings; @@ -9,7 +9,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use workspace::AppState; +use workspace2::AppState; // use zed::AppState; // todo!(); @@ -59,7 +59,7 @@ pub fn init(_: Arc, cx: &mut AppContext) { // cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx)); } -pub fn new_journal_entry(_: Arc, cx: &mut AppContext) { +pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { let settings = JournalSettings::get_global(cx); let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) { Some(journal_dir) => journal_dir, @@ -77,7 +77,7 @@ pub fn new_journal_entry(_: Arc, cx: &mut AppContext) { let now = now.time(); let _entry_heading = heading_entry(now, &settings.hour_format); - let _create_entry = cx.executor().spawn(async move { + let create_entry = cx.background_executor().spawn(async move { std::fs::create_dir_all(month_dir)?; OpenOptions::new() .create(true) @@ -86,37 +86,38 @@ pub fn new_journal_entry(_: Arc, cx: &mut AppContext) { Ok::<_, std::io::Error>((journal_dir, entry_path)) }); - // todo!("workspace") - // cx.spawn(|cx| async move { - // let (journal_dir, entry_path) = create_entry.await?; - // let (workspace, _) = - // cx.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?; + cx.spawn(|mut cx| async move { + let (journal_dir, entry_path) = create_entry.await?; + let (workspace, _) = cx + .update(|cx| workspace2::open_paths(&[journal_dir], &app_state, None, cx))? + .await?; - // let opened = workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_paths(vec![entry_path], true, cx) - // })? - // .await; + let _opened = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_paths(vec![entry_path], true, cx) + })? + .await; - // if let Some(Some(Ok(item))) = opened.first() { - // if let Some(editor) = item.downcast::().map(|editor| editor.downgrade()) { - // editor.update(&mut cx, |editor, cx| { - // let len = editor.buffer().read(cx).len(cx); - // editor.change_selections(Some(Autoscroll::center()), cx, |s| { - // s.select_ranges([len..len]) - // }); - // if len > 0 { - // editor.insert("\n\n", cx); - // } - // editor.insert(&entry_heading, cx); - // editor.insert("\n\n", cx); - // })?; - // } - // } + // todo!("editor") + // if let Some(Some(Ok(item))) = opened.first() { + // if let Some(editor) = item.downcast::().map(|editor| editor.downgrade()) { + // editor.update(&mut cx, |editor, cx| { + // let len = editor.buffer().read(cx).len(cx); + // editor.change_selections(Some(Autoscroll::center()), cx, |s| { + // s.select_ranges([len..len]) + // }); + // if len > 0 { + // editor.insert("\n\n", cx); + // } + // editor.insert(&entry_heading, cx); + // editor.insert("\n\n", cx); + // })?; + // } + // } - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } fn journal_dir(path: &str) -> Option { diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 1e49d0890f..0e4d9addfa 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -11,28 +11,28 @@ doctest = false [features] test-support = [ "rand", - "client2/test-support", + "client/test-support", "collections/test-support", - "lsp2/test-support", + "lsp/test-support", "text/test-support", "tree-sitter-rust", "tree-sitter-typescript", - "settings2/test-support", + "settings/test-support", "util/test-support", ] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -fuzzy2 = { path = "../fuzzy2" } -git = { path = "../git" } -gpui2 = { path = "../gpui2" } -lsp2 = { path = "../lsp2" } -rpc2 = { path = "../rpc2" } -settings2 = { path = "../settings2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +git = { package = "git3", path = "../git3" } +gpui = { package = "gpui2", path = "../gpui2" } +lsp = { package = "lsp2", path = "../lsp2" } +rpc = { package = "rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } -theme2 = { path = "../theme2" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } anyhow.workspace = true @@ -60,12 +60,12 @@ tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } [dev-dependencies] -client2 = { path = "../client2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -lsp2 = { path = "../lsp2", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } -settings2 = { path = "../settings2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 3ab68d9f44..36c1f39e1c 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -16,8 +16,8 @@ use crate::{ use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use futures::FutureExt as _; -use gpui2::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task}; -use lsp2::LanguageServerId; +use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task}; +use lsp::LanguageServerId; use parking_lot::Mutex; use similar::{ChangeTag, TextDiff}; use smallvec::SmallVec; @@ -40,7 +40,7 @@ use std::{ use sum_tree::TreeMap; use text::operation_queue::OperationQueue; pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *}; -use theme2::SyntaxTheme; +use theme::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; use util::{RangeExt, TryFutureExt as _}; @@ -48,7 +48,7 @@ use util::{RangeExt, TryFutureExt as _}; #[cfg(any(test, feature = "test-support"))] pub use {tree_sitter_rust, tree_sitter_typescript}; -pub use lsp2::DiagnosticSeverity; +pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, @@ -149,14 +149,14 @@ pub struct Completion { pub new_text: String, pub label: CodeLabel, pub server_id: LanguageServerId, - pub lsp_completion: lsp2::CompletionItem, + pub lsp_completion: lsp::CompletionItem, } #[derive(Clone, Debug)] pub struct CodeAction { pub server_id: LanguageServerId, pub range: Range, - pub lsp_action: lsp2::CodeAction, + pub lsp_action: lsp::CodeAction, } #[derive(Clone, Debug, PartialEq)] @@ -226,7 +226,7 @@ pub trait File: Send + Sync { fn as_any(&self) -> &dyn Any; - fn to_proto(&self) -> rpc2::proto::File; + fn to_proto(&self) -> rpc::proto::File; } pub trait LocalFile: File { @@ -375,7 +375,7 @@ impl Buffer { file, ); this.text.set_line_ending(proto::deserialize_line_ending( - rpc2::proto::LineEnding::from_i32(message.line_ending) + rpc::proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, )); this.saved_version = proto::deserialize_version(&message.saved_version); @@ -434,7 +434,7 @@ impl Buffer { )); let text_operations = self.text.operations().clone(); - cx.spawn(|_| async move { + cx.background_executor().spawn(async move { let since = since.unwrap_or_default(); operations.extend( text_operations @@ -652,7 +652,7 @@ impl Buffer { if !self.is_dirty() { let reload = self.reload(cx).log_err().map(drop); - task = cx.executor().spawn(reload); + task = cx.background_executor().spawn(reload); } } } @@ -684,7 +684,7 @@ impl Buffer { let snapshot = self.snapshot(); let mut diff = self.git_diff.clone(); - let diff = cx.executor().spawn(async move { + let diff = cx.background_executor().spawn(async move { diff.update(&diff_base, &snapshot).await; diff }); @@ -793,7 +793,7 @@ impl Buffer { let mut syntax_snapshot = syntax_map.snapshot(); drop(syntax_map); - let parse_task = cx.executor().spawn({ + let parse_task = cx.background_executor().spawn({ let language = language.clone(); let language_registry = language_registry.clone(); async move { @@ -803,7 +803,7 @@ impl Buffer { }); match cx - .executor() + .background_executor() .block_with_timeout(self.sync_parse_timeout, parse_task) { Ok(new_syntax_snapshot) => { @@ -866,9 +866,9 @@ impl Buffer { fn request_autoindent(&mut self, cx: &mut ModelContext) { if let Some(indent_sizes) = self.compute_autoindents() { - let indent_sizes = cx.executor().spawn(indent_sizes); + let indent_sizes = cx.background_executor().spawn(indent_sizes); match cx - .executor() + .background_executor() .block_with_timeout(Duration::from_micros(500), indent_sizes) { Ok(indent_sizes) => self.apply_autoindents(indent_sizes, cx), @@ -1117,7 +1117,7 @@ impl Buffer { pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { let old_text = self.as_rope().clone(); let base_version = self.version(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let old_text = old_text.to_string(); let line_ending = LineEnding::detect(&new_text); LineEnding::normalize(&mut new_text); @@ -1155,7 +1155,7 @@ impl Buffer { let old_text = self.as_rope().clone(); let line_ending = self.line_ending(); let base_version = self.version(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let ranges = trailing_whitespace_ranges(&old_text); let empty = Arc::::from(""); Diff { @@ -3003,14 +3003,14 @@ impl IndentSize { impl Completion { pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { - Some(lsp2::CompletionItemKind::VARIABLE) => 0, + Some(lsp::CompletionItemKind::VARIABLE) => 0, _ => 1, }; (kind_key, &self.label.text[self.label.filter_range.clone()]) } pub fn is_snippet(&self) -> bool { - self.lsp_completion.insert_text_format == Some(lsp2::InsertTextFormat::SNIPPET) + self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) } } diff --git a/crates/language2/src/buffer_tests.rs b/crates/language2/src/buffer_tests.rs index d2d886dd84..c0bd068973 100644 --- a/crates/language2/src/buffer_tests.rs +++ b/crates/language2/src/buffer_tests.rs @@ -5,13 +5,13 @@ use crate::language_settings::{ use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; -use gpui2::{AppContext, Model}; -use gpui2::{Context, TestAppContext}; +use gpui::{AppContext, Model}; +use gpui::{Context, TestAppContext}; use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; -use settings2::SettingsStore; +use settings::SettingsStore; use std::{ env, ops::Range, @@ -38,8 +38,8 @@ fn init_logger() { } } -#[gpui2::test] -fn test_line_endings(cx: &mut gpui2::AppContext) { +#[gpui::test] +fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.build_model(|cx| { @@ -63,7 +63,7 @@ fn test_line_endings(cx: &mut gpui2::AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_select_language() { let registry = Arc::new(LanguageRegistry::test()); registry.add(Arc::new(Language::new( @@ -132,8 +132,8 @@ fn test_select_language() { ); } -#[gpui2::test] -fn test_edit_events(cx: &mut gpui2::AppContext) { +#[gpui::test] +fn test_edit_events(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer_1_events = Arc::new(Mutex::new(Vec::new())); let buffer_2_events = Arc::new(Mutex::new(Vec::new())); @@ -215,7 +215,7 @@ fn test_edit_events(cx: &mut gpui2::AppContext) { ); } -#[gpui2::test] +#[gpui::test] async fn test_apply_diff(cx: &mut TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); @@ -238,8 +238,8 @@ async fn test_apply_diff(cx: &mut TestAppContext) { }); } -#[gpui2::test(iterations = 10)] -async fn test_normalize_whitespace(cx: &mut gpui2::TestAppContext) { +#[gpui::test(iterations = 10)] +async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { let text = [ "zero", // "one ", // 2 trailing spaces @@ -311,8 +311,8 @@ async fn test_normalize_whitespace(cx: &mut gpui2::TestAppContext) { }); } -#[gpui2::test] -async fn test_reparse(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; let buffer = cx.build_model(|cx| { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx) @@ -440,8 +440,8 @@ async fn test_reparse(cx: &mut gpui2::TestAppContext) { ); } -#[gpui2::test] -async fn test_resetting_language(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.build_model(|cx| { let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "{}").with_language(Arc::new(rust_lang()), cx); @@ -463,8 +463,8 @@ async fn test_resetting_language(cx: &mut gpui2::TestAppContext) { assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))"); } -#[gpui2::test] -async fn test_outline(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_outline(cx: &mut gpui::TestAppContext) { let text = r#" struct Person { name: String, @@ -556,10 +556,10 @@ async fn test_outline(cx: &mut gpui2::TestAppContext) { async fn search<'a>( outline: &'a Outline, query: &'a str, - cx: &'a gpui2::TestAppContext, + cx: &'a gpui::TestAppContext, ) -> Vec<(&'a str, Vec)> { let matches = cx - .update(|cx| outline.search(query, cx.executor().clone())) + .update(|cx| outline.search(query, cx.background_executor().clone())) .await; matches .into_iter() @@ -568,8 +568,8 @@ async fn test_outline(cx: &mut gpui2::TestAppContext) { } } -#[gpui2::test] -async fn test_outline_nodes_with_newlines(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { let text = r#" impl A for B< C @@ -595,8 +595,8 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui2::TestAppContext) { ); } -#[gpui2::test] -async fn test_outline_with_extra_context(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { let language = javascript_lang() .with_outline_query( r#" @@ -643,8 +643,8 @@ async fn test_outline_with_extra_context(cx: &mut gpui2::TestAppContext) { ); } -#[gpui2::test] -async fn test_symbols_containing(cx: &mut gpui2::TestAppContext) { +#[gpui::test] +async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { let text = r#" impl Person { fn one() { @@ -731,7 +731,7 @@ async fn test_symbols_containing(cx: &mut gpui2::TestAppContext) { } } -#[gpui2::test] +#[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut AppContext) { let mut assert = |selection_text, range_markers| { assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx) @@ -847,7 +847,7 @@ fn test_enclosing_bracket_ranges(cx: &mut AppContext) { ); } -#[gpui2::test] +#[gpui::test] fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut AppContext) { let mut assert = |selection_text, bracket_pair_texts| { assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx) @@ -879,7 +879,7 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: & ); } -#[gpui2::test] +#[gpui::test] fn test_range_for_syntax_ancestor(cx: &mut AppContext) { cx.build_model(|cx| { let text = "fn a() { b(|c| {}) }"; @@ -918,7 +918,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) { } } -#[gpui2::test] +#[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -959,7 +959,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { init_settings(cx, |settings| { settings.defaults.hard_tabs = Some(true); @@ -1002,7 +1002,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1143,7 +1143,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC eprintln!("DONE"); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1205,7 +1205,7 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1262,7 +1262,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1280,7 +1280,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1322,7 +1322,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_block_mode(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1406,7 +1406,7 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1486,7 +1486,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1530,7 +1530,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_with_injected_languages(cx: &mut AppContext) { init_settings(cx, |settings| { settings.languages.extend([ @@ -1604,7 +1604,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { init_settings(cx, |settings| { settings.defaults.tab_size = Some(2.try_into().unwrap()); @@ -1649,7 +1649,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_language_scope_at_with_javascript(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1738,7 +1738,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_language_scope_at_with_rust(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1806,7 +1806,7 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { }); } -#[gpui2::test] +#[gpui::test] fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { init_settings(cx, |_| {}); @@ -1854,8 +1854,8 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { }); } -#[gpui2::test] -fn test_serialization(cx: &mut gpui2::AppContext) { +#[gpui::test] +fn test_serialization(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer1 = cx.build_model(|cx| { @@ -1879,7 +1879,7 @@ fn test_serialization(cx: &mut gpui2::AppContext) { let state = buffer1.read(cx).to_proto(); let ops = cx - .executor() + .background_executor() .block(buffer1.read(cx).serialize_ops(None, cx)); let buffer2 = cx.build_model(|cx| { let mut buffer = Buffer::from_proto(1, state, None).unwrap(); @@ -1895,7 +1895,7 @@ fn test_serialization(cx: &mut gpui2::AppContext) { assert_eq!(buffer2.read(cx).text(), "abcDF"); } -#[gpui2::test(iterations = 100)] +#[gpui::test(iterations = 100)] fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let min_peers = env::var("MIN_PEERS") .map(|i| i.parse().expect("invalid `MIN_PEERS` variable")) @@ -1921,7 +1921,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let buffer = cx.build_model(|cx| { let state = base_buffer.read(cx).to_proto(); let ops = cx - .executor() + .background_executor() .block(base_buffer.read(cx).serialize_ops(None, cx)); let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap(); buffer @@ -1943,6 +1943,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { .detach(); buffer }); + buffers.push(buffer); replica_ids.push(i as ReplicaId); network.lock().add_peer(i as ReplicaId); @@ -2025,7 +2026,9 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { } 50..=59 if replica_ids.len() < max_peers => { let old_buffer_state = buffer.read(cx).to_proto(); - let old_buffer_ops = cx.executor().block(buffer.read(cx).serialize_ops(None, cx)); + let old_buffer_ops = cx + .background_executor() + .block(buffer.read(cx).serialize_ops(None, cx)); let new_replica_id = (0..=replica_ids.len() as ReplicaId) .filter(|replica_id| *replica_id != buffer.read(cx).replica_id()) .choose(&mut rng) @@ -2196,7 +2199,7 @@ fn test_contiguous_ranges() { ); } -#[gpui2::test(iterations = 500)] +#[gpui::test(iterations = 500)] fn test_trailing_whitespace_ranges(mut rng: StdRng) { // Generate a random multi-line string containing // some lines with trailing whitespace. @@ -2397,7 +2400,7 @@ fn javascript_lang() -> Language { .unwrap() } -fn get_tree_sexp(buffer: &Model, cx: &mut gpui2::TestAppContext) -> String { +fn get_tree_sexp(buffer: &Model, cx: &mut gpui::TestAppContext) -> String { buffer.update(cx, |buffer, _| { let snapshot = buffer.snapshot(); let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); diff --git a/crates/language2/src/diagnostic_set.rs b/crates/language2/src/diagnostic_set.rs index 5247af285e..f269fce88d 100644 --- a/crates/language2/src/diagnostic_set.rs +++ b/crates/language2/src/diagnostic_set.rs @@ -1,6 +1,6 @@ use crate::Diagnostic; use collections::HashMap; -use lsp2::LanguageServerId; +use lsp::LanguageServerId; use std::{ cmp::{Ordering, Reverse}, iter, @@ -37,14 +37,14 @@ pub struct Summary { impl DiagnosticEntry { // Used to provide diagnostic context to lsp codeAction request - pub fn to_lsp_diagnostic_stub(&self) -> lsp2::Diagnostic { + pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { let code = self .diagnostic .code .clone() - .map(lsp2::NumberOrString::String); + .map(lsp::NumberOrString::String); - lsp2::Diagnostic { + lsp::Diagnostic { code, severity: Some(self.diagnostic.severity), ..Default::default() diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index b394d0446e..aeeda546bf 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -1,6 +1,6 @@ -use gpui2::HighlightStyle; +use gpui::HighlightStyle; use std::sync::Arc; -use theme2::SyntaxTheme; +use theme::SyntaxTheme; #[derive(Clone, Debug)] pub struct HighlightMap(Arc<[HighlightId]>); @@ -79,7 +79,7 @@ impl Default for HighlightId { #[cfg(test)] mod tests { use super::*; - use gpui2::rgba; + use gpui::rgba; #[test] fn test_highlight_map() { diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 717a80619b..381284659b 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -17,10 +17,10 @@ use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt as _, }; -use gpui2::{AppContext, AsyncAppContext, Executor, Task}; +use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task}; pub use highlight_map::HighlightMap; use lazy_static::lazy_static; -use lsp2::{CodeActionKind, LanguageServerBinary}; +use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; use postage::watch; use regex::Regex; @@ -42,7 +42,7 @@ use std::{ }, }; use syntax_map::SyntaxSnapshot; -use theme2::{SyntaxTheme, Theme}; +use theme::{SyntaxTheme, ThemeVariant}; use tree_sitter::{self, Query}; use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; @@ -51,7 +51,7 @@ use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; pub use buffer::Operation; pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; -pub use lsp2::LanguageServerId; +pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; pub use text::LineEnding; @@ -98,7 +98,7 @@ lazy_static! { } pub trait ToLspPosition { - fn to_lsp_position(self) -> lsp2::Position; + fn to_lsp_position(self) -> lsp::Position; } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -203,17 +203,17 @@ impl CachedLspAdapter { self.adapter.workspace_configuration(cx) } - pub fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) { + pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { self.adapter.process_diagnostics(params) } - pub async fn process_completion(&self, completion_item: &mut lsp2::CompletionItem) { + pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { self.adapter.process_completion(completion_item).await } pub async fn label_for_completion( &self, - completion_item: &lsp2::CompletionItem, + completion_item: &lsp::CompletionItem, language: &Arc, ) -> Option { self.adapter @@ -224,7 +224,7 @@ impl CachedLspAdapter { pub async fn label_for_symbol( &self, name: &str, - kind: lsp2::SymbolKind, + kind: lsp::SymbolKind, language: &Arc, ) -> Option { self.adapter.label_for_symbol(name, kind, language).await @@ -289,13 +289,13 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - async fn process_completion(&self, _: &mut lsp2::CompletionItem) {} + async fn process_completion(&self, _: &mut lsp::CompletionItem) {} async fn label_for_completion( &self, - _: &lsp2::CompletionItem, + _: &lsp::CompletionItem, _: &Arc, ) -> Option { None @@ -304,7 +304,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn label_for_symbol( &self, _: &str, - _: lsp2::SymbolKind, + _: lsp::SymbolKind, _: &Arc, ) -> Option { None @@ -476,8 +476,8 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D pub struct FakeLspAdapter { pub name: &'static str, pub initialization_options: Option, - pub capabilities: lsp2::ServerCapabilities, - pub initializer: Option>, + pub capabilities: lsp::ServerCapabilities, + pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, pub prettier_plugins: Vec<&'static str>, @@ -532,7 +532,7 @@ pub struct Language { #[cfg(any(test, feature = "test-support"))] fake_adapter: Option<( - mpsc::UnboundedSender, + mpsc::UnboundedSender, Arc, )>, } @@ -631,7 +631,7 @@ pub struct LanguageRegistry { lsp_binary_paths: Mutex< HashMap>>>>, >, - executor: Option, + executor: Option, lsp_binary_status_tx: LspBinaryStatusSender, } @@ -642,14 +642,14 @@ struct LanguageRegistryState { next_available_language_id: AvailableLanguageId, loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), - theme: Option>, + theme: Option>, version: usize, reload_count: usize, } pub struct PendingLanguageServer { pub server_id: LanguageServerId, - pub task: Task>, + pub task: Task>, pub container_dir: Option>, } @@ -680,7 +680,7 @@ impl LanguageRegistry { Self::new(Task::ready(())) } - pub fn set_executor(&mut self, executor: Executor) { + pub fn set_executor(&mut self, executor: BackgroundExecutor) { self.executor = Some(executor); } @@ -743,11 +743,11 @@ impl LanguageRegistry { self.state.read().reload_count } - pub fn set_theme(&self, theme: Arc) { + pub fn set_theme(&self, theme: Arc) { let mut state = self.state.write(); state.theme = Some(theme.clone()); for language in &state.languages { - language.set_theme(&theme.syntax); + language.set_theme(&theme.syntax()); } } @@ -905,7 +905,7 @@ impl LanguageRegistry { if language.fake_adapter.is_some() { let task = cx.spawn(|cx| async move { let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); - let (server, mut fake_server) = lsp2::LanguageServer::fake( + let (server, mut fake_server) = lsp::LanguageServer::fake( fake_adapter.name.to_string(), fake_adapter.capabilities.clone(), cx.clone(), @@ -916,10 +916,10 @@ impl LanguageRegistry { } let servers_tx = servers_tx.clone(); - cx.executor() + cx.background_executor() .spawn(async move { if fake_server - .try_receive_notification::() + .try_receive_notification::() .await .is_some() { @@ -988,7 +988,7 @@ impl LanguageRegistry { task.await?; } - lsp2::LanguageServer::new( + lsp::LanguageServer::new( stderr_capture, server_id, binary, @@ -1048,7 +1048,7 @@ impl LanguageRegistryState { fn add(&mut self, language: Arc) { if let Some(theme) = self.theme.as_ref() { - language.set_theme(&theme.syntax); + language.set_theme(&theme.syntax()); } self.languages.push(language); self.version += 1; @@ -1471,7 +1471,7 @@ impl Language { pub async fn set_fake_lsp_adapter( &mut self, fake_lsp_adapter: Arc, - ) -> mpsc::UnboundedReceiver { + ) -> mpsc::UnboundedReceiver { let (servers_tx, servers_rx) = mpsc::unbounded(); self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await; @@ -1501,7 +1501,7 @@ impl Language { None } - pub async fn process_completion(self: &Arc, completion: &mut lsp2::CompletionItem) { + pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { for adapter in &self.adapters { adapter.process_completion(completion).await; } @@ -1509,7 +1509,7 @@ impl Language { pub async fn label_for_completion( self: &Arc, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, ) -> Option { self.adapters .first() @@ -1521,7 +1521,7 @@ impl Language { pub async fn label_for_symbol( self: &Arc, name: &str, - kind: lsp2::SymbolKind, + kind: lsp::SymbolKind, ) -> Option { self.adapters .first() @@ -1745,7 +1745,7 @@ impl Default for FakeLspAdapter { fn default() -> Self { Self { name: "the-fake-language-server", - capabilities: lsp2::LanguageServer::full_capabilities(), + capabilities: lsp::LanguageServer::full_capabilities(), initializer: None, disk_based_diagnostics_progress_token: None, initialization_options: None, @@ -1794,7 +1794,7 @@ impl LspAdapter for Arc { unreachable!(); } - fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} async fn disk_based_diagnostic_sources(&self) -> Vec { self.disk_based_diagnostics_sources.clone() @@ -1824,22 +1824,22 @@ fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) } } -pub fn point_to_lsp(point: PointUtf16) -> lsp2::Position { - lsp2::Position::new(point.row, point.column) +pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { + lsp::Position::new(point.row, point.column) } -pub fn point_from_lsp(point: lsp2::Position) -> Unclipped { +pub fn point_from_lsp(point: lsp::Position) -> Unclipped { Unclipped(PointUtf16::new(point.line, point.character)) } -pub fn range_to_lsp(range: Range) -> lsp2::Range { - lsp2::Range { +pub fn range_to_lsp(range: Range) -> lsp::Range { + lsp::Range { start: point_to_lsp(range.start), end: point_to_lsp(range.end), } } -pub fn range_from_lsp(range: lsp2::Range) -> Range> { +pub fn range_from_lsp(range: lsp::Range) -> Range> { let mut start = point_from_lsp(range.start); let mut end = point_from_lsp(range.end); if start > end { @@ -1851,9 +1851,9 @@ pub fn range_from_lsp(range: lsp2::Range) -> Range> { #[cfg(test)] mod tests { use super::*; - use gpui2::TestAppContext; + use gpui::TestAppContext; - #[gpui2::test(iterations = 10)] + #[gpui::test(iterations = 10)] async fn test_first_line_pattern(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); @@ -1891,7 +1891,7 @@ mod tests { ); } - #[gpui2::test(iterations = 10)] + #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.executor().clone()); diff --git a/crates/language2/src/language_settings.rs b/crates/language2/src/language_settings.rs index 4816e506db..49977f690c 100644 --- a/crates/language2/src/language_settings.rs +++ b/crates/language2/src/language_settings.rs @@ -2,13 +2,13 @@ use crate::{File, Language}; use anyhow::Result; use collections::{HashMap, HashSet}; use globset::GlobMatcher; -use gpui2::AppContext; +use gpui::AppContext; use schemars::{ schema::{InstanceType, ObjectValidation, Schema, SchemaObject}, JsonSchema, }; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use std::{num::NonZeroU32, path::Path, sync::Arc}; pub fn init(cx: &mut AppContext) { @@ -255,7 +255,7 @@ impl InlayHintKind { } } -impl settings2::Settings for AllLanguageSettings { +impl settings::Settings for AllLanguageSettings { const KEY: Option<&'static str> = None; type FileContent = AllLanguageSettingsContent; @@ -332,7 +332,7 @@ impl settings2::Settings for AllLanguageSettings { fn json_schema( generator: &mut schemars::gen::SchemaGenerator, - params: &settings2::SettingsJsonSchemaParams, + params: &settings::SettingsJsonSchemaParams, _: &AppContext, ) -> schemars::schema::RootSchema { let mut root_schema = generator.root_schema_for::(); diff --git a/crates/language2/src/outline.rs b/crates/language2/src/outline.rs index dd3a4acf6b..4bcbdcd27f 100644 --- a/crates/language2/src/outline.rs +++ b/crates/language2/src/outline.rs @@ -1,5 +1,5 @@ -use fuzzy2::{StringMatch, StringMatchCandidate}; -use gpui2::{Executor, HighlightStyle}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{BackgroundExecutor, HighlightStyle}; use std::ops::Range; #[derive(Debug)] @@ -57,11 +57,11 @@ impl Outline { } } - pub async fn search(&self, query: &str, executor: Executor) -> Vec { + pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec { let query = query.trim_start(); let is_path_query = query.contains(' '); let smart_case = query.chars().any(|c| c.is_uppercase()); - let mut matches = fuzzy2::match_strings( + let mut matches = fuzzy::match_strings( if is_path_query { &self.path_candidates } else { diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index f90bb94742..c4abe39d47 100644 --- a/crates/language2/src/proto.rs +++ b/crates/language2/src/proto.rs @@ -4,8 +4,8 @@ use crate::{ }; use anyhow::{anyhow, Result}; use clock::ReplicaId; -use lsp2::{DiagnosticSeverity, LanguageServerId}; -use rpc2::proto; +use lsp::{DiagnosticSeverity, LanguageServerId}; +use rpc::proto; use std::{ops::Range, sync::Arc}; use text::*; diff --git a/crates/language2/src/syntax_map.rs b/crates/language2/src/syntax_map.rs index 4abb9afe7e..18f2e9b264 100644 --- a/crates/language2/src/syntax_map.rs +++ b/crates/language2/src/syntax_map.rs @@ -234,7 +234,6 @@ impl SyntaxMap { self.snapshot.interpolate(text); } - #[allow(dead_code)] // todo!() #[cfg(test)] pub fn reparse(&mut self, language: Arc, text: &BufferSnapshot) { self.snapshot @@ -786,7 +785,6 @@ impl SyntaxSnapshot { ) } - #[allow(dead_code)] // todo!() #[cfg(test)] pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec { self.layers_for_range(0..buffer.len(), buffer).collect() diff --git a/crates/language2/src/syntax_map/syntax_map_tests.rs b/crates/language2/src/syntax_map/syntax_map_tests.rs index 732ed7e936..bd50608122 100644 --- a/crates/language2/src/syntax_map/syntax_map_tests.rs +++ b/crates/language2/src/syntax_map/syntax_map_tests.rs @@ -78,7 +78,7 @@ fn test_splice_included_ranges() { } } -#[gpui2::test] +#[gpui::test] fn test_syntax_map_layers_for_range() { let registry = Arc::new(LanguageRegistry::test()); let language = Arc::new(rust_lang()); @@ -175,7 +175,7 @@ fn test_syntax_map_layers_for_range() { ); } -#[gpui2::test] +#[gpui::test] fn test_dynamic_language_injection() { let registry = Arc::new(LanguageRegistry::test()); let markdown = Arc::new(markdown_lang()); @@ -253,7 +253,7 @@ fn test_dynamic_language_injection() { assert!(!syntax_map.contains_unknown_injections()); } -#[gpui2::test] +#[gpui::test] fn test_typing_multiple_new_injections() { let (buffer, syntax_map) = test_edit_sequence( "Rust", @@ -282,7 +282,7 @@ fn test_typing_multiple_new_injections() { ); } -#[gpui2::test] +#[gpui::test] fn test_pasting_new_injection_line_between_others() { let (buffer, syntax_map) = test_edit_sequence( "Rust", @@ -329,7 +329,7 @@ fn test_pasting_new_injection_line_between_others() { ); } -#[gpui2::test] +#[gpui::test] fn test_joining_injections_with_child_injections() { let (buffer, syntax_map) = test_edit_sequence( "Rust", @@ -373,7 +373,7 @@ fn test_joining_injections_with_child_injections() { ); } -#[gpui2::test] +#[gpui::test] fn test_editing_edges_of_injection() { test_edit_sequence( "Rust", @@ -402,7 +402,7 @@ fn test_editing_edges_of_injection() { ); } -#[gpui2::test] +#[gpui::test] fn test_edits_preceding_and_intersecting_injection() { test_edit_sequence( "Rust", @@ -414,7 +414,7 @@ fn test_edits_preceding_and_intersecting_injection() { ); } -#[gpui2::test] +#[gpui::test] fn test_non_local_changes_create_injections() { test_edit_sequence( "Rust", @@ -433,7 +433,7 @@ fn test_non_local_changes_create_injections() { ); } -#[gpui2::test] +#[gpui::test] fn test_creating_many_injections_in_one_edit() { test_edit_sequence( "Rust", @@ -463,7 +463,7 @@ fn test_creating_many_injections_in_one_edit() { ); } -#[gpui2::test] +#[gpui::test] fn test_editing_across_injection_boundary() { test_edit_sequence( "Rust", @@ -491,7 +491,7 @@ fn test_editing_across_injection_boundary() { ); } -#[gpui2::test] +#[gpui::test] fn test_removing_injection_by_replacing_across_boundary() { test_edit_sequence( "Rust", @@ -517,7 +517,7 @@ fn test_removing_injection_by_replacing_across_boundary() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_simple() { let (buffer, syntax_map) = test_edit_sequence( "ERB", @@ -564,7 +564,7 @@ fn test_combined_injections_simple() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_empty_ranges() { test_edit_sequence( "ERB", @@ -582,7 +582,7 @@ fn test_combined_injections_empty_ranges() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_edit_edges_of_ranges() { let (buffer, syntax_map) = test_edit_sequence( "ERB", @@ -613,7 +613,7 @@ fn test_combined_injections_edit_edges_of_ranges() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_splitting_some_injections() { let (_buffer, _syntax_map) = test_edit_sequence( "ERB", @@ -638,7 +638,7 @@ fn test_combined_injections_splitting_some_injections() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_editing_after_last_injection() { test_edit_sequence( "ERB", @@ -658,7 +658,7 @@ fn test_combined_injections_editing_after_last_injection() { ); } -#[gpui2::test] +#[gpui::test] fn test_combined_injections_inside_injections() { let (buffer, syntax_map) = test_edit_sequence( "Markdown", @@ -734,7 +734,7 @@ fn test_combined_injections_inside_injections() { ); } -#[gpui2::test] +#[gpui::test] fn test_empty_combined_injections_inside_injections() { let (buffer, syntax_map) = test_edit_sequence( "Markdown", @@ -762,7 +762,7 @@ fn test_empty_combined_injections_inside_injections() { ); } -#[gpui2::test(iterations = 50)] +#[gpui::test(iterations = 50)] fn test_random_syntax_map_edits_rust_macros(rng: StdRng) { let text = r#" fn test_something() { @@ -788,7 +788,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) { test_random_edits(text, registry, language, rng); } -#[gpui2::test(iterations = 50)] +#[gpui::test(iterations = 50)] fn test_random_syntax_map_edits_with_erb(rng: StdRng) { let text = r#"
@@ -817,7 +817,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) { test_random_edits(text, registry, language, rng); } -#[gpui2::test(iterations = 50)] +#[gpui::test(iterations = 50)] fn test_random_syntax_map_edits_with_heex(rng: StdRng) { let text = r#" defmodule TheModule do diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index b925bc8f0d..85ae088565 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "ce20dc083ee485524b802669890291c0d8090170", - "version": "1.22.1" + "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", + "version": "1.21.0" } } ] diff --git a/crates/live_kit_client2/.cargo/config.toml b/crates/live_kit_client2/.cargo/config.toml new file mode 100644 index 0000000000..b33fe211bd --- /dev/null +++ b/crates/live_kit_client2/.cargo/config.toml @@ -0,0 +1,2 @@ +[live_kit_client_test] +rustflags = ["-C", "link-args=-ObjC"] diff --git a/crates/live_kit_client2/Cargo.toml b/crates/live_kit_client2/Cargo.toml new file mode 100644 index 0000000000..5adb711948 --- /dev/null +++ b/crates/live_kit_client2/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "live_kit_client2" +version = "0.1.0" +edition = "2021" +description = "Bindings to LiveKit Swift client SDK" +publish = false + +[lib] +path = "src/live_kit_client2.rs" +doctest = false + +[[example]] +name = "test_app" + +[features] +test-support = [ + "async-trait", + "collections/test-support", + "gpui2/test-support", + "live_kit_server", + "nanoid", +] + +[dependencies] +collections = { path = "../collections", optional = true } +gpui2 = { package = "gpui2", path = "../gpui2", optional = true } +live_kit_server = { path = "../live_kit_server", optional = true } +media = { path = "../media" } + +anyhow.workspace = true +async-broadcast = "0.4" +core-foundation = "0.9.3" +core-graphics = "0.22.3" +futures.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true + +async-trait = { workspace = true, optional = true } +nanoid = { version ="0.4", optional = true} + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui2 = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +live_kit_server = { path = "../live_kit_server" } +media = { path = "../media" } +nanoid = "0.4" + +anyhow.workspace = true +async-trait.workspace = true +block = "0.1" +bytes = "1.2" +byteorder = "1.4" +cocoa = "0.24" +core-foundation = "0.9.3" +core-graphics = "0.22.3" +foreign-types = "0.3" +futures.workspace = true +hmac = "0.12" +jwt = "0.16" +objc = "0.2" +parking_lot.workspace = true +serde.workspace = true +serde_derive.workspace = true +sha2 = "0.10" +simplelog = "0.9" + +[build-dependencies] +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true diff --git a/crates/live_kit_client2/LiveKitBridge2/Package.resolved b/crates/live_kit_client2/LiveKitBridge2/Package.resolved new file mode 100644 index 0000000000..b925bc8f0d --- /dev/null +++ b/crates/live_kit_client2/LiveKitBridge2/Package.resolved @@ -0,0 +1,52 @@ +{ + "object": { + "pins": [ + { + "package": "LiveKit", + "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", + "state": { + "branch": null, + "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", + "version": "1.0.12" + } + }, + { + "package": "Promises", + "repositoryURL": "https://github.com/google/promises.git", + "state": { + "branch": null, + "revision": "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a", + "version": "2.2.0" + } + }, + { + "package": "WebRTC", + "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", + "state": { + "branch": null, + "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", + "version": "104.5112.17" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "32e8d724467f8fe623624570367e3d50c5638e46", + "version": "1.5.2" + } + }, + { + "package": "SwiftProtobuf", + "repositoryURL": "https://github.com/apple/swift-protobuf.git", + "state": { + "branch": null, + "revision": "ce20dc083ee485524b802669890291c0d8090170", + "version": "1.22.1" + } + } + ] + }, + "version": 1 +} diff --git a/crates/live_kit_client2/LiveKitBridge2/Package.swift b/crates/live_kit_client2/LiveKitBridge2/Package.swift new file mode 100644 index 0000000000..890eaa2f6d --- /dev/null +++ b/crates/live_kit_client2/LiveKitBridge2/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.5 + +import PackageDescription + +let package = Package( + name: "LiveKitBridge2", + platforms: [ + .macOS(.v10_15) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "LiveKitBridge2", + type: .static, + targets: ["LiveKitBridge2"]), + ], + dependencies: [ + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "LiveKitBridge2", + dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]), + ] +) diff --git a/crates/live_kit_client2/LiveKitBridge2/README.md b/crates/live_kit_client2/LiveKitBridge2/README.md new file mode 100644 index 0000000000..1fceed8165 --- /dev/null +++ b/crates/live_kit_client2/LiveKitBridge2/README.md @@ -0,0 +1,3 @@ +# LiveKitBridge2 + +A description of this package. diff --git a/crates/live_kit_client2/LiveKitBridge2/Sources/LiveKitBridge2/LiveKitBridge2.swift b/crates/live_kit_client2/LiveKitBridge2/Sources/LiveKitBridge2/LiveKitBridge2.swift new file mode 100644 index 0000000000..5f22acf581 --- /dev/null +++ b/crates/live_kit_client2/LiveKitBridge2/Sources/LiveKitBridge2/LiveKitBridge2.swift @@ -0,0 +1,327 @@ +import Foundation +import LiveKit +import WebRTC +import ScreenCaptureKit + +class LKRoomDelegate: RoomDelegate { + var data: UnsafeRawPointer + var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void + var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void + var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void + var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void + var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void + var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void + var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void + + init( + data: UnsafeRawPointer, + onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, + onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void, + onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) + { + self.data = data + self.onDidDisconnect = onDidDisconnect + self.onDidSubscribeToRemoteAudioTrack = onDidSubscribeToRemoteAudioTrack + self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack + self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack + self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack + self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack + self.onActiveSpeakersChanged = onActiveSpeakersChanged + } + + func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { + if connectionState.isDisconnected { + self.onDidDisconnect(self.data) + } + } + + func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) { + if track.kind == .video { + self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) + } else if track.kind == .audio { + self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque()) + } + } + + func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) { + if publication.kind == .audio { + self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted) + } + } + + func room(_ room: Room, didUpdate speakers: [Participant]) { + guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return } + self.onActiveSpeakersChanged(self.data, speaker_ids) + } + + func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) { + if track.kind == .video { + self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString) + } else if track.kind == .audio { + self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString) + } + } +} + +class LKVideoRenderer: NSObject, VideoRenderer { + var data: UnsafeRawPointer + var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool + var onDrop: @convention(c) (UnsafeRawPointer) -> Void + var adaptiveStreamIsEnabled: Bool = false + var adaptiveStreamSize: CGSize = .zero + weak var track: VideoTrack? + + init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) { + self.data = data + self.onFrame = onFrame + self.onDrop = onDrop + } + + deinit { + self.onDrop(self.data) + } + + func setSize(_ size: CGSize) { + } + + func renderFrame(_ frame: RTCVideoFrame?) { + let buffer = frame?.buffer as? RTCCVPixelBuffer + if let pixelBuffer = buffer?.pixelBuffer { + if !self.onFrame(self.data, pixelBuffer) { + DispatchQueue.main.async { + self.track?.remove(videoRenderer: self) + } + } + } + } +} + +@_cdecl("LKRoomDelegateCreate") +public func LKRoomDelegateCreate( + data: UnsafeRawPointer, + onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, + onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void, + onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void +) -> UnsafeMutableRawPointer { + let delegate = LKRoomDelegate( + data: data, + onDidDisconnect: onDidDisconnect, + onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack, + onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack, + onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack, + onActiveSpeakersChanged: onActiveSpeakerChanged, + onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, + onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack + ) + return Unmanaged.passRetained(delegate).toOpaque() +} + +@_cdecl("LKRoomCreate") +public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer { + let delegate = Unmanaged.fromOpaque(delegate).takeUnretainedValue() + return Unmanaged.passRetained(Room(delegate: delegate)).toOpaque() +} + +@_cdecl("LKRoomConnect") +public func LKRoomConnect(room: UnsafeRawPointer, url: CFString, token: CFString, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + room.connect(url as String, token as String).then { _ in + callback(callback_data, UnsafeRawPointer(nil) as! CFString?) + }.catch { error in + callback(callback_data, error.localizedDescription as CFString) + } +} + +@_cdecl("LKRoomDisconnect") +public func LKRoomDisconnect(room: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + room.disconnect() +} + +@_cdecl("LKRoomPublishVideoTrack") +public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + room.localParticipant?.publishVideoTrack(track: track).then { publication in + callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil) + }.catch { error in + callback(callback_data, nil, error.localizedDescription as CFString) + } +} + +@_cdecl("LKRoomPublishAudioTrack") +public func LKRoomPublishAudioTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + room.localParticipant?.publishAudioTrack(track: track).then { publication in + callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil) + }.catch { error in + callback(callback_data, nil, error.localizedDescription as CFString) + } +} + + +@_cdecl("LKRoomUnpublishTrack") +public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + let _ = room.localParticipant?.unpublish(publication: publication) +} + +@_cdecl("LKRoomAudioTracksForRemoteParticipant") +public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + for (_, participant) in room.remoteParticipants { + if participant.identity == participantId as String { + return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray? + } + } + + return nil; +} + +@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant") +public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + for (_, participant) in room.remoteParticipants { + if participant.identity == participantId as String { + return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray? + } + } + + return nil; +} + +@_cdecl("LKRoomVideoTracksForRemoteParticipant") +public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + for (_, participant) in room.remoteParticipants { + if participant.identity == participantId as String { + return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray? + } + } + + return nil; +} + +@_cdecl("LKLocalAudioTrackCreateTrack") +public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer { + let track = LocalAudioTrack.createTrack(options: AudioCaptureOptions( + echoCancellation: true, + noiseSuppression: true + )) + + return Unmanaged.passRetained(track).toOpaque() +} + + +@_cdecl("LKCreateScreenShareTrackForDisplay") +public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + let display = Unmanaged.fromOpaque(display).takeUnretainedValue() + let track = LocalVideoTrack.createMacOSScreenShareTrack(source: display, preferredMethod: .legacy) + return Unmanaged.passRetained(track).toOpaque() +} + +@_cdecl("LKVideoRendererCreate") +public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer { + Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque() +} + +@_cdecl("LKVideoTrackAddRenderer") +public func LKVideoTrackAddRenderer(track: UnsafeRawPointer, renderer: UnsafeRawPointer) { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() as! VideoTrack + let renderer = Unmanaged.fromOpaque(renderer).takeRetainedValue() + renderer.track = track + track.add(videoRenderer: renderer) +} + +@_cdecl("LKRemoteVideoTrackGetSid") +public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + return track.sid! as CFString +} + +@_cdecl("LKRemoteAudioTrackGetSid") +public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + return track.sid! as CFString +} + +@_cdecl("LKDisplaySources") +public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) { + MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in + callback(data, displaySources as CFArray, nil) + }.catch { error in + callback(data, nil, error.localizedDescription as CFString) + } +} + +@_cdecl("LKLocalTrackPublicationSetMute") +public func LKLocalTrackPublicationSetMute( + publication: UnsafeRawPointer, + muted: Bool, + on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, + callback_data: UnsafeRawPointer +) { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + if muted { + publication.mute().then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } + } else { + publication.unmute().then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } + } +} + +@_cdecl("LKRemoteTrackPublicationSetEnabled") +public func LKRemoteTrackPublicationSetEnabled( + publication: UnsafeRawPointer, + enabled: Bool, + on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, + callback_data: UnsafeRawPointer +) { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + publication.set(enabled: enabled).then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } +} + +@_cdecl("LKRemoteTrackPublicationIsMuted") +public func LKRemoteTrackPublicationIsMuted( + publication: UnsafeRawPointer +) -> Bool { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + return publication.muted +} + +@_cdecl("LKRemoteTrackPublicationGetSid") +public func LKRemoteTrackPublicationGetSid( + publication: UnsafeRawPointer +) -> CFString { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + return publication.sid as CFString +} diff --git a/crates/live_kit_client2/build.rs b/crates/live_kit_client2/build.rs new file mode 100644 index 0000000000..b346b3168b --- /dev/null +++ b/crates/live_kit_client2/build.rs @@ -0,0 +1,172 @@ +use serde::Deserialize; +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge2"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwiftTargetInfo { + pub triple: String, + pub unversioned_triple: String, + pub module_triple: String, + pub swift_runtime_compatibility_version: String, + #[serde(rename = "librariesRequireRPath")] + pub libraries_require_rpath: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwiftPaths { + pub runtime_library_paths: Vec, + pub runtime_library_import_paths: Vec, + pub runtime_resource_path: String, +} + +#[derive(Debug, Deserialize)] +pub struct SwiftTarget { + pub target: SwiftTargetInfo, + pub paths: SwiftPaths, +} + +const MACOS_TARGET_VERSION: &str = "10.15.7"; + +fn main() { + if cfg!(not(any(test, feature = "test-support"))) { + let swift_target = get_swift_target(); + + build_bridge(&swift_target); + link_swift_stdlib(&swift_target); + link_webrtc_framework(&swift_target); + + // Register exported Objective-C selectors, protocols, etc when building example binaries. + println!("cargo:rustc-link-arg=-Wl,-ObjC"); + } +} + +fn build_bridge(swift_target: &SwiftTarget) { + println!("cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET"); + println!("cargo:rerun-if-changed={}/Sources", SWIFT_PACKAGE_NAME); + println!( + "cargo:rerun-if-changed={}/Package.swift", + SWIFT_PACKAGE_NAME + ); + println!( + "cargo:rerun-if-changed={}/Package.resolved", + SWIFT_PACKAGE_NAME + ); + + let swift_package_root = swift_package_root(); + let swift_target_folder = swift_target_folder(); + if !Command::new("swift") + .arg("build") + .arg("--disable-automatic-resolution") + .args(["--configuration", &env::var("PROFILE").unwrap()]) + .args(["--triple", &swift_target.target.triple]) + .args(["--build-path".into(), swift_target_folder]) + .current_dir(&swift_package_root) + .status() + .unwrap() + .success() + { + panic!( + "Failed to compile swift package in {}", + swift_package_root.display() + ); + } + + println!( + "cargo:rustc-link-search=native={}", + swift_target.out_dir_path().display() + ); + println!("cargo:rustc-link-lib=static={}", SWIFT_PACKAGE_NAME); +} + +fn link_swift_stdlib(swift_target: &SwiftTarget) { + for path in &swift_target.paths.runtime_library_paths { + println!("cargo:rustc-link-search=native={}", path); + } +} + +fn link_webrtc_framework(swift_target: &SwiftTarget) { + let swift_out_dir_path = swift_target.out_dir_path(); + println!("cargo:rustc-link-lib=framework=WebRTC"); + println!( + "cargo:rustc-link-search=framework={}", + swift_out_dir_path.display() + ); + // Find WebRTC.framework as a sibling of the executable when running tests. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); + // Find WebRTC.framework in parent directory of the executable when running examples. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/.."); + + let source_path = swift_out_dir_path.join("WebRTC.framework"); + let deps_dir_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../deps/WebRTC.framework"); + let target_dir_path = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../WebRTC.framework"); + copy_dir(&source_path, &deps_dir_path); + copy_dir(&source_path, &target_dir_path); +} + +fn get_swift_target() -> SwiftTarget { + let mut arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if arch == "aarch64" { + arch = "arm64".into(); + } + let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION); + + let swift_target_info_str = Command::new("swift") + .args(["-target", &target, "-print-target-info"]) + .output() + .unwrap() + .stdout; + + serde_json::from_slice(&swift_target_info_str).unwrap() +} + +fn swift_package_root() -> PathBuf { + env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME) +} + +fn swift_target_folder() -> PathBuf { + env::current_dir() + .unwrap() + .join(format!("../../target/{SWIFT_PACKAGE_NAME}")) +} + +fn copy_dir(source: &Path, destination: &Path) { + assert!( + Command::new("rm") + .arg("-rf") + .arg(destination) + .status() + .unwrap() + .success(), + "could not remove {:?} before copying", + destination + ); + + assert!( + Command::new("cp") + .arg("-R") + .args([source, destination]) + .status() + .unwrap() + .success(), + "could not copy {:?} to {:?}", + source, + destination + ); +} + +impl SwiftTarget { + fn out_dir_path(&self) -> PathBuf { + swift_target_folder() + .join(&self.target.unversioned_triple) + .join(env::var("PROFILE").unwrap()) + } +} diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app.rs new file mode 100644 index 0000000000..4062441a06 --- /dev/null +++ b/crates/live_kit_client2/examples/test_app.rs @@ -0,0 +1,178 @@ +use std::{sync::Arc, time::Duration}; + +use futures::StreamExt; +use gpui2::KeyBinding; +use live_kit_client2::{ + LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, +}; +use live_kit_server::token::{self, VideoGrant}; +use log::LevelFilter; +use serde_derive::Deserialize; +use simplelog::SimpleLogger; + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +struct Quit; + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + gpui2::App::production(Arc::new(())).run(|cx| { + #[cfg(any(test, feature = "test-support"))] + println!("USING TEST LIVEKIT"); + + #[cfg(not(any(test, feature = "test-support")))] + println!("USING REAL LIVEKIT"); + + cx.activate(true); + + cx.on_action(quit); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + + // todo!() + // cx.set_menus(vec![Menu { + // name: "Zed", + // items: vec![MenuItem::Action { + // name: "Quit", + // action: Box::new(Quit), + // os_action: None, + // }], + // }]); + + let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into()); + let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into()); + let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap_or("secret".into()); + + cx.spawn(|cx| async move { + let user_a_token = token::create( + &live_kit_key, + &live_kit_secret, + Some("test-participant-1"), + VideoGrant::to_join("test-room"), + ) + .unwrap(); + let room_a = Room::new(); + room_a.connect(&live_kit_url, &user_a_token).await.unwrap(); + + let user2_token = token::create( + &live_kit_key, + &live_kit_secret, + Some("test-participant-2"), + VideoGrant::to_join("test-room"), + ) + .unwrap(); + let room_b = Room::new(); + room_b.connect(&live_kit_url, &user2_token).await.unwrap(); + + let mut audio_track_updates = room_b.remote_audio_track_updates(); + let audio_track = LocalAudioTrack::create(); + let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap(); + + if let RemoteAudioTrackUpdate::Subscribed(track, _) = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); + assert_eq!(remote_tracks.len(), 1); + assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1"); + assert_eq!(track.publisher_id(), "test-participant-1"); + } else { + panic!("unexpected message"); + } + + audio_track_publication.set_mute(true).await.unwrap(); + + println!("waiting for mute changed!"); + if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); + assert_eq!(remote_tracks[0].sid(), track_id); + assert_eq!(muted, true); + } else { + panic!("unexpected message"); + } + + audio_track_publication.set_mute(false).await.unwrap(); + + if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); + assert_eq!(remote_tracks[0].sid(), track_id); + assert_eq!(muted, false); + } else { + panic!("unexpected message"); + } + + println!("Pausing for 5 seconds to test audio, make some noise!"); + let timer = cx.background_executor().timer(Duration::from_secs(5)); + timer.await; + let remote_audio_track = room_b + .remote_audio_tracks("test-participant-1") + .pop() + .unwrap(); + room_a.unpublish_track(audio_track_publication); + + // Clear out any active speakers changed messages + let mut next = audio_track_updates.next().await.unwrap(); + while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next { + println!("Speakers changed: {:?}", speakers); + next = audio_track_updates.next().await.unwrap(); + } + + if let RemoteAudioTrackUpdate::Unsubscribed { + publisher_id, + track_id, + } = next + { + assert_eq!(publisher_id, "test-participant-1"); + assert_eq!(remote_audio_track.sid(), track_id); + assert_eq!(room_b.remote_audio_tracks("test-participant-1").len(), 0); + } else { + panic!("unexpected message"); + } + + let mut video_track_updates = room_b.remote_video_track_updates(); + let displays = room_a.display_sources().await.unwrap(); + let display = displays.into_iter().next().unwrap(); + + let local_video_track = LocalVideoTrack::screen_share_for_display(&display); + let local_video_track_publication = + room_a.publish_video_track(local_video_track).await.unwrap(); + + if let RemoteVideoTrackUpdate::Subscribed(track) = + video_track_updates.next().await.unwrap() + { + let remote_video_tracks = room_b.remote_video_tracks("test-participant-1"); + assert_eq!(remote_video_tracks.len(), 1); + assert_eq!(remote_video_tracks[0].publisher_id(), "test-participant-1"); + assert_eq!(track.publisher_id(), "test-participant-1"); + } else { + panic!("unexpected message"); + } + + let remote_video_track = room_b + .remote_video_tracks("test-participant-1") + .pop() + .unwrap(); + room_a.unpublish_track(local_video_track_publication); + if let RemoteVideoTrackUpdate::Unsubscribed { + publisher_id, + track_id, + } = video_track_updates.next().await.unwrap() + { + assert_eq!(publisher_id, "test-participant-1"); + assert_eq!(remote_video_track.sid(), track_id); + assert_eq!(room_b.remote_video_tracks("test-participant-1").len(), 0); + } else { + panic!("unexpected message"); + } + + cx.update(|cx| cx.quit()).ok(); + }) + .detach(); + }); +} + +fn quit(_: &Quit, cx: &mut gpui2::AppContext) { + cx.quit(); +} diff --git a/crates/live_kit_client2/src/live_kit_client2.rs b/crates/live_kit_client2/src/live_kit_client2.rs new file mode 100644 index 0000000000..47cc3873ff --- /dev/null +++ b/crates/live_kit_client2/src/live_kit_client2.rs @@ -0,0 +1,11 @@ +#[cfg(not(any(test, feature = "test-support")))] +pub mod prod; + +#[cfg(not(any(test, feature = "test-support")))] +pub use prod::*; + +#[cfg(any(test, feature = "test-support"))] +pub mod test; + +#[cfg(any(test, feature = "test-support"))] +pub use test::*; diff --git a/crates/live_kit_client2/src/prod.rs b/crates/live_kit_client2/src/prod.rs new file mode 100644 index 0000000000..b2b83e95fc --- /dev/null +++ b/crates/live_kit_client2/src/prod.rs @@ -0,0 +1,947 @@ +use anyhow::{anyhow, Context, Result}; +use core_foundation::{ + array::{CFArray, CFArrayRef}, + base::{CFRelease, CFRetain, TCFType}, + string::{CFString, CFStringRef}, +}; +use futures::{ + channel::{mpsc, oneshot}, + Future, +}; +pub use media::core_video::CVImageBuffer; +use media::core_video::CVImageBufferRef; +use parking_lot::Mutex; +use postage::watch; +use std::{ + ffi::c_void, + sync::{Arc, Weak}, +}; + +// SAFETY: Most live kit types are threadsafe: +// https://github.com/livekit/client-sdk-swift#thread-safety +macro_rules! pointer_type { + ($pointer_name:ident) => { + #[repr(transparent)] + #[derive(Copy, Clone, Debug)] + pub struct $pointer_name(pub *const std::ffi::c_void); + unsafe impl Send for $pointer_name {} + }; +} + +mod swift { + pointer_type!(Room); + pointer_type!(LocalAudioTrack); + pointer_type!(RemoteAudioTrack); + pointer_type!(LocalVideoTrack); + pointer_type!(RemoteVideoTrack); + pointer_type!(LocalTrackPublication); + pointer_type!(RemoteTrackPublication); + pointer_type!(MacOSDisplay); + pointer_type!(RoomDelegate); +} + +extern "C" { + fn LKRoomDelegateCreate( + callback_data: *mut c_void, + on_did_disconnect: extern "C" fn(callback_data: *mut c_void), + on_did_subscribe_to_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + remote_track: swift::RemoteAudioTrack, + remote_publication: swift::RemoteTrackPublication, + ), + on_did_unsubscribe_from_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ), + on_mute_changed_from_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + track_id: CFStringRef, + muted: bool, + ), + on_active_speakers_changed: extern "C" fn( + callback_data: *mut c_void, + participants: CFArrayRef, + ), + on_did_subscribe_to_remote_video_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + remote_track: swift::RemoteVideoTrack, + ), + on_did_unsubscribe_from_remote_video_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ), + ) -> swift::RoomDelegate; + + fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room; + fn LKRoomConnect( + room: swift::Room, + url: CFStringRef, + token: CFStringRef, + callback: extern "C" fn(*mut c_void, CFStringRef), + callback_data: *mut c_void, + ); + fn LKRoomDisconnect(room: swift::Room); + fn LKRoomPublishVideoTrack( + room: swift::Room, + track: swift::LocalVideoTrack, + callback: extern "C" fn(*mut c_void, swift::LocalTrackPublication, CFStringRef), + callback_data: *mut c_void, + ); + fn LKRoomPublishAudioTrack( + room: swift::Room, + track: swift::LocalAudioTrack, + callback: extern "C" fn(*mut c_void, swift::LocalTrackPublication, CFStringRef), + callback_data: *mut c_void, + ); + fn LKRoomUnpublishTrack(room: swift::Room, publication: swift::LocalTrackPublication); + + fn LKRoomAudioTracksForRemoteParticipant( + room: swift::Room, + participant_id: CFStringRef, + ) -> CFArrayRef; + + fn LKRoomAudioTrackPublicationsForRemoteParticipant( + room: swift::Room, + participant_id: CFStringRef, + ) -> CFArrayRef; + + fn LKRoomVideoTracksForRemoteParticipant( + room: swift::Room, + participant_id: CFStringRef, + ) -> CFArrayRef; + + fn LKVideoRendererCreate( + callback_data: *mut c_void, + on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool, + on_drop: extern "C" fn(callback_data: *mut c_void), + ) -> *const c_void; + + fn LKRemoteAudioTrackGetSid(track: swift::RemoteAudioTrack) -> CFStringRef; + fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void); + fn LKRemoteVideoTrackGetSid(track: swift::RemoteVideoTrack) -> CFStringRef; + + fn LKDisplaySources( + callback_data: *mut c_void, + callback: extern "C" fn( + callback_data: *mut c_void, + sources: CFArrayRef, + error: CFStringRef, + ), + ); + fn LKCreateScreenShareTrackForDisplay(display: swift::MacOSDisplay) -> swift::LocalVideoTrack; + fn LKLocalAudioTrackCreateTrack() -> swift::LocalAudioTrack; + + fn LKLocalTrackPublicationSetMute( + publication: swift::LocalTrackPublication, + muted: bool, + on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef), + callback_data: *mut c_void, + ); + + fn LKRemoteTrackPublicationSetEnabled( + publication: swift::RemoteTrackPublication, + enabled: bool, + on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef), + callback_data: *mut c_void, + ); + + fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool; + fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef; +} + +pub type Sid = String; + +#[derive(Clone, Eq, PartialEq)] +pub enum ConnectionState { + Disconnected, + Connected { url: String, token: String }, +} + +pub struct Room { + native_room: Mutex, + connection: Mutex<( + watch::Sender, + watch::Receiver, + )>, + remote_audio_track_subscribers: Mutex>>, + remote_video_track_subscribers: Mutex>>, + _delegate: Mutex, +} + +trait AssertSendSync: Send {} +impl AssertSendSync for Room {} + +impl Room { + pub fn new() -> Arc { + Arc::new_cyclic(|weak_room| { + let delegate = RoomDelegate::new(weak_room.clone()); + Self { + native_room: Mutex::new(unsafe { LKRoomCreate(delegate.native_delegate) }), + connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)), + remote_audio_track_subscribers: Default::default(), + remote_video_track_subscribers: Default::default(), + _delegate: Mutex::new(delegate), + } + }) + } + + pub fn status(&self) -> watch::Receiver { + self.connection.lock().1.clone() + } + + pub fn connect(self: &Arc, url: &str, token: &str) -> impl Future> { + let url = CFString::new(url); + let token = CFString::new(token); + let (did_connect, tx, rx) = Self::build_done_callback(); + unsafe { + LKRoomConnect( + *self.native_room.lock(), + url.as_concrete_TypeRef(), + token.as_concrete_TypeRef(), + did_connect, + tx, + ) + } + + let this = self.clone(); + let url = url.to_string(); + let token = token.to_string(); + async move { + rx.await.unwrap().context("error connecting to room")?; + *this.connection.lock().0.borrow_mut() = ConnectionState::Connected { url, token }; + Ok(()) + } + } + + fn did_disconnect(&self) { + *self.connection.lock().0.borrow_mut() = ConnectionState::Disconnected; + } + + pub fn display_sources(self: &Arc) -> impl Future>> { + extern "C" fn callback(tx: *mut c_void, sources: CFArrayRef, error: CFStringRef) { + unsafe { + let tx = Box::from_raw(tx as *mut oneshot::Sender>>); + + if sources.is_null() { + let _ = tx.send(Err(anyhow!("{}", CFString::wrap_under_get_rule(error)))); + } else { + let sources = CFArray::wrap_under_get_rule(sources) + .into_iter() + .map(|source| MacOSDisplay::new(swift::MacOSDisplay(*source))) + .collect(); + + let _ = tx.send(Ok(sources)); + } + } + } + + let (tx, rx) = oneshot::channel(); + + unsafe { + LKDisplaySources(Box::into_raw(Box::new(tx)) as *mut _, callback); + } + + async move { rx.await.unwrap() } + } + + pub fn publish_video_track( + self: &Arc, + track: LocalVideoTrack, + ) -> impl Future> { + let (tx, rx) = oneshot::channel::>(); + extern "C" fn callback( + tx: *mut c_void, + publication: swift::LocalTrackPublication, + error: CFStringRef, + ) { + let tx = + unsafe { Box::from_raw(tx as *mut oneshot::Sender>) }; + if error.is_null() { + let _ = tx.send(Ok(LocalTrackPublication::new(publication))); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + let _ = tx.send(Err(anyhow!(error))); + } + } + unsafe { + LKRoomPublishVideoTrack( + *self.native_room.lock(), + track.0, + callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ); + } + async { rx.await.unwrap().context("error publishing video track") } + } + + pub fn publish_audio_track( + self: &Arc, + track: LocalAudioTrack, + ) -> impl Future> { + let (tx, rx) = oneshot::channel::>(); + extern "C" fn callback( + tx: *mut c_void, + publication: swift::LocalTrackPublication, + error: CFStringRef, + ) { + let tx = + unsafe { Box::from_raw(tx as *mut oneshot::Sender>) }; + if error.is_null() { + let _ = tx.send(Ok(LocalTrackPublication::new(publication))); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + let _ = tx.send(Err(anyhow!(error))); + } + } + unsafe { + LKRoomPublishAudioTrack( + *self.native_room.lock(), + track.0, + callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ); + } + async { rx.await.unwrap().context("error publishing audio track") } + } + + pub fn unpublish_track(&self, publication: LocalTrackPublication) { + unsafe { + LKRoomUnpublishTrack(*self.native_room.lock(), publication.0); + } + } + + pub fn remote_video_tracks(&self, participant_id: &str) -> Vec> { + unsafe { + let tracks = LKRoomVideoTracksForRemoteParticipant( + *self.native_room.lock(), + CFString::new(participant_id).as_concrete_TypeRef(), + ); + + if tracks.is_null() { + Vec::new() + } else { + let tracks = CFArray::wrap_under_get_rule(tracks); + tracks + .into_iter() + .map(|native_track| { + let native_track = swift::RemoteVideoTrack(*native_track); + let id = + CFString::wrap_under_get_rule(LKRemoteVideoTrackGetSid(native_track)) + .to_string(); + Arc::new(RemoteVideoTrack::new( + native_track, + id, + participant_id.into(), + )) + }) + .collect() + } + } + } + + pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec> { + unsafe { + let tracks = LKRoomAudioTracksForRemoteParticipant( + *self.native_room.lock(), + CFString::new(participant_id).as_concrete_TypeRef(), + ); + + if tracks.is_null() { + Vec::new() + } else { + let tracks = CFArray::wrap_under_get_rule(tracks); + tracks + .into_iter() + .map(|native_track| { + let native_track = swift::RemoteAudioTrack(*native_track); + let id = + CFString::wrap_under_get_rule(LKRemoteAudioTrackGetSid(native_track)) + .to_string(); + Arc::new(RemoteAudioTrack::new( + native_track, + id, + participant_id.into(), + )) + }) + .collect() + } + } + } + + pub fn remote_audio_track_publications( + &self, + participant_id: &str, + ) -> Vec> { + unsafe { + let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant( + *self.native_room.lock(), + CFString::new(participant_id).as_concrete_TypeRef(), + ); + + if tracks.is_null() { + Vec::new() + } else { + let tracks = CFArray::wrap_under_get_rule(tracks); + tracks + .into_iter() + .map(|native_track_publication| { + let native_track_publication = + swift::RemoteTrackPublication(*native_track_publication); + Arc::new(RemoteTrackPublication::new(native_track_publication)) + }) + .collect() + } + } + } + + pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver { + let (tx, rx) = mpsc::unbounded(); + self.remote_audio_track_subscribers.lock().push(tx); + rx + } + + pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver { + let (tx, rx) = mpsc::unbounded(); + self.remote_video_track_subscribers.lock().push(tx); + rx + } + + fn did_subscribe_to_remote_audio_track( + &self, + track: RemoteAudioTrack, + publication: RemoteTrackPublication, + ) { + let track = Arc::new(track); + let publication = Arc::new(publication); + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + )) + .is_ok() + }); + } + + fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) { + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed { + publisher_id: publisher_id.clone(), + track_id: track_id.clone(), + }) + .is_ok() + }); + } + + fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) { + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged { + track_id: track_id.clone(), + muted, + }) + .is_ok() + }); + } + + // A vec of publisher IDs + fn active_speakers_changed(&self, speakers: Vec) { + self.remote_audio_track_subscribers + .lock() + .retain(move |tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged { + speakers: speakers.clone(), + }) + .is_ok() + }); + } + + fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) { + let track = Arc::new(track); + self.remote_video_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone())) + .is_ok() + }); + } + + fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) { + self.remote_video_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed { + publisher_id: publisher_id.clone(), + track_id: track_id.clone(), + }) + .is_ok() + }); + } + + fn build_done_callback() -> ( + extern "C" fn(*mut c_void, CFStringRef), + *mut c_void, + oneshot::Receiver>, + ) { + let (tx, rx) = oneshot::channel(); + extern "C" fn done_callback(tx: *mut c_void, error: CFStringRef) { + let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender>) }; + if error.is_null() { + let _ = tx.send(Ok(())); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + let _ = tx.send(Err(anyhow!(error))); + } + } + ( + done_callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + rx, + ) + } + + pub fn set_display_sources(&self, _: Vec) { + unreachable!("This is a test-only function") + } +} + +impl Drop for Room { + fn drop(&mut self) { + unsafe { + let native_room = &*self.native_room.lock(); + LKRoomDisconnect(*native_room); + CFRelease(native_room.0); + } + } +} + +struct RoomDelegate { + native_delegate: swift::RoomDelegate, + _weak_room: Weak, +} + +impl RoomDelegate { + fn new(weak_room: Weak) -> Self { + let native_delegate = unsafe { + LKRoomDelegateCreate( + weak_room.as_ptr() as *mut c_void, + Self::on_did_disconnect, + Self::on_did_subscribe_to_remote_audio_track, + Self::on_did_unsubscribe_from_remote_audio_track, + Self::on_mute_change_from_remote_audio_track, + Self::on_active_speakers_changed, + Self::on_did_subscribe_to_remote_video_track, + Self::on_did_unsubscribe_from_remote_video_track, + ) + }; + Self { + native_delegate, + _weak_room: weak_room, + } + } + + extern "C" fn on_did_disconnect(room: *mut c_void) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + if let Some(room) = room.upgrade() { + room.did_disconnect(); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_subscribe_to_remote_audio_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + track: swift::RemoteAudioTrack, + publication: swift::RemoteTrackPublication, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + let track = RemoteAudioTrack::new(track, track_id, publisher_id); + let publication = RemoteTrackPublication::new(publication); + if let Some(room) = room.upgrade() { + room.did_subscribe_to_remote_audio_track(track, publication); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_unsubscribe_from_remote_audio_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + if let Some(room) = room.upgrade() { + room.did_unsubscribe_from_remote_audio_track(publisher_id, track_id); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_mute_change_from_remote_audio_track( + room: *mut c_void, + track_id: CFStringRef, + muted: bool, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + if let Some(room) = room.upgrade() { + room.mute_changed_from_remote_audio_track(track_id, muted); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) { + if participants.is_null() { + return; + } + + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let speakers = unsafe { + CFArray::wrap_under_get_rule(participants) + .into_iter() + .map( + |speaker: core_foundation::base::ItemRef<'_, *const c_void>| { + CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string() + }, + ) + .collect() + }; + + if let Some(room) = room.upgrade() { + room.active_speakers_changed(speakers); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_subscribe_to_remote_video_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + track: swift::RemoteVideoTrack, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + let track = RemoteVideoTrack::new(track, track_id, publisher_id); + if let Some(room) = room.upgrade() { + room.did_subscribe_to_remote_video_track(track); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_unsubscribe_from_remote_video_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + if let Some(room) = room.upgrade() { + room.did_unsubscribe_from_remote_video_track(publisher_id, track_id); + } + let _ = Weak::into_raw(room); + } +} + +impl Drop for RoomDelegate { + fn drop(&mut self) { + unsafe { + CFRelease(self.native_delegate.0); + } + } +} + +pub struct LocalAudioTrack(swift::LocalAudioTrack); + +impl LocalAudioTrack { + pub fn create() -> Self { + Self(unsafe { LKLocalAudioTrackCreateTrack() }) + } +} + +impl Drop for LocalAudioTrack { + fn drop(&mut self) { + unsafe { CFRelease(self.0 .0) } + } +} + +pub struct LocalVideoTrack(swift::LocalVideoTrack); + +impl LocalVideoTrack { + pub fn screen_share_for_display(display: &MacOSDisplay) -> Self { + Self(unsafe { LKCreateScreenShareTrackForDisplay(display.0) }) + } +} + +impl Drop for LocalVideoTrack { + fn drop(&mut self) { + unsafe { CFRelease(self.0 .0) } + } +} + +pub struct LocalTrackPublication(swift::LocalTrackPublication); + +impl LocalTrackPublication { + pub fn new(native_track_publication: swift::LocalTrackPublication) -> Self { + unsafe { + CFRetain(native_track_publication.0); + } + Self(native_track_publication) + } + + pub fn set_mute(&self, muted: bool) -> impl Future> { + let (tx, rx) = futures::channel::oneshot::channel(); + + extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) { + let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender>) }; + if error.is_null() { + tx.send(Ok(())).ok(); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + tx.send(Err(anyhow!(error))).ok(); + } + } + + unsafe { + LKLocalTrackPublicationSetMute( + self.0, + muted, + complete_callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ) + } + + async move { rx.await.unwrap() } + } +} + +impl Drop for LocalTrackPublication { + fn drop(&mut self) { + unsafe { CFRelease(self.0 .0) } + } +} + +pub struct RemoteTrackPublication { + native_publication: Mutex, +} + +impl RemoteTrackPublication { + pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self { + unsafe { + CFRetain(native_track_publication.0); + } + Self { + native_publication: Mutex::new(native_track_publication), + } + } + + pub fn sid(&self) -> String { + unsafe { + CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid( + *self.native_publication.lock(), + )) + .to_string() + } + } + + pub fn is_muted(&self) -> bool { + unsafe { LKRemoteTrackPublicationIsMuted(*self.native_publication.lock()) } + } + + pub fn set_enabled(&self, enabled: bool) -> impl Future> { + let (tx, rx) = futures::channel::oneshot::channel(); + + extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) { + let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender>) }; + if error.is_null() { + tx.send(Ok(())).ok(); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + tx.send(Err(anyhow!(error))).ok(); + } + } + + unsafe { + LKRemoteTrackPublicationSetEnabled( + *self.native_publication.lock(), + enabled, + complete_callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ) + } + + async move { rx.await.unwrap() } + } +} + +impl Drop for RemoteTrackPublication { + fn drop(&mut self) { + unsafe { CFRelease((*self.native_publication.lock()).0) } + } +} + +#[derive(Debug)] +pub struct RemoteAudioTrack { + native_track: Mutex, + sid: Sid, + publisher_id: String, +} + +impl RemoteAudioTrack { + fn new(native_track: swift::RemoteAudioTrack, sid: Sid, publisher_id: String) -> Self { + unsafe { + CFRetain(native_track.0); + } + Self { + native_track: Mutex::new(native_track), + sid, + publisher_id, + } + } + + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn enable(&self) -> impl Future> { + async { Ok(()) } + } + + pub fn disable(&self) -> impl Future> { + async { Ok(()) } + } +} + +impl Drop for RemoteAudioTrack { + fn drop(&mut self) { + unsafe { CFRelease(self.native_track.lock().0) } + } +} + +#[derive(Debug)] +pub struct RemoteVideoTrack { + native_track: Mutex, + sid: Sid, + publisher_id: String, +} + +impl RemoteVideoTrack { + fn new(native_track: swift::RemoteVideoTrack, sid: Sid, publisher_id: String) -> Self { + unsafe { + CFRetain(native_track.0); + } + Self { + native_track: Mutex::new(native_track), + sid, + publisher_id, + } + } + + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn frames(&self) -> async_broadcast::Receiver { + extern "C" fn on_frame(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool { + unsafe { + let tx = Box::from_raw(callback_data as *mut async_broadcast::Sender); + let buffer = CVImageBuffer::wrap_under_get_rule(frame); + let result = tx.try_broadcast(Frame(buffer)); + let _ = Box::into_raw(tx); + match result { + Ok(_) => true, + Err(async_broadcast::TrySendError::Closed(_)) + | Err(async_broadcast::TrySendError::Inactive(_)) => { + log::warn!("no active receiver for frame"); + false + } + Err(async_broadcast::TrySendError::Full(_)) => { + log::warn!("skipping frame as receiver is not keeping up"); + true + } + } + } + } + + extern "C" fn on_drop(callback_data: *mut c_void) { + unsafe { + let _ = Box::from_raw(callback_data as *mut async_broadcast::Sender); + } + } + + let (tx, rx) = async_broadcast::broadcast(64); + unsafe { + let renderer = LKVideoRendererCreate( + Box::into_raw(Box::new(tx)) as *mut c_void, + on_frame, + on_drop, + ); + LKVideoTrackAddRenderer(*self.native_track.lock(), renderer); + rx + } + } +} + +impl Drop for RemoteVideoTrack { + fn drop(&mut self) { + unsafe { CFRelease(self.native_track.lock().0) } + } +} + +pub enum RemoteVideoTrackUpdate { + Subscribed(Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + +pub enum RemoteAudioTrackUpdate { + ActiveSpeakersChanged { speakers: Vec }, + MuteChanged { track_id: Sid, muted: bool }, + Subscribed(Arc, Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + +pub struct MacOSDisplay(swift::MacOSDisplay); + +impl MacOSDisplay { + fn new(ptr: swift::MacOSDisplay) -> Self { + unsafe { + CFRetain(ptr.0); + } + Self(ptr) + } +} + +impl Drop for MacOSDisplay { + fn drop(&mut self) { + unsafe { CFRelease(self.0 .0) } + } +} + +#[derive(Clone)] +pub struct Frame(CVImageBuffer); + +impl Frame { + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn image(&self) -> CVImageBuffer { + self.0.clone() + } +} diff --git a/crates/live_kit_client2/src/test.rs b/crates/live_kit_client2/src/test.rs new file mode 100644 index 0000000000..10c97e8d81 --- /dev/null +++ b/crates/live_kit_client2/src/test.rs @@ -0,0 +1,651 @@ +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use collections::{BTreeMap, HashMap}; +use futures::Stream; +use gpui2::BackgroundExecutor; +use live_kit_server::token; +use media::core_video::CVImageBuffer; +use parking_lot::Mutex; +use postage::watch; +use std::{future::Future, mem, sync::Arc}; + +static SERVERS: Mutex>> = Mutex::new(BTreeMap::new()); + +pub struct TestServer { + pub url: String, + pub api_key: String, + pub secret_key: String, + rooms: Mutex>, + executor: Arc, +} + +impl TestServer { + pub fn create( + url: String, + api_key: String, + secret_key: String, + executor: Arc, + ) -> Result> { + let mut servers = SERVERS.lock(); + if servers.contains_key(&url) { + Err(anyhow!("a server with url {:?} already exists", url)) + } else { + let server = Arc::new(TestServer { + url: url.clone(), + api_key, + secret_key, + rooms: Default::default(), + executor, + }); + servers.insert(url, server.clone()); + Ok(server) + } + } + + fn get(url: &str) -> Result> { + Ok(SERVERS + .lock() + .get(url) + .ok_or_else(|| anyhow!("no server found for url"))? + .clone()) + } + + pub fn teardown(&self) -> Result<()> { + SERVERS + .lock() + .remove(&self.url) + .ok_or_else(|| anyhow!("server with url {:?} does not exist", self.url))?; + Ok(()) + } + + pub fn create_api_client(&self) -> TestApiClient { + TestApiClient { + url: self.url.clone(), + } + } + + pub async fn create_room(&self, room: String) -> Result<()> { + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + if server_rooms.contains_key(&room) { + Err(anyhow!("room {:?} already exists", room)) + } else { + server_rooms.insert(room, Default::default()); + Ok(()) + } + } + + async fn delete_room(&self, room: String) -> Result<()> { + // TODO: clear state associated with all `Room`s. + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + server_rooms + .remove(&room) + .ok_or_else(|| anyhow!("room {:?} does not exist", room))?; + Ok(()) + } + + async fn join_room(&self, token: String, client_room: Arc) -> Result<()> { + self.executor.simulate_random_delay().await; + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let identity = claims.sub.unwrap().to_string(); + let room_name = claims.video.room.unwrap(); + let mut server_rooms = self.rooms.lock(); + let room = (*server_rooms).entry(room_name.to_string()).or_default(); + + if room.client_rooms.contains_key(&identity) { + Err(anyhow!( + "{:?} attempted to join room {:?} twice", + identity, + room_name + )) + } else { + for track in &room.video_tracks { + client_room + .0 + .lock() + .video_track_updates + .0 + .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone())) + .unwrap(); + } + room.client_rooms.insert(identity, client_room); + Ok(()) + } + } + + async fn leave_room(&self, token: String) -> Result<()> { + self.executor.simulate_random_delay().await; + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let identity = claims.sub.unwrap().to_string(); + let room_name = claims.video.room.unwrap(); + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.client_rooms.remove(&identity).ok_or_else(|| { + anyhow!( + "{:?} attempted to leave room {:?} before joining it", + identity, + room_name + ) + })?; + Ok(()) + } + + async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> { + // TODO: clear state associated with the `Room`. + + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.client_rooms.remove(&identity).ok_or_else(|| { + anyhow!( + "participant {:?} did not join room {:?}", + identity, + room_name + ) + })?; + Ok(()) + } + + pub async fn disconnect_client(&self, client_identity: String) { + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + for room in server_rooms.values_mut() { + if let Some(room) = room.client_rooms.remove(&client_identity) { + *room.0.lock().connection.0.borrow_mut() = ConnectionState::Disconnected; + } + } + } + + async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> { + self.executor.simulate_random_delay().await; + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let identity = claims.sub.unwrap().to_string(); + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + + let track = Arc::new(RemoteVideoTrack { + sid: nanoid::nanoid!(17), + publisher_id: identity.clone(), + frames_rx: local_track.frames_rx.clone(), + }); + + room.video_tracks.push(track.clone()); + + for (id, client_room) in &room.client_rooms { + if *id != identity { + let _ = client_room + .0 + .lock() + .video_track_updates + .0 + .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone())) + .unwrap(); + } + } + + Ok(()) + } + + async fn publish_audio_track( + &self, + token: String, + _local_track: &LocalAudioTrack, + ) -> Result<()> { + self.executor.simulate_random_delay().await; + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let identity = claims.sub.unwrap().to_string(); + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + + let track = Arc::new(RemoteAudioTrack { + sid: nanoid::nanoid!(17), + publisher_id: identity.clone(), + }); + + let publication = Arc::new(RemoteTrackPublication); + + room.audio_tracks.push(track.clone()); + + for (id, client_room) in &room.client_rooms { + if *id != identity { + let _ = client_room + .0 + .lock() + .audio_track_updates + .0 + .try_broadcast(RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + )) + .unwrap(); + } + } + + Ok(()) + } + + fn video_tracks(&self, token: String) -> Result>> { + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + Ok(room.video_tracks.clone()) + } + + fn audio_tracks(&self, token: String) -> Result>> { + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + Ok(room.audio_tracks.clone()) + } +} + +#[derive(Default)] +struct TestServerRoom { + client_rooms: HashMap>, + video_tracks: Vec>, + audio_tracks: Vec>, +} + +impl TestServerRoom {} + +pub struct TestApiClient { + url: String, +} + +#[async_trait] +impl live_kit_server::api::Client for TestApiClient { + fn url(&self) -> &str { + &self.url + } + + async fn create_room(&self, name: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server.create_room(name).await?; + Ok(()) + } + + async fn delete_room(&self, name: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server.delete_room(name).await?; + Ok(()) + } + + async fn remove_participant(&self, room: String, identity: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server.remove_participant(room, identity).await?; + Ok(()) + } + + fn room_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::to_join(room), + ) + } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } +} + +pub type Sid = String; + +struct RoomState { + connection: ( + watch::Sender, + watch::Receiver, + ), + display_sources: Vec, + audio_track_updates: ( + async_broadcast::Sender, + async_broadcast::Receiver, + ), + video_track_updates: ( + async_broadcast::Sender, + async_broadcast::Receiver, + ), +} + +#[derive(Clone, Eq, PartialEq)] +pub enum ConnectionState { + Disconnected, + Connected { url: String, token: String }, +} + +pub struct Room(Mutex); + +impl Room { + pub fn new() -> Arc { + Arc::new(Self(Mutex::new(RoomState { + connection: watch::channel_with(ConnectionState::Disconnected), + display_sources: Default::default(), + video_track_updates: async_broadcast::broadcast(128), + audio_track_updates: async_broadcast::broadcast(128), + }))) + } + + pub fn status(&self) -> watch::Receiver { + self.0.lock().connection.1.clone() + } + + pub fn connect(self: &Arc, url: &str, token: &str) -> impl Future> { + let this = self.clone(); + let url = url.to_string(); + let token = token.to_string(); + async move { + let server = TestServer::get(&url)?; + server + .join_room(token.clone(), this.clone()) + .await + .context("room join")?; + *this.0.lock().connection.0.borrow_mut() = ConnectionState::Connected { url, token }; + Ok(()) + } + } + + pub fn display_sources(self: &Arc) -> impl Future>> { + let this = self.clone(); + async move { + let server = this.test_server(); + server.executor.simulate_random_delay().await; + Ok(this.0.lock().display_sources.clone()) + } + } + + pub fn publish_video_track( + self: &Arc, + track: LocalVideoTrack, + ) -> impl Future> { + let this = self.clone(); + let track = track.clone(); + async move { + this.test_server() + .publish_video_track(this.token(), track) + .await?; + Ok(LocalTrackPublication) + } + } + pub fn publish_audio_track( + self: &Arc, + track: LocalAudioTrack, + ) -> impl Future> { + let this = self.clone(); + let track = track.clone(); + async move { + this.test_server() + .publish_audio_track(this.token(), &track) + .await?; + Ok(LocalTrackPublication) + } + } + + pub fn unpublish_track(&self, _publication: LocalTrackPublication) {} + + pub fn remote_audio_tracks(&self, publisher_id: &str) -> Vec> { + if !self.is_connected() { + return Vec::new(); + } + + self.test_server() + .audio_tracks(self.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == publisher_id) + .collect() + } + + pub fn remote_audio_track_publications( + &self, + publisher_id: &str, + ) -> Vec> { + if !self.is_connected() { + return Vec::new(); + } + + self.test_server() + .audio_tracks(self.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == publisher_id) + .map(|_track| Arc::new(RemoteTrackPublication {})) + .collect() + } + + pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec> { + if !self.is_connected() { + return Vec::new(); + } + + self.test_server() + .video_tracks(self.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == publisher_id) + .collect() + } + + pub fn remote_audio_track_updates(&self) -> impl Stream { + self.0.lock().audio_track_updates.1.clone() + } + + pub fn remote_video_track_updates(&self) -> impl Stream { + self.0.lock().video_track_updates.1.clone() + } + + pub fn set_display_sources(&self, sources: Vec) { + self.0.lock().display_sources = sources; + } + + fn test_server(&self) -> Arc { + match self.0.lock().connection.1.borrow().clone() { + ConnectionState::Disconnected => panic!("must be connected to call this method"), + ConnectionState::Connected { url, .. } => TestServer::get(&url).unwrap(), + } + } + + fn token(&self) -> String { + match self.0.lock().connection.1.borrow().clone() { + ConnectionState::Disconnected => panic!("must be connected to call this method"), + ConnectionState::Connected { token, .. } => token, + } + } + + fn is_connected(&self) -> bool { + match *self.0.lock().connection.1.borrow() { + ConnectionState::Disconnected => false, + ConnectionState::Connected { .. } => true, + } + } +} + +impl Drop for Room { + fn drop(&mut self) { + if let ConnectionState::Connected { token, .. } = mem::replace( + &mut *self.0.lock().connection.0.borrow_mut(), + ConnectionState::Disconnected, + ) { + if let Ok(server) = TestServer::get(&token) { + let executor = server.executor.clone(); + executor + .spawn(async move { server.leave_room(token).await.unwrap() }) + .detach(); + } + } + } +} + +pub struct LocalTrackPublication; + +impl LocalTrackPublication { + pub fn set_mute(&self, _mute: bool) -> impl Future> { + async { Ok(()) } + } +} + +pub struct RemoteTrackPublication; + +impl RemoteTrackPublication { + pub fn set_enabled(&self, _enabled: bool) -> impl Future> { + async { Ok(()) } + } + + pub fn is_muted(&self) -> bool { + false + } + + pub fn sid(&self) -> String { + "".to_string() + } +} + +#[derive(Clone)] +pub struct LocalVideoTrack { + frames_rx: async_broadcast::Receiver, +} + +impl LocalVideoTrack { + pub fn screen_share_for_display(display: &MacOSDisplay) -> Self { + Self { + frames_rx: display.frames.1.clone(), + } + } +} + +#[derive(Clone)] +pub struct LocalAudioTrack; + +impl LocalAudioTrack { + pub fn create() -> Self { + Self + } +} + +#[derive(Debug)] +pub struct RemoteVideoTrack { + sid: Sid, + publisher_id: Sid, + frames_rx: async_broadcast::Receiver, +} + +impl RemoteVideoTrack { + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn frames(&self) -> async_broadcast::Receiver { + self.frames_rx.clone() + } +} + +#[derive(Debug)] +pub struct RemoteAudioTrack { + sid: Sid, + publisher_id: Sid, +} + +impl RemoteAudioTrack { + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn enable(&self) -> impl Future> { + async { Ok(()) } + } + + pub fn disable(&self) -> impl Future> { + async { Ok(()) } + } +} + +#[derive(Clone)] +pub enum RemoteVideoTrackUpdate { + Subscribed(Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + +#[derive(Clone)] +pub enum RemoteAudioTrackUpdate { + ActiveSpeakersChanged { speakers: Vec }, + MuteChanged { track_id: Sid, muted: bool }, + Subscribed(Arc, Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + +#[derive(Clone)] +pub struct MacOSDisplay { + frames: ( + async_broadcast::Sender, + async_broadcast::Receiver, + ), +} + +impl MacOSDisplay { + pub fn new() -> Self { + Self { + frames: async_broadcast::broadcast(128), + } + } + + pub fn send_frame(&self, frame: Frame) { + self.frames.0.try_broadcast(frame).unwrap(); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Frame { + pub label: String, + pub width: usize, + pub height: usize, +} + +impl Frame { + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + pub fn image(&self) -> CVImageBuffer { + unimplemented!("you can't call this in test mode") + } +} diff --git a/crates/lsp2/Cargo.toml b/crates/lsp2/Cargo.toml index a32dd2b6b2..12993eedb6 100644 --- a/crates/lsp2/Cargo.toml +++ b/crates/lsp2/Cargo.toml @@ -13,7 +13,7 @@ test-support = ["async-pipe"] [dependencies] collections = { path = "../collections" } -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } anyhow.workspace = true @@ -29,7 +29,7 @@ serde_json.workspace = true smol.workspace = true [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs index 27b7b36e73..356d029c58 100644 --- a/crates/lsp2/src/lsp2.rs +++ b/crates/lsp2/src/lsp2.rs @@ -5,7 +5,7 @@ pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; -use gpui2::{AsyncAppContext, Executor, Task}; +use gpui::{AsyncAppContext, BackgroundExecutor, Task}; use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -62,7 +62,7 @@ pub struct LanguageServer { notification_handlers: Arc>>, response_handlers: Arc>>>, io_handlers: Arc>>, - executor: Executor, + executor: BackgroundExecutor, #[allow(clippy::type_complexity)] io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, @@ -248,7 +248,7 @@ impl LanguageServer { let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); stdout.or(stderr) }); - let output_task = cx.executor().spawn({ + let output_task = cx.background_executor().spawn({ Self::handle_output( stdin, outbound_rx, @@ -269,7 +269,7 @@ impl LanguageServer { code_action_kinds, next_id: Default::default(), outbound_tx, - executor: cx.executor().clone(), + executor: cx.background_executor().clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), @@ -595,8 +595,8 @@ impl LanguageServer { where T: request::Request, T::Params: 'static + Send, - F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut, - Fut: 'static + Future> + Send, + F: 'static + FnMut(T::Params, AsyncAppContext) -> Fut + Send, + Fut: 'static + Future>, { self.on_custom_request(T::METHOD, f) } @@ -629,7 +629,7 @@ impl LanguageServer { #[must_use] pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where - F: 'static + Send + FnMut(Params, AsyncAppContext), + F: 'static + FnMut(Params, AsyncAppContext) + Send, Params: DeserializeOwned, { let prev_handler = self.notification_handlers.lock().insert( @@ -657,8 +657,8 @@ impl LanguageServer { mut f: F, ) -> Subscription where - F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut, - Fut: 'static + Future> + Send, + F: 'static + FnMut(Params, AsyncAppContext) -> Fut + Send, + Fut: 'static + Future>, Params: DeserializeOwned + Send + 'static, Res: Serialize, { @@ -670,10 +670,10 @@ impl LanguageServer { match serde_json::from_str(params) { Ok(params) => { let response = f(params, cx.clone()); - cx.executor() - .spawn_on_main({ + cx.foreground_executor() + .spawn({ let outbound_tx = outbound_tx.clone(); - move || async move { + async move { let response = match response.await { Ok(result) => Response { jsonrpc: JSON_RPC_VERSION, @@ -769,7 +769,7 @@ impl LanguageServer { next_id: &AtomicUsize, response_handlers: &Mutex>>, outbound_tx: &channel::Sender, - executor: &Executor, + executor: &BackgroundExecutor, params: T::Params, ) -> impl 'static + Future> where @@ -1038,7 +1038,7 @@ impl FakeLanguageServer { where T: 'static + request::Request, T::Params: 'static + Send, - F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext) -> Fut, + F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, Fut: 'static + Send + Future>, { let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); @@ -1047,8 +1047,9 @@ impl FakeLanguageServer { .on_request::(move |params, cx| { let result = handler(params, cx.clone()); let responded_tx = responded_tx.clone(); + let executor = cx.background_executor().clone(); async move { - cx.executor().simulate_random_delay().await; + executor.simulate_random_delay().await; let result = result.await; responded_tx.unbounded_send(()).ok(); result @@ -1065,7 +1066,7 @@ impl FakeLanguageServer { where T: 'static + notification::Notification, T::Params: 'static + Send, - F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext), + F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext), { let (handled_tx, handled_rx) = futures::channel::mpsc::unbounded(); self.server.remove_notification_handler::(); @@ -1106,74 +1107,74 @@ impl FakeLanguageServer { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use gpui::TestAppContext; +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } -// #[gpui::test] -// async fn test_fake(cx: &mut TestAppContext) { -// let (server, mut fake) = -// LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async()); + #[gpui::test] + async fn test_fake(cx: &mut TestAppContext) { + let (server, mut fake) = + LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async()); -// let (message_tx, message_rx) = channel::unbounded(); -// let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); -// server -// .on_notification::(move |params, _| { -// message_tx.try_send(params).unwrap() -// }) -// .detach(); -// server -// .on_notification::(move |params, _| { -// diagnostics_tx.try_send(params).unwrap() -// }) -// .detach(); + let (message_tx, message_rx) = channel::unbounded(); + let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); + server + .on_notification::(move |params, _| { + message_tx.try_send(params).unwrap() + }) + .detach(); + server + .on_notification::(move |params, _| { + diagnostics_tx.try_send(params).unwrap() + }) + .detach(); -// let server = server.initialize(None).await.unwrap(); -// server -// .notify::(DidOpenTextDocumentParams { -// text_document: TextDocumentItem::new( -// Url::from_str("file://a/b").unwrap(), -// "rust".to_string(), -// 0, -// "".to_string(), -// ), -// }) -// .unwrap(); -// assert_eq!( -// fake.receive_notification::() -// .await -// .text_document -// .uri -// .as_str(), -// "file://a/b" -// ); + let server = server.initialize(None).await.unwrap(); + server + .notify::(DidOpenTextDocumentParams { + text_document: TextDocumentItem::new( + Url::from_str("file://a/b").unwrap(), + "rust".to_string(), + 0, + "".to_string(), + ), + }) + .unwrap(); + assert_eq!( + fake.receive_notification::() + .await + .text_document + .uri + .as_str(), + "file://a/b" + ); -// fake.notify::(ShowMessageParams { -// typ: MessageType::ERROR, -// message: "ok".to_string(), -// }); -// fake.notify::(PublishDiagnosticsParams { -// uri: Url::from_str("file://b/c").unwrap(), -// version: Some(5), -// diagnostics: vec![], -// }); -// assert_eq!(message_rx.recv().await.unwrap().message, "ok"); -// assert_eq!( -// diagnostics_rx.recv().await.unwrap().uri.as_str(), -// "file://b/c" -// ); + fake.notify::(ShowMessageParams { + typ: MessageType::ERROR, + message: "ok".to_string(), + }); + fake.notify::(PublishDiagnosticsParams { + uri: Url::from_str("file://b/c").unwrap(), + version: Some(5), + diagnostics: vec![], + }); + assert_eq!(message_rx.recv().await.unwrap().message, "ok"); + assert_eq!( + diagnostics_rx.recv().await.unwrap().uri.as_str(), + "file://b/c" + ); -// fake.handle_request::(|_, _| async move { Ok(()) }); + fake.handle_request::(|_, _| async move { Ok(()) }); -// drop(server); -// fake.receive_notification::().await; -// } -// } + drop(server); + fake.receive_notification::().await; + } +} diff --git a/crates/menu2/Cargo.toml b/crates/menu2/Cargo.toml index c366de6866..9bf61db82c 100644 --- a/crates/menu2/Cargo.toml +++ b/crates/menu2/Cargo.toml @@ -9,4 +9,4 @@ path = "src/menu2.rs" doctest = false [dependencies] -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } diff --git a/crates/multi_buffer2/Cargo.toml b/crates/multi_buffer2/Cargo.toml new file mode 100644 index 0000000000..98b96dfa1d --- /dev/null +++ b/crates/multi_buffer2/Cargo.toml @@ -0,0 +1,78 @@ +[package] +name = "multi_buffer2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/multi_buffer2.rs" +doctest = false + +[features] +test-support = [ + "copilot/test-support", + "text/test-support", + "language/test-support", + "gpui/test-support", + "util/test-support", + "tree-sitter-rust", + "tree-sitter-typescript" +] + +[dependencies] +client = { package = "client2", path = "../client2" } +clock = { path = "../clock" } +collections = { path = "../collections" } +git = { package = "git3", path = "../git3" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +lsp = { package = "lsp2", path = "../lsp2" } +rich_text = { package = "rich_text2", path = "../rich_text2" } +settings = { package = "settings2", path = "../settings2" } +snippet = { path = "../snippet" } +sum_tree = { path = "../sum_tree" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } + +aho-corasick = "1.1" +anyhow.workspace = true +convert_case = "0.6.0" +futures.workspace = true +indoc = "1.0.4" +itertools = "0.10" +lazy_static.workspace = true +log.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +postage.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +rand.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +smol.workspace = true + +tree-sitter-rust = { workspace = true, optional = true } +tree-sitter-html = { workspace = true, optional = true } +tree-sitter-typescript = { workspace = true, optional = true } + +[dev-dependencies] +copilot = { package = "copilot2", path = "../copilot2", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +rand.workspace = true +unindent.workspace = true +tree-sitter.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-html.workspace = true +tree-sitter-typescript.workspace = true diff --git a/crates/multi_buffer2/src/anchor.rs b/crates/multi_buffer2/src/anchor.rs new file mode 100644 index 0000000000..39a8182da1 --- /dev/null +++ b/crates/multi_buffer2/src/anchor.rs @@ -0,0 +1,138 @@ +use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; +use language::{OffsetUtf16, Point, TextDimension}; +use std::{ + cmp::Ordering, + ops::{Range, Sub}, +}; +use sum_tree::Bias; + +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] +pub struct Anchor { + pub buffer_id: Option, + pub excerpt_id: ExcerptId, + pub text_anchor: text::Anchor, +} + +impl Anchor { + pub fn min() -> Self { + Self { + buffer_id: None, + excerpt_id: ExcerptId::min(), + text_anchor: text::Anchor::MIN, + } + } + + pub fn max() -> Self { + Self { + buffer_id: None, + excerpt_id: ExcerptId::max(), + text_anchor: text::Anchor::MAX, + } + } + + pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { + let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot); + if excerpt_id_cmp.is_eq() { + if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { + Ordering::Equal + } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) + } else { + Ordering::Equal + } + } else { + excerpt_id_cmp + } + } + + pub fn bias(&self) -> Bias { + self.text_anchor.bias + } + + pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { + if self.text_anchor.bias != Bias::Left { + if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id.clone(), + text_anchor: self.text_anchor.bias_left(&excerpt.buffer), + }; + } + } + self.clone() + } + + pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { + if self.text_anchor.bias != Bias::Right { + if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id.clone(), + text_anchor: self.text_anchor.bias_right(&excerpt.buffer), + }; + } + } + self.clone() + } + + pub fn summary(&self, snapshot: &MultiBufferSnapshot) -> D + where + D: TextDimension + Ord + Sub, + { + snapshot.summary_for_anchor(self) + } + + pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool { + if *self == Anchor::min() || *self == Anchor::max() { + true + } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + excerpt.contains(self) + && (self.text_anchor == excerpt.range.context.start + || self.text_anchor == excerpt.range.context.end + || self.text_anchor.is_valid(&excerpt.buffer)) + } else { + false + } + } +} + +impl ToOffset for Anchor { + fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize { + self.summary(snapshot) + } +} + +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + self.summary(snapshot) + } +} + +impl ToPoint for Anchor { + fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { + self.summary(snapshot) + } +} + +pub trait AnchorRangeExt { + fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Ordering; + fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; + fn to_point(&self, content: &MultiBufferSnapshot) -> Range; +} + +impl AnchorRangeExt for Range { + fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { + match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), + ord => ord, + } + } + + fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { + self.start.to_offset(content)..self.end.to_offset(content) + } + + fn to_point(&self, content: &MultiBufferSnapshot) -> Range { + self.start.to_point(content)..self.end.to_point(content) + } +} diff --git a/crates/multi_buffer2/src/multi_buffer2.rs b/crates/multi_buffer2/src/multi_buffer2.rs new file mode 100644 index 0000000000..df33f98b4b --- /dev/null +++ b/crates/multi_buffer2/src/multi_buffer2.rs @@ -0,0 +1,5393 @@ +mod anchor; + +pub use anchor::{Anchor, AnchorRangeExt}; +use anyhow::{anyhow, Result}; +use clock::ReplicaId; +use collections::{BTreeMap, Bound, HashMap, HashSet}; +use futures::{channel::mpsc, SinkExt}; +use git::diff::DiffHunk; +use gpui::{AppContext, EventEmitter, Model, ModelContext}; +pub use language::Completion; +use language::{ + char_kind, + language_settings::{language_settings, LanguageSettings}, + AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, + DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, + Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, + ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, +}; +use std::{ + borrow::Cow, + cell::{Ref, RefCell}, + cmp, fmt, + future::Future, + io, + iter::{self, FromIterator}, + mem, + ops::{Range, RangeBounds, Sub}, + str, + sync::Arc, + time::{Duration, Instant}, +}; +use sum_tree::{Bias, Cursor, SumTree}; +use text::{ + locator::Locator, + subscription::{Subscription, Topic}, + Edit, TextSummary, +}; +use theme::SyntaxTheme; +use util::post_inc; + +#[cfg(any(test, feature = "test-support"))] +use gpui::Context; + +const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; + +#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExcerptId(usize); + +pub struct MultiBuffer { + snapshot: RefCell, + buffers: RefCell>, + next_excerpt_id: usize, + subscriptions: Topic, + singleton: bool, + replica_id: ReplicaId, + history: History, + title: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + ExcerptsAdded { + buffer: Model, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, + ExcerptsEdited { + ids: Vec, + }, + Edited { + sigleton_buffer_edited: bool, + }, + TransactionUndone { + transaction_id: TransactionId, + }, + Reloaded, + DiffBaseChanged, + LanguageChanged, + Reparsed, + Saved, + FileHandleChanged, + Closed, + DirtyChanged, + DiagnosticsUpdated, +} + +#[derive(Clone)] +struct History { + next_transaction_id: TransactionId, + undo_stack: Vec, + redo_stack: Vec, + transaction_depth: usize, + group_interval: Duration, +} + +#[derive(Clone)] +struct Transaction { + id: TransactionId, + buffer_transactions: HashMap, + first_edit_at: Instant, + last_edit_at: Instant, + suppress_grouping: bool, +} + +pub trait ToOffset: 'static + fmt::Debug { + fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize; +} + +pub trait ToOffsetUtf16: 'static + fmt::Debug { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16; +} + +pub trait ToPoint: 'static + fmt::Debug { + fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; +} + +pub trait ToPointUtf16: 'static + fmt::Debug { + fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16; +} + +struct BufferState { + buffer: Model, + last_version: clock::Global, + last_parse_count: usize, + last_selections_update_count: usize, + last_diagnostics_update_count: usize, + last_file_update_count: usize, + last_git_diff_update_count: usize, + excerpts: Vec, + _subscriptions: [gpui::Subscription; 2], +} + +#[derive(Clone, Default)] +pub struct MultiBufferSnapshot { + singleton: bool, + excerpts: SumTree, + excerpt_ids: SumTree, + parse_count: usize, + diagnostics_update_count: usize, + trailing_excerpt_update_count: usize, + git_diff_update_count: usize, + edit_count: usize, + is_dirty: bool, + has_conflict: bool, +} + +pub struct ExcerptBoundary { + pub id: ExcerptId, + pub row: u32, + pub buffer: BufferSnapshot, + pub range: ExcerptRange, + pub starts_new_buffer: bool, +} + +#[derive(Clone)] +struct Excerpt { + id: ExcerptId, + locator: Locator, + buffer_id: u64, + buffer: BufferSnapshot, + range: ExcerptRange, + max_buffer_row: u32, + text_summary: TextSummary, + has_trailing_newline: bool, +} + +#[derive(Clone, Debug)] +struct ExcerptIdMapping { + id: ExcerptId, + locator: Locator, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExcerptRange { + pub context: Range, + pub primary: Option>, +} + +#[derive(Clone, Debug, Default)] +struct ExcerptSummary { + excerpt_id: ExcerptId, + excerpt_locator: Locator, + max_buffer_row: u32, + text: TextSummary, +} + +#[derive(Clone)] +pub struct MultiBufferRows<'a> { + buffer_row_range: Range, + excerpts: Cursor<'a, Excerpt, Point>, +} + +pub struct MultiBufferChunks<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_chunks: Option>, + language_aware: bool, +} + +pub struct MultiBufferBytes<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_bytes: Option>, + chunk: &'a [u8], +} + +pub struct ReversedMultiBufferBytes<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_bytes: Option>, + chunk: &'a [u8], +} + +struct ExcerptChunks<'a> { + content_chunks: BufferChunks<'a>, + footer_height: usize, +} + +struct ExcerptBytes<'a> { + content_bytes: text::Bytes<'a>, + footer_height: usize, +} + +impl MultiBuffer { + pub fn new(replica_id: ReplicaId) -> Self { + Self { + snapshot: Default::default(), + buffers: Default::default(), + next_excerpt_id: 1, + subscriptions: Default::default(), + singleton: false, + replica_id, + history: History { + next_transaction_id: Default::default(), + undo_stack: Default::default(), + redo_stack: Default::default(), + transaction_depth: 0, + group_interval: Duration::from_millis(300), + }, + title: Default::default(), + } + } + + pub fn clone(&self, new_cx: &mut ModelContext) -> Self { + let mut buffers = HashMap::default(); + for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + buffers.insert( + *buffer_id, + BufferState { + buffer: buffer_state.buffer.clone(), + last_version: buffer_state.last_version.clone(), + last_parse_count: buffer_state.last_parse_count, + last_selections_update_count: buffer_state.last_selections_update_count, + last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, + last_file_update_count: buffer_state.last_file_update_count, + last_git_diff_update_count: buffer_state.last_git_diff_update_count, + excerpts: buffer_state.excerpts.clone(), + _subscriptions: [ + new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), + new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event), + ], + }, + ); + } + Self { + snapshot: RefCell::new(self.snapshot.borrow().clone()), + buffers: RefCell::new(buffers), + next_excerpt_id: 1, + subscriptions: Default::default(), + singleton: self.singleton, + replica_id: self.replica_id, + history: self.history.clone(), + title: self.title.clone(), + } + } + + pub fn with_title(mut self, title: String) -> Self { + self.title = Some(title); + self + } + + pub fn singleton(buffer: Model, cx: &mut ModelContext) -> Self { + let mut this = Self::new(buffer.read(cx).replica_id()); + this.singleton = true; + this.push_excerpts( + buffer, + [ExcerptRange { + context: text::Anchor::MIN..text::Anchor::MAX, + primary: None, + }], + cx, + ); + this.snapshot.borrow_mut().singleton = true; + this + } + + pub fn replica_id(&self) -> ReplicaId { + self.replica_id + } + + pub fn snapshot(&self, cx: &AppContext) -> MultiBufferSnapshot { + self.sync(cx); + self.snapshot.borrow().clone() + } + + pub fn read(&self, cx: &AppContext) -> Ref { + self.sync(cx); + self.snapshot.borrow() + } + + pub fn as_singleton(&self) -> Option> { + if self.singleton { + return Some( + self.buffers + .borrow() + .values() + .next() + .unwrap() + .buffer + .clone(), + ); + } else { + None + } + } + + pub fn is_singleton(&self) -> bool { + self.singleton + } + + pub fn subscribe(&mut self) -> Subscription { + self.subscriptions.subscribe() + } + + pub fn is_dirty(&self, cx: &AppContext) -> bool { + self.read(cx).is_dirty() + } + + pub fn has_conflict(&self, cx: &AppContext) -> bool { + self.read(cx).has_conflict() + } + + // The `is_empty` signature doesn't match what clippy expects + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, cx: &AppContext) -> usize { + self.read(cx).len() + } + + pub fn is_empty(&self, cx: &AppContext) -> bool { + self.len(cx) != 0 + } + + pub fn symbols_containing( + &self, + offset: T, + theme: Option<&SyntaxTheme>, + cx: &AppContext, + ) -> Option<(u64, Vec>)> { + self.read(cx).symbols_containing(offset, theme) + } + + pub fn edit( + &mut self, + edits: I, + mut autoindent_mode: Option, + cx: &mut ModelContext, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.buffers.borrow().is_empty() { + return; + } + + let snapshot = self.read(cx); + let edits = edits.into_iter().map(|(range, new_text)| { + let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + if range.start > range.end { + mem::swap(&mut range.start, &mut range.end); + } + (range, new_text) + }); + + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| { + buffer.edit(edits, autoindent_mode, cx); + }); + } + + let original_indent_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) => mem::take(original_indent_columns), + _ => Default::default(), + }; + + struct BufferEdit { + range: Range, + new_text: Arc, + is_insertion: bool, + original_indent_column: u32, + } + let mut buffer_edits: HashMap> = Default::default(); + let mut edited_excerpt_ids = Vec::new(); + let mut cursor = snapshot.excerpts.cursor::(); + for (ix, (range, new_text)) in edits.enumerate() { + let new_text: Arc = new_text.into(); + let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); + cursor.seek(&range.start, Bias::Right, &()); + if cursor.item().is_none() && range.start == *cursor.start() { + cursor.prev(&()); + } + let start_excerpt = cursor.item().expect("start offset out of bounds"); + let start_overshoot = range.start - cursor.start(); + let buffer_start = start_excerpt + .range + .context + .start + .to_offset(&start_excerpt.buffer) + + start_overshoot; + edited_excerpt_ids.push(start_excerpt.id); + + cursor.seek(&range.end, Bias::Right, &()); + if cursor.item().is_none() && range.end == *cursor.start() { + cursor.prev(&()); + } + let end_excerpt = cursor.item().expect("end offset out of bounds"); + let end_overshoot = range.end - cursor.start(); + let buffer_end = end_excerpt + .range + .context + .start + .to_offset(&end_excerpt.buffer) + + end_overshoot; + + if start_excerpt.id == end_excerpt.id { + buffer_edits + .entry(start_excerpt.buffer_id) + .or_insert(Vec::new()) + .push(BufferEdit { + range: buffer_start..buffer_end, + new_text, + is_insertion: true, + original_indent_column, + }); + } else { + edited_excerpt_ids.push(end_excerpt.id); + let start_excerpt_range = buffer_start + ..start_excerpt + .range + .context + .end + .to_offset(&start_excerpt.buffer); + let end_excerpt_range = end_excerpt + .range + .context + .start + .to_offset(&end_excerpt.buffer) + ..buffer_end; + buffer_edits + .entry(start_excerpt.buffer_id) + .or_insert(Vec::new()) + .push(BufferEdit { + range: start_excerpt_range, + new_text: new_text.clone(), + is_insertion: true, + original_indent_column, + }); + buffer_edits + .entry(end_excerpt.buffer_id) + .or_insert(Vec::new()) + .push(BufferEdit { + range: end_excerpt_range, + new_text: new_text.clone(), + is_insertion: false, + original_indent_column, + }); + + cursor.seek(&range.start, Bias::Right, &()); + cursor.next(&()); + while let Some(excerpt) = cursor.item() { + if excerpt.id == end_excerpt.id { + break; + } + buffer_edits + .entry(excerpt.buffer_id) + .or_insert(Vec::new()) + .push(BufferEdit { + range: excerpt.range.context.to_offset(&excerpt.buffer), + new_text: new_text.clone(), + is_insertion: false, + original_indent_column, + }); + edited_excerpt_ids.push(excerpt.id); + cursor.next(&()); + } + } + } + + drop(cursor); + drop(snapshot); + // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. + fn tail( + this: &mut MultiBuffer, + buffer_edits: HashMap>, + autoindent_mode: Option, + edited_excerpt_ids: Vec, + cx: &mut ModelContext, + ) { + for (buffer_id, mut edits) in buffer_edits { + edits.sort_unstable_by_key(|edit| edit.range.start); + this.buffers.borrow()[&buffer_id] + .buffer + .update(cx, |buffer, cx| { + let mut edits = edits.into_iter().peekable(); + let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); + let mut deletions = Vec::new(); + let empty_str: Arc = "".into(); + while let Some(BufferEdit { + mut range, + new_text, + mut is_insertion, + original_indent_column, + }) = edits.next() + { + while let Some(BufferEdit { + range: next_range, + is_insertion: next_is_insertion, + .. + }) = edits.peek() + { + if range.end >= next_range.start { + range.end = cmp::max(next_range.end, range.end); + is_insertion |= *next_is_insertion; + edits.next(); + } else { + break; + } + } + + if is_insertion { + original_indent_columns.push(original_indent_column); + insertions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + new_text.clone(), + )); + } else if !range.is_empty() { + deletions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + empty_str.clone(), + )); + } + } + + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + None + }; + + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + }) + } + + cx.emit(Event::ExcerptsEdited { + ids: edited_excerpt_ids, + }); + } + tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx); + } + + pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { + self.start_transaction_at(Instant::now(), cx) + } + + pub fn start_transaction_at( + &mut self, + now: Instant, + cx: &mut ModelContext, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + + for BufferState { buffer, .. } in self.buffers.borrow().values() { + buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + self.history.start_transaction(now) + } + + pub fn end_transaction(&mut self, cx: &mut ModelContext) -> Option { + self.end_transaction_at(Instant::now(), cx) + } + + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut ModelContext, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); + } + + let mut buffer_transactions = HashMap::default(); + for BufferState { buffer, .. } in self.buffers.borrow().values() { + if let Some(transaction_id) = + buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); + } + } + + if self.history.end_transaction(now, buffer_transactions) { + let transaction_id = self.history.group().unwrap(); + Some(transaction_id) + } else { + None + } + } + + pub fn merge_transactions( + &mut self, + transaction: TransactionId, + destination: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.merge_transactions(transaction, destination) + }); + } else { + if let Some(transaction) = self.history.forget(transaction) { + if let Some(destination) = self.history.transaction_mut(destination) { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.borrow().get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); + } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); + } + } + } + } + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext) { + self.history.finalize_last_transaction(); + for BufferState { buffer, .. } in self.buffers.borrow().values() { + buffer.update(cx, |buffer, _| { + buffer.finalize_last_transaction(); + }); + } + } + + pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext) + where + T: IntoIterator, &'a language::Transaction)>, + { + self.history + .push_transaction(buffer_transactions, Instant::now(), cx); + self.history.finalize_last_transaction(); + } + + pub fn group_until_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.group_until_transaction(transaction_id) + }); + } else { + self.history.group_until(transaction_id); + } + } + + pub fn set_active_selections( + &mut self, + selections: &[Selection], + line_mode: bool, + cursor_shape: CursorShape, + cx: &mut ModelContext, + ) { + let mut selections_by_buffer: HashMap>> = + Default::default(); + let snapshot = self.read(cx); + let mut cursor = snapshot.excerpts.cursor::>(); + for selection in selections { + let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); + let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); + + cursor.seek(&Some(start_locator), Bias::Left, &()); + while let Some(excerpt) = cursor.item() { + if excerpt.locator > *end_locator { + break; + } + + let mut start = excerpt.range.context.start; + let mut end = excerpt.range.context.end; + if excerpt.id == selection.start.excerpt_id { + start = selection.start.text_anchor; + } + if excerpt.id == selection.end.excerpt_id { + end = selection.end.text_anchor; + } + selections_by_buffer + .entry(excerpt.buffer_id) + .or_default() + .push(Selection { + id: selection.id, + start, + end, + reversed: selection.reversed, + goal: selection.goal, + }); + + cursor.next(&()); + } + } + + for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + if !selections_by_buffer.contains_key(buffer_id) { + buffer_state + .buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + } + } + + for (buffer_id, mut selections) in selections_by_buffer { + self.buffers.borrow()[&buffer_id] + .buffer + .update(cx, |buffer, cx| { + selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); + let mut selections = selections.into_iter().peekable(); + let merged_selections = Arc::from_iter(iter::from_fn(|| { + let mut selection = selections.next()?; + while let Some(next_selection) = selections.peek() { + if selection.end.cmp(&next_selection.start, buffer).is_ge() { + let next_selection = selections.next().unwrap(); + if next_selection.end.cmp(&selection.end, buffer).is_ge() { + selection.end = next_selection.end; + } + } else { + break; + } + } + Some(selection) + })); + buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); + }); + } + } + + pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { + for buffer in self.buffers.borrow().values() { + buffer + .buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + } + } + + pub fn undo(&mut self, cx: &mut ModelContext) -> Option { + let mut transaction_id = None; + if let Some(buffer) = self.as_singleton() { + transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); + } else { + while let Some(transaction) = self.history.pop_undo() { + let mut undone = false; + for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + undone |= buffer.update(cx, |buffer, cx| { + let undo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_undo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.undo_to_transaction(undo_to, cx) + }); + } + } + + if undone { + transaction_id = Some(transaction.id); + break; + } + } + } + + if let Some(transaction_id) = transaction_id { + cx.emit(Event::TransactionUndone { transaction_id }); + } + + transaction_id + } + + pub fn redo(&mut self, cx: &mut ModelContext) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.redo(cx)); + } + + while let Some(transaction) = self.history.pop_redo() { + let mut redone = false; + for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + redone |= buffer.update(cx, |buffer, cx| { + let redo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_redo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.redo_to_transaction(redo_to, cx) + }); + } + } + + if redone { + return Some(transaction.id); + } + } + + None + } + + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { + for (buffer_id, transaction_id) in &transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.undo_transaction(*transaction_id, cx) + }); + } + } + } + } + + pub fn stream_excerpts_with_context_lines( + &mut self, + buffer: Model, + ranges: Vec>, + context_line_count: u32, + cx: &mut ModelContext, + ) -> mpsc::Receiver> { + let (buffer_id, buffer_snapshot) = + buffer.update(cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + + let (mut tx, rx) = mpsc::channel(256); + cx.spawn(move |this, mut cx| async move { + let mut excerpt_ranges = Vec::new(); + let mut range_counts = Vec::new(); + cx.background_executor() + .scoped(|scope| { + scope.spawn(async { + let (ranges, counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + excerpt_ranges = ranges; + range_counts = counts; + }); + }) + .await; + + let mut ranges = ranges.into_iter(); + let mut range_counts = range_counts.into_iter(); + for excerpt_ranges in excerpt_ranges.chunks(100) { + let excerpt_ids = match this.update(&mut cx, |this, cx| { + this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + }) { + Ok(excerpt_ids) => excerpt_ids, + Err(_) => return, + }; + + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) + { + for range in ranges.by_ref().take(range_count) { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.start, + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.end, + }; + if tx.send(start..end).await.is_err() { + break; + } + } + } + } + }) + .detach(); + + rx + } + + pub fn push_excerpts( + &mut self, + buffer: Model, + ranges: impl IntoIterator>, + cx: &mut ModelContext, + ) -> Vec + where + O: text::ToOffset, + { + self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) + } + + pub fn push_excerpts_with_context_lines( + &mut self, + buffer: Model, + ranges: Vec>, + context_line_count: u32, + cx: &mut ModelContext, + ) -> Vec> + where + O: text::ToPoint + text::ToOffset, + { + let buffer_id = buffer.read(cx).remote_id(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let (excerpt_ranges, range_counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + + let excerpt_ids = self.push_excerpts(buffer, excerpt_ranges, cx); + + let mut anchor_ranges = Vec::new(); + let mut ranges = ranges.into_iter(); + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.into_iter()) { + anchor_ranges.extend(ranges.by_ref().take(range_count).map(|range| { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer_snapshot.anchor_after(range.start), + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer_snapshot.anchor_after(range.end), + }; + start..end + })) + } + anchor_ranges + } + + pub fn insert_excerpts_after( + &mut self, + prev_excerpt_id: ExcerptId, + buffer: Model, + ranges: impl IntoIterator>, + cx: &mut ModelContext, + ) -> Vec + where + O: text::ToOffset, + { + let mut ids = Vec::new(); + let mut next_excerpt_id = self.next_excerpt_id; + self.insert_excerpts_with_ids_after( + prev_excerpt_id, + buffer, + ranges.into_iter().map(|range| { + let id = ExcerptId(post_inc(&mut next_excerpt_id)); + ids.push(id); + (id, range) + }), + cx, + ); + ids + } + + pub fn insert_excerpts_with_ids_after( + &mut self, + prev_excerpt_id: ExcerptId, + buffer: Model, + ranges: impl IntoIterator)>, + cx: &mut ModelContext, + ) where + O: text::ToOffset, + { + assert_eq!(self.history.transaction_depth, 0); + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_none() { + return Default::default(); + } + + self.sync(cx); + + let buffer_id = buffer.read(cx).remote_id(); + let buffer_snapshot = buffer.read(cx).snapshot(); + + let mut buffers = self.buffers.borrow_mut(); + let buffer_state = buffers.entry(buffer_id).or_insert_with(|| BufferState { + last_version: buffer_snapshot.version().clone(), + last_parse_count: buffer_snapshot.parse_count(), + last_selections_update_count: buffer_snapshot.selections_update_count(), + last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(), + last_file_update_count: buffer_snapshot.file_update_count(), + last_git_diff_update_count: buffer_snapshot.git_diff_update_count(), + excerpts: Default::default(), + _subscriptions: [ + cx.observe(&buffer, |_, _, cx| cx.notify()), + cx.subscribe(&buffer, Self::on_buffer_event), + ], + buffer: buffer.clone(), + }); + + let mut snapshot = self.snapshot.borrow_mut(); + + let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); + let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); + let mut cursor = snapshot.excerpts.cursor::>(); + let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &()); + prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); + + let edit_start = new_excerpts.summary().text.len; + new_excerpts.update_last( + |excerpt| { + excerpt.has_trailing_newline = true; + }, + &(), + ); + + let next_locator = if let Some(excerpt) = cursor.item() { + excerpt.locator.clone() + } else { + Locator::max() + }; + + let mut excerpts = Vec::new(); + while let Some((id, range)) = ranges.next() { + let locator = Locator::between(&prev_locator, &next_locator); + if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { + buffer_state.excerpts.insert(ix, locator.clone()); + } + let range = ExcerptRange { + context: buffer_snapshot.anchor_before(&range.context.start) + ..buffer_snapshot.anchor_after(&range.context.end), + primary: range.primary.map(|primary| { + buffer_snapshot.anchor_before(&primary.start) + ..buffer_snapshot.anchor_after(&primary.end) + }), + }; + if id.0 >= self.next_excerpt_id { + self.next_excerpt_id = id.0 + 1; + } + excerpts.push((id, range.clone())); + let excerpt = Excerpt::new( + id, + locator.clone(), + buffer_id, + buffer_snapshot.clone(), + range, + ranges.peek().is_some() || cursor.item().is_some(), + ); + new_excerpts.push(excerpt, &()); + prev_locator = locator.clone(); + new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); + } + + let edit_end = new_excerpts.summary().text.len; + + let suffix = cursor.suffix(&()); + let changed_trailing_excerpt = suffix.is_empty(); + new_excerpts.append(suffix, &()); + drop(cursor); + snapshot.excerpts = new_excerpts; + snapshot.excerpt_ids = new_excerpt_ids; + if changed_trailing_excerpt { + snapshot.trailing_excerpt_update_count += 1; + } + + self.subscriptions.publish_mut([Edit { + old: edit_start..edit_start, + new: edit_start..edit_end, + }]); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); + cx.emit(Event::ExcerptsAdded { + buffer, + predecessor: prev_excerpt_id, + excerpts, + }); + cx.notify(); + } + + pub fn clear(&mut self, cx: &mut ModelContext) { + self.sync(cx); + let ids = self.excerpt_ids(); + self.buffers.borrow_mut().clear(); + let mut snapshot = self.snapshot.borrow_mut(); + let prev_len = snapshot.len(); + snapshot.excerpts = Default::default(); + snapshot.trailing_excerpt_update_count += 1; + snapshot.is_dirty = false; + snapshot.has_conflict = false; + + self.subscriptions.publish_mut([Edit { + old: 0..prev_len, + new: 0..0, + }]); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); + cx.emit(Event::ExcerptsRemoved { ids }); + cx.notify(); + } + + pub fn excerpts_for_buffer( + &self, + buffer: &Model, + cx: &AppContext, + ) -> Vec<(ExcerptId, ExcerptRange)> { + let mut excerpts = Vec::new(); + let snapshot = self.read(cx); + let buffers = self.buffers.borrow(); + let mut cursor = snapshot.excerpts.cursor::>(); + for locator in buffers + .get(&buffer.read(cx).remote_id()) + .map(|state| &state.excerpts) + .into_iter() + .flatten() + { + cursor.seek_forward(&Some(locator), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.locator == *locator { + excerpts.push((excerpt.id.clone(), excerpt.range.clone())); + } + } + } + + excerpts + } + + pub fn excerpt_ids(&self) -> Vec { + self.snapshot + .borrow() + .excerpts + .iter() + .map(|entry| entry.id) + .collect() + } + + pub fn excerpt_containing( + &self, + position: impl ToOffset, + cx: &AppContext, + ) -> Option<(ExcerptId, Model, Range)> { + let snapshot = self.read(cx); + let position = position.to_offset(&snapshot); + + let mut cursor = snapshot.excerpts.cursor::(); + cursor.seek(&position, Bias::Right, &()); + cursor + .item() + .or_else(|| snapshot.excerpts.last()) + .map(|excerpt| { + ( + excerpt.id.clone(), + self.buffers + .borrow() + .get(&excerpt.buffer_id) + .unwrap() + .buffer + .clone(), + excerpt.range.context.clone(), + ) + }) + } + + // If point is at the end of the buffer, the last excerpt is returned + pub fn point_to_buffer_offset( + &self, + point: T, + cx: &AppContext, + ) -> Option<(Model, usize, ExcerptId)> { + let snapshot = self.read(cx); + let offset = point.to_offset(&snapshot); + let mut cursor = snapshot.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + cursor.item().map(|excerpt| { + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let buffer_point = excerpt_start + offset - *cursor.start(); + let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); + + (buffer, buffer_point, excerpt.id) + }) + } + + pub fn range_to_buffer_ranges( + &self, + range: Range, + cx: &AppContext, + ) -> Vec<(Model, Range, ExcerptId)> { + let snapshot = self.read(cx); + let start = range.start.to_offset(&snapshot); + let end = range.end.to_offset(&snapshot); + + let mut result = Vec::new(); + let mut cursor = snapshot.excerpts.cursor::(); + cursor.seek(&start, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + while let Some(excerpt) = cursor.item() { + if *cursor.start() > end { + break; + } + + let mut end_before_newline = cursor.end(&()); + if excerpt.has_trailing_newline { + end_before_newline -= 1; + } + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); + let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); + let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); + result.push((buffer, start..end, excerpt.id)); + cursor.next(&()); + } + + result + } + + pub fn remove_excerpts( + &mut self, + excerpt_ids: impl IntoIterator, + cx: &mut ModelContext, + ) { + self.sync(cx); + let ids = excerpt_ids.into_iter().collect::>(); + if ids.is_empty() { + return; + } + + let mut buffers = self.buffers.borrow_mut(); + let mut snapshot = self.snapshot.borrow_mut(); + let mut new_excerpts = SumTree::new(); + let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); + let mut edits = Vec::new(); + let mut excerpt_ids = ids.iter().copied().peekable(); + + while let Some(excerpt_id) = excerpt_ids.next() { + // Seek to the next excerpt to remove, preserving any preceding excerpts. + let locator = snapshot.excerpt_locator_for_id(excerpt_id); + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); + + if let Some(mut excerpt) = cursor.item() { + if excerpt.id != excerpt_id { + continue; + } + let mut old_start = cursor.start().1; + + // Skip over the removed excerpt. + 'remove_excerpts: loop { + if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) { + buffer_state.excerpts.retain(|l| l != &excerpt.locator); + if buffer_state.excerpts.is_empty() { + buffers.remove(&excerpt.buffer_id); + } + } + cursor.next(&()); + + // Skip over any subsequent excerpts that are also removed. + while let Some(&next_excerpt_id) = excerpt_ids.peek() { + let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); + if let Some(next_excerpt) = cursor.item() { + if next_excerpt.locator == *next_locator { + excerpt_ids.next(); + excerpt = next_excerpt; + continue 'remove_excerpts; + } + } + break; + } + + break; + } + + // When removing the last excerpt, remove the trailing newline from + // the previous excerpt. + if cursor.item().is_none() && old_start > 0 { + old_start -= 1; + new_excerpts.update_last(|e| e.has_trailing_newline = false, &()); + } + + // Push an edit for the removal of this run of excerpts. + let old_end = cursor.start().1; + let new_start = new_excerpts.summary().text.len; + edits.push(Edit { + old: old_start..old_end, + new: new_start..new_start, + }); + } + } + let suffix = cursor.suffix(&()); + let changed_trailing_excerpt = suffix.is_empty(); + new_excerpts.append(suffix, &()); + drop(cursor); + snapshot.excerpts = new_excerpts; + + if changed_trailing_excerpt { + snapshot.trailing_excerpt_update_count += 1; + } + + self.subscriptions.publish_mut(edits); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); + cx.emit(Event::ExcerptsRemoved { ids }); + cx.notify(); + } + + pub fn wait_for_anchors<'a>( + &self, + anchors: impl 'a + Iterator, + cx: &mut ModelContext, + ) -> impl 'static + Future> { + let borrow = self.buffers.borrow(); + let mut error = None; + let mut futures = Vec::new(); + for anchor in anchors { + if let Some(buffer_id) = anchor.buffer_id { + if let Some(buffer) = borrow.get(&buffer_id) { + buffer.buffer.update(cx, |buffer, _| { + futures.push(buffer.wait_for_anchors([anchor.text_anchor])) + }); + } else { + error = Some(anyhow!( + "buffer {buffer_id} is not part of this multi-buffer" + )); + break; + } + } + } + async move { + if let Some(error) = error { + Err(error)?; + } + for future in futures { + future.await?; + } + Ok(()) + } + } + + pub fn text_anchor_for_position( + &self, + position: T, + cx: &AppContext, + ) -> Option<(Model, language::Anchor)> { + let snapshot = self.read(cx); + let anchor = snapshot.anchor_before(position); + let buffer = self + .buffers + .borrow() + .get(&anchor.buffer_id?)? + .buffer + .clone(); + Some((buffer, anchor.text_anchor)) + } + + fn on_buffer_event( + &mut self, + _: Model, + event: &language::Event, + cx: &mut ModelContext, + ) { + cx.emit(match event { + language::Event::Edited => Event::Edited { + sigleton_buffer_edited: true, + }, + language::Event::DirtyChanged => Event::DirtyChanged, + language::Event::Saved => Event::Saved, + language::Event::FileHandleChanged => Event::FileHandleChanged, + language::Event::Reloaded => Event::Reloaded, + language::Event::DiffBaseChanged => Event::DiffBaseChanged, + language::Event::LanguageChanged => Event::LanguageChanged, + language::Event::Reparsed => Event::Reparsed, + language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, + language::Event::Closed => Event::Closed, + + // + language::Event::Operation(_) => return, + }); + } + + pub fn all_buffers(&self) -> HashSet> { + self.buffers + .borrow() + .values() + .map(|state| state.buffer.clone()) + .collect() + } + + pub fn buffer(&self, buffer_id: u64) -> Option> { + self.buffers + .borrow() + .get(&buffer_id) + .map(|state| state.buffer.clone()) + } + + pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + let snapshot = self.snapshot(cx); + let position = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(position); + if char_kind(&scope, char) == CharKind::Word { + return true; + } + + let anchor = snapshot.anchor_before(position); + anchor + .buffer_id + .and_then(|buffer_id| { + let buffer = self.buffers.borrow().get(&buffer_id)?.buffer.clone(); + Some( + buffer + .read(cx) + .completion_triggers() + .iter() + .any(|string| string == text), + ) + }) + .unwrap_or(false) + } + + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option> { + self.point_to_buffer_offset(point, cx) + .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) + } + + pub fn settings_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let mut language = None; + let mut file = None; + if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { + let buffer = buffer.read(cx); + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language.as_ref(), file, cx) + } + + pub fn for_each_buffer(&self, mut f: impl FnMut(&Model)) { + self.buffers + .borrow() + .values() + .for_each(|state| f(&state.buffer)) + } + + pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> { + if let Some(title) = self.title.as_ref() { + return title.into(); + } + + if let Some(buffer) = self.as_singleton() { + if let Some(file) = buffer.read(cx).file() { + return file.file_name(cx).to_string_lossy(); + } + } + + "untitled".into() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn is_parsing(&self, cx: &AppContext) -> bool { + self.as_singleton().unwrap().read(cx).is_parsing() + } + + fn sync(&self, cx: &AppContext) { + let mut snapshot = self.snapshot.borrow_mut(); + let mut excerpts_to_edit = Vec::new(); + let mut reparsed = false; + let mut diagnostics_updated = false; + let mut git_diff_updated = false; + let mut is_dirty = false; + let mut has_conflict = false; + let mut edited = false; + let mut buffers = self.buffers.borrow_mut(); + for buffer_state in buffers.values_mut() { + let buffer = buffer_state.buffer.read(cx); + let version = buffer.version(); + let parse_count = buffer.parse_count(); + let selections_update_count = buffer.selections_update_count(); + let diagnostics_update_count = buffer.diagnostics_update_count(); + let file_update_count = buffer.file_update_count(); + let git_diff_update_count = buffer.git_diff_update_count(); + + let buffer_edited = version.changed_since(&buffer_state.last_version); + let buffer_reparsed = parse_count > buffer_state.last_parse_count; + let buffer_selections_updated = + selections_update_count > buffer_state.last_selections_update_count; + let buffer_diagnostics_updated = + diagnostics_update_count > buffer_state.last_diagnostics_update_count; + let buffer_file_updated = file_update_count > buffer_state.last_file_update_count; + let buffer_git_diff_updated = + git_diff_update_count > buffer_state.last_git_diff_update_count; + if buffer_edited + || buffer_reparsed + || buffer_selections_updated + || buffer_diagnostics_updated + || buffer_file_updated + || buffer_git_diff_updated + { + buffer_state.last_version = version; + buffer_state.last_parse_count = parse_count; + buffer_state.last_selections_update_count = selections_update_count; + buffer_state.last_diagnostics_update_count = diagnostics_update_count; + buffer_state.last_file_update_count = file_update_count; + buffer_state.last_git_diff_update_count = git_diff_update_count; + excerpts_to_edit.extend( + buffer_state + .excerpts + .iter() + .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)), + ); + } + + edited |= buffer_edited; + reparsed |= buffer_reparsed; + diagnostics_updated |= buffer_diagnostics_updated; + git_diff_updated |= buffer_git_diff_updated; + is_dirty |= buffer.is_dirty(); + has_conflict |= buffer.has_conflict(); + } + if edited { + snapshot.edit_count += 1; + } + if reparsed { + snapshot.parse_count += 1; + } + if diagnostics_updated { + snapshot.diagnostics_update_count += 1; + } + if git_diff_updated { + snapshot.git_diff_update_count += 1; + } + snapshot.is_dirty = is_dirty; + snapshot.has_conflict = has_conflict; + + excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); + + let mut edits = Vec::new(); + let mut new_excerpts = SumTree::new(); + let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); + + for (locator, buffer, buffer_edited) in excerpts_to_edit { + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); + let old_excerpt = cursor.item().unwrap(); + let buffer = buffer.read(cx); + let buffer_id = buffer.remote_id(); + + let mut new_excerpt; + if buffer_edited { + edits.extend( + buffer + .edits_since_in_range::( + old_excerpt.buffer.version(), + old_excerpt.range.context.clone(), + ) + .map(|mut edit| { + let excerpt_old_start = cursor.start().1; + let excerpt_new_start = new_excerpts.summary().text.len; + edit.old.start += excerpt_old_start; + edit.old.end += excerpt_old_start; + edit.new.start += excerpt_new_start; + edit.new.end += excerpt_new_start; + edit + }), + ); + + new_excerpt = Excerpt::new( + old_excerpt.id, + locator.clone(), + buffer_id, + buffer.snapshot(), + old_excerpt.range.clone(), + old_excerpt.has_trailing_newline, + ); + } else { + new_excerpt = old_excerpt.clone(); + new_excerpt.buffer = buffer.snapshot(); + } + + new_excerpts.push(new_excerpt, &()); + cursor.next(&()); + } + new_excerpts.append(cursor.suffix(&()), &()); + + drop(cursor); + snapshot.excerpts = new_excerpts; + + self.subscriptions.publish(edits); + } +} + +#[cfg(any(test, feature = "test-support"))] +impl MultiBuffer { + pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> Model { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + cx.build_model(|cx| Self::singleton(buffer, cx)) + } + + pub fn build_multi( + excerpts: [(&str, Vec>); COUNT], + cx: &mut gpui::AppContext, + ) -> Model { + let multi = cx.build_model(|_| Self::new(0)); + for (text, ranges) in excerpts { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { + context: range, + primary: None, + }); + multi.update(cx, |multi, cx| { + multi.push_excerpts(buffer, excerpt_ranges, cx) + }); + } + + multi + } + + pub fn build_from_buffer(buffer: Model, cx: &mut gpui::AppContext) -> Model { + cx.build_model(|cx| Self::singleton(buffer, cx)) + } + + pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model { + cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + let mutation_count = rng.gen_range(1..=5); + multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); + multibuffer + }) + } + + pub fn randomly_edit( + &mut self, + rng: &mut impl rand::Rng, + edit_count: usize, + cx: &mut ModelContext, + ) { + use util::RandomCharIter; + + let snapshot = self.read(cx); + let mut edits: Vec<(Range, Arc)> = Vec::new(); + let mut last_end = None; + for _ in 0..edit_count { + if last_end.map_or(false, |last_end| last_end >= snapshot.len()) { + break; + } + + let new_start = last_end.map_or(0, |last_end| last_end + 1); + let end = snapshot.clip_offset(rng.gen_range(new_start..=snapshot.len()), Bias::Right); + let start = snapshot.clip_offset(rng.gen_range(new_start..=end), Bias::Right); + last_end = Some(end); + + let mut range = start..end; + if rng.gen_bool(0.2) { + mem::swap(&mut range.start, &mut range.end); + } + + let new_text_len = rng.gen_range(0..10); + let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); + + edits.push((range, new_text.into())); + } + log::info!("mutating multi-buffer with {:?}", edits); + drop(snapshot); + + self.edit(edits, None, cx); + } + + pub fn randomly_edit_excerpts( + &mut self, + rng: &mut impl rand::Rng, + mutation_count: usize, + cx: &mut ModelContext, + ) { + use rand::prelude::*; + use std::env; + use util::RandomCharIter; + + let max_excerpts = env::var("MAX_EXCERPTS") + .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) + .unwrap_or(5); + + let mut buffers = Vec::new(); + for _ in 0..mutation_count { + if rng.gen_bool(0.05) { + log::info!("Clearing multi-buffer"); + self.clear(cx); + continue; + } + + let excerpt_ids = self.excerpt_ids(); + if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { + let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { + let text = RandomCharIter::new(&mut *rng).take(10).collect::(); + buffers + .push(cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text))); + let buffer = buffers.last().unwrap().read(cx); + log::info!( + "Creating new buffer {} with text: {:?}", + buffer.remote_id(), + buffer.text() + ); + buffers.last().unwrap().clone() + } else { + self.buffers + .borrow() + .values() + .choose(rng) + .unwrap() + .buffer + .clone() + }; + + let buffer = buffer_handle.read(cx); + let buffer_text = buffer.text(); + let ranges = (0..rng.gen_range(0..5)) + .map(|_| { + let end_ix = + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + ExcerptRange { + context: start_ix..end_ix, + primary: None, + } + }) + .collect::>(); + log::info!( + "Inserting excerpts from buffer {} and ranges {:?}: {:?}", + buffer_handle.read(cx).remote_id(), + ranges.iter().map(|r| &r.context).collect::>(), + ranges + .iter() + .map(|r| &buffer_text[r.context.clone()]) + .collect::>() + ); + + let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); + log::info!("Inserted with ids: {:?}", excerpt_id); + } else { + let remove_count = rng.gen_range(1..=excerpt_ids.len()); + let mut excerpts_to_remove = excerpt_ids + .choose_multiple(rng, remove_count) + .cloned() + .collect::>(); + let snapshot = self.snapshot.borrow(); + excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot)); + drop(snapshot); + log::info!("Removing excerpts {:?}", excerpts_to_remove); + self.remove_excerpts(excerpts_to_remove, cx); + } + } + } + + pub fn randomly_mutate( + &mut self, + rng: &mut impl rand::Rng, + mutation_count: usize, + cx: &mut ModelContext, + ) { + use rand::prelude::*; + + if rng.gen_bool(0.7) || self.singleton { + let buffer = self + .buffers + .borrow() + .values() + .choose(rng) + .map(|state| state.buffer.clone()); + + if let Some(buffer) = buffer { + buffer.update(cx, |buffer, cx| { + if rng.gen() { + buffer.randomly_edit(rng, mutation_count, cx); + } else { + buffer.randomly_undo_redo(rng, cx); + } + }); + } else { + self.randomly_edit(rng, mutation_count, cx); + } + } else { + self.randomly_edit_excerpts(rng, mutation_count, cx); + } + + self.check_invariants(cx); + } + + fn check_invariants(&self, cx: &mut ModelContext) { + let snapshot = self.read(cx); + let excerpts = snapshot.excerpts.items(&()); + let excerpt_ids = snapshot.excerpt_ids.items(&()); + + for (ix, excerpt) in excerpts.iter().enumerate() { + if ix == 0 { + if excerpt.locator <= Locator::min() { + panic!("invalid first excerpt locator {:?}", excerpt.locator); + } + } else { + if excerpt.locator <= excerpts[ix - 1].locator { + panic!("excerpts are out-of-order: {:?}", excerpts); + } + } + } + + for (ix, entry) in excerpt_ids.iter().enumerate() { + if ix == 0 { + if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() { + panic!("invalid first excerpt id {:?}", entry.id); + } + } else { + if entry.id <= excerpt_ids[ix - 1].id { + panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); + } + } + } + } +} + +impl EventEmitter for MultiBuffer { + type Event = Event; +} + +impl MultiBufferSnapshot { + pub fn text(&self) -> String { + self.chunks(0..self.len(), false) + .map(|chunk| chunk.text) + .collect() + } + + pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { + let mut offset = position.to_offset(self); + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Left, &()); + let mut excerpt_chunks = cursor.item().map(|excerpt| { + let end_before_footer = cursor.start() + excerpt.text_summary.len; + let start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let end = start + (cmp::min(offset, end_before_footer) - cursor.start()); + excerpt.buffer.reversed_chunks_in_range(start..end) + }); + iter::from_fn(move || { + if offset == *cursor.start() { + cursor.prev(&()); + let excerpt = cursor.item()?; + excerpt_chunks = Some( + excerpt + .buffer + .reversed_chunks_in_range(excerpt.range.context.clone()), + ); + } + + let excerpt = cursor.item().unwrap(); + if offset == cursor.end(&()) && excerpt.has_trailing_newline { + offset -= 1; + Some("\n") + } else { + let chunk = excerpt_chunks.as_mut().unwrap().next().unwrap(); + offset -= chunk.len(); + Some(chunk) + } + }) + .flat_map(|c| c.chars().rev()) + } + + pub fn chars_at(&self, position: T) -> impl Iterator + '_ { + let offset = position.to_offset(self); + self.text_for_range(offset..self.len()) + .flat_map(|chunk| chunk.chars()) + } + + pub fn text_for_range(&self, range: Range) -> impl Iterator + '_ { + self.chunks(range, false).map(|chunk| chunk.text) + } + + pub fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + + pub fn contains_str_at(&self, position: T, needle: &str) -> bool + where + T: ToOffset, + { + let position = position.to_offset(self); + position == self.clip_offset(position, Bias::Left) + && self + .bytes_in_range(position..self.len()) + .flatten() + .copied() + .take(needle.len()) + .eq(needle.bytes()) + } + + pub fn surrounding_word(&self, start: T) -> (Range, Option) { + let mut start = start.to_offset(self); + let mut end = start; + let mut next_chars = self.chars_at(start).peekable(); + let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); + let word_kind = cmp::max( + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), + ); + + for ch in prev_chars { + if Some(kind(ch)) == word_kind && ch != '\n' { + start -= ch.len_utf8(); + } else { + break; + } + } + + for ch in next_chars { + if Some(kind(ch)) == word_kind && ch != '\n' { + end += ch.len_utf8(); + } else { + break; + } + } + + (start..end, word_kind) + } + + pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> { + if self.singleton { + self.excerpts + .iter() + .next() + .map(|e| (&e.id, e.buffer_id, &e.buffer)) + } else { + None + } + } + + pub fn len(&self) -> usize { + self.excerpts.summary().text.len + } + + pub fn is_empty(&self) -> bool { + self.excerpts.summary().text.len == 0 + } + + pub fn max_buffer_row(&self) -> u32 { + self.excerpts.summary().max_buffer_row + } + + pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset(offset, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let buffer_offset = excerpt + .buffer + .clip_offset(excerpt_start + (offset - cursor.start()), bias); + buffer_offset.saturating_sub(excerpt_start) + } else { + 0 + }; + cursor.start() + overshoot + } + + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point(point, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&point, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); + let buffer_point = excerpt + .buffer + .clip_point(excerpt_start + (point - cursor.start()), bias); + buffer_point.saturating_sub(excerpt_start) + } else { + Point::zero() + }; + *cursor.start() + overshoot + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset_utf16(offset, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let buffer_offset = excerpt + .buffer + .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias); + OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0)) + } else { + OffsetUtf16(0) + }; + *cursor.start() + overshoot + } + + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point_utf16(point, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&point.0, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt + .buffer + .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); + let buffer_point = excerpt + .buffer + .clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias); + buffer_point.saturating_sub(excerpt_start) + } else { + PointUtf16::zero() + }; + *cursor.start() + overshoot + } + + pub fn bytes_in_range(&self, range: Range) -> MultiBufferBytes { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut excerpts = self.excerpts.cursor::(); + excerpts.seek(&range.start, Bias::Right, &()); + + let mut chunk = &[][..]; + let excerpt_bytes = if let Some(excerpt) = excerpts.item() { + let mut excerpt_bytes = excerpt + .bytes_in_range(range.start - excerpts.start()..range.end - excerpts.start()); + chunk = excerpt_bytes.next().unwrap_or(&[][..]); + Some(excerpt_bytes) + } else { + None + }; + MultiBufferBytes { + range, + excerpts, + excerpt_bytes, + chunk, + } + } + + pub fn reversed_bytes_in_range( + &self, + range: Range, + ) -> ReversedMultiBufferBytes { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut excerpts = self.excerpts.cursor::(); + excerpts.seek(&range.end, Bias::Left, &()); + + let mut chunk = &[][..]; + let excerpt_bytes = if let Some(excerpt) = excerpts.item() { + let mut excerpt_bytes = excerpt.reversed_bytes_in_range( + range.start - excerpts.start()..range.end - excerpts.start(), + ); + chunk = excerpt_bytes.next().unwrap_or(&[][..]); + Some(excerpt_bytes) + } else { + None + }; + + ReversedMultiBufferBytes { + range, + excerpts, + excerpt_bytes, + chunk, + } + } + + pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { + let mut result = MultiBufferRows { + buffer_row_range: 0..0, + excerpts: self.excerpts.cursor(), + }; + result.seek(start_row); + result + } + + pub fn chunks(&self, range: Range, language_aware: bool) -> MultiBufferChunks { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut chunks = MultiBufferChunks { + range: range.clone(), + excerpts: self.excerpts.cursor(), + excerpt_chunks: None, + language_aware, + }; + chunks.seek(range.start); + chunks + } + + pub fn offset_to_point(&self, offset: usize) -> Point { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point(offset); + } + + let mut cursor = self.excerpts.cursor::<(usize, Point)>(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_point) = cursor.start(); + let overshoot = offset - start_offset; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); + let buffer_point = excerpt + .buffer + .offset_to_point(excerpt_start_offset + overshoot); + *start_point + (buffer_point - excerpt_start_point) + } else { + self.excerpts.summary().text.lines + } + } + + pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point_utf16(offset); + } + + let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_point) = cursor.start(); + let overshoot = offset - start_offset; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_point = excerpt.range.context.start.to_point_utf16(&excerpt.buffer); + let buffer_point = excerpt + .buffer + .offset_to_point_utf16(excerpt_start_offset + overshoot); + *start_point + (buffer_point - excerpt_start_point) + } else { + self.excerpts.summary().text.lines_utf16() + } + } + + pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_point_utf16(point); + } + + let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(); + cursor.seek(&point, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_point) = cursor.start(); + let overshoot = point - start_offset; + let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); + let excerpt_start_point_utf16 = + excerpt.range.context.start.to_point_utf16(&excerpt.buffer); + let buffer_point = excerpt + .buffer + .point_to_point_utf16(excerpt_start_point + overshoot); + *start_point + (buffer_point - excerpt_start_point_utf16) + } else { + self.excerpts.summary().text.lines_utf16() + } + } + + pub fn point_to_offset(&self, point: Point) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_offset(point); + } + + let mut cursor = self.excerpts.cursor::<(Point, usize)>(); + cursor.seek(&point, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_point, start_offset) = cursor.start(); + let overshoot = point - start_point; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); + let buffer_offset = excerpt + .buffer + .point_to_offset(excerpt_start_point + overshoot); + *start_offset + buffer_offset - excerpt_start_offset + } else { + self.excerpts.summary().text.len + } + } + + pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_utf16_to_offset(offset_utf16); + } + + let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset_utf16, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset_utf16, start_offset) = cursor.start(); + let overshoot = offset_utf16 - start_offset_utf16; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_offset_utf16 = + excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset); + let buffer_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot); + *start_offset + (buffer_offset - excerpt_start_offset) + } else { + self.excerpts.summary().text.len + } + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_offset_utf16(offset); + } + + let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_offset_utf16) = cursor.start(); + let overshoot = offset - start_offset; + let excerpt_start_offset_utf16 = + excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let excerpt_start_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16); + let buffer_offset_utf16 = excerpt + .buffer + .offset_to_offset_utf16(excerpt_start_offset + overshoot); + *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) + } else { + self.excerpts.summary().text.len_utf16 + } + } + + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_utf16_to_offset(point); + } + + let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); + cursor.seek(&point, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_point, start_offset) = cursor.start(); + let overshoot = point - start_point; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_point = excerpt + .buffer + .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); + let buffer_offset = excerpt + .buffer + .point_utf16_to_offset(excerpt_start_point + overshoot); + *start_offset + (buffer_offset - excerpt_start_offset) + } else { + self.excerpts.summary().text.len + } + } + + pub fn point_to_buffer_offset( + &self, + point: T, + ) -> Option<(&BufferSnapshot, usize)> { + let offset = point.to_offset(&self); + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + cursor.item().map(|excerpt| { + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let buffer_point = excerpt_start + offset - *cursor.start(); + (&excerpt.buffer, buffer_point) + }) + } + + pub fn suggested_indents( + &self, + rows: impl IntoIterator, + cx: &AppContext, + ) -> BTreeMap { + let mut result = BTreeMap::new(); + + let mut rows_for_excerpt = Vec::new(); + let mut cursor = self.excerpts.cursor::(); + let mut rows = rows.into_iter().peekable(); + let mut prev_row = u32::MAX; + let mut prev_language_indent_size = IndentSize::default(); + + while let Some(row) = rows.next() { + cursor.seek(&Point::new(row, 0), Bias::Right, &()); + let excerpt = match cursor.item() { + Some(excerpt) => excerpt, + _ => continue, + }; + + // Retrieve the language and indent size once for each disjoint region being indented. + let single_indent_size = if row.saturating_sub(1) == prev_row { + prev_language_indent_size + } else { + excerpt + .buffer + .language_indent_size_at(Point::new(row, 0), cx) + }; + prev_language_indent_size = single_indent_size; + prev_row = row; + + let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row; + let start_multibuffer_row = cursor.start().row; + + rows_for_excerpt.push(row); + while let Some(next_row) = rows.peek().copied() { + if cursor.end(&()).row > next_row { + rows_for_excerpt.push(next_row); + rows.next(); + } else { + break; + } + } + + let buffer_rows = rows_for_excerpt + .drain(..) + .map(|row| start_buffer_row + row - start_multibuffer_row); + let buffer_indents = excerpt + .buffer + .suggested_indents(buffer_rows, single_indent_size); + let multibuffer_indents = buffer_indents + .into_iter() + .map(|(row, indent)| (start_multibuffer_row + row - start_buffer_row, indent)); + result.extend(multibuffer_indents); + } + + result + } + + pub fn indent_size_for_line(&self, row: u32) -> IndentSize { + if let Some((buffer, range)) = self.buffer_line_for_row(row) { + let mut size = buffer.indent_size_for_line(range.start.row); + size.len = size + .len + .min(range.end.column) + .saturating_sub(range.start.column); + size + } else { + IndentSize::spaces(0) + } + } + + pub fn prev_non_blank_row(&self, mut row: u32) -> Option { + while row > 0 { + row -= 1; + if !self.is_line_blank(row) { + return Some(row); + } + } + None + } + + pub fn line_len(&self, row: u32) -> u32 { + if let Some((_, range)) = self.buffer_line_for_row(row) { + range.end.column - range.start.column + } else { + 0 + } + } + + pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { + let mut cursor = self.excerpts.cursor::(); + let point = Point::new(row, 0); + cursor.seek(&point, Bias::Right, &()); + if cursor.item().is_none() && *cursor.start() == point { + cursor.prev(&()); + } + if let Some(excerpt) = cursor.item() { + let overshoot = row - cursor.start().row; + let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); + let excerpt_end = excerpt.range.context.end.to_point(&excerpt.buffer); + let buffer_row = excerpt_start.row + overshoot; + let line_start = Point::new(buffer_row, 0); + let line_end = Point::new(buffer_row, excerpt.buffer.line_len(buffer_row)); + return Some(( + &excerpt.buffer, + line_start.max(excerpt_start)..line_end.min(excerpt_end), + )); + } + None + } + + pub fn max_point(&self) -> Point { + self.text_summary().lines + } + + pub fn text_summary(&self) -> TextSummary { + self.excerpts.summary().text.clone() + } + + pub fn text_summary_for_range(&self, range: Range) -> D + where + D: TextDimension, + O: ToOffset, + { + let mut summary = D::default(); + let mut range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&range.start, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let mut end_before_newline = cursor.end(&()); + if excerpt.has_trailing_newline { + end_before_newline -= 1; + } + + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let start_in_excerpt = excerpt_start + (range.start - cursor.start()); + let end_in_excerpt = + excerpt_start + (cmp::min(end_before_newline, range.end) - cursor.start()); + summary.add_assign( + &excerpt + .buffer + .text_summary_for_range(start_in_excerpt..end_in_excerpt), + ); + + if range.end > end_before_newline { + summary.add_assign(&D::from_text_summary(&TextSummary::from("\n"))); + } + + cursor.next(&()); + } + + if range.end > *cursor.start() { + summary.add_assign(&D::from_text_summary(&cursor.summary::<_, TextSummary>( + &range.end, + Bias::Right, + &(), + ))); + if let Some(excerpt) = cursor.item() { + range.end = cmp::max(*cursor.start(), range.end); + + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let end_in_excerpt = excerpt_start + (range.end - cursor.start()); + summary.add_assign( + &excerpt + .buffer + .text_summary_for_range(excerpt_start..end_in_excerpt), + ); + } + } + + summary + } + + pub fn summary_for_anchor(&self, anchor: &Anchor) -> D + where + D: TextDimension + Ord + Sub, + { + let mut cursor = self.excerpts.cursor::(); + let locator = self.excerpt_locator_for_id(anchor.excerpt_id); + + cursor.seek(locator, Bias::Left, &()); + if cursor.item().is_none() { + cursor.next(&()); + } + + let mut position = D::from_text_summary(&cursor.start().text); + if let Some(excerpt) = cursor.item() { + if excerpt.id == anchor.excerpt_id { + let excerpt_buffer_start = + excerpt.range.context.start.summary::(&excerpt.buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::(&excerpt.buffer); + let buffer_position = cmp::min( + excerpt_buffer_end, + anchor.text_anchor.summary::(&excerpt.buffer), + ); + if buffer_position > excerpt_buffer_start { + position.add_assign(&(buffer_position - excerpt_buffer_start)); + } + } + } + position + } + + pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec + where + D: TextDimension + Ord + Sub, + I: 'a + IntoIterator, + { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer + .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) + .collect(); + } + + let mut anchors = anchors.into_iter().peekable(); + let mut cursor = self.excerpts.cursor::(); + let mut summaries = Vec::new(); + while let Some(anchor) = anchors.peek() { + let excerpt_id = anchor.excerpt_id; + let excerpt_anchors = iter::from_fn(|| { + let anchor = anchors.peek()?; + if anchor.excerpt_id == excerpt_id { + Some(&anchors.next().unwrap().text_anchor) + } else { + None + } + }); + + let locator = self.excerpt_locator_for_id(excerpt_id); + cursor.seek_forward(locator, Bias::Left, &()); + if cursor.item().is_none() { + cursor.next(&()); + } + + let position = D::from_text_summary(&cursor.start().text); + if let Some(excerpt) = cursor.item() { + if excerpt.id == excerpt_id { + let excerpt_buffer_start = + excerpt.range.context.start.summary::(&excerpt.buffer); + let excerpt_buffer_end = + excerpt.range.context.end.summary::(&excerpt.buffer); + summaries.extend( + excerpt + .buffer + .summaries_for_anchors::(excerpt_anchors) + .map(move |summary| { + let summary = cmp::min(excerpt_buffer_end.clone(), summary); + let mut position = position.clone(); + let excerpt_buffer_start = excerpt_buffer_start.clone(); + if summary > excerpt_buffer_start { + position.add_assign(&(summary - excerpt_buffer_start)); + } + position + }), + ); + continue; + } + } + + summaries.extend(excerpt_anchors.map(|_| position.clone())); + } + + summaries + } + + pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)> + where + I: 'a + IntoIterator, + { + let mut anchors = anchors.into_iter().enumerate().peekable(); + let mut cursor = self.excerpts.cursor::>(); + cursor.next(&()); + + let mut result = Vec::new(); + + while let Some((_, anchor)) = anchors.peek() { + let old_excerpt_id = anchor.excerpt_id; + + // Find the location where this anchor's excerpt should be. + let old_locator = self.excerpt_locator_for_id(old_excerpt_id); + cursor.seek_forward(&Some(old_locator), Bias::Left, &()); + + if cursor.item().is_none() { + cursor.next(&()); + } + + let next_excerpt = cursor.item(); + let prev_excerpt = cursor.prev_item(); + + // Process all of the anchors for this excerpt. + while let Some((_, anchor)) = anchors.peek() { + if anchor.excerpt_id != old_excerpt_id { + break; + } + let (anchor_ix, anchor) = anchors.next().unwrap(); + let mut anchor = *anchor; + + // Leave min and max anchors unchanged if invalid or + // if the old excerpt still exists at this location + let mut kept_position = next_excerpt + .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) + || old_excerpt_id == ExcerptId::max() + || old_excerpt_id == ExcerptId::min(); + + // If the old excerpt no longer exists at this location, then attempt to + // find an equivalent position for this anchor in an adjacent excerpt. + if !kept_position { + for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { + if excerpt.contains(&anchor) { + anchor.excerpt_id = excerpt.id.clone(); + kept_position = true; + break; + } + } + } + + // If there's no adjacent excerpt that contains the anchor's position, + // then report that the anchor has lost its position. + if !kept_position { + anchor = if let Some(excerpt) = next_excerpt { + let mut text_anchor = excerpt + .range + .context + .start + .bias(anchor.text_anchor.bias, &excerpt.buffer); + if text_anchor + .cmp(&excerpt.range.context.end, &excerpt.buffer) + .is_gt() + { + text_anchor = excerpt.range.context.end; + } + Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id: excerpt.id.clone(), + text_anchor, + } + } else if let Some(excerpt) = prev_excerpt { + let mut text_anchor = excerpt + .range + .context + .end + .bias(anchor.text_anchor.bias, &excerpt.buffer); + if text_anchor + .cmp(&excerpt.range.context.start, &excerpt.buffer) + .is_lt() + { + text_anchor = excerpt.range.context.start; + } + Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id: excerpt.id.clone(), + text_anchor, + } + } else if anchor.text_anchor.bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + } + + result.push((anchor_ix, anchor, kept_position)); + } + } + result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); + result + } + + pub fn anchor_before(&self, position: T) -> Anchor { + self.anchor_at(position, Bias::Left) + } + + pub fn anchor_after(&self, position: T) -> Anchor { + self.anchor_at(position, Bias::Right) + } + + pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { + let offset = position.to_offset(self); + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { + return Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer.anchor_at(offset, bias), + }; + } + + let mut cursor = self.excerpts.cursor::<(usize, Option)>(); + cursor.seek(&offset, Bias::Right, &()); + if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { + cursor.prev(&()); + } + if let Some(excerpt) = cursor.item() { + let mut overshoot = offset.saturating_sub(cursor.start().0); + if excerpt.has_trailing_newline && offset == cursor.end(&()).0 { + overshoot -= 1; + bias = Bias::Right; + } + + let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let text_anchor = + excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); + Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id: excerpt.id.clone(), + text_anchor, + } + } else if offset == 0 && bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + } + } + + pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor { + let locator = self.excerpt_locator_for_id(excerpt_id); + let mut cursor = self.excerpts.cursor::>(); + cursor.seek(locator, Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.id == excerpt_id { + let text_anchor = excerpt.clip_anchor(text_anchor); + drop(cursor); + return Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id, + text_anchor, + }; + } + } + panic!("excerpt not found"); + } + + pub fn can_resolve(&self, anchor: &Anchor) -> bool { + if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() { + true + } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) { + excerpt.buffer.can_resolve(&anchor.text_anchor) + } else { + false + } + } + + pub fn excerpts( + &self, + ) -> impl Iterator)> { + self.excerpts + .iter() + .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) + } + + pub fn excerpt_boundaries_in_range( + &self, + range: R, + ) -> impl Iterator + '_ + where + R: RangeBounds, + T: ToOffset, + { + let start_offset; + let start = match range.start_bound() { + Bound::Included(start) => { + start_offset = start.to_offset(self); + Bound::Included(start_offset) + } + Bound::Excluded(start) => { + start_offset = start.to_offset(self); + Bound::Excluded(start_offset) + } + Bound::Unbounded => { + start_offset = 0; + Bound::Unbounded + } + }; + let end = match range.end_bound() { + Bound::Included(end) => Bound::Included(end.to_offset(self)), + Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)), + Bound::Unbounded => Bound::Unbounded, + }; + let bounds = (start, end); + + let mut cursor = self.excerpts.cursor::<(usize, Point)>(); + cursor.seek(&start_offset, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + if !bounds.contains(&cursor.start().0) { + cursor.next(&()); + } + + let mut prev_buffer_id = cursor.prev_item().map(|excerpt| excerpt.buffer_id); + std::iter::from_fn(move || { + if self.singleton { + None + } else if bounds.contains(&cursor.start().0) { + let excerpt = cursor.item()?; + let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; + let boundary = ExcerptBoundary { + id: excerpt.id.clone(), + row: cursor.start().1.row, + buffer: excerpt.buffer.clone(), + range: excerpt.range.clone(), + starts_new_buffer, + }; + + prev_buffer_id = Some(excerpt.buffer_id); + cursor.next(&()); + Some(boundary) + } else { + None + } + }) + } + + pub fn edit_count(&self) -> usize { + self.edit_count + } + + pub fn parse_count(&self) -> usize { + self.parse_count + } + + /// Returns the smallest enclosing bracket ranges containing the given range or + /// None if no brackets contain range or the range is not contained in a single + /// excerpt + pub fn innermost_enclosing_bracket_ranges( + &self, + range: Range, + ) -> Option<(Range, Range)> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + // Get the ranges of the innermost pair of brackets. + let mut result: Option<(Range, Range)> = None; + + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { + return None; + }; + + for (open, close) in enclosing_bracket_ranges { + let len = close.end - open.start; + + if let Some((existing_open, existing_close)) = &result { + let existing_len = existing_close.end - existing_open.start; + if len > existing_len { + continue; + } + } + + result = Some((open, close)); + } + + result + } + + /// Returns enclosing bracket ranges containing the given range or returns None if the range is + /// not contained in a single excerpt + pub fn enclosing_bracket_ranges<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option, Range)> + 'a> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + self.bracket_ranges(range.clone()).map(|range_pairs| { + range_pairs + .filter(move |(open, close)| open.start <= range.start && close.end >= range.end) + }) + } + + /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is + /// not contained in a single excerpt + pub fn bracket_ranges<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option, Range)> + 'a> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone()); + excerpt.map(|(excerpt, excerpt_offset)| { + let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; + + let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); + let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); + + excerpt + .buffer + .bracket_ranges(start_in_buffer..end_in_buffer) + .filter_map(move |(start_bracket_range, end_bracket_range)| { + if start_bracket_range.start < excerpt_buffer_start + || end_bracket_range.end > excerpt_buffer_end + { + return None; + } + + let mut start_bracket_range = start_bracket_range.clone(); + start_bracket_range.start = + excerpt_offset + (start_bracket_range.start - excerpt_buffer_start); + start_bracket_range.end = + excerpt_offset + (start_bracket_range.end - excerpt_buffer_start); + + let mut end_bracket_range = end_bracket_range.clone(); + end_bracket_range.start = + excerpt_offset + (end_bracket_range.start - excerpt_buffer_start); + end_bracket_range.end = + excerpt_offset + (end_bracket_range.end - excerpt_buffer_start); + Some((start_bracket_range, end_bracket_range)) + }) + }) + } + + pub fn diagnostics_update_count(&self) -> usize { + self.diagnostics_update_count + } + + pub fn git_diff_update_count(&self) -> usize { + self.git_diff_update_count + } + + pub fn trailing_excerpt_update_count(&self) -> usize { + self.trailing_excerpt_update_count + } + + pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + self.point_to_buffer_offset(point) + .and_then(|(buffer, _)| buffer.file()) + } + + pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + self.point_to_buffer_offset(point) + .and_then(|(buffer, offset)| buffer.language_at(offset)) + } + + pub fn settings_at<'a, T: ToOffset>( + &'a self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language, file, cx) + } + + pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { + self.point_to_buffer_offset(point) + .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) + } + + pub fn language_indent_size_at( + &self, + position: T, + cx: &AppContext, + ) -> Option { + let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?; + Some(buffer_snapshot.language_indent_size_at(offset, cx)) + } + + pub fn is_dirty(&self) -> bool { + self.is_dirty + } + + pub fn has_conflict(&self) -> bool { + self.has_conflict + } + + pub fn diagnostic_group<'a, O>( + &'a self, + group_id: usize, + ) -> impl Iterator> + 'a + where + O: text::FromAnchor + 'a, + { + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id)) + } + + pub fn diagnostics_in_range<'a, T, O>( + &'a self, + range: Range, + reversed: bool, + ) -> impl Iterator> + 'a + where + T: 'a + ToOffset, + O: 'a + text::FromAnchor + Ord, + { + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| { + buffer.diagnostics_in_range( + range.start.to_offset(self)..range.end.to_offset(self), + reversed, + ) + }) + } + + pub fn has_git_diffs(&self) -> bool { + for excerpt in self.excerpts.iter() { + if !excerpt.buffer.git_diff.is_empty() { + return true; + } + } + false + } + + pub fn git_diff_hunks_in_range_rev<'a>( + &'a self, + row_range: Range, + ) -> impl 'a + Iterator> { + let mut cursor = self.excerpts.cursor::(); + + cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let multibuffer_start = *cursor.start(); + let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; + if multibuffer_start.row >= row_range.end { + return None; + } + + let mut buffer_start = excerpt.range.context.start; + let mut buffer_end = excerpt.range.context.end; + let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); + let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; + + if row_range.start > multibuffer_start.row { + let buffer_start_point = + excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); + buffer_start = excerpt.buffer.anchor_before(buffer_start_point); + } + + if row_range.end < multibuffer_end.row { + let buffer_end_point = + excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); + buffer_end = excerpt.buffer.anchor_before(buffer_end_point); + } + + let buffer_hunks = excerpt + .buffer + .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) + .filter_map(move |hunk| { + let start = multibuffer_start.row + + hunk + .buffer_range + .start + .saturating_sub(excerpt_start_point.row); + let end = multibuffer_start.row + + hunk + .buffer_range + .end + .min(excerpt_end_point.row + 1) + .saturating_sub(excerpt_start_point.row); + + Some(DiffHunk { + buffer_range: start..end, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }); + + cursor.prev(&()); + + Some(buffer_hunks) + }) + .flatten() + } + + pub fn git_diff_hunks_in_range<'a>( + &'a self, + row_range: Range, + ) -> impl 'a + Iterator> { + let mut cursor = self.excerpts.cursor::(); + + cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); + + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let multibuffer_start = *cursor.start(); + let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; + if multibuffer_start.row >= row_range.end { + return None; + } + + let mut buffer_start = excerpt.range.context.start; + let mut buffer_end = excerpt.range.context.end; + let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); + let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; + + if row_range.start > multibuffer_start.row { + let buffer_start_point = + excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); + buffer_start = excerpt.buffer.anchor_before(buffer_start_point); + } + + if row_range.end < multibuffer_end.row { + let buffer_end_point = + excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); + buffer_end = excerpt.buffer.anchor_before(buffer_end_point); + } + + let buffer_hunks = excerpt + .buffer + .git_diff_hunks_intersecting_range(buffer_start..buffer_end) + .filter_map(move |hunk| { + let start = multibuffer_start.row + + hunk + .buffer_range + .start + .saturating_sub(excerpt_start_point.row); + let end = multibuffer_start.row + + hunk + .buffer_range + .end + .min(excerpt_end_point.row + 1) + .saturating_sub(excerpt_start_point.row); + + Some(DiffHunk { + buffer_range: start..end, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }); + + cursor.next(&()); + + Some(buffer_hunks) + }) + .flatten() + } + + pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + self.excerpt_containing(range.clone()) + .and_then(|(excerpt, excerpt_offset)| { + let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; + + let start_in_buffer = + excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); + let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); + let mut ancestor_buffer_range = excerpt + .buffer + .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?; + ancestor_buffer_range.start = + cmp::max(ancestor_buffer_range.start, excerpt_buffer_start); + ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end); + + let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start); + let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start); + Some(start..end) + }) + } + + pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { + let (excerpt_id, _, buffer) = self.as_singleton()?; + let outline = buffer.outline(theme)?; + Some(Outline::new( + outline + .items + .into_iter() + .map(|item| OutlineItem { + depth: item.depth, + range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) + ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), + text: item.text, + highlight_ranges: item.highlight_ranges, + name_ranges: item.name_ranges, + }) + .collect(), + )) + } + + pub fn symbols_containing( + &self, + offset: T, + theme: Option<&SyntaxTheme>, + ) -> Option<(u64, Vec>)> { + let anchor = self.anchor_before(offset); + let excerpt_id = anchor.excerpt_id; + let excerpt = self.excerpt(excerpt_id)?; + Some(( + excerpt.buffer_id, + excerpt + .buffer + .symbols_containing(anchor.text_anchor, theme) + .into_iter() + .flatten() + .map(|item| OutlineItem { + depth: item.depth, + range: self.anchor_in_excerpt(excerpt_id, item.range.start) + ..self.anchor_in_excerpt(excerpt_id, item.range.end), + text: item.text, + highlight_ranges: item.highlight_ranges, + name_ranges: item.name_ranges, + }) + .collect(), + )) + } + + fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator { + if id == ExcerptId::min() { + Locator::min_ref() + } else if id == ExcerptId::max() { + Locator::max_ref() + } else { + let mut cursor = self.excerpt_ids.cursor::(); + cursor.seek(&id, Bias::Left, &()); + if let Some(entry) = cursor.item() { + if entry.id == id { + return &entry.locator; + } + } + panic!("invalid excerpt id {:?}", id) + } + } + + pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { + Some(self.excerpt(excerpt_id)?.buffer_id) + } + + pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> { + Some(&self.excerpt(excerpt_id)?.buffer) + } + + fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { + let mut cursor = self.excerpts.cursor::>(); + let locator = self.excerpt_locator_for_id(excerpt_id); + cursor.seek(&Some(locator), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.id == excerpt_id { + return Some(excerpt); + } + } + None + } + + /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts + fn excerpt_containing<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option<(&'a Excerpt, usize)> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&range.start, Bias::Right, &()); + let start_excerpt = cursor.item(); + + if range.start == range.end { + return start_excerpt.map(|excerpt| (excerpt, *cursor.start())); + } + + cursor.seek(&range.end, Bias::Right, &()); + let end_excerpt = cursor.item(); + + start_excerpt + .zip(end_excerpt) + .and_then(|(start_excerpt, end_excerpt)| { + if start_excerpt.id != end_excerpt.id { + return None; + } + + Some((start_excerpt, *cursor.start())) + }) + } + + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + ) -> impl 'a + Iterator)> { + let mut cursor = self.excerpts.cursor::(); + let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); + let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); + cursor.seek(start_locator, Bias::Left, &()); + cursor + .take_while(move |excerpt| excerpt.locator <= *end_locator) + .flat_map(move |excerpt| { + let mut query_range = excerpt.range.context.start..excerpt.range.context.end; + if excerpt.id == range.start.excerpt_id { + query_range.start = range.start.text_anchor; + } + if excerpt.id == range.end.excerpt_id { + query_range.end = range.end.text_anchor; + } + + excerpt + .buffer + .remote_selections_in_range(query_range) + .flat_map(move |(replica_id, line_mode, cursor_shape, selections)| { + selections.map(move |selection| { + let mut start = Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id: excerpt.id.clone(), + text_anchor: selection.start, + }; + let mut end = Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id: excerpt.id.clone(), + text_anchor: selection.end, + }; + if range.start.cmp(&start, self).is_gt() { + start = range.start.clone(); + } + if range.end.cmp(&end, self).is_lt() { + end = range.end.clone(); + } + + ( + replica_id, + line_mode, + cursor_shape, + Selection { + id: selection.id, + start, + end, + reversed: selection.reversed, + goal: selection.goal, + }, + ) + }) + }) + }) + } +} + +#[cfg(any(test, feature = "test-support"))] +impl MultiBufferSnapshot { + pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { + let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); + start..end + } +} + +impl History { + fn start_transaction(&mut self, now: Instant) -> Option { + self.transaction_depth += 1; + if self.transaction_depth == 1 { + let id = self.next_transaction_id.tick(); + self.undo_stack.push(Transaction { + id, + buffer_transactions: Default::default(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + Some(id) + } else { + None + } + } + + fn end_transaction( + &mut self, + now: Instant, + buffer_transactions: HashMap, + ) -> bool { + assert_ne!(self.transaction_depth, 0); + self.transaction_depth -= 1; + if self.transaction_depth == 0 { + if buffer_transactions.is_empty() { + self.undo_stack.pop(); + false + } else { + self.redo_stack.clear(); + let transaction = self.undo_stack.last_mut().unwrap(); + transaction.last_edit_at = now; + for (buffer_id, transaction_id) in buffer_transactions { + transaction + .buffer_transactions + .entry(buffer_id) + .or_insert(transaction_id); + } + true + } + } else { + false + } + } + + fn push_transaction<'a, T>( + &mut self, + buffer_transactions: T, + now: Instant, + cx: &mut ModelContext, + ) where + T: IntoIterator, &'a language::Transaction)>, + { + assert_eq!(self.transaction_depth, 0); + let transaction = Transaction { + id: self.next_transaction_id.tick(), + buffer_transactions: buffer_transactions + .into_iter() + .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) + .collect(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }; + if !transaction.buffer_transactions.is_empty() { + self.undo_stack.push(transaction); + self.redo_stack.clear(); + } + } + + fn finalize_last_transaction(&mut self) { + if let Some(transaction) = self.undo_stack.last_mut() { + transaction.suppress_grouping = true; + } + } + + fn forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(ix) = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.undo_stack.remove(ix)) + } else if let Some(ix) = self + .redo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.redo_stack.remove(ix)) + } else { + None + } + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + self.undo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + }) + } + + fn pop_undo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.undo_stack.pop() { + self.redo_stack.push(transaction); + self.redo_stack.last_mut() + } else { + None + } + } + + fn pop_redo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.redo_stack.pop() { + self.undo_stack.push(transaction); + self.undo_stack.last_mut() + } else { + None + } + } + + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { + let ix = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id)?; + let transaction = self.undo_stack.remove(ix); + self.redo_stack.push(transaction); + self.redo_stack.last() + } + + fn group(&mut self) -> Option { + let mut count = 0; + let mut transactions = self.undo_stack.iter(); + if let Some(mut transaction) = transactions.next_back() { + while let Some(prev_transaction) = transactions.next_back() { + if !prev_transaction.suppress_grouping + && transaction.first_edit_at - prev_transaction.last_edit_at + <= self.group_interval + { + transaction = prev_transaction; + count += 1; + } else { + break; + } + } + } + self.group_trailing(count) + } + + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for transaction in self.undo_stack.iter().rev() { + if transaction.id == transaction_id { + self.group_trailing(count); + break; + } else if transaction.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; + let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); + if let Some(last_transaction) = transactions_to_keep.last_mut() { + if let Some(transaction) = transactions_to_merge.last() { + last_transaction.last_edit_at = transaction.last_edit_at; + } + for to_merge in transactions_to_merge { + for (buffer_id, transaction_id) in &to_merge.buffer_transactions { + last_transaction + .buffer_transactions + .entry(*buffer_id) + .or_insert(*transaction_id); + } + } + } + + self.undo_stack.truncate(new_len); + self.undo_stack.last().map(|t| t.id) + } +} + +impl Excerpt { + fn new( + id: ExcerptId, + locator: Locator, + buffer_id: u64, + buffer: BufferSnapshot, + range: ExcerptRange, + has_trailing_newline: bool, + ) -> Self { + Excerpt { + id, + locator, + max_buffer_row: range.context.end.to_point(&buffer).row, + text_summary: buffer + .text_summary_for_range::(range.context.to_offset(&buffer)), + buffer_id, + buffer, + range, + has_trailing_newline, + } + } + + fn chunks_in_range(&self, range: Range, language_aware: bool) -> ExcerptChunks { + let content_start = self.range.context.start.to_offset(&self.buffer); + let chunks_start = content_start + range.start; + let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); + + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + + let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); + + ExcerptChunks { + content_chunks, + footer_height, + } + } + + fn bytes_in_range(&self, range: Range) -> ExcerptBytes { + let content_start = self.range.context.start.to_offset(&self.buffer); + let bytes_start = content_start + range.start; + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + let content_bytes = self.buffer.bytes_in_range(bytes_start..bytes_end); + + ExcerptBytes { + content_bytes, + footer_height, + } + } + + fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { + let content_start = self.range.context.start.to_offset(&self.buffer); + let bytes_start = content_start + range.start; + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); + + ExcerptBytes { + content_bytes, + footer_height, + } + } + + fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { + if text_anchor + .cmp(&self.range.context.start, &self.buffer) + .is_lt() + { + self.range.context.start + } else if text_anchor + .cmp(&self.range.context.end, &self.buffer) + .is_gt() + { + self.range.context.end + } else { + text_anchor + } + } + + fn contains(&self, anchor: &Anchor) -> bool { + Some(self.buffer_id) == anchor.buffer_id + && self + .range + .context + .start + .cmp(&anchor.text_anchor, &self.buffer) + .is_le() + && self + .range + .context + .end + .cmp(&anchor.text_anchor, &self.buffer) + .is_ge() + } +} + +impl ExcerptId { + pub fn min() -> Self { + Self(0) + } + + pub fn max() -> Self { + Self(usize::MAX) + } + + pub fn to_proto(&self) -> u64 { + self.0 as _ + } + + pub fn from_proto(proto: u64) -> Self { + Self(proto as _) + } + + pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { + let a = snapshot.excerpt_locator_for_id(*self); + let b = snapshot.excerpt_locator_for_id(*other); + a.cmp(&b).then_with(|| self.0.cmp(&other.0)) + } +} + +impl Into for ExcerptId { + fn into(self) -> usize { + self.0 + } +} + +impl fmt::Debug for Excerpt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Excerpt") + .field("id", &self.id) + .field("locator", &self.locator) + .field("buffer_id", &self.buffer_id) + .field("range", &self.range) + .field("text_summary", &self.text_summary) + .field("has_trailing_newline", &self.has_trailing_newline) + .finish() + } +} + +impl sum_tree::Item for Excerpt { + type Summary = ExcerptSummary; + + fn summary(&self) -> Self::Summary { + let mut text = self.text_summary.clone(); + if self.has_trailing_newline { + text += TextSummary::from("\n"); + } + ExcerptSummary { + excerpt_id: self.id, + excerpt_locator: self.locator.clone(), + max_buffer_row: self.max_buffer_row, + text, + } + } +} + +impl sum_tree::Item for ExcerptIdMapping { + type Summary = ExcerptId; + + fn summary(&self) -> Self::Summary { + self.id + } +} + +impl sum_tree::KeyedItem for ExcerptIdMapping { + type Key = ExcerptId; + + fn key(&self) -> Self::Key { + self.id + } +} + +impl sum_tree::Summary for ExcerptId { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + *self = *other; + } +} + +impl sum_tree::Summary for ExcerptSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + debug_assert!(summary.excerpt_locator > self.excerpt_locator); + self.excerpt_locator = summary.excerpt_locator.clone(); + self.text.add_summary(&summary.text, &()); + self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row); + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += &summary.text; + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += summary.text.len; + } +} + +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { + fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { + Ord::cmp(self, &cursor_location.text.len) + } +} + +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator { + fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering { + Ord::cmp(&Some(self), cursor_location) + } +} + +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator { + fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { + Ord::cmp(self, &cursor_location.excerpt_locator) + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += summary.text.len_utf16; + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += summary.text.lines; + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += summary.text.lines_utf16() + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self = Some(&summary.excerpt_locator); + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self = Some(summary.excerpt_id); + } +} + +impl<'a> MultiBufferRows<'a> { + pub fn seek(&mut self, row: u32) { + self.buffer_row_range = 0..0; + + self.excerpts + .seek_forward(&Point::new(row, 0), Bias::Right, &()); + if self.excerpts.item().is_none() { + self.excerpts.prev(&()); + + if self.excerpts.item().is_none() && row == 0 { + self.buffer_row_range = 0..1; + return; + } + } + + if let Some(excerpt) = self.excerpts.item() { + let overshoot = row - self.excerpts.start().row; + let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer).row; + self.buffer_row_range.start = excerpt_start + overshoot; + self.buffer_row_range.end = excerpt_start + excerpt.text_summary.lines.row + 1; + } + } +} + +impl<'a> Iterator for MultiBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + loop { + if !self.buffer_row_range.is_empty() { + let row = Some(self.buffer_row_range.start); + self.buffer_row_range.start += 1; + return Some(row); + } + self.excerpts.item()?; + self.excerpts.next(&()); + let excerpt = self.excerpts.item()?; + self.buffer_row_range.start = excerpt.range.context.start.to_point(&excerpt.buffer).row; + self.buffer_row_range.end = + self.buffer_row_range.start + excerpt.text_summary.lines.row + 1; + } + } +} + +impl<'a> MultiBufferChunks<'a> { + pub fn offset(&self) -> usize { + self.range.start + } + + pub fn seek(&mut self, offset: usize) { + self.range.start = offset; + self.excerpts.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = self.excerpts.item() { + self.excerpt_chunks = Some(excerpt.chunks_in_range( + self.range.start - self.excerpts.start()..self.range.end - self.excerpts.start(), + self.language_aware, + )); + } else { + self.excerpt_chunks = None; + } + } +} + +impl<'a> Iterator for MultiBufferChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.range.is_empty() { + None + } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { + self.range.start += chunk.text.len(); + Some(chunk) + } else { + self.excerpts.next(&()); + let excerpt = self.excerpts.item()?; + self.excerpt_chunks = Some(excerpt.chunks_in_range( + 0..self.range.end - self.excerpts.start(), + self.language_aware, + )); + self.next() + } + } +} + +impl<'a> MultiBufferBytes<'a> { + fn consume(&mut self, len: usize) { + self.range.start += len; + self.chunk = &self.chunk[len..]; + + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { + self.chunk = chunk; + } else { + self.excerpts.next(&()); + if let Some(excerpt) = self.excerpts.item() { + let mut excerpt_bytes = + excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); + self.chunk = excerpt_bytes.next().unwrap(); + self.excerpt_bytes = Some(excerpt_bytes); + } + } + } + } +} + +impl<'a> Iterator for MultiBufferBytes<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + let chunk = self.chunk; + if chunk.is_empty() { + None + } else { + self.consume(chunk.len()); + Some(chunk) + } + } +} + +impl<'a> io::Read for MultiBufferBytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = cmp::min(buf.len(), self.chunk.len()); + buf[..len].copy_from_slice(&self.chunk[..len]); + if len > 0 { + self.consume(len); + } + Ok(len) + } +} + +impl<'a> ReversedMultiBufferBytes<'a> { + fn consume(&mut self, len: usize) { + self.range.end -= len; + self.chunk = &self.chunk[..self.chunk.len() - len]; + + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { + self.chunk = chunk; + } else { + self.excerpts.next(&()); + if let Some(excerpt) = self.excerpts.item() { + let mut excerpt_bytes = + excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); + self.chunk = excerpt_bytes.next().unwrap(); + self.excerpt_bytes = Some(excerpt_bytes); + } + } + } + } +} + +impl<'a> io::Read for ReversedMultiBufferBytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = cmp::min(buf.len(), self.chunk.len()); + buf[..len].copy_from_slice(&self.chunk[..len]); + buf[..len].reverse(); + if len > 0 { + self.consume(len); + } + Ok(len) + } +} +impl<'a> Iterator for ExcerptBytes<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + if let Some(chunk) = self.content_bytes.next() { + if !chunk.is_empty() { + return Some(chunk); + } + } + + if self.footer_height > 0 { + let result = &NEWLINES[..self.footer_height]; + self.footer_height = 0; + return Some(result); + } + + None + } +} + +impl<'a> Iterator for ExcerptChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if let Some(chunk) = self.content_chunks.next() { + return Some(chunk); + } + + if self.footer_height > 0 { + let text = unsafe { str::from_utf8_unchecked(&NEWLINES[..self.footer_height]) }; + self.footer_height = 0; + return Some(Chunk { + text, + ..Default::default() + }); + } + + None + } +} + +impl ToOffset for Point { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.point_to_offset(*self) + } +} + +impl ToOffset for usize { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + assert!(*self <= snapshot.len(), "offset is out of range"); + *self + } +} + +impl ToOffset for OffsetUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.offset_utf16_to_offset(*self) + } +} + +impl ToOffset for PointUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.point_utf16_to_offset(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + *self + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + +impl ToPoint for usize { + fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { + snapshot.offset_to_point(*self) + } +} + +impl ToPoint for Point { + fn to_point<'a>(&self, _: &MultiBufferSnapshot) -> Point { + *self + } +} + +impl ToPointUtf16 for usize { + fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { + snapshot.offset_to_point_utf16(*self) + } +} + +impl ToPointUtf16 for Point { + fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { + snapshot.point_to_point_utf16(*self) + } +} + +impl ToPointUtf16 for PointUtf16 { + fn to_point_utf16<'a>(&self, _: &MultiBufferSnapshot) -> PointUtf16 { + *self + } +} + +fn build_excerpt_ranges( + buffer: &BufferSnapshot, + ranges: &[Range], + context_line_count: u32, +) -> (Vec>, Vec) +where + T: text::ToPoint, +{ + let max_point = buffer.max_point(); + let mut range_counts = Vec::new(); + let mut excerpt_ranges = Vec::new(); + let mut range_iter = ranges + .iter() + .map(|range| range.start.to_point(buffer)..range.end.to_point(buffer)) + .peekable(); + while let Some(range) = range_iter.next() { + let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0); + let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point); + let mut ranges_in_excerpt = 1; + + while let Some(next_range) = range_iter.peek() { + if next_range.start.row <= excerpt_end.row + context_line_count { + excerpt_end = + Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point); + ranges_in_excerpt += 1; + range_iter.next(); + } else { + break; + } + } + + excerpt_ranges.push(ExcerptRange { + context: excerpt_start..excerpt_end, + primary: Some(range), + }); + range_counts.push(ranges_in_excerpt); + } + + (excerpt_ranges, range_counts) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use gpui::{AppContext, Context, TestAppContext}; + use language::{Buffer, Rope}; + use parking_lot::RwLock; + use rand::prelude::*; + use settings::SettingsStore; + use std::env; + use util::test::sample_text; + + #[gpui::test] + fn test_singleton(cx: &mut AppContext) { + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); + let multibuffer = cx.build_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), buffer.read(cx).text()); + + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + (0..buffer.read(cx).row_count()) + .map(Some) + .collect::>() + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(snapshot.text(), buffer.read(cx).text()); + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + (0..buffer.read(cx).row_count()) + .map(Some) + .collect::>() + ); + } + + #[gpui::test] + fn test_remote(cx: &mut AppContext) { + let host_buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a")); + let guest_buffer = cx.build_model(|cx| { + let state = host_buffer.read(cx).to_proto(); + let ops = cx + .background_executor() + .block(host_buffer.read(cx).serialize_ops(None, cx)); + let mut buffer = Buffer::from_proto(1, state, None).unwrap(); + buffer + .apply_ops( + ops.into_iter() + .map(|op| language::proto::deserialize_operation(op).unwrap()), + cx, + ) + .unwrap(); + buffer + }); + let multibuffer = cx.build_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "a"); + + guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "ab"); + + guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "abc"); + } + + #[gpui::test] + fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { + let buffer_1 = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); + let buffer_2 = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g'))); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + + let events = Arc::new(RwLock::new(Vec::::new())); + multibuffer.update(cx, |_, cx| { + let events = events.clone(); + cx.subscribe(&multibuffer, move |_, _, event, _| { + if let Event::Edited { .. } = event { + events.write().push(event.clone()) + } + }) + .detach(); + }); + + let subscription = multibuffer.update(cx, |multibuffer, cx| { + let subscription = multibuffer.subscribe(); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(1, 2)..Point::new(2, 5), + primary: None, + }], + cx, + ); + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 0..0, + new: 0..10 + }] + ); + + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(3, 3)..Point::new(4, 4), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(3, 1)..Point::new(3, 3), + primary: None, + }], + cx, + ); + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 10..10, + new: 10..22 + }] + ); + + subscription + }); + + // Adding excerpts emits an edited event. + assert_eq!( + events.read().as_slice(), + &[ + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + } + ] + ); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "ccccc\n", // + "ddd\n", // + "eeee\n", // + "jj" // + ) + ); + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + [Some(1), Some(2), Some(3), Some(4), Some(3)] + ); + assert_eq!( + snapshot.buffer_rows(2).collect::>(), + [Some(3), Some(4), Some(3)] + ); + assert_eq!(snapshot.buffer_rows(4).collect::>(), [Some(3)]); + assert_eq!(snapshot.buffer_rows(5).collect::>(), []); + + assert_eq!( + boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), + &[ + (0, "bbbb\nccccc".to_string(), true), + (2, "ddd\neeee".to_string(), false), + (4, "jj".to_string(), true), + ] + ); + assert_eq!( + boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), + &[(0, "bbbb\nccccc".to_string(), true)] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), + &[] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), + &[] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), + &[(2, "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), + &[(2, "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), + &[(2, "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), + &[(4, "jj".to_string(), true)] + ); + assert_eq!( + boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), + &[] + ); + + buffer_1.update(cx, |buffer, cx| { + let text = "\n"; + buffer.edit( + [ + (Point::new(0, 0)..Point::new(0, 0), text), + (Point::new(2, 1)..Point::new(2, 3), text), + ], + None, + cx, + ); + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "c\n", // + "cc\n", // + "ddd\n", // + "eeee\n", // + "jj" // + ) + ); + + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 6..8, + new: 6..7 + }] + ); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.clip_point(Point::new(0, 5), Bias::Left), + Point::new(0, 4) + ); + assert_eq!( + snapshot.clip_point(Point::new(0, 5), Bias::Right), + Point::new(0, 4) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 1), Bias::Right), + Point::new(5, 1) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 2), Bias::Right), + Point::new(5, 2) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 3), Bias::Right), + Point::new(5, 2) + ); + + let snapshot = multibuffer.update(cx, |multibuffer, cx| { + let (buffer_2_excerpt_id, _) = + multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); + multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); + multibuffer.snapshot(cx) + }); + + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "c\n", // + "cc\n", // + "ddd\n", // + "eeee", // + ) + ); + + fn boundaries_in_range( + range: Range, + snapshot: &MultiBufferSnapshot, + ) -> Vec<(u32, String, bool)> { + snapshot + .excerpt_boundaries_in_range(range) + .map(|boundary| { + ( + boundary.row, + boundary + .buffer + .text_for_range(boundary.range.context) + .collect::(), + boundary.starts_new_buffer, + ) + }) + .collect::>() + } + } + + #[gpui::test] + fn test_excerpt_events(cx: &mut AppContext) { + let buffer_1 = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'a'))); + let buffer_2 = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm'))); + + let leader_multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let follower_multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let follower_edit_event_count = Arc::new(RwLock::new(0)); + + follower_multibuffer.update(cx, |_, cx| { + let follower_edit_event_count = follower_edit_event_count.clone(); + cx.subscribe( + &leader_multibuffer, + move |follower, _, event, cx| match event.clone() { + Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), + Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), + Event::Edited { .. } => { + *follower_edit_event_count.write() += 1; + } + _ => {} + }, + ) + .detach(); + }); + + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 0..8, + primary: None, + }, + ExcerptRange { + context: 12..16, + primary: None, + }, + ], + cx, + ); + leader.insert_excerpts_after( + leader.excerpt_ids()[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 0..5, + primary: None, + }, + ExcerptRange { + context: 10..15, + primary: None, + }, + ], + cx, + ) + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 2); + + leader_multibuffer.update(cx, |leader, cx| { + let excerpt_ids = leader.excerpt_ids(); + leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + // Removing an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.remove_excerpts([], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + // Adding an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts::(buffer_2.clone(), [], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + leader_multibuffer.update(cx, |leader, cx| { + leader.clear(cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 4); + } + + #[gpui::test] + fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + vec![ + Point::new(3, 2)..Point::new(4, 2), + Point::new(7, 1)..Point::new(7, 3), + Point::new(15, 0)..Point::new(15, 0), + ], + 2, + cx, + ) + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + "bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n" + ); + + assert_eq!( + anchor_ranges + .iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(), + vec![ + Point::new(2, 2)..Point::new(3, 2), + Point::new(6, 1)..Point::new(6, 3), + Point::new(12, 0)..Point::new(12, 0) + ] + ); + } + + #[gpui::test] + async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { + let snapshot = buffer.read(cx); + let ranges = vec![ + snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), + snapshot.anchor_before(Point::new(7, 1))..snapshot.anchor_before(Point::new(7, 3)), + snapshot.anchor_before(Point::new(15, 0)) + ..snapshot.anchor_before(Point::new(15, 0)), + ]; + multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) + }); + + let anchor_ranges = anchor_ranges.collect::>().await; + + let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + assert_eq!( + snapshot.text(), + "bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n" + ); + + assert_eq!( + anchor_ranges + .iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(), + vec![ + Point::new(2, 2)..Point::new(3, 2), + Point::new(6, 1)..Point::new(6, 3), + Point::new(12, 0)..Point::new(12, 0) + ] + ); + } + + #[gpui::test] + fn test_empty_multibuffer(cx: &mut AppContext) { + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), ""); + assert_eq!(snapshot.buffer_rows(0).collect::>(), &[Some(0)]); + assert_eq!(snapshot.buffer_rows(1).collect::>(), &[]); + } + + #[gpui::test] + fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let multibuffer = cx.build_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + let old_snapshot = multibuffer.read(cx).snapshot(cx); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "X")], None, cx); + buffer.edit([(5..5, "Y")], None, cx); + }); + let new_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.text(), "abcd"); + assert_eq!(new_snapshot.text(), "XabcdY"); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); + assert_eq!(old_snapshot.anchor_before(4).to_offset(&new_snapshot), 5); + assert_eq!(old_snapshot.anchor_after(4).to_offset(&new_snapshot), 6); + } + + #[gpui::test] + fn test_multibuffer_anchors(cx: &mut AppContext) { + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi")); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..4, + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..5, + primary: None, + }], + cx, + ); + multibuffer + }); + let old_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&old_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&old_snapshot), 0); + assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); + assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); + assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); + assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); + + buffer_1.update(cx, |buffer, cx| { + buffer.edit([(0..0, "W")], None, cx); + buffer.edit([(5..5, "X")], None, cx); + }); + buffer_2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "Y")], None, cx); + buffer.edit([(6..6, "Z")], None, cx); + }); + let new_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.text(), "abcd\nefghi"); + assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); + assert_eq!(old_snapshot.anchor_before(1).to_offset(&new_snapshot), 2); + assert_eq!(old_snapshot.anchor_after(1).to_offset(&new_snapshot), 2); + assert_eq!(old_snapshot.anchor_before(2).to_offset(&new_snapshot), 3); + assert_eq!(old_snapshot.anchor_after(2).to_offset(&new_snapshot), 3); + assert_eq!(old_snapshot.anchor_before(5).to_offset(&new_snapshot), 7); + assert_eq!(old_snapshot.anchor_after(5).to_offset(&new_snapshot), 8); + assert_eq!(old_snapshot.anchor_before(10).to_offset(&new_snapshot), 13); + assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); + } + + #[gpui::test] + fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let buffer_2 = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP")); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + + // Create an insertion id in buffer 1 that doesn't exist in buffer 2. + // Add an excerpt from buffer 1 that spans this new insertion. + buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); + let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..7, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + let snapshot_1 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_1.text(), "abcd123"); + + // Replace the buffer 1 excerpt with new excerpts from buffer 2. + let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt_id_1], cx); + let mut ids = multibuffer + .push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: 0..4, + primary: None, + }, + ExcerptRange { + context: 6..10, + primary: None, + }, + ExcerptRange { + context: 12..16, + primary: None, + }, + ], + cx, + ) + .into_iter(); + (ids.next().unwrap(), ids.next().unwrap()) + }); + let snapshot_2 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); + + // The old excerpt id doesn't get reused. + assert_ne!(excerpt_id_2, excerpt_id_1); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // The current excerpts are from a different buffer, so we don't attempt to + // resolve the old text anchor in the new buffer. + assert_eq!( + snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), + 0 + ); + assert_eq!( + snapshot_2.summaries_for_anchors::(&[ + snapshot_1.anchor_before(2), + snapshot_1.anchor_after(3) + ]), + vec![0, 0] + ); + + // Refresh anchors from the old snapshot. The return value indicates that both + // anchors lost their original excerpt. + let refresh = + snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); + assert_eq!( + refresh, + &[ + (0, snapshot_2.anchor_before(0), false), + (1, snapshot_2.anchor_after(0), false), + ] + ); + + // Replace the middle excerpt with a smaller excerpt in buffer 2, + // that intersects the old excerpt. + let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt_id_3], cx); + multibuffer + .insert_excerpts_after( + excerpt_id_2, + buffer_2.clone(), + [ExcerptRange { + context: 5..8, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + let snapshot_3 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); + assert_ne!(excerpt_id_5, excerpt_id_3); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // The third anchor can't be resolved, since its excerpt has been removed, + // so it resolves to the same position as its predecessor. + let anchors = [ + snapshot_2.anchor_before(0), + snapshot_2.anchor_after(2), + snapshot_2.anchor_after(6), + snapshot_2.anchor_after(14), + ]; + assert_eq!( + snapshot_3.summaries_for_anchors::(&anchors), + &[0, 2, 9, 13] + ); + + let new_anchors = snapshot_3.refresh_anchors(&anchors); + assert_eq!( + new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), + &[(0, true), (1, true), (2, true), (3, true)] + ); + assert_eq!( + snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), + &[0, 2, 7, 13] + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut buffers: Vec> = Vec::new(); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let mut excerpt_ids = Vec::::new(); + let mut expected_excerpts = Vec::<(Model, Range)>::new(); + let mut anchors = Vec::new(); + let mut old_versions = Vec::new(); + + for _ in 0..operations { + match rng.gen_range(0..100) { + 0..=19 if !buffers.is_empty() => { + let buffer = buffers.choose(&mut rng).unwrap(); + buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx)); + } + 20..=29 if !expected_excerpts.is_empty() => { + let mut ids_to_remove = vec![]; + for _ in 0..rng.gen_range(1..=3) { + if expected_excerpts.is_empty() { + break; + } + + let ix = rng.gen_range(0..expected_excerpts.len()); + ids_to_remove.push(excerpt_ids.remove(ix)); + let (buffer, range) = expected_excerpts.remove(ix); + let buffer = buffer.read(cx); + log::info!( + "Removing excerpt {}: {:?}", + ix, + buffer + .text_for_range(range.to_offset(buffer)) + .collect::(), + ); + } + let snapshot = multibuffer.read(cx).read(cx); + ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot)); + drop(snapshot); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(ids_to_remove, cx) + }); + } + 30..=39 if !expected_excerpts.is_empty() => { + let multibuffer = multibuffer.read(cx).read(cx); + let offset = + multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); + let bias = if rng.gen() { Bias::Left } else { Bias::Right }; + log::info!("Creating anchor at {} with bias {:?}", offset, bias); + anchors.push(multibuffer.anchor_at(offset, bias)); + anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); + } + 40..=44 if !anchors.is_empty() => { + let multibuffer = multibuffer.read(cx).read(cx); + let prev_len = anchors.len(); + anchors = multibuffer + .refresh_anchors(&anchors) + .into_iter() + .map(|a| a.1) + .collect(); + + // Ensure the newly-refreshed anchors point to a valid excerpt and don't + // overshoot its boundaries. + assert_eq!(anchors.len(), prev_len); + for anchor in &anchors { + if anchor.excerpt_id == ExcerptId::min() + || anchor.excerpt_id == ExcerptId::max() + { + continue; + } + + let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); + assert_eq!(excerpt.id, anchor.excerpt_id); + assert!(excerpt.contains(anchor)); + } + } + _ => { + let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { + let base_text = util::RandomCharIter::new(&mut rng) + .take(10) + .collect::(); + buffers.push( + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), base_text)), + ); + buffers.last().unwrap() + } else { + buffers.choose(&mut rng).unwrap() + }; + + let buffer = buffer_handle.read(cx); + let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); + let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len()); + let prev_excerpt_id = excerpt_ids + .get(prev_excerpt_ix) + .cloned() + .unwrap_or_else(ExcerptId::max); + let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len()); + + log::info!( + "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", + excerpt_ix, + expected_excerpts.len(), + buffer_handle.read(cx).remote_id(), + buffer.text(), + start_ix..end_ix, + &buffer.text()[start_ix..end_ix] + ); + + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .insert_excerpts_after( + prev_excerpt_id, + buffer_handle.clone(), + [ExcerptRange { + context: start_ix..end_ix, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + excerpt_ids.insert(excerpt_ix, excerpt_id); + expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range)); + } + } + + if rng.gen_bool(0.3) { + multibuffer.update(cx, |multibuffer, cx| { + old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); + }) + } + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let mut excerpt_starts = Vec::new(); + let mut expected_text = String::new(); + let mut expected_buffer_rows = Vec::new(); + for (buffer, range) in &expected_excerpts { + let buffer = buffer.read(cx); + let buffer_range = range.to_offset(buffer); + + excerpt_starts.push(TextSummary::from(expected_text.as_str())); + expected_text.extend(buffer.text_for_range(buffer_range.clone())); + expected_text.push('\n'); + + let buffer_row_range = buffer.offset_to_point(buffer_range.start).row + ..=buffer.offset_to_point(buffer_range.end).row; + for row in buffer_row_range { + expected_buffer_rows.push(Some(row)); + } + } + // Remove final trailing newline. + if !expected_excerpts.is_empty() { + expected_text.pop(); + } + + // Always report one buffer row + if expected_buffer_rows.is_empty() { + expected_buffer_rows.push(Some(0)); + } + + assert_eq!(snapshot.text(), expected_text); + log::info!("MultiBuffer text: {:?}", expected_text); + + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + expected_buffer_rows, + ); + + for _ in 0..5 { + let start_row = rng.gen_range(0..=expected_buffer_rows.len()); + assert_eq!( + snapshot.buffer_rows(start_row as u32).collect::>(), + &expected_buffer_rows[start_row..], + "buffer_rows({})", + start_row + ); + } + + assert_eq!( + snapshot.max_buffer_row(), + expected_buffer_rows.into_iter().flatten().max().unwrap() + ); + + let mut excerpt_starts = excerpt_starts.into_iter(); + for (buffer, range) in &expected_excerpts { + let buffer = buffer.read(cx); + let buffer_id = buffer.remote_id(); + let buffer_range = range.to_offset(buffer); + let buffer_start_point = buffer.offset_to_point(buffer_range.start); + let buffer_start_point_utf16 = + buffer.text_summary_for_range::(0..buffer_range.start); + + let excerpt_start = excerpt_starts.next().unwrap(); + let mut offset = excerpt_start.len; + let mut buffer_offset = buffer_range.start; + let mut point = excerpt_start.lines; + let mut buffer_point = buffer_start_point; + let mut point_utf16 = excerpt_start.lines_utf16(); + let mut buffer_point_utf16 = buffer_start_point_utf16; + for ch in buffer + .snapshot() + .chunks(buffer_range.clone(), false) + .flat_map(|c| c.text.chars()) + { + for _ in 0..ch.len_utf8() { + let left_offset = snapshot.clip_offset(offset, Bias::Left); + let right_offset = snapshot.clip_offset(offset, Bias::Right); + let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left); + let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); + assert_eq!( + left_offset, + excerpt_start.len + (buffer_left_offset - buffer_range.start), + "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", + offset, + buffer_id, + buffer_offset, + ); + assert_eq!( + right_offset, + excerpt_start.len + (buffer_right_offset - buffer_range.start), + "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", + offset, + buffer_id, + buffer_offset, + ); + + let left_point = snapshot.clip_point(point, Bias::Left); + let right_point = snapshot.clip_point(point, Bias::Right); + let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left); + let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right); + assert_eq!( + left_point, + excerpt_start.lines + (buffer_left_point - buffer_start_point), + "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}", + point, + buffer_id, + buffer_point, + ); + assert_eq!( + right_point, + excerpt_start.lines + (buffer_right_point - buffer_start_point), + "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}", + point, + buffer_id, + buffer_point, + ); + + assert_eq!( + snapshot.point_to_offset(left_point), + left_offset, + "point_to_offset({:?})", + left_point, + ); + assert_eq!( + snapshot.offset_to_point(left_offset), + left_point, + "offset_to_point({:?})", + left_offset, + ); + + offset += 1; + buffer_offset += 1; + if ch == '\n' { + point += Point::new(1, 0); + buffer_point += Point::new(1, 0); + } else { + point += Point::new(0, 1); + buffer_point += Point::new(0, 1); + } + } + + for _ in 0..ch.len_utf16() { + let left_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); + let right_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); + let buffer_left_point_utf16 = + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); + let buffer_right_point_utf16 = + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); + assert_eq!( + left_point_utf16, + excerpt_start.lines_utf16() + + (buffer_left_point_utf16 - buffer_start_point_utf16), + "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", + point_utf16, + buffer_id, + buffer_point_utf16, + ); + assert_eq!( + right_point_utf16, + excerpt_start.lines_utf16() + + (buffer_right_point_utf16 - buffer_start_point_utf16), + "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", + point_utf16, + buffer_id, + buffer_point_utf16, + ); + + if ch == '\n' { + point_utf16 += PointUtf16::new(1, 0); + buffer_point_utf16 += PointUtf16::new(1, 0); + } else { + point_utf16 += PointUtf16::new(0, 1); + buffer_point_utf16 += PointUtf16::new(0, 1); + } + } + } + } + + for (row, line) in expected_text.split('\n').enumerate() { + assert_eq!( + snapshot.line_len(row as u32), + line.len() as u32, + "line_len({}).", + row + ); + } + + let text_rope = Rope::from(expected_text.as_str()); + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); + let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + + let text_for_range = snapshot + .text_for_range(start_ix..end_ix) + .collect::(); + assert_eq!( + text_for_range, + &expected_text[start_ix..end_ix], + "incorrect text for range {:?}", + start_ix..end_ix + ); + + let excerpted_buffer_ranges = multibuffer + .read(cx) + .range_to_buffer_ranges(start_ix..end_ix, cx); + let excerpted_buffers_text = excerpted_buffer_ranges + .iter() + .map(|(buffer, buffer_range, _)| { + buffer + .read(cx) + .text_for_range(buffer_range.clone()) + .collect::() + }) + .collect::>() + .join("\n"); + assert_eq!(excerpted_buffers_text, text_for_range); + if !expected_excerpts.is_empty() { + assert!(!excerpted_buffer_ranges.is_empty()); + } + + let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); + assert_eq!( + snapshot.text_summary_for_range::(start_ix..end_ix), + expected_summary, + "incorrect summary for range {:?}", + start_ix..end_ix + ); + } + + // Anchor resolution + let summaries = snapshot.summaries_for_anchors::(&anchors); + assert_eq!(anchors.len(), summaries.len()); + for (anchor, resolved_offset) in anchors.iter().zip(summaries) { + assert!(resolved_offset <= snapshot.len()); + assert_eq!( + snapshot.summary_for_anchor::(anchor), + resolved_offset + ); + } + + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); + assert_eq!( + snapshot.reversed_chars_at(end_ix).collect::(), + expected_text[..end_ix].chars().rev().collect::(), + ); + } + + for _ in 0..10 { + let end_ix = rng.gen_range(0..=text_rope.len()); + let start_ix = rng.gen_range(0..=end_ix); + assert_eq!( + snapshot + .bytes_in_range(start_ix..end_ix) + .flatten() + .copied() + .collect::>(), + expected_text.as_bytes()[start_ix..end_ix].to_vec(), + "bytes_in_range({:?})", + start_ix..end_ix, + ); + } + } + + let snapshot = multibuffer.read(cx).snapshot(cx); + for (old_snapshot, subscription) in old_versions { + let edits = subscription.consume().into_inner(); + + log::info!( + "applying subscription edits to old text: {:?}: {:?}", + old_snapshot.text(), + edits, + ); + + let mut text = old_snapshot.text(); + for edit in edits { + let new_text: String = snapshot.text_for_range(edit.new.clone()).collect(); + text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text); + } + assert_eq!(text.to_string(), snapshot.text()); + } + } + + #[gpui::test] + fn test_history(cx: &mut AppContext) { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234")); + let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678")); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let group_interval = multibuffer.read(cx).history.group_interval; + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let mut now = Instant::now(); + + multibuffer.update(cx, |multibuffer, cx| { + let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); + multibuffer.edit( + [ + (Point::new(0, 0)..Point::new(0, 0), "A"), + (Point::new(1, 0)..Point::new(1, 0), "A"), + ], + None, + cx, + ); + multibuffer.edit( + [ + (Point::new(0, 1)..Point::new(0, 1), "B"), + (Point::new(1, 1)..Point::new(1, 1), "B"), + ], + None, + cx, + ); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + // Edit buffer 1 through the multibuffer + now += 2 * group_interval; + multibuffer.start_transaction_at(now, cx); + multibuffer.edit([(2..2, "C")], None, cx); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); + + // Edit buffer 1 independently + buffer_1.update(cx, |buffer_1, cx| { + buffer_1.start_transaction_at(now); + buffer_1.edit([(3..3, "D")], None, cx); + buffer_1.end_transaction_at(now, cx); + + now += 2 * group_interval; + buffer_1.start_transaction_at(now); + buffer_1.edit([(4..4, "E")], None, cx); + buffer_1.end_transaction_at(now, cx); + }); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); + + // An undo in the multibuffer undoes the multibuffer transaction + // and also any individual buffer edits that have occurred since + // that transaction. + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); + + // Undo buffer 2 independently. + buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); + + // An undo in the multibuffer undoes the components of the + // the last multibuffer transaction that are not already undone. + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); + + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); + assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); + + // Redo stack gets cleared after an edit. + now += 2 * group_interval; + multibuffer.start_transaction_at(now, cx); + multibuffer.edit([(0..0, "X")], None, cx); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + // Transactions can be grouped manually. + multibuffer.redo(cx); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.group_until_transaction(transaction_1, cx); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + }); + } +} diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 7517b4ee43..06c1b66977 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -1,9 +1,8 @@ -use std::collections::VecDeque; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Context; -use collections::HashMap; +use collections::{HashMap, HashSet}; use fs::Fs; use gpui::{AsyncAppContext, ModelHandle}; use language::language_settings::language_settings; @@ -11,7 +10,7 @@ use language::{Buffer, Diff}; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; -use util::paths::DEFAULT_PRETTIER_DIR; +use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; pub enum Prettier { Real(RealPrettier), @@ -20,7 +19,6 @@ pub enum Prettier { } pub struct RealPrettier { - worktree_id: Option, default: bool, prettier_dir: PathBuf, server: Arc, @@ -28,17 +26,10 @@ pub struct RealPrettier { #[cfg(any(test, feature = "test-support"))] pub struct TestPrettier { - worktree_id: Option, prettier_dir: PathBuf, default: bool, } -#[derive(Debug)] -pub struct LocateStart { - pub worktree_root_path: Arc, - pub starting_path: Arc, -} - pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; @@ -63,79 +54,106 @@ impl Prettier { ".editorconfig", ]; - pub async fn locate( - starting_path: Option, - fs: Arc, - ) -> anyhow::Result { - fn is_node_modules(path_component: &std::path::Component<'_>) -> bool { - path_component.as_os_str().to_string_lossy() == "node_modules" + pub async fn locate_prettier_installation( + fs: &dyn Fs, + installed_prettiers: &HashSet, + locate_from: &Path, + ) -> anyhow::Result> { + let mut path_to_check = locate_from + .components() + .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") + .collect::(); + let path_to_check_metadata = fs + .metadata(&path_to_check) + .await + .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? + .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; + if !path_to_check_metadata.is_dir { + path_to_check.pop(); } - let paths_to_check = match starting_path.as_ref() { - Some(starting_path) => { - let worktree_root = starting_path - .worktree_root_path - .components() - .into_iter() - .take_while(|path_component| !is_node_modules(path_component)) - .collect::(); - if worktree_root != starting_path.worktree_root_path.as_ref() { - vec![worktree_root] + let mut project_path_with_prettier_dependency = None; + loop { + if installed_prettiers.contains(&path_to_check) { + log::debug!("Found prettier path {path_to_check:?} in installed prettiers"); + return Ok(Some(path_to_check)); + } else if let Some(package_json_contents) = + read_package_json(fs, &path_to_check).await? + { + if has_prettier_in_package_json(&package_json_contents) { + if has_prettier_in_node_modules(fs, &path_to_check).await? { + log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules"); + return Ok(Some(path_to_check)); + } else if project_path_with_prettier_dependency.is_none() { + project_path_with_prettier_dependency = Some(path_to_check.clone()); + } } else { - if starting_path.starting_path.as_ref() == Path::new("") { - worktree_root - .parent() - .map(|path| vec![path.to_path_buf()]) - .unwrap_or_default() - } else { - let file_to_format = starting_path.starting_path.as_ref(); - let mut paths_to_check = VecDeque::new(); - let mut current_path = worktree_root; - for path_component in file_to_format.components().into_iter() { - let new_path = current_path.join(path_component); - let old_path = std::mem::replace(&mut current_path, new_path); - paths_to_check.push_front(old_path); - if is_node_modules(&path_component) { - break; + match package_json_contents.get("workspaces") { + Some(serde_json::Value::Array(workspaces)) => { + match &project_path_with_prettier_dependency { + Some(project_path_with_prettier_dependency) => { + let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix"); + if workspaces.iter().filter_map(|value| { + if let serde_json::Value::String(s) = value { + Some(s.clone()) + } else { + log::warn!("Skipping non-string 'workspaces' value: {value:?}"); + None + } + }).any(|workspace_definition| { + if let Some(path_matcher) = PathMatcher::new(&workspace_definition).ok() { + path_matcher.is_match(subproject_path) + } else { + workspace_definition == subproject_path.to_string_lossy() + } + }) { + anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules"); + log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}"); + return Ok(Some(path_to_check)); + } else { + log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}"); + } + } + None => { + log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json"); + } } - } - Vec::from(paths_to_check) + }, + Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."), + None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"), } } } - None => Vec::new(), - }; - match find_closest_prettier_dir(paths_to_check, fs.as_ref()) - .await - .with_context(|| format!("finding prettier starting with {starting_path:?}"))? - { - Some(prettier_dir) => Ok(prettier_dir), - None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()), + if !path_to_check.pop() { + match project_path_with_prettier_dependency { + Some(closest_prettier_discovered) => { + anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}") + } + None => { + log::debug!("Found no prettier in ancestors of {locate_from:?}"); + return Ok(None); + } + } + } } } #[cfg(any(test, feature = "test-support"))] pub async fn start( - worktree_id: Option, _: LanguageServerId, prettier_dir: PathBuf, _: Arc, _: AsyncAppContext, ) -> anyhow::Result { - Ok( - #[cfg(any(test, feature = "test-support"))] - Self::Test(TestPrettier { - worktree_id, - default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), - prettier_dir, - }), - ) + Ok(Self::Test(TestPrettier { + default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + prettier_dir, + })) } #[cfg(not(any(test, feature = "test-support")))] pub async fn start( - worktree_id: Option, server_id: LanguageServerId, prettier_dir: PathBuf, node: Arc, @@ -143,7 +161,7 @@ impl Prettier { ) -> anyhow::Result { use lsp::LanguageServerBinary; - let backgroud = cx.background(); + let background = cx.background(); anyhow::ensure!( prettier_dir.is_dir(), "Prettier dir {prettier_dir:?} is not a directory" @@ -154,7 +172,7 @@ impl Prettier { "no prettier server package found at {prettier_server:?}" ); - let node_path = backgroud + let node_path = background .spawn(async move { node.binary_path().await }) .await?; let server = LanguageServer::new( @@ -169,12 +187,11 @@ impl Prettier { cx, ) .context("prettier server creation")?; - let server = backgroud + let server = background .spawn(server.initialize(None)) .await .context("prettier server initialization")?; Ok(Self::Real(RealPrettier { - worktree_id, server, default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), prettier_dir, @@ -340,64 +357,61 @@ impl Prettier { Self::Test(test_prettier) => &test_prettier.prettier_dir, } } - - pub fn worktree_id(&self) -> Option { - match self { - Self::Real(local) => local.worktree_id, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => test_prettier.worktree_id, - } - } } -async fn find_closest_prettier_dir( - paths_to_check: Vec, - fs: &dyn Fs, -) -> anyhow::Result> { - for path in paths_to_check { - let possible_package_json = path.join("package.json"); - if let Some(package_json_metadata) = fs - .metadata(&possible_package_json) - .await - .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))? - { - if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - if let Ok(json_contents) = serde_json::from_str::>( - &package_json_contents, - ) { - if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") { - if o.contains_key(PRETTIER_PACKAGE_NAME) { - return Ok(Some(path)); - } - } - if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies") - { - if o.contains_key(PRETTIER_PACKAGE_NAME) { - return Ok(Some(path)); - } - } - } - } - } +async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result { + let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); + if let Some(node_modules_location_metadata) = fs + .metadata(&possible_node_modules_location) + .await + .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? + { + return Ok(node_modules_location_metadata.is_dir); + } + Ok(false) +} - let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); - if let Some(node_modules_location_metadata) = fs - .metadata(&possible_node_modules_location) - .await - .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? - { - if node_modules_location_metadata.is_dir { - return Ok(Some(path)); - } +async fn read_package_json( + fs: &dyn Fs, + path: &Path, +) -> anyhow::Result>> { + let possible_package_json = path.join("package.json"); + if let Some(package_json_metadata) = fs + .metadata(&possible_package_json) + .await + .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? + { + if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + return serde_json::from_str::>( + &package_json_contents, + ) + .map(Some) + .with_context(|| format!("parsing {possible_package_json:?} file contents")); } } Ok(None) } +fn has_prettier_in_package_json( + package_json_contents: &HashMap, +) -> bool { + if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return true; + } + } + if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return true; + } + } + false +} + enum Format {} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -436,3 +450,316 @@ impl lsp::request::Request for ClearCache { type Result = (); const METHOD: &'static str = "prettier/clear_cache"; } + +#[cfg(test)] +mod tests { + use fs::FakeFs; + use serde_json::json; + + use super::*; + + #[gpui::test] + async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + ".config": { + "zed": { + "settings.json": r#"{ "formatter": "auto" }"#, + }, + }, + "work": { + "project": { + "src": { + "index.js": "// index.js file contents", + }, + "node_modules": { + "expect": { + "build": { + "print.js": "// print.js file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.5.1" + } + }"#, + }, + "prettier": { + "index.js": "// Dummy prettier package file", + }, + }, + "package.json": r#"{}"# + }, + } + }), + ) + .await; + + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/.config/zed/settings.json"), + ) + .await + .unwrap() + .is_none(), + "Should successfully find no prettier for path hierarchy without it" + ); + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/project/src/index.js") + ) + .await + .unwrap() + .is_none(), + "Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it" + ); + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/project/node_modules/expect/build/print.js") + ) + .await + .unwrap() + .is_none(), + "Even though it has package.json with prettier in it and no prettier on node_modules along the path, nothing should fail since declared inside node_modules" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "web_blog": { + "node_modules": { + "prettier": { + "index.js": "// Dummy prettier package file", + }, + "expect": { + "build": { + "print.js": "// print.js file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.5.1" + } + }"#, + }, + }, + "pages": { + "[slug].tsx": "// [slug].tsx file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.3.0" + }, + "prettier": { + "semi": false, + "printWidth": 80, + "htmlWhitespaceSensitivity": "strict", + "tabWidth": 4 + } + }"# + } + }), + ) + .await; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/web_blog/pages/[slug].tsx") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/web_blog")), + "Should find a preinstalled prettier in the project root" + ); + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/web_blog/node_modules/expect/build/print.js") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/web_blog")), + "Should find a preinstalled prettier in the project root even for node_modules files" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "work": { + "web_blog": { + "pages": { + "[slug].tsx": "// [slug].tsx file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.3.0" + }, + "prettier": { + "semi": false, + "printWidth": 80, + "htmlWhitespaceSensitivity": "strict", + "tabWidth": 4 + } + }"# + } + } + }), + ) + .await; + + let path = "/root/work/web_blog/node_modules/pages/[slug].tsx"; + match Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new(path) + ) + .await { + Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), + Err(e) => { + let message = e.to_string(); + assert!(message.contains(path), "Error message should mention which start file was used for location"); + assert!(message.contains("/root/work/web_blog"), "Error message should mention potential candidates without prettier node_modules contents"); + }, + }; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::from_iter( + [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() + ), + Path::new("/root/work/web_blog/node_modules/pages/[slug].tsx") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/work")), + "Should return first cached value found without path checks" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "work": { + "full-stack-foundations": { + "exercises": { + "03.loading": { + "01.problem.loader": { + "app": { + "routes": { + "users+": { + "$username_+": { + "notes.tsx": "// notes.tsx file contents", + }, + }, + }, + }, + "node_modules": {}, + "package.json": r#"{ + "devDependencies": { + "prettier": "^3.0.3" + } + }"# + }, + }, + }, + "package.json": r#"{ + "workspaces": ["exercises/*/*", "examples/*"] + }"#, + "node_modules": { + "prettier": { + "index.js": "// Dummy prettier package file", + }, + }, + }, + } + }), + ) + .await; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"), + ).await.unwrap(), + Some(PathBuf::from("/root/work/full-stack-foundations")), + "Should ascend to the multi-workspace root and find the prettier there", + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( + cx: &mut gpui::TestAppContext, + ) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "work": { + "full-stack-foundations": { + "exercises": { + "03.loading": { + "01.problem.loader": { + "app": { + "routes": { + "users+": { + "$username_+": { + "notes.tsx": "// notes.tsx file contents", + }, + }, + }, + }, + "node_modules": {}, + "package.json": r#"{ + "devDependencies": { + "prettier": "^3.0.3" + } + }"# + }, + }, + }, + "package.json": r#"{ + "workspaces": ["exercises/*/*", "examples/*"] + }"#, + }, + } + }), + ) + .await; + + match Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx") + ) + .await { + Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), + Err(e) => { + let message = e.to_string(); + assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined"); + assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents"); + }, + }; + } +} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js index 9967aec50f..191431da0b 100644 --- a/crates/prettier/src/prettier_server.js +++ b/crates/prettier/src/prettier_server.js @@ -1,11 +1,13 @@ -const { Buffer } = require('buffer'); +const { Buffer } = require("buffer"); const fs = require("fs"); const path = require("path"); -const { once } = require('events'); +const { once } = require("events"); const prettierContainerPath = process.argv[2]; if (prettierContainerPath == null || prettierContainerPath.length == 0) { - process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`); + process.stderr.write( + `Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`, + ); process.exit(1); } fs.stat(prettierContainerPath, (err, stats) => { @@ -19,7 +21,7 @@ fs.stat(prettierContainerPath, (err, stats) => { process.exit(1); } }); -const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier'); +const prettierPath = path.join(prettierContainerPath, "node_modules/prettier"); class Prettier { constructor(path, prettier, config) { @@ -34,7 +36,7 @@ class Prettier { let config; try { prettier = await loadPrettier(prettierPath); - config = await prettier.resolveConfig(prettierPath) || {}; + config = (await prettier.resolveConfig(prettierPath)) || {}; } catch (e) { process.stderr.write(`Failed to load prettier: ${e}\n`); process.exit(1); @@ -42,7 +44,7 @@ class Prettier { process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); process.stdin.resume(); handleBuffer(new Prettier(prettierPath, prettier, config)); -})() +})(); async function handleBuffer(prettier) { for await (const messageText of readStdin()) { @@ -54,25 +56,29 @@ async function handleBuffer(prettier) { continue; } // allow concurrent request handling by not `await`ing the message handling promise (async function) - handleMessage(message, prettier).catch(e => { + handleMessage(message, prettier).catch((e) => { const errorMessage = message; if ((errorMessage.params || {}).text !== undefined) { errorMessage.params.text = "..snip.."; } - sendResponse({ id: message.id, ...makeError(`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`) }); }); + sendResponse({ + id: message.id, + ...makeError(`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`), + }); + }); } } const headerSeparator = "\r\n"; -const contentLengthHeaderName = 'Content-Length'; +const contentLengthHeaderName = "Content-Length"; async function* readStdin() { let buffer = Buffer.alloc(0); let streamEnded = false; - process.stdin.on('end', () => { + process.stdin.on("end", () => { streamEnded = true; }); - process.stdin.on('data', (data) => { + process.stdin.on("data", (data) => { buffer = Buffer.concat([buffer, data]); }); @@ -80,7 +86,7 @@ async function* readStdin() { sendResponse(makeError(errorMessage)); buffer = Buffer.alloc(0); messageLength = null; - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); streamEnded = false; } @@ -91,20 +97,25 @@ async function* readStdin() { if (messageLength === null) { while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { if (streamEnded) { - await handleStreamEnded('Unexpected end of stream: headers not found'); + await handleStreamEnded("Unexpected end of stream: headers not found"); continue main_loop; } else if (buffer.length > contentLengthHeaderName.length * 10) { - await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`); + await handleStreamEnded( + `Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`, + ); continue main_loop; } - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); } - const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii'); - const contentLengthHeader = headers.split(headerSeparator) - .map(header => header.split(':')) - .filter(header => header[2] === undefined) - .filter(header => (header[1] || '').length > 0) - .find(header => (header[0] || '').trim() === contentLengthHeaderName); + const headers = buffer + .subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)) + .toString("ascii"); + const contentLengthHeader = headers + .split(headerSeparator) + .map((header) => header.split(":")) + .filter((header) => header[2] === undefined) + .filter((header) => (header[1] || "").length > 0) + .find((header) => (header[0] || "").trim() === contentLengthHeaderName); const contentLength = (contentLengthHeader || [])[1]; if (contentLength === undefined) { await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); @@ -114,13 +125,14 @@ async function* readStdin() { messageLength = parseInt(contentLength, 10); } - while (buffer.length < (headersLength + messageLength)) { + while (buffer.length < headersLength + messageLength) { if (streamEnded) { await handleStreamEnded( - `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`); + `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`, + ); continue main_loop; } - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); } const messageEnd = headersLength + messageLength; @@ -128,12 +140,12 @@ async function* readStdin() { buffer = buffer.subarray(messageEnd); headersLength = null; messageLength = null; - yield message.toString('utf8'); + yield message.toString("utf8"); } } catch (e) { sendResponse(makeError(`Error reading stdin: ${e}`)); } finally { - process.stdin.off('data', () => { }); + process.stdin.off("data", () => {}); } } @@ -146,7 +158,7 @@ async function handleMessage(message, prettier) { throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); } - if (method === 'prettier/format') { + if (method === "prettier/format") { if (params === undefined || params.text === undefined) { throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); } @@ -156,7 +168,7 @@ async function handleMessage(message, prettier) { let resolvedConfig = {}; if (params.options.filepath !== undefined) { - resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {}; + resolvedConfig = (await prettier.prettier.resolveConfig(params.options.filepath)) || {}; } const options = { @@ -164,21 +176,25 @@ async function handleMessage(message, prettier) { ...resolvedConfig, parser: params.options.parser, plugins: params.options.plugins, - path: params.options.filepath + path: params.options.filepath, }; - process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`); + process.stderr.write( + `Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${ + params.options.filepath || "" + }' with options: ${JSON.stringify(options)}\n`, + ); const formattedText = await prettier.prettier.format(params.text, options); sendResponse({ id, result: { text: formattedText } }); - } else if (method === 'prettier/clear_cache') { + } else if (method === "prettier/clear_cache") { prettier.prettier.clearConfigCache(); - prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {}; + prettier.config = (await prettier.prettier.resolveConfig(prettier.path)) || {}; sendResponse({ id, result: null }); - } else if (method === 'initialize') { + } else if (method === "initialize") { sendResponse({ - id: id || 0, + id, result: { - "capabilities": {} - } + capabilities: {}, + }, }); } else { throw new Error(`Unknown method: ${method}`); @@ -188,18 +204,20 @@ async function handleMessage(message, prettier) { function makeError(message) { return { error: { - "code": -32600, // invalid request code + code: -32600, // invalid request code message, - } + }, }; } function sendResponse(response) { const responsePayloadString = JSON.stringify({ jsonrpc: "2.0", - ...response + ...response, }); - const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`; + const headers = `${contentLengthHeaderName}: ${Buffer.byteLength( + responsePayloadString, + )}${headerSeparator}${headerSeparator}`; process.stdout.write(headers + responsePayloadString); } diff --git a/crates/prettier2/Cargo.toml b/crates/prettier2/Cargo.toml index b98124f72c..de229dcc70 100644 --- a/crates/prettier2/Cargo.toml +++ b/crates/prettier2/Cargo.toml @@ -12,12 +12,12 @@ doctest = false test-support = [] [dependencies] -client2 = { path = "../client2" } +client = { package = "client2", path = "../client2" } collections = { path = "../collections"} -language2 = { path = "../language2" } -gpui2 = { path = "../gpui2" } -fs2 = { path = "../fs2" } -lsp2 = { path = "../lsp2" } +language = { package = "language2", path = "../language2" } +gpui = { package = "gpui2", path = "../gpui2" } +fs = { package = "fs2", path = "../fs2" } +lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } @@ -30,6 +30,6 @@ futures.workspace = true parking_lot.workspace = true [dev-dependencies] -language2 = { path = "../language2", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -fs2 = { path = "../fs2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index a71bf1a8b0..44151774ae 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -1,17 +1,16 @@ use anyhow::Context; -use collections::HashMap; -use fs2::Fs; -use gpui2::{AsyncAppContext, Model}; -use language2::{language_settings::language_settings, Buffer, Diff}; -use lsp2::{LanguageServer, LanguageServerId}; +use collections::{HashMap, HashSet}; +use fs::Fs; +use gpui::{AsyncAppContext, Model}; +use language::{language_settings::language_settings, Buffer, Diff}; +use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; use std::{ - collections::VecDeque, path::{Path, PathBuf}, sync::Arc, }; -use util::paths::DEFAULT_PRETTIER_DIR; +use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; pub enum Prettier { Real(RealPrettier), @@ -20,7 +19,6 @@ pub enum Prettier { } pub struct RealPrettier { - worktree_id: Option, default: bool, prettier_dir: PathBuf, server: Arc, @@ -28,17 +26,10 @@ pub struct RealPrettier { #[cfg(any(test, feature = "test-support"))] pub struct TestPrettier { - worktree_id: Option, prettier_dir: PathBuf, default: bool, } -#[derive(Debug)] -pub struct LocateStart { - pub worktree_root_path: Arc, - pub starting_path: Arc, -} - pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; @@ -63,87 +54,114 @@ impl Prettier { ".editorconfig", ]; - pub async fn locate( - starting_path: Option, - fs: Arc, - ) -> anyhow::Result { - fn is_node_modules(path_component: &std::path::Component<'_>) -> bool { - path_component.as_os_str().to_string_lossy() == "node_modules" + pub async fn locate_prettier_installation( + fs: &dyn Fs, + installed_prettiers: &HashSet, + locate_from: &Path, + ) -> anyhow::Result> { + let mut path_to_check = locate_from + .components() + .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") + .collect::(); + let path_to_check_metadata = fs + .metadata(&path_to_check) + .await + .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? + .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; + if !path_to_check_metadata.is_dir { + path_to_check.pop(); } - let paths_to_check = match starting_path.as_ref() { - Some(starting_path) => { - let worktree_root = starting_path - .worktree_root_path - .components() - .into_iter() - .take_while(|path_component| !is_node_modules(path_component)) - .collect::(); - if worktree_root != starting_path.worktree_root_path.as_ref() { - vec![worktree_root] + let mut project_path_with_prettier_dependency = None; + loop { + if installed_prettiers.contains(&path_to_check) { + log::debug!("Found prettier path {path_to_check:?} in installed prettiers"); + return Ok(Some(path_to_check)); + } else if let Some(package_json_contents) = + read_package_json(fs, &path_to_check).await? + { + if has_prettier_in_package_json(&package_json_contents) { + if has_prettier_in_node_modules(fs, &path_to_check).await? { + log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules"); + return Ok(Some(path_to_check)); + } else if project_path_with_prettier_dependency.is_none() { + project_path_with_prettier_dependency = Some(path_to_check.clone()); + } } else { - if starting_path.starting_path.as_ref() == Path::new("") { - worktree_root - .parent() - .map(|path| vec![path.to_path_buf()]) - .unwrap_or_default() - } else { - let file_to_format = starting_path.starting_path.as_ref(); - let mut paths_to_check = VecDeque::new(); - let mut current_path = worktree_root; - for path_component in file_to_format.components().into_iter() { - let new_path = current_path.join(path_component); - let old_path = std::mem::replace(&mut current_path, new_path); - paths_to_check.push_front(old_path); - if is_node_modules(&path_component) { - break; - } + match package_json_contents.get("workspaces") { + Some(serde_json::Value::Array(workspaces)) => { + match &project_path_with_prettier_dependency { + Some(project_path_with_prettier_dependency) => { + let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix"); + if workspaces.iter().filter_map(|value| { + if let serde_json::Value::String(s) = value { + Some(s.clone()) + } else { + log::warn!("Skipping non-string 'workspaces' value: {value:?}"); + None + } + }).any(|workspace_definition| { + if let Some(path_matcher) = PathMatcher::new(&workspace_definition).ok() { + path_matcher.is_match(subproject_path) + } else { + workspace_definition == subproject_path.to_string_lossy() + } + }) { + anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules"); + log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}"); + return Ok(Some(path_to_check)); + } else { + log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}"); + } + } + None => { + log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json"); + } + } + }, + Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."), + None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"), } - Vec::from(paths_to_check) + } + } + + if !path_to_check.pop() { + match project_path_with_prettier_dependency { + Some(closest_prettier_discovered) => { + anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}") + } + None => { + log::debug!("Found no prettier in ancestors of {locate_from:?}"); + return Ok(None); } } } - None => Vec::new(), - }; - - match find_closest_prettier_dir(paths_to_check, fs.as_ref()) - .await - .with_context(|| format!("finding prettier starting with {starting_path:?}"))? - { - Some(prettier_dir) => Ok(prettier_dir), - None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()), } } #[cfg(any(test, feature = "test-support"))] pub async fn start( - worktree_id: Option, _: LanguageServerId, prettier_dir: PathBuf, _: Arc, _: AsyncAppContext, ) -> anyhow::Result { - Ok( - #[cfg(any(test, feature = "test-support"))] - Self::Test(TestPrettier { - worktree_id, - default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), - prettier_dir, - }), - ) + Ok(Self::Test(TestPrettier { + default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + prettier_dir, + })) } #[cfg(not(any(test, feature = "test-support")))] pub async fn start( - worktree_id: Option, server_id: LanguageServerId, prettier_dir: PathBuf, node: Arc, cx: AsyncAppContext, ) -> anyhow::Result { - use lsp2::LanguageServerBinary; + use lsp::LanguageServerBinary; - let executor = cx.executor().clone(); + let executor = cx.background_executor().clone(); anyhow::ensure!( prettier_dir.is_dir(), "Prettier dir {prettier_dir:?} is not a directory" @@ -174,7 +192,6 @@ impl Prettier { .await .context("prettier server initialization")?; Ok(Self::Real(RealPrettier { - worktree_id, server, default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), prettier_dir, @@ -370,64 +387,61 @@ impl Prettier { Self::Test(test_prettier) => &test_prettier.prettier_dir, } } - - pub fn worktree_id(&self) -> Option { - match self { - Self::Real(local) => local.worktree_id, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => test_prettier.worktree_id, - } - } } -async fn find_closest_prettier_dir( - paths_to_check: Vec, - fs: &dyn Fs, -) -> anyhow::Result> { - for path in paths_to_check { - let possible_package_json = path.join("package.json"); - if let Some(package_json_metadata) = fs - .metadata(&possible_package_json) - .await - .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))? - { - if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - if let Ok(json_contents) = serde_json::from_str::>( - &package_json_contents, - ) { - if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") { - if o.contains_key(PRETTIER_PACKAGE_NAME) { - return Ok(Some(path)); - } - } - if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies") - { - if o.contains_key(PRETTIER_PACKAGE_NAME) { - return Ok(Some(path)); - } - } - } - } - } +async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result { + let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); + if let Some(node_modules_location_metadata) = fs + .metadata(&possible_node_modules_location) + .await + .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? + { + return Ok(node_modules_location_metadata.is_dir); + } + Ok(false) +} - let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); - if let Some(node_modules_location_metadata) = fs - .metadata(&possible_node_modules_location) - .await - .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? - { - if node_modules_location_metadata.is_dir { - return Ok(Some(path)); - } +async fn read_package_json( + fs: &dyn Fs, + path: &Path, +) -> anyhow::Result>> { + let possible_package_json = path.join("package.json"); + if let Some(package_json_metadata) = fs + .metadata(&possible_package_json) + .await + .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? + { + if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + return serde_json::from_str::>( + &package_json_contents, + ) + .map(Some) + .with_context(|| format!("parsing {possible_package_json:?} file contents")); } } Ok(None) } +fn has_prettier_in_package_json( + package_json_contents: &HashMap, +) -> bool { + if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return true; + } + } + if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return true; + } + } + false +} + enum Format {} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -453,7 +467,7 @@ struct FormatResult { text: String, } -impl lsp2::request::Request for Format { +impl lsp::request::Request for Format { type Params = FormatParams; type Result = FormatResult; const METHOD: &'static str = "prettier/format"; @@ -461,8 +475,321 @@ impl lsp2::request::Request for Format { enum ClearCache {} -impl lsp2::request::Request for ClearCache { +impl lsp::request::Request for ClearCache { type Params = (); type Result = (); const METHOD: &'static str = "prettier/clear_cache"; } + +#[cfg(test)] +mod tests { + use fs::FakeFs; + use serde_json::json; + + use super::*; + + #[gpui::test] + async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + ".config": { + "zed": { + "settings.json": r#"{ "formatter": "auto" }"#, + }, + }, + "work": { + "project": { + "src": { + "index.js": "// index.js file contents", + }, + "node_modules": { + "expect": { + "build": { + "print.js": "// print.js file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.5.1" + } + }"#, + }, + "prettier": { + "index.js": "// Dummy prettier package file", + }, + }, + "package.json": r#"{}"# + }, + } + }), + ) + .await; + + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/.config/zed/settings.json"), + ) + .await + .unwrap() + .is_none(), + "Should successfully find no prettier for path hierarchy without it" + ); + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/project/src/index.js") + ) + .await + .unwrap() + .is_none(), + "Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it" + ); + assert!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/project/node_modules/expect/build/print.js") + ) + .await + .unwrap() + .is_none(), + "Even though it has package.json with prettier in it and no prettier on node_modules along the path, nothing should fail since declared inside node_modules" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "web_blog": { + "node_modules": { + "prettier": { + "index.js": "// Dummy prettier package file", + }, + "expect": { + "build": { + "print.js": "// print.js file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.5.1" + } + }"#, + }, + }, + "pages": { + "[slug].tsx": "// [slug].tsx file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.3.0" + }, + "prettier": { + "semi": false, + "printWidth": 80, + "htmlWhitespaceSensitivity": "strict", + "tabWidth": 4 + } + }"# + } + }), + ) + .await; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/web_blog/pages/[slug].tsx") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/web_blog")), + "Should find a preinstalled prettier in the project root" + ); + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/web_blog/node_modules/expect/build/print.js") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/web_blog")), + "Should find a preinstalled prettier in the project root even for node_modules files" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "work": { + "web_blog": { + "pages": { + "[slug].tsx": "// [slug].tsx file contents", + }, + "package.json": r#"{ + "devDependencies": { + "prettier": "2.3.0" + }, + "prettier": { + "semi": false, + "printWidth": 80, + "htmlWhitespaceSensitivity": "strict", + "tabWidth": 4 + } + }"# + } + } + }), + ) + .await; + + let path = "/root/work/web_blog/node_modules/pages/[slug].tsx"; + match Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new(path) + ) + .await { + Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), + Err(e) => { + let message = e.to_string(); + assert!(message.contains(path), "Error message should mention which start file was used for location"); + assert!(message.contains("/root/work/web_blog"), "Error message should mention potential candidates without prettier node_modules contents"); + }, + }; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::from_iter( + [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() + ), + Path::new("/root/work/web_blog/node_modules/pages/[slug].tsx") + ) + .await + .unwrap(), + Some(PathBuf::from("/root/work")), + "Should return first cached value found without path checks" + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "work": { + "full-stack-foundations": { + "exercises": { + "03.loading": { + "01.problem.loader": { + "app": { + "routes": { + "users+": { + "$username_+": { + "notes.tsx": "// notes.tsx file contents", + }, + }, + }, + }, + "node_modules": {}, + "package.json": r#"{ + "devDependencies": { + "prettier": "^3.0.3" + } + }"# + }, + }, + }, + "package.json": r#"{ + "workspaces": ["exercises/*/*", "examples/*"] + }"#, + "node_modules": { + "prettier": { + "index.js": "// Dummy prettier package file", + }, + }, + }, + } + }), + ) + .await; + + assert_eq!( + Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"), + ).await.unwrap(), + Some(PathBuf::from("/root/work/full-stack-foundations")), + "Should ascend to the multi-workspace root and find the prettier there", + ); + } + + #[gpui::test] + async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( + cx: &mut gpui::TestAppContext, + ) { + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "work": { + "full-stack-foundations": { + "exercises": { + "03.loading": { + "01.problem.loader": { + "app": { + "routes": { + "users+": { + "$username_+": { + "notes.tsx": "// notes.tsx file contents", + }, + }, + }, + }, + "node_modules": {}, + "package.json": r#"{ + "devDependencies": { + "prettier": "^3.0.3" + } + }"# + }, + }, + }, + "package.json": r#"{ + "workspaces": ["exercises/*/*", "examples/*"] + }"#, + }, + } + }), + ) + .await; + + match Prettier::locate_prettier_installation( + fs.as_ref(), + &HashSet::default(), + Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx") + ) + .await { + Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), + Err(e) => { + let message = e.to_string(); + assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined"); + assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents"); + }, + }; + } +} diff --git a/crates/prettier2/src/prettier_server.js b/crates/prettier2/src/prettier_server.js index a56c220f20..191431da0b 100644 --- a/crates/prettier2/src/prettier_server.js +++ b/crates/prettier2/src/prettier_server.js @@ -1,11 +1,13 @@ -const { Buffer } = require('buffer'); +const { Buffer } = require("buffer"); const fs = require("fs"); const path = require("path"); -const { once } = require('events'); +const { once } = require("events"); const prettierContainerPath = process.argv[2]; if (prettierContainerPath == null || prettierContainerPath.length == 0) { - process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`); + process.stderr.write( + `Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`, + ); process.exit(1); } fs.stat(prettierContainerPath, (err, stats) => { @@ -19,7 +21,7 @@ fs.stat(prettierContainerPath, (err, stats) => { process.exit(1); } }); -const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier'); +const prettierPath = path.join(prettierContainerPath, "node_modules/prettier"); class Prettier { constructor(path, prettier, config) { @@ -34,7 +36,7 @@ class Prettier { let config; try { prettier = await loadPrettier(prettierPath); - config = await prettier.resolveConfig(prettierPath) || {}; + config = (await prettier.resolveConfig(prettierPath)) || {}; } catch (e) { process.stderr.write(`Failed to load prettier: ${e}\n`); process.exit(1); @@ -42,7 +44,7 @@ class Prettier { process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); process.stdin.resume(); handleBuffer(new Prettier(prettierPath, prettier, config)); -})() +})(); async function handleBuffer(prettier) { for await (const messageText of readStdin()) { @@ -54,22 +56,29 @@ async function handleBuffer(prettier) { continue; } // allow concurrent request handling by not `await`ing the message handling promise (async function) - handleMessage(message, prettier).catch(e => { - sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) }); + handleMessage(message, prettier).catch((e) => { + const errorMessage = message; + if ((errorMessage.params || {}).text !== undefined) { + errorMessage.params.text = "..snip.."; + } + sendResponse({ + id: message.id, + ...makeError(`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`), + }); }); } } const headerSeparator = "\r\n"; -const contentLengthHeaderName = 'Content-Length'; +const contentLengthHeaderName = "Content-Length"; async function* readStdin() { let buffer = Buffer.alloc(0); let streamEnded = false; - process.stdin.on('end', () => { + process.stdin.on("end", () => { streamEnded = true; }); - process.stdin.on('data', (data) => { + process.stdin.on("data", (data) => { buffer = Buffer.concat([buffer, data]); }); @@ -77,7 +86,7 @@ async function* readStdin() { sendResponse(makeError(errorMessage)); buffer = Buffer.alloc(0); messageLength = null; - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); streamEnded = false; } @@ -88,20 +97,25 @@ async function* readStdin() { if (messageLength === null) { while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { if (streamEnded) { - await handleStreamEnded('Unexpected end of stream: headers not found'); + await handleStreamEnded("Unexpected end of stream: headers not found"); continue main_loop; } else if (buffer.length > contentLengthHeaderName.length * 10) { - await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`); + await handleStreamEnded( + `Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`, + ); continue main_loop; } - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); } - const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii'); - const contentLengthHeader = headers.split(headerSeparator) - .map(header => header.split(':')) - .filter(header => header[2] === undefined) - .filter(header => (header[1] || '').length > 0) - .find(header => (header[0] || '').trim() === contentLengthHeaderName); + const headers = buffer + .subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)) + .toString("ascii"); + const contentLengthHeader = headers + .split(headerSeparator) + .map((header) => header.split(":")) + .filter((header) => header[2] === undefined) + .filter((header) => (header[1] || "").length > 0) + .find((header) => (header[0] || "").trim() === contentLengthHeaderName); const contentLength = (contentLengthHeader || [])[1]; if (contentLength === undefined) { await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); @@ -111,13 +125,14 @@ async function* readStdin() { messageLength = parseInt(contentLength, 10); } - while (buffer.length < (headersLength + messageLength)) { + while (buffer.length < headersLength + messageLength) { if (streamEnded) { await handleStreamEnded( - `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`); + `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`, + ); continue main_loop; } - await once(process.stdin, 'readable'); + await once(process.stdin, "readable"); } const messageEnd = headersLength + messageLength; @@ -125,12 +140,12 @@ async function* readStdin() { buffer = buffer.subarray(messageEnd); headersLength = null; messageLength = null; - yield message.toString('utf8'); + yield message.toString("utf8"); } } catch (e) { sendResponse(makeError(`Error reading stdin: ${e}`)); } finally { - process.stdin.off('data', () => { }); + process.stdin.off("data", () => {}); } } @@ -143,7 +158,7 @@ async function handleMessage(message, prettier) { throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); } - if (method === 'prettier/format') { + if (method === "prettier/format") { if (params === undefined || params.text === undefined) { throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); } @@ -153,7 +168,7 @@ async function handleMessage(message, prettier) { let resolvedConfig = {}; if (params.options.filepath !== undefined) { - resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {}; + resolvedConfig = (await prettier.prettier.resolveConfig(params.options.filepath)) || {}; } const options = { @@ -161,21 +176,25 @@ async function handleMessage(message, prettier) { ...resolvedConfig, parser: params.options.parser, plugins: params.options.plugins, - path: params.options.filepath + path: params.options.filepath, }; - process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`); + process.stderr.write( + `Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${ + params.options.filepath || "" + }' with options: ${JSON.stringify(options)}\n`, + ); const formattedText = await prettier.prettier.format(params.text, options); sendResponse({ id, result: { text: formattedText } }); - } else if (method === 'prettier/clear_cache') { + } else if (method === "prettier/clear_cache") { prettier.prettier.clearConfigCache(); - prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {}; + prettier.config = (await prettier.prettier.resolveConfig(prettier.path)) || {}; sendResponse({ id, result: null }); - } else if (method === 'initialize') { + } else if (method === "initialize") { sendResponse({ id, result: { - "capabilities": {} - } + capabilities: {}, + }, }); } else { throw new Error(`Unknown method: ${method}`); @@ -185,18 +204,20 @@ async function handleMessage(message, prettier) { function makeError(message) { return { error: { - "code": -32600, // invalid request code + code: -32600, // invalid request code message, - } + }, }; } function sendResponse(response) { const responsePayloadString = JSON.stringify({ jsonrpc: "2.0", - ...response + ...response, }); - const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`; + const headers = `${contentLengthHeaderName}: ${Buffer.byteLength( + responsePayloadString, + )}${headerSeparator}${headerSeparator}`; process.stdout.write(headers + responsePayloadString); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f7c13c86f7..3aef577a57 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -51,7 +51,7 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::Mutex; use postage::watch; -use prettier::{LocateStart, Prettier}; +use prettier::Prettier; use project_settings::{LspSettings, ProjectSettings}; use project_types::*; use rand::prelude::*; @@ -80,8 +80,11 @@ use std::{ use terminals::Terminals; use text::Anchor; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -160,17 +163,15 @@ pub struct Project { copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, node: Option>, - #[cfg(not(any(test, feature = "test-support")))] default_prettier: Option, - prettier_instances: HashMap< - (Option, PathBuf), - Shared, Arc>>>, - >, + prettiers_per_worktree: HashMap>>, + prettier_instances: HashMap, Arc>>>>, } -#[cfg(not(any(test, feature = "test-support")))] struct DefaultPrettier { - installation_process: Option>>, + instance: Option, Arc>>>>, + installation_process: Option>>>>, + #[cfg(not(any(test, feature = "test-support")))] installed_plugins: HashSet<&'static str>, } @@ -563,8 +564,8 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), node: Some(node), - #[cfg(not(any(test, feature = "test-support")))] default_prettier: None, + prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), } }) @@ -664,8 +665,8 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), node: None, - #[cfg(not(any(test, feature = "test-support")))] default_prettier: None, + prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; for worktree in worktrees { @@ -802,8 +803,7 @@ impl Project { } for (worktree, language, settings) in language_formatters_to_check { - self.install_default_formatters(worktree, &language, &settings, cx) - .detach_and_log_err(cx); + self.install_default_formatters(worktree, &language, &settings, cx); } // Start all the newly-enabled language servers. @@ -2559,20 +2559,7 @@ impl Project { let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); - let task_buffer = buffer.clone(); - let prettier_installation_task = - self.install_default_formatters(worktree, &new_language, &settings, cx); - cx.spawn(|project, mut cx| async move { - prettier_installation_task.await?; - let _ = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(&task_buffer, cx) - }) - .await; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - + self.install_default_formatters(worktree, &new_language, &settings, cx); if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { @@ -3907,7 +3894,7 @@ impl Project { } pub fn format( - &self, + &mut self, buffers: HashSet>, push_to_history: bool, trigger: FormatTrigger, @@ -3927,10 +3914,10 @@ impl Project { }) .collect::>(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|project, mut cx| async move { // Do not allow multiple concurrent formatting requests for the // same buffer. - this.update(&mut cx, |this, cx| { + project.update(&mut cx, |this, cx| { buffers_with_paths_and_servers.retain(|(buffer, _, _)| { this.buffers_being_formatted .insert(buffer.read(cx).remote_id()) @@ -3938,7 +3925,7 @@ impl Project { }); let _cleanup = defer({ - let this = this.clone(); + let this = project.clone(); let mut cx = cx.clone(); let buffers = &buffers_with_paths_and_servers; move || { @@ -4006,7 +3993,7 @@ impl Project { { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( - &this, + &project, &buffer, buffer_abs_path, &language_server, @@ -4041,14 +4028,14 @@ impl Project { } } (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { - if let Some(prettier_task) = this + if let Some((prettier_path, prettier_task)) = project .update(&mut cx, |project, cx| { project.prettier_instance_for_buffer(buffer, cx) }).await { match prettier_task.await { Ok(prettier) => { - let buffer_path = buffer.read_with(&cx, |buffer, cx| { + let buffer_path = buffer.update(&mut cx, |buffer, cx| { File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) }); format_operation = Some(FormatOperation::Prettier( @@ -4058,16 +4045,35 @@ impl Project { .context("formatting via prettier")?, )); } - Err(e) => anyhow::bail!( - "Failed to create prettier instance for buffer during autoformatting: {e:#}" - ), + Err(e) => { + project.update(&mut cx, |project, _| { + match &prettier_path { + Some(prettier_path) => { + project.prettier_instances.remove(prettier_path); + }, + None => { + if let Some(default_prettier) = project.default_prettier.as_mut() { + default_prettier.instance = None; + } + }, + } + }); + match &prettier_path { + Some(prettier_path) => { + log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); + }, + None => { + log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); + }, + } + } } } else if let Some((language_server, buffer_abs_path)) = language_server.as_ref().zip(buffer_abs_path.as_ref()) { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( - &this, + &project, &buffer, buffer_abs_path, &language_server, @@ -4080,14 +4086,14 @@ impl Project { } } (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { - if let Some(prettier_task) = this + if let Some((prettier_path, prettier_task)) = project .update(&mut cx, |project, cx| { project.prettier_instance_for_buffer(buffer, cx) }).await { match prettier_task.await { Ok(prettier) => { - let buffer_path = buffer.read_with(&cx, |buffer, cx| { + let buffer_path = buffer.update(&mut cx, |buffer, cx| { File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) }); format_operation = Some(FormatOperation::Prettier( @@ -4097,9 +4103,28 @@ impl Project { .context("formatting via prettier")?, )); } - Err(e) => anyhow::bail!( - "Failed to create prettier instance for buffer during formatting: {e:#}" - ), + Err(e) => { + project.update(&mut cx, |project, _| { + match &prettier_path { + Some(prettier_path) => { + project.prettier_instances.remove(prettier_path); + }, + None => { + if let Some(default_prettier) = project.default_prettier.as_mut() { + default_prettier.instance = None; + } + }, + } + }); + match &prettier_path { + Some(prettier_path) => { + log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); + }, + None => { + log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); + }, + } + } } } } @@ -6309,15 +6334,25 @@ impl Project { "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" ); let prettiers_to_reload = self - .prettier_instances + .prettiers_per_worktree + .get(¤t_worktree_id) .iter() - .filter_map(|((worktree_id, prettier_path), prettier_task)| { - if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) { - Some((*worktree_id, prettier_path.clone(), prettier_task.clone())) - } else { - None - } + .flat_map(|prettier_paths| prettier_paths.iter()) + .flatten() + .filter_map(|prettier_path| { + Some(( + current_worktree_id, + Some(prettier_path.clone()), + self.prettier_instances.get(prettier_path)?.clone(), + )) }) + .chain(self.default_prettier.iter().filter_map(|default_prettier| { + Some(( + current_worktree_id, + None, + default_prettier.instance.clone()?, + )) + })) .collect::>(); cx.background() @@ -6328,9 +6363,15 @@ impl Project { .clear_cache() .await .with_context(|| { - format!( - "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" - ) + match prettier_path { + Some(prettier_path) => format!( + "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" + ), + None => format!( + "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update" + ), + } + }) .map_err(Arc::new) } @@ -8242,7 +8283,12 @@ impl Project { &mut self, buffer: &ModelHandle, cx: &mut ModelContext, - ) -> Task, Arc>>>>> { + ) -> Task< + Option<( + Option, + Shared, Arc>>>, + )>, + > { let buffer = buffer.read(cx); let buffer_file = buffer.file(); let Some(buffer_language) = buffer.language() else { @@ -8252,136 +8298,119 @@ impl Project { return Task::ready(None); } - let buffer_file = File::from_dyn(buffer_file); - let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); - let worktree_path = buffer_file - .as_ref() - .and_then(|file| Some(file.worktree.read(cx).abs_path())); - let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); - if self.is_local() || worktree_id.is_none() || worktree_path.is_none() { + if self.is_local() { let Some(node) = self.node.as_ref().map(Arc::clone) else { return Task::ready(None); }; - cx.spawn(|this, mut cx| async move { - let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs)); - let prettier_dir = match cx - .background() - .spawn(Prettier::locate( - worktree_path.zip(buffer_path).map( - |(worktree_root_path, starting_path)| LocateStart { - worktree_root_path, - starting_path, - }, - ), - fs, - )) - .await - { - Ok(path) => path, - Err(e) => { - return Some( - Task::ready(Err(Arc::new(e.context( - "determining prettier path for worktree {worktree_path:?}", - )))) - .shared(), - ); - } - }; - - if let Some(existing_prettier) = this.update(&mut cx, |project, _| { - project - .prettier_instances - .get(&(worktree_id, prettier_dir.clone())) - .cloned() - }) { - return Some(existing_prettier); - } - - log::info!("Found prettier in {prettier_dir:?}, starting."); - let task_prettier_dir = prettier_dir.clone(); - let weak_project = this.downgrade(); - let new_server_id = - this.update(&mut cx, |this, _| this.languages.next_language_server_id()); - let new_prettier_task = cx - .spawn(|mut cx| async move { - let prettier = Prettier::start( - worktree_id.map(|id| id.to_usize()), - new_server_id, - task_prettier_dir, - node, - cx.clone(), - ) - .await - .context("prettier start") - .map_err(Arc::new)?; - log::info!("Started prettier in {:?}", prettier.prettier_dir()); - - if let Some((project, prettier_server)) = - weak_project.upgrade(&mut cx).zip(prettier.server()) + match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) + { + Some((worktree_id, buffer_path)) => { + let fs = Arc::clone(&self.fs); + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + return cx.spawn(|project, mut cx| async move { + match cx + .background() + .spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + &buffer_path, + ) + .await + }) + .await { - project.update(&mut cx, |project, cx| { - let name = if prettier.is_default() { - LanguageServerName(Arc::from("prettier (default)")) - } else { - let prettier_dir = prettier.prettier_dir(); - let worktree_path = prettier - .worktree_id() - .map(WorktreeId::from_usize) - .and_then(|id| project.worktree_for_id(id, cx)) - .map(|worktree| worktree.read(cx).abs_path()); - match worktree_path { - Some(worktree_path) => { - if worktree_path.as_ref() == prettier_dir { - LanguageServerName(Arc::from(format!( - "prettier ({})", - prettier_dir - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default() - ))) - } else { - let dir_to_display = match prettier_dir - .strip_prefix(&worktree_path) - .ok() - { - Some(relative_path) => relative_path, - None => prettier_dir, - }; - LanguageServerName(Arc::from(format!( - "prettier ({})", - dir_to_display.display(), - ))) - } - } - None => LanguageServerName(Arc::from(format!( - "prettier ({})", - prettier_dir.display(), - ))), + Ok(None) => { + let started_default_prettier = + project.update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(None); + project.default_prettier.as_ref().and_then( + |default_prettier| default_prettier.instance.clone(), + ) + }); + match started_default_prettier { + Some(old_task) => return Some((None, old_task)), + None => { + let new_default_prettier = project + .update(&mut cx, |_, cx| { + start_default_prettier(node, Some(worktree_id), cx) + }) + .await; + return Some((None, new_default_prettier)); } - }; + } + } + Ok(Some(prettier_dir)) => { + project.update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(Some(prettier_dir.clone())) + }); + if let Some(existing_prettier) = + project.update(&mut cx, |project, _| { + project.prettier_instances.get(&prettier_dir).cloned() + }) + { + log::debug!( + "Found already started prettier in {prettier_dir:?}" + ); + return Some((Some(prettier_dir), existing_prettier)); + } - project - .supplementary_language_servers - .insert(new_server_id, (name, Arc::clone(prettier_server))); - cx.emit(Event::LanguageServerAdded(new_server_id)); - }); + log::info!("Found prettier in {prettier_dir:?}, starting."); + let new_prettier_task = project.update(&mut cx, |project, cx| { + let new_prettier_task = start_prettier( + node, + prettier_dir.clone(), + Some(worktree_id), + cx, + ); + project + .prettier_instances + .insert(prettier_dir.clone(), new_prettier_task.clone()); + new_prettier_task + }); + Some((Some(prettier_dir), new_prettier_task)) + } + Err(e) => { + return Some(( + None, + Task::ready(Err(Arc::new( + e.context("determining prettier path"), + ))) + .shared(), + )); + } } - Ok(Arc::new(prettier)).map_err(Arc::new) - }) - .shared(); - this.update(&mut cx, |project, _| { - project - .prettier_instances - .insert((worktree_id, prettier_dir), new_prettier_task.clone()); - }); - Some(new_prettier_task) - }) + }); + } + None => { + let started_default_prettier = self + .default_prettier + .as_ref() + .and_then(|default_prettier| default_prettier.instance.clone()); + match started_default_prettier { + Some(old_task) => return Task::ready(Some((None, old_task))), + None => { + let new_task = start_default_prettier(node, None, cx); + return cx.spawn(|_, _| async move { Some((None, new_task.await)) }); + } + } + } + } } else if self.remote_id().is_some() { return Task::ready(None); } else { - Task::ready(Some( + Task::ready(Some(( + None, Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), - )) + ))) } } @@ -8392,8 +8421,7 @@ impl Project { _new_language: &Language, _language_settings: &LanguageSettings, _cx: &mut ModelContext, - ) -> Task> { - return Task::ready(Ok(())); + ) { } #[cfg(not(any(test, feature = "test-support")))] @@ -8403,19 +8431,19 @@ impl Project { new_language: &Language, language_settings: &LanguageSettings, cx: &mut ModelContext, - ) -> Task> { + ) { match &language_settings.formatter { Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())), + Formatter::LanguageServer | Formatter::External { .. } => return, }; let Some(node) = self.node.as_ref().cloned() else { - return Task::ready(Ok(())); + return; }; let mut prettier_plugins = None; if new_language.prettier_parser_name().is_some() { prettier_plugins - .get_or_insert_with(|| HashSet::default()) + .get_or_insert_with(|| HashSet::<&'static str>::default()) .extend( new_language .lsp_adapters() @@ -8424,114 +8452,270 @@ impl Project { ) } let Some(prettier_plugins) = prettier_plugins else { - return Task::ready(Ok(())); + return; }; + let fs = Arc::clone(&self.fs); + let locate_prettier_installation = match worktree.and_then(|worktree_id| { + self.worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + }) { + Some(locate_from) => { + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + cx.background().spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + locate_from.as_ref(), + ) + .await + }) + } + None => Task::ready(Ok(None)), + }; let mut plugins_to_install = prettier_plugins; - let (mut install_success_tx, mut install_success_rx) = - futures::channel::mpsc::channel::>(1); - let new_installation_process = cx - .spawn(|this, mut cx| async move { - if let Some(installed_plugins) = install_success_rx.next().await { - this.update(&mut cx, |this, _| { - let default_prettier = - this.default_prettier - .get_or_insert_with(|| DefaultPrettier { - installation_process: None, - installed_plugins: HashSet::default(), - }); - if !installed_plugins.is_empty() { - log::info!("Installed new prettier plugins: {installed_plugins:?}"); - default_prettier.installed_plugins.extend(installed_plugins); - } - }) - } - }) - .shared(); let previous_installation_process = if let Some(default_prettier) = &mut self.default_prettier { plugins_to_install .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); if plugins_to_install.is_empty() { - return Task::ready(Ok(())); + return; } - std::mem::replace( - &mut default_prettier.installation_process, - Some(new_installation_process.clone()), - ) + default_prettier.installation_process.clone() } else { None }; - - let default_prettier_dir = util::paths::DEFAULT_PRETTIER_DIR.as_path(); - let already_running_prettier = self - .prettier_instances - .get(&(worktree, default_prettier_dir.to_path_buf())) - .cloned(); let fs = Arc::clone(&self.fs); - cx.spawn(|this, mut cx| async move { - if let Some(previous_installation_process) = previous_installation_process { - previous_installation_process.await; - } - let mut everything_was_installed = false; - this.update(&mut cx, |this, _| { - match &mut this.default_prettier { - Some(default_prettier) => { - plugins_to_install - .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); - everything_was_installed = plugins_to_install.is_empty(); - }, - None => this.default_prettier = Some(DefaultPrettier { installation_process: Some(new_installation_process), installed_plugins: HashSet::default() }), - } + let default_prettier = self + .default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: None, + installed_plugins: HashSet::default(), }); - if everything_was_installed { - return Ok(()); - } - - cx.background() - .spawn(async move { - let prettier_wrapper_path = default_prettier_dir.join(prettier::PRETTIER_SERVER_FILE); - // method creates parent directory if it doesn't exist - fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await - .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?; - - let packages_to_versions = future::try_join_all( - plugins_to_install - .iter() - .chain(Some(&"prettier")) - .map(|package_name| async { - let returned_package_name = package_name.to_string(); - let latest_version = node.npm_package_latest_version(package_name) - .await - .with_context(|| { - format!("fetching latest npm version for package {returned_package_name}") - })?; - anyhow::Ok((returned_package_name, latest_version)) - }), - ) + default_prettier.installation_process = Some( + cx.spawn(|this, mut cx| async move { + match locate_prettier_installation .await - .context("fetching latest npm versions")?; - - log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); - let borrowed_packages = packages_to_versions.iter().map(|(package, version)| { - (package.as_str(), version.as_str()) - }).collect::>(); - node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?; - let installed_packages = !plugins_to_install.is_empty(); - install_success_tx.try_send(plugins_to_install).ok(); - - if !installed_packages { - if let Some(prettier) = already_running_prettier { - prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?; + .context("locate prettier installation") + .map_err(Arc::new)? + { + Some(_non_default_prettier) => return Ok(()), + None => { + let mut needs_install = match previous_installation_process { + Some(previous_installation_process) => { + previous_installation_process.await.is_err() + } + None => true, + }; + this.update(&mut cx, |this, _| { + if let Some(default_prettier) = &mut this.default_prettier { + plugins_to_install.retain(|plugin| { + !default_prettier.installed_plugins.contains(plugin) + }); + needs_install |= !plugins_to_install.is_empty(); + } + }); + if needs_install { + let installed_plugins = plugins_to_install.clone(); + cx.background() + .spawn(async move { + install_default_prettier(plugins_to_install, node, fs).await + }) + .await + .context("prettier & plugins install") + .map_err(Arc::new)?; + this.update(&mut cx, |this, _| { + let default_prettier = + this.default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: Some( + Task::ready(Ok(())).shared(), + ), + installed_plugins: HashSet::default(), + }); + default_prettier.instance = None; + default_prettier.installed_plugins.extend(installed_plugins); + }); } } - - anyhow::Ok(()) - }).await - }) + } + Ok(()) + }) + .shared(), + ); } } +fn start_default_prettier( + node: Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Task, Arc>>>> { + cx.spawn(|project, mut cx| async move { + loop { + let default_prettier_installing = project.update(&mut cx, |project, _| { + project + .default_prettier + .as_ref() + .and_then(|default_prettier| default_prettier.installation_process.clone()) + }); + match default_prettier_installing { + Some(installation_task) => { + if installation_task.await.is_ok() { + break; + } + } + None => break, + } + } + + project.update(&mut cx, |project, cx| { + match project + .default_prettier + .as_mut() + .and_then(|default_prettier| default_prettier.instance.as_mut()) + { + Some(default_prettier) => default_prettier.clone(), + None => { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project + .default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: None, + #[cfg(not(any(test, feature = "test-support")))] + installed_plugins: HashSet::default(), + }) + .instance = Some(new_default_prettier.clone()); + new_default_prettier + } + } + }) + }) +} + +fn start_prettier( + node: Arc, + prettier_dir: PathBuf, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Shared, Arc>>> { + cx.spawn(|project, mut cx| async move { + let new_server_id = project.update(&mut cx, |project, _| { + project.languages.next_language_server_id() + }); + let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; + register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); + Ok(new_prettier) + }) + .shared() +} + +fn register_new_prettier( + project: &ModelHandle, + prettier: &Prettier, + worktree_id: Option, + new_server_id: LanguageServerId, + cx: &mut AsyncAppContext, +) { + let prettier_dir = prettier.prettier_dir(); + let is_default = prettier.is_default(); + if is_default { + log::info!("Started default prettier in {prettier_dir:?}"); + } else { + log::info!("Started prettier in {prettier_dir:?}"); + } + if let Some(prettier_server) = prettier.server() { + project.update(cx, |project, cx| { + let name = if is_default { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let worktree_path = worktree_id + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); + let name = match worktree_path { + Some(worktree_path) => { + if prettier_dir == worktree_path.as_ref() { + let name = prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + format!("prettier ({name})") + } else { + let dir_to_display = prettier_dir + .strip_prefix(worktree_path.as_ref()) + .ok() + .unwrap_or(prettier_dir); + format!("prettier ({})", dir_to_display.display()) + } + } + None => format!("prettier ({})", prettier_dir.display()), + }; + LanguageServerName(Arc::from(name)) + }; + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }); + } +} + +#[cfg(not(any(test, feature = "test-support")))] +async fn install_default_prettier( + plugins_to_install: HashSet<&'static str>, + node: Arc, + fs: Arc, +) -> anyhow::Result<()> { + let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); + // method creates parent directory if it doesn't exist + fs.save( + &prettier_wrapper_path, + &text::Rope::from(prettier::PRETTIER_SERVER_JS), + text::LineEnding::Unix, + ) + .await + .with_context(|| { + format!( + "writing {} file at {prettier_wrapper_path:?}", + prettier::PRETTIER_SERVER_FILE + ) + })?; + + let packages_to_versions = + future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( + |package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node + .npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }, + )) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions + .iter() + .map(|(package, version)| (package.as_str(), version.as_str())) + .collect::>(); + node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) + .await + .context("fetching formatter packages")?; + anyhow::Ok(()) +} + fn subscribe_for_copilot_events( copilot: &ModelHandle, cx: &mut ModelContext<'_, Project>, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 540915f354..90d32643d5 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; +use crate::{worktree::WorktreeModelHandle, Event, *}; use fs::{FakeFs, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; @@ -13,7 +13,7 @@ use pretty_assertions::assert_eq; use serde_json::json; use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; use unindent::Unindent as _; -use util::{assert_set_eq, test::temp_tree}; +use util::{assert_set_eq, paths::PathMatcher, test::temp_tree}; #[cfg(test)] #[ctor::ctor] @@ -2604,64 +2604,64 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); } -#[gpui::test] -async fn test_save_as(cx: &mut gpui::TestAppContext) { - init_test(cx); +// #[gpui::test] +// async fn test_save_as(cx: &mut gpui::TestAppContext) { +// init_test(cx); - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({})).await; +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({})).await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let languages = project.read_with(cx, |project, _| project.languages().clone()); - languages.register( - "/some/path", - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - tree_sitter_rust::language(), - vec![], - |_| Default::default(), - ); +// let languages = project.read_with(cx, |project, _| project.languages().clone()); +// languages.register( +// "/some/path", +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".into()], +// ..Default::default() +// }, +// tree_sitter_rust::language(), +// vec![], +// |_| Default::default(), +// ); - let buffer = project.update(cx, |project, cx| { - project.create_buffer("", None, cx).unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "abc")], None, cx); - assert!(buffer.is_dirty()); - assert!(!buffer.has_conflict()); - assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); - }); - project - .update(cx, |project, cx| { - project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) - }) - .await - .unwrap(); - assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); +// let buffer = project.update(cx, |project, cx| { +// project.create_buffer("", None, cx).unwrap() +// }); +// buffer.update(cx, |buffer, cx| { +// buffer.edit([(0..0, "abc")], None, cx); +// assert!(buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); +// }); +// project +// .update(cx, |project, cx| { +// project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); - cx.foreground().run_until_parked(); - buffer.read_with(cx, |buffer, cx| { - assert_eq!( - buffer.file().unwrap().full_path(cx), - Path::new("dir/file1.rs") - ); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); - }); +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, cx| { +// assert_eq!( +// buffer.file().unwrap().full_path(cx), +// Path::new("dir/file1.rs") +// ); +// assert!(!buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); +// }); - let opened_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/dir/file1.rs", cx) - }) - .await - .unwrap(); - assert_eq!(opened_buffer, buffer); -} +// let opened_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/dir/file1.rs", cx) +// }) +// .await +// .unwrap(); +// assert_eq!(opened_buffer, buffer); +// } #[gpui::test(retries = 5)] async fn test_rescan_and_remote_updates( diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 46dd30c8a0..7e360e22ee 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,7 +1,6 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::{Context, Result}; use client::proto; -use globset::{Glob, GlobMatcher}; use itertools::Itertools; use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; @@ -10,9 +9,10 @@ use std::{ borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, - path::{Path, PathBuf}, + path::Path, sync::Arc, }; +use util::paths::PathMatcher; #[derive(Clone, Debug)] pub struct SearchInputs { @@ -52,31 +52,6 @@ pub enum SearchQuery { }, } -#[derive(Clone, Debug)] -pub struct PathMatcher { - maybe_path: PathBuf, - glob: GlobMatcher, -} - -impl std::fmt::Display for PathMatcher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.maybe_path.to_string_lossy().fmt(f) - } -} - -impl PathMatcher { - pub fn new(maybe_glob: &str) -> Result { - Ok(PathMatcher { - glob: Glob::new(&maybe_glob)?.compile_matcher(), - maybe_path: PathBuf::from(maybe_glob), - }) - } - - pub fn is_match>(&self, other: P) -> bool { - other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other) - } -} - impl SearchQuery { pub fn text( query: impl ToString, diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml index b135b5367c..892ddb91c7 100644 --- a/crates/project2/Cargo.toml +++ b/crates/project2/Cargo.toml @@ -10,34 +10,35 @@ doctest = false [features] test-support = [ - "client2/test-support", - "db2/test-support", - "language2/test-support", - "settings2/test-support", + "client/test-support", + "db/test-support", + "language/test-support", + "settings/test-support", "text/test-support", - "prettier2/test-support", + "prettier/test-support", + "gpui/test-support", ] [dependencies] -text = { path = "../text" } -copilot2 = { path = "../copilot2" } -client2 = { path = "../client2" } +text = { package = "text2", path = "../text2" } +copilot = { package = "copilot2", path = "../copilot2" } +client = { package = "client2", path = "../client2" } clock = { path = "../clock" } collections = { path = "../collections" } -db2 = { path = "../db2" } -fs2 = { path = "../fs2" } +db = { package = "db2", path = "../db2" } +fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } -fuzzy2 = { path = "../fuzzy2" } -git = { path = "../git" } -gpui2 = { path = "../gpui2" } -language2 = { path = "../language2" } -lsp2 = { path = "../lsp2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +git = { package = "git3", path = "../git3" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime" } -prettier2 = { path = "../prettier2" } -rpc2 = { path = "../rpc2" } -settings2 = { path = "../settings2" } +prettier = { package = "prettier2", path = "../prettier2" } +rpc = { package = "rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } sum_tree = { path = "../sum_tree" } -terminal2 = { path = "../terminal2" } +terminal = { package = "terminal2", path = "../terminal2" } util = { path = "../util" } aho-corasick = "1.1" @@ -68,17 +69,17 @@ itertools = "0.10" ctor.workspace = true env_logger.workspace = true pretty_assertions.workspace = true -client2 = { path = "../client2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } -db2 = { path = "../db2", features = ["test-support"] } -fs2 = { path = "../fs2", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -language2 = { path = "../language2", features = ["test-support"] } -lsp2 = { path = "../lsp2", features = ["test-support"] } -settings2 = { path = "../settings2", features = ["test-support"] } -prettier2 = { path = "../prettier2", features = ["test-support"] } +db = { package = "db2", path = "../db2", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +prettier = { package = "prettier2", path = "../prettier2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } -rpc2 = { path = "../rpc2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } git2.workspace = true tempdir.workspace = true unindent.workspace = true diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index 84a6c0517c..cc1821d3ff 100644 --- a/crates/project2/src/lsp_command.rs +++ b/crates/project2/src/lsp_command.rs @@ -5,10 +5,10 @@ use crate::{ }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client2::proto::{self, PeerId}; +use client::proto::{self, PeerId}; use futures::future; -use gpui2::{AppContext, AsyncAppContext, Model}; -use language2::{ +use gpui::{AppContext, AsyncAppContext, Model}; +use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -16,29 +16,29 @@ use language2::{ CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp2::{ +use lsp::{ CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities, }; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; use text::LineEnding; -pub fn lsp_formatting_options(tab_size: u32) -> lsp2::FormattingOptions { - lsp2::FormattingOptions { +pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { + lsp::FormattingOptions { tab_size, insert_spaces: true, insert_final_newline: Some(true), - ..lsp2::FormattingOptions::default() + ..lsp::FormattingOptions::default() } } -#[async_trait] +#[async_trait(?Send)] pub(crate) trait LspCommand: 'static + Sized + Send { type Response: 'static + Default + Send; - type LspRequest: 'static + Send + lsp2::request::Request; + type LspRequest: 'static + Send + lsp::request::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; - fn check_capabilities(&self, _: &lsp2::ServerCapabilities) -> bool { + fn check_capabilities(&self, _: &lsp::ServerCapabilities) -> bool { true } @@ -48,11 +48,11 @@ pub(crate) trait LspCommand: 'static + Sized + Send { buffer: &Buffer, language_server: &Arc, cx: &AppContext, - ) -> ::Params; + ) -> ::Params; async fn response_from_lsp( self, - message: ::Result, + message: ::Result, project: Model, buffer: Model, server_id: LanguageServerId, @@ -140,22 +140,22 @@ pub(crate) struct FormattingOptions { tab_size: u32, } -impl From for FormattingOptions { - fn from(value: lsp2::FormattingOptions) -> Self { +impl From for FormattingOptions { + fn from(value: lsp::FormattingOptions) -> Self { Self { tab_size: value.tab_size, } } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = Option>; - type LspRequest = lsp2::request::PrepareRenameRequest; + type LspRequest = lsp::request::PrepareRenameRequest; type ProtoRequest = proto::PrepareRename; fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { - if let Some(lsp2::OneOf::Right(rename)) = &capabilities.rename_provider { + if let Some(lsp::OneOf::Right(rename)) = &capabilities.rename_provider { rename.prepare_provider == Some(true) } else { false @@ -168,10 +168,10 @@ impl LspCommand for PrepareRename { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::TextDocumentPositionParams { - lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::TextDocumentPositionParams { + lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), } @@ -179,7 +179,7 @@ impl LspCommand for PrepareRename { async fn response_from_lsp( self, - message: Option, + message: Option, _: Model, buffer: Model, _: LanguageServerId, @@ -187,8 +187,8 @@ impl LspCommand for PrepareRename { ) -> Result>> { buffer.update(&mut cx, |buffer, _| { if let Some( - lsp2::PrepareRenameResponse::Range(range) - | lsp2::PrepareRenameResponse::RangeWithPlaceholder { range, .. }, + lsp::PrepareRenameResponse::Range(range) + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }, ) = message { let Range { start, end } = range_from_lsp(range); @@ -206,7 +206,7 @@ impl LspCommand for PrepareRename { proto::PrepareRename { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version()), @@ -245,10 +245,10 @@ impl LspCommand for PrepareRename { can_rename: range.is_some(), start: range .as_ref() - .map(|range| language2::proto::serialize_anchor(&range.start)), + .map(|range| language::proto::serialize_anchor(&range.start)), end: range .as_ref() - .map(|range| language2::proto::serialize_anchor(&range.end)), + .map(|range| language::proto::serialize_anchor(&range.end)), version: serialize_version(buffer_version), } } @@ -279,10 +279,10 @@ impl LspCommand for PrepareRename { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for PerformRename { type Response = ProjectTransaction; - type LspRequest = lsp2::request::Rename; + type LspRequest = lsp::request::Rename; type ProtoRequest = proto::PerformRename; fn to_lsp( @@ -291,11 +291,11 @@ impl LspCommand for PerformRename { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::RenameParams { - lsp2::RenameParams { - text_document_position: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::RenameParams { + lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, @@ -306,7 +306,7 @@ impl LspCommand for PerformRename { async fn response_from_lsp( self, - message: Option, + message: Option, project: Model, buffer: Model, server_id: LanguageServerId, @@ -333,7 +333,7 @@ impl LspCommand for PerformRename { proto::PerformRename { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), new_name: self.new_name.clone(), @@ -398,10 +398,10 @@ impl LspCommand for PerformRename { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetDefinition { type Response = Vec; - type LspRequest = lsp2::request::GotoDefinition; + type LspRequest = lsp::request::GotoDefinition; type ProtoRequest = proto::GetDefinition; fn to_lsp( @@ -410,11 +410,11 @@ impl LspCommand for GetDefinition { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::GotoDefinitionParams { - lsp2::GotoDefinitionParams { - text_document_position_params: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::GotoDefinitionParams { + lsp::GotoDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, @@ -425,7 +425,7 @@ impl LspCommand for GetDefinition { async fn response_from_lsp( self, - message: Option, + message: Option, project: Model, buffer: Model, server_id: LanguageServerId, @@ -438,7 +438,7 @@ impl LspCommand for GetDefinition { proto::GetDefinition { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version()), @@ -491,16 +491,16 @@ impl LspCommand for GetDefinition { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetTypeDefinition { type Response = Vec; - type LspRequest = lsp2::request::GotoTypeDefinition; + type LspRequest = lsp::request::GotoTypeDefinition; type ProtoRequest = proto::GetTypeDefinition; fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { match &capabilities.type_definition_provider { None => false, - Some(lsp2::TypeDefinitionProviderCapability::Simple(false)) => false, + Some(lsp::TypeDefinitionProviderCapability::Simple(false)) => false, _ => true, } } @@ -511,11 +511,11 @@ impl LspCommand for GetTypeDefinition { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::GotoTypeDefinitionParams { - lsp2::GotoTypeDefinitionParams { - text_document_position_params: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::GotoTypeDefinitionParams { + lsp::GotoTypeDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, @@ -526,7 +526,7 @@ impl LspCommand for GetTypeDefinition { async fn response_from_lsp( self, - message: Option, + message: Option, project: Model, buffer: Model, server_id: LanguageServerId, @@ -539,7 +539,7 @@ impl LspCommand for GetTypeDefinition { proto::GetTypeDefinition { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version()), @@ -670,7 +670,7 @@ async fn location_links_from_proto( } async fn location_links_from_lsp( - message: Option, + message: Option, project: Model, buffer: Model, server_id: LanguageServerId, @@ -683,15 +683,15 @@ async fn location_links_from_lsp( let mut unresolved_links = Vec::new(); match message { - lsp2::GotoDefinitionResponse::Scalar(loc) => { + lsp::GotoDefinitionResponse::Scalar(loc) => { unresolved_links.push((None, loc.uri, loc.range)); } - lsp2::GotoDefinitionResponse::Array(locs) => { + lsp::GotoDefinitionResponse::Array(locs) => { unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); } - lsp2::GotoDefinitionResponse::Link(links) => { + lsp::GotoDefinitionResponse::Link(links) => { unresolved_links.extend(links.into_iter().map(|l| { ( l.origin_selection_range, @@ -783,10 +783,10 @@ fn location_links_to_proto( .collect() } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetReferences { type Response = Vec; - type LspRequest = lsp2::request::References; + type LspRequest = lsp::request::References; type ProtoRequest = proto::GetReferences; fn to_lsp( @@ -795,17 +795,17 @@ impl LspCommand for GetReferences { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::ReferenceParams { - lsp2::ReferenceParams { - text_document_position: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::ReferenceParams { + lsp::ReferenceParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), - context: lsp2::ReferenceContext { + context: lsp::ReferenceContext { include_declaration: true, }, } @@ -813,7 +813,7 @@ impl LspCommand for GetReferences { async fn response_from_lsp( self, - locations: Option>, + locations: Option>, project: Model, buffer: Model, server_id: LanguageServerId, @@ -859,7 +859,7 @@ impl LspCommand for GetReferences { proto::GetReferences { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version()), @@ -945,10 +945,10 @@ impl LspCommand for GetReferences { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetDocumentHighlights { type Response = Vec; - type LspRequest = lsp2::request::DocumentHighlightRequest; + type LspRequest = lsp::request::DocumentHighlightRequest; type ProtoRequest = proto::GetDocumentHighlights; fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { @@ -961,11 +961,11 @@ impl LspCommand for GetDocumentHighlights { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::DocumentHighlightParams { - lsp2::DocumentHighlightParams { - text_document_position_params: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::DocumentHighlightParams { + lsp::DocumentHighlightParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, @@ -976,7 +976,7 @@ impl LspCommand for GetDocumentHighlights { async fn response_from_lsp( self, - lsp_highlights: Option>, + lsp_highlights: Option>, _: Model, buffer: Model, _: LanguageServerId, @@ -996,7 +996,7 @@ impl LspCommand for GetDocumentHighlights { range: buffer.anchor_after(start)..buffer.anchor_before(end), kind: lsp_highlight .kind - .unwrap_or(lsp2::DocumentHighlightKind::READ), + .unwrap_or(lsp::DocumentHighlightKind::READ), } }) .collect() @@ -1007,7 +1007,7 @@ impl LspCommand for GetDocumentHighlights { proto::GetDocumentHighlights { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version()), @@ -1096,10 +1096,10 @@ impl LspCommand for GetDocumentHighlights { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetHover { type Response = Option; - type LspRequest = lsp2::request::HoverRequest; + type LspRequest = lsp::request::HoverRequest; type ProtoRequest = proto::GetHover; fn to_lsp( @@ -1108,11 +1108,11 @@ impl LspCommand for GetHover { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::HoverParams { - lsp2::HoverParams { - text_document_position_params: lsp2::TextDocumentPositionParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::HoverParams { + lsp::HoverParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }, @@ -1122,7 +1122,7 @@ impl LspCommand for GetHover { async fn response_from_lsp( self, - message: Option, + message: Option, _: Model, buffer: Model, _: LanguageServerId, @@ -1144,15 +1144,13 @@ impl LspCommand for GetHover { ) })?; - fn hover_blocks_from_marked_string( - marked_string: lsp2::MarkedString, - ) -> Option { + fn hover_blocks_from_marked_string(marked_string: lsp::MarkedString) -> Option { let block = match marked_string { - lsp2::MarkedString::String(content) => HoverBlock { + lsp::MarkedString::String(content) => HoverBlock { text: content, kind: HoverBlockKind::Markdown, }, - lsp2::MarkedString::LanguageString(lsp2::LanguageString { language, value }) => { + lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { HoverBlock { text: value, kind: HoverBlockKind::Code { language }, @@ -1167,18 +1165,18 @@ impl LspCommand for GetHover { } let contents = match hover.contents { - lsp2::HoverContents::Scalar(marked_string) => { + lsp::HoverContents::Scalar(marked_string) => { hover_blocks_from_marked_string(marked_string) .into_iter() .collect() } - lsp2::HoverContents::Array(marked_strings) => marked_strings + lsp::HoverContents::Array(marked_strings) => marked_strings .into_iter() .filter_map(hover_blocks_from_marked_string) .collect(), - lsp2::HoverContents::Markup(markup_content) => vec![HoverBlock { + lsp::HoverContents::Markup(markup_content) => vec![HoverBlock { text: markup_content.value, - kind: if markup_content.kind == lsp2::MarkupKind::Markdown { + kind: if markup_content.kind == lsp::MarkupKind::Markdown { HoverBlockKind::Markdown } else { HoverBlockKind::PlainText @@ -1197,7 +1195,7 @@ impl LspCommand for GetHover { proto::GetHover { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), version: serialize_version(&buffer.version), @@ -1234,8 +1232,8 @@ impl LspCommand for GetHover { if let Some(response) = response { let (start, end) = if let Some(range) = response.range { ( - Some(language2::proto::serialize_anchor(&range.start)), - Some(language2::proto::serialize_anchor(&range.end)), + Some(language::proto::serialize_anchor(&range.start)), + Some(language::proto::serialize_anchor(&range.end)), ) } else { (None, None) @@ -1296,8 +1294,8 @@ impl LspCommand for GetHover { let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?; let range = if let (Some(start), Some(end)) = (message.start, message.end) { - language2::proto::deserialize_anchor(start) - .and_then(|start| language2::proto::deserialize_anchor(end).map(|end| start..end)) + language::proto::deserialize_anchor(start) + .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) } else { None }; @@ -1314,10 +1312,10 @@ impl LspCommand for GetHover { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetCompletions { type Response = Vec; - type LspRequest = lsp2::request::Completion; + type LspRequest = lsp::request::Completion; type ProtoRequest = proto::GetCompletions; fn to_lsp( @@ -1326,10 +1324,10 @@ impl LspCommand for GetCompletions { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::CompletionParams { - lsp2::CompletionParams { - text_document_position: lsp2::TextDocumentPositionParams::new( - lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()), + ) -> lsp::CompletionParams { + lsp::CompletionParams { + text_document_position: lsp::TextDocumentPositionParams::new( + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()), point_to_lsp(self.position), ), context: Default::default(), @@ -1340,7 +1338,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, - completions: Option, + completions: Option, _: Model, buffer: Model, server_id: LanguageServerId, @@ -1349,9 +1347,9 @@ impl LspCommand for GetCompletions { let mut response_list = None; let completions = if let Some(completions) = completions { match completions { - lsp2::CompletionResponse::Array(completions) => completions, + lsp::CompletionResponse::Array(completions) => completions, - lsp2::CompletionResponse::List(mut list) => { + lsp::CompletionResponse::List(mut list) => { let items = std::mem::take(&mut list.items); response_list = Some(list); items @@ -1373,7 +1371,7 @@ impl LspCommand for GetCompletions { let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { // If the language server provides a range to overwrite, then // check that the range is valid. - Some(lsp2::CompletionTextEdit::Edit(edit)) => { + Some(lsp::CompletionTextEdit::Edit(edit)) => { let range = range_from_lsp(edit.range); let start = snapshot.clip_point_utf16(range.start, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left); @@ -1439,7 +1437,7 @@ impl LspCommand for GetCompletions { (range, text) } - Some(lsp2::CompletionTextEdit::InsertAndReplace(_)) => { + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; } @@ -1457,7 +1455,7 @@ impl LspCommand for GetCompletions { old_range, new_text, label: label.unwrap_or_else(|| { - language2::CodeLabel::plain( + language::CodeLabel::plain( lsp_completion.label.clone(), lsp_completion.filter_text.as_deref(), ) @@ -1477,7 +1475,7 @@ impl LspCommand for GetCompletions { proto::GetCompletions { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor(&anchor)), + position: Some(language::proto::serialize_anchor(&anchor)), version: serialize_version(&buffer.version()), } } @@ -1494,7 +1492,7 @@ impl LspCommand for GetCompletions { .await?; let position = message .position - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .map(|p| { buffer.update(&mut cx, |buffer, _| { buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) @@ -1514,7 +1512,7 @@ impl LspCommand for GetCompletions { proto::GetCompletionsResponse { completions: completions .iter() - .map(language2::proto::serialize_completion) + .map(language::proto::serialize_completion) .collect(), version: serialize_version(&buffer_version), } @@ -1535,7 +1533,7 @@ impl LspCommand for GetCompletions { let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?; let completions = message.completions.into_iter().map(|completion| { - language2::proto::deserialize_completion(completion, language.clone()) + language::proto::deserialize_completion(completion, language.clone()) }); future::try_join_all(completions).await } @@ -1545,16 +1543,16 @@ impl LspCommand for GetCompletions { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for GetCodeActions { type Response = Vec; - type LspRequest = lsp2::request::CodeActionRequest; + type LspRequest = lsp::request::CodeActionRequest; type ProtoRequest = proto::GetCodeActions; fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { match &capabilities.code_action_provider { None => false, - Some(lsp2::CodeActionProviderCapability::Simple(false)) => false, + Some(lsp::CodeActionProviderCapability::Simple(false)) => false, _ => true, } } @@ -1565,30 +1563,30 @@ impl LspCommand for GetCodeActions { buffer: &Buffer, language_server: &Arc, _: &AppContext, - ) -> lsp2::CodeActionParams { + ) -> lsp::CodeActionParams { let relevant_diagnostics = buffer .snapshot() .diagnostics_in_range::<_, usize>(self.range.clone(), false) .map(|entry| entry.to_lsp_diagnostic_stub()) .collect(); - lsp2::CodeActionParams { - text_document: lsp2::TextDocumentIdentifier::new( - lsp2::Url::from_file_path(path).unwrap(), + lsp::CodeActionParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(path).unwrap(), ), range: range_to_lsp(self.range.to_point_utf16(buffer)), work_done_progress_params: Default::default(), partial_result_params: Default::default(), - context: lsp2::CodeActionContext { + context: lsp::CodeActionContext { diagnostics: relevant_diagnostics, only: language_server.code_action_kinds(), - ..lsp2::CodeActionContext::default() + ..lsp::CodeActionContext::default() }, } } async fn response_from_lsp( self, - actions: Option, + actions: Option, _: Model, _: Model, server_id: LanguageServerId, @@ -1598,7 +1596,7 @@ impl LspCommand for GetCodeActions { .unwrap_or_default() .into_iter() .filter_map(|entry| { - if let lsp2::CodeActionOrCommand::CodeAction(lsp_action) = entry { + if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry { Some(CodeAction { server_id, range: self.range.clone(), @@ -1615,8 +1613,8 @@ impl LspCommand for GetCodeActions { proto::GetCodeActions { project_id, buffer_id: buffer.remote_id(), - start: Some(language2::proto::serialize_anchor(&self.range.start)), - end: Some(language2::proto::serialize_anchor(&self.range.end)), + start: Some(language::proto::serialize_anchor(&self.range.start)), + end: Some(language::proto::serialize_anchor(&self.range.end)), version: serialize_version(&buffer.version()), } } @@ -1629,11 +1627,11 @@ impl LspCommand for GetCodeActions { ) -> Result { let start = message .start - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid start"))?; let end = message .end - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid end"))?; buffer .update(&mut cx, |buffer, _| { @@ -1654,7 +1652,7 @@ impl LspCommand for GetCodeActions { proto::GetCodeActionsResponse { actions: code_actions .iter() - .map(language2::proto::serialize_code_action) + .map(language::proto::serialize_code_action) .collect(), version: serialize_version(&buffer_version), } @@ -1675,7 +1673,7 @@ impl LspCommand for GetCodeActions { message .actions .into_iter() - .map(language2::proto::deserialize_code_action) + .map(language::proto::deserialize_code_action) .collect() } @@ -1684,13 +1682,13 @@ impl LspCommand for GetCodeActions { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; - type LspRequest = lsp2::request::OnTypeFormatting; + type LspRequest = lsp::request::OnTypeFormatting; type ProtoRequest = proto::OnTypeFormatting; - fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool { + fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { @@ -1712,10 +1710,10 @@ impl LspCommand for OnTypeFormatting { _: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::DocumentOnTypeFormattingParams { - lsp2::DocumentOnTypeFormattingParams { - text_document_position: lsp2::TextDocumentPositionParams::new( - lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()), + ) -> lsp::DocumentOnTypeFormattingParams { + lsp::DocumentOnTypeFormattingParams { + text_document_position: lsp::TextDocumentPositionParams::new( + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()), point_to_lsp(self.position), ), ch: self.trigger.clone(), @@ -1725,7 +1723,7 @@ impl LspCommand for OnTypeFormatting { async fn response_from_lsp( self, - message: Option>, + message: Option>, project: Model, buffer: Model, server_id: LanguageServerId, @@ -1753,7 +1751,7 @@ impl LspCommand for OnTypeFormatting { proto::OnTypeFormatting { project_id, buffer_id: buffer.remote_id(), - position: Some(language2::proto::serialize_anchor( + position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), trigger: self.trigger.clone(), @@ -1798,7 +1796,7 @@ impl LspCommand for OnTypeFormatting { ) -> proto::OnTypeFormattingResponse { proto::OnTypeFormattingResponse { transaction: response - .map(|transaction| language2::proto::serialize_transaction(&transaction)), + .map(|transaction| language::proto::serialize_transaction(&transaction)), } } @@ -1812,9 +1810,7 @@ impl LspCommand for OnTypeFormatting { let Some(transaction) = message.transaction else { return Ok(None); }; - Ok(Some(language2::proto::deserialize_transaction( - transaction, - )?)) + Ok(Some(language::proto::deserialize_transaction(transaction)?)) } fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 { @@ -1824,7 +1820,7 @@ impl LspCommand for OnTypeFormatting { impl InlayHints { pub async fn lsp_to_project_hint( - lsp_hint: lsp2::InlayHint, + lsp_hint: lsp::InlayHint, buffer_handle: &Model, server_id: LanguageServerId, resolve_state: ResolveState, @@ -1832,8 +1828,8 @@ impl InlayHints { cx: &mut AsyncAppContext, ) -> anyhow::Result { let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp2::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp2::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), _ => None, }); @@ -1861,12 +1857,12 @@ impl InlayHints { label, kind, tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp2::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp2::InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { InlayHintTooltip::MarkupContent(MarkupContent { kind: match markup_content.kind { - lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown, + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, }, value: markup_content.value, }) @@ -1877,25 +1873,25 @@ impl InlayHints { } async fn lsp_inlay_label_to_project( - lsp_label: lsp2::InlayHintLabel, + lsp_label: lsp::InlayHintLabel, server_id: LanguageServerId, ) -> anyhow::Result { let label = match lsp_label { - lsp2::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp2::InlayHintLabel::LabelParts(lsp_parts) => { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { let mut parts = Vec::with_capacity(lsp_parts.len()); for lsp_part in lsp_parts { parts.push(InlayHintLabelPart { value: lsp_part.value, tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { - lsp2::InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) => { InlayHintLabelPartTooltip::String(s) } - lsp2::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { InlayHintLabelPartTooltip::MarkupContent(MarkupContent { kind: match markup_content.kind { - lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown, + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, }, value: markup_content.value, }) @@ -1933,7 +1929,7 @@ impl InlayHints { lsp_resolve_state, }); proto::InlayHint { - position: Some(language2::proto::serialize_anchor(&response_hint.position)), + position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, label: Some(proto::InlayHintLabel { @@ -1992,7 +1988,7 @@ impl InlayHints { let resolve_state_data = resolve_state .lsp_resolve_state.as_ref() .map(|lsp_resolve_state| { - serde_json::from_str::>(&lsp_resolve_state.value) + serde_json::from_str::>(&lsp_resolve_state.value) .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) }) @@ -2015,7 +2011,7 @@ impl InlayHints { Ok(InlayHint { position: message_hint .position - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .context("invalid position")?, label: match message_hint .label @@ -2058,10 +2054,10 @@ impl InlayHints { { Some(((uri, range), server_id)) => Some(( LanguageServerId(server_id as usize), - lsp2::Location { - uri: lsp2::Url::parse(&uri) + lsp::Location { + uri: lsp::Url::parse(&uri) .context("invalid uri in hint part {part:?}")?, - range: lsp2::Range::new( + range: lsp::Range::new( point_to_lsp(PointUtf16::new( range.start.row, range.start.column, @@ -2107,22 +2103,22 @@ impl InlayHints { }) } - pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp2::InlayHint { - lsp2::InlayHint { + pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { + lsp::InlayHint { position: point_to_lsp(hint.position.to_point_utf16(snapshot)), kind: hint.kind.map(|kind| match kind { - InlayHintKind::Type => lsp2::InlayHintKind::TYPE, - InlayHintKind::Parameter => lsp2::InlayHintKind::PARAMETER, + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, }), text_edits: None, tooltip: hint.tooltip.and_then(|tooltip| { Some(match tooltip { - InlayHintTooltip::String(s) => lsp2::InlayHintTooltip::String(s), + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), InlayHintTooltip::MarkupContent(markup_content) => { - lsp2::InlayHintTooltip::MarkupContent(lsp2::MarkupContent { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { kind: match markup_content.kind { - HoverBlockKind::PlainText => lsp2::MarkupKind::PlainText, - HoverBlockKind::Markdown => lsp2::MarkupKind::Markdown, + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, HoverBlockKind::Code { .. } => return None, }, value: markup_content.value, @@ -2131,26 +2127,26 @@ impl InlayHints { }) }), label: match hint.label { - InlayHintLabel::String(s) => lsp2::InlayHintLabel::String(s), - InlayHintLabel::LabelParts(label_parts) => lsp2::InlayHintLabel::LabelParts( + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( label_parts .into_iter() - .map(|part| lsp2::InlayHintLabelPart { + .map(|part| lsp::InlayHintLabelPart { value: part.value, tooltip: part.tooltip.and_then(|tooltip| { Some(match tooltip { InlayHintLabelPartTooltip::String(s) => { - lsp2::InlayHintLabelPartTooltip::String(s) + lsp::InlayHintLabelPartTooltip::String(s) } InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - lsp2::InlayHintLabelPartTooltip::MarkupContent( - lsp2::MarkupContent { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { kind: match markup_content.kind { HoverBlockKind::PlainText => { - lsp2::MarkupKind::PlainText + lsp::MarkupKind::PlainText } HoverBlockKind::Markdown => { - lsp2::MarkupKind::Markdown + lsp::MarkupKind::Markdown } HoverBlockKind::Code { .. } => return None, }, @@ -2182,8 +2178,8 @@ impl InlayHints { .and_then(|options| match options { OneOf::Left(_is_supported) => None, OneOf::Right(capabilities) => match capabilities { - lsp2::InlayHintServerCapabilities::Options(o) => o.resolve_provider, - lsp2::InlayHintServerCapabilities::RegistrationOptions(o) => { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { o.inlay_hint_options.resolve_provider } }, @@ -2192,21 +2188,21 @@ impl InlayHints { } } -#[async_trait] +#[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; - type LspRequest = lsp2::InlayHintRequest; + type LspRequest = lsp::InlayHintRequest; type ProtoRequest = proto::InlayHints; - fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool { + fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false; }; match inlay_hint_provider { - lsp2::OneOf::Left(enabled) => *enabled, - lsp2::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { - lsp2::InlayHintServerCapabilities::Options(_) => true, - lsp2::InlayHintServerCapabilities::RegistrationOptions(_) => false, + lsp::OneOf::Left(enabled) => *enabled, + lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { + lsp::InlayHintServerCapabilities::Options(_) => true, + lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false, }, } } @@ -2217,10 +2213,10 @@ impl LspCommand for InlayHints { buffer: &Buffer, _: &Arc, _: &AppContext, - ) -> lsp2::InlayHintParams { - lsp2::InlayHintParams { - text_document: lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(path).unwrap(), + ) -> lsp::InlayHintParams { + lsp::InlayHintParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), }, range: range_to_lsp(self.range.to_point_utf16(buffer)), work_done_progress_params: Default::default(), @@ -2229,7 +2225,7 @@ impl LspCommand for InlayHints { async fn response_from_lsp( self, - message: Option>, + message: Option>, project: Model, buffer: Model, server_id: LanguageServerId, @@ -2278,8 +2274,8 @@ impl LspCommand for InlayHints { proto::InlayHints { project_id, buffer_id: buffer.remote_id(), - start: Some(language2::proto::serialize_anchor(&self.range.start)), - end: Some(language2::proto::serialize_anchor(&self.range.end)), + start: Some(language::proto::serialize_anchor(&self.range.start)), + end: Some(language::proto::serialize_anchor(&self.range.end)), version: serialize_version(&buffer.version()), } } @@ -2292,11 +2288,11 @@ impl LspCommand for InlayHints { ) -> Result { let start = message .start - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .context("invalid start")?; let end = message .end - .and_then(language2::proto::deserialize_anchor) + .and_then(language::proto::deserialize_anchor) .context("invalid end")?; buffer .update(&mut cx, |buffer, _| { diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 4f422bb0e2..5d7c976e77 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -11,10 +11,10 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context as _, Result}; -use client2::{proto, Client, Collaborator, TypedEnvelope, UserStore}; +use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; -use copilot2::Copilot; +use copilot::Copilot; use futures::{ channel::{ mpsc::{self, UnboundedReceiver}, @@ -25,12 +25,12 @@ use futures::{ AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; use globset::{Glob, GlobSet, GlobSetBuilder}; -use gpui2::{ - AnyModel, AppContext, AsyncAppContext, Context, Entity, EventEmitter, Executor, Model, - ModelContext, Task, WeakModel, +use gpui::{ + AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, Context, Entity, EventEmitter, + Model, ModelContext, Task, WeakModel, }; use itertools::Itertools; -use language2::{ +use language::{ language_settings::{ language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, }, @@ -46,7 +46,7 @@ use language2::{ ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; -use lsp2::{ +use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, }; @@ -54,12 +54,12 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::Mutex; use postage::watch; -use prettier2::{LocateStart, Prettier}; +use prettier::Prettier; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; use serde::Serialize; -use settings2::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; @@ -82,13 +82,16 @@ use std::{ use terminals::Terminals; use text::Anchor; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; -pub use fs2::*; +pub use fs::*; #[cfg(any(test, feature = "test-support"))] -pub use prettier2::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; +pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::*; const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; @@ -123,7 +126,7 @@ pub struct Project { language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, language_server_statuses: BTreeMap, last_workspace_edits_by_language_server: HashMap, - client: Arc, + client: Arc, next_entry_id: Arc, join_project_response_message_id: u32, next_diagnostic_group_id: usize, @@ -131,8 +134,8 @@ pub struct Project { fs: Arc, client_state: Option, collaborators: HashMap, - client_subscriptions: Vec, - _subscriptions: Vec, + client_subscriptions: Vec, + _subscriptions: Vec, next_buffer_id: u64, opened_buffer: (watch::Sender<()>, watch::Receiver<()>), shared_buffers: HashMap>, @@ -158,21 +161,19 @@ pub struct Project { _maintain_buffer_languages: Task<()>, _maintain_workspace_config: Task>, terminals: Terminals, - copilot_lsp_subscription: Option, - copilot_log_subscription: Option, + copilot_lsp_subscription: Option, + copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, node: Option>, - #[cfg(not(any(test, feature = "test-support")))] default_prettier: Option, - prettier_instances: HashMap< - (Option, PathBuf), - Shared, Arc>>>, - >, + prettiers_per_worktree: HashMap>>, + prettier_instances: HashMap, Arc>>>>, } -#[cfg(not(any(test, feature = "test-support")))] struct DefaultPrettier { - installation_process: Option>>, + instance: Option, Arc>>>>, + installation_process: Option>>>>, + #[cfg(not(any(test, feature = "test-support")))] installed_plugins: HashSet<&'static str>, } @@ -207,7 +208,7 @@ impl DelayedDebounced { let previous_task = self.task.take(); self.task = Some(cx.spawn(move |project, mut cx| async move { - let mut timer = cx.executor().timer(delay).fuse(); + let mut timer = cx.background_executor().timer(delay).fuse(); if let Some(previous_task) = previous_task { previous_task.await; } @@ -352,12 +353,12 @@ pub struct DiagnosticSummary { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Location { pub buffer: Model, - pub range: Range, + pub range: Range, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub position: language2::Anchor, + pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, pub padding_left: bool, @@ -369,7 +370,7 @@ pub struct InlayHint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, - CanResolve(LanguageServerId, Option), + CanResolve(LanguageServerId, Option), Resolving, } @@ -392,7 +393,7 @@ pub enum InlayHintLabel { pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, - pub location: Option<(LanguageServerId, lsp2::Location)>, + pub location: Option<(LanguageServerId, lsp::Location)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -421,7 +422,7 @@ pub struct LocationLink { #[derive(Debug)] pub struct DocumentHighlight { - pub range: Range, + pub range: Range, pub kind: DocumentHighlightKind, } @@ -432,7 +433,7 @@ pub struct Symbol { pub path: ProjectPath, pub label: CodeLabel, pub name: String, - pub kind: lsp2::SymbolKind, + pub kind: lsp::SymbolKind, pub range: Range>, pub signature: [u8; 32], } @@ -453,7 +454,7 @@ pub enum HoverBlockKind { #[derive(Debug)] pub struct Hover { pub contents: Vec, - pub range: Option>, + pub range: Option>, pub language: Option>, } @@ -464,7 +465,7 @@ impl Hover { } #[derive(Default)] -pub struct ProjectTransaction(pub HashMap, language2::Transaction>); +pub struct ProjectTransaction(pub HashMap, language::Transaction>); impl DiagnosticSummary { fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { @@ -686,8 +687,8 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), node: Some(node), - #[cfg(not(any(test, feature = "test-support")))] default_prettier: None, + prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), } }) @@ -789,8 +790,8 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), node: None, - #[cfg(not(any(test, feature = "test-support")))] default_prettier: None, + prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; for worktree in worktrees { @@ -855,39 +856,39 @@ impl Project { } } - // #[cfg(any(test, feature = "test-support"))] - // pub async fn test( - // fs: Arc, - // root_paths: impl IntoIterator, - // cx: &mut gpui::TestAppContext, - // ) -> Handle { - // let mut languages = LanguageRegistry::test(); - // languages.set_executor(cx.background()); - // let http_client = util::http::FakeHttpClient::with_404_response(); - // let client = cx.update(|cx| client2::Client::new(http_client.clone(), cx)); - // let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - // let project = cx.update(|cx| { - // Project::local( - // client, - // node_runtime::FakeNodeRuntime::new(), - // user_store, - // Arc::new(languages), - // fs, - // cx, - // ) - // }); - // for path in root_paths { - // let (tree, _) = project - // .update(cx, |project, cx| { - // project.find_or_create_local_worktree(path, true, cx) - // }) - // .await - // .unwrap(); - // tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) - // .await; - // } - // project - // } + #[cfg(any(test, feature = "test-support"))] + pub async fn test( + fs: Arc, + root_paths: impl IntoIterator, + cx: &mut gpui::TestAppContext, + ) -> Model { + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.executor().clone()); + let http_client = util::http::FakeHttpClient::with_404_response(); + let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let project = cx.update(|cx| { + Project::local( + client, + node_runtime::FakeNodeRuntime::new(), + user_store, + Arc::new(languages), + fs, + cx, + ) + }); + for path in root_paths { + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + } + project + } fn on_settings_changed(&mut self, cx: &mut ModelContext) { let mut language_servers_to_start = Vec::new(); @@ -963,8 +964,7 @@ impl Project { } for (worktree, language, settings) in language_formatters_to_check { - self.install_default_formatters(worktree, &language, &settings, cx) - .detach_and_log_err(cx); + self.install_default_formatters(worktree, &language, &settings, cx); } // Start all the newly-enabled language servers. @@ -1453,7 +1453,7 @@ impl Project { }; if client.send(initial_state).log_err().is_some() { let client = client.clone(); - cx.executor() + cx.background_executor() .spawn(async move { let mut chunks = split_operations(operations).peekable(); while let Some(chunk) = chunks.next() { @@ -1669,10 +1669,8 @@ impl Project { } let id = post_inc(&mut self.next_buffer_id); let buffer = cx.build_model(|cx| { - Buffer::new(self.replica_id(), id, text).with_language( - language.unwrap_or_else(|| language2::PLAIN_TEXT.clone()), - cx, - ) + Buffer::new(self.replica_id(), id, text) + .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); self.register_buffer(&buffer, cx)?; Ok(buffer) @@ -1758,7 +1756,7 @@ impl Project { } }; - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { wait_for_loading_buffer(loading_watch) .await .map_err(|error| anyhow!("{}", error)) @@ -1812,7 +1810,7 @@ impl Project { /// LanguageServerName is owned, because it is inserted into a map pub fn open_local_buffer_via_lsp( &mut self, - abs_path: lsp2::Url, + abs_path: lsp::Url, language_server_id: LanguageServerId, language_server_name: LanguageServerName, cx: &mut ModelContext, @@ -2019,13 +2017,13 @@ impl Project { cx.observe_release(buffer, |this, buffer, cx| { if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { - let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap(); + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); for server in this.language_servers_for_buffer(buffer, cx) { server .1 - .notify::( - lsp2::DidCloseTextDocumentParams { - text_document: lsp2::TextDocumentIdentifier::new(uri.clone()), + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(uri.clone()), }, ) .log_err(); @@ -2053,7 +2051,7 @@ impl Project { } let abs_path = file.abs_path(cx); - let uri = lsp2::Url::from_file_path(&abs_path) + let uri = lsp::Url::from_file_path(&abs_path) .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}")); let initial_snapshot = buffer.text_snapshot(); let language = buffer.language().cloned(); @@ -2086,9 +2084,9 @@ impl Project { }; server - .notify::( - lsp2::DidOpenTextDocumentParams { - text_document: lsp2::TextDocumentItem::new( + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( uri.clone(), language_id.unwrap_or_default(), 0, @@ -2145,12 +2143,12 @@ impl Project { } self.buffer_snapshots.remove(&buffer.remote_id()); - let file_url = lsp2::Url::from_file_path(old_path).unwrap(); + let file_url = lsp::Url::from_file_path(old_path).unwrap(); for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { language_server - .notify::( - lsp2::DidCloseTextDocumentParams { - text_document: lsp2::TextDocumentIdentifier::new(file_url.clone()), + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(file_url.clone()), }, ) .log_err(); @@ -2294,7 +2292,7 @@ impl Project { self.buffer_ordered_messages_tx .unbounded_send(BufferOrderedMessage::Operation { buffer_id: buffer.read(cx).remote_id(), - operation: language2::proto::serialize_operation(operation), + operation: language::proto::serialize_operation(operation), }) .ok(); } @@ -2303,7 +2301,7 @@ impl Project { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); - let uri = lsp2::Url::from_file_path(abs_path).unwrap(); + let uri = lsp::Url::from_file_path(abs_path).unwrap(); let next_snapshot = buffer.text_snapshot(); let language_servers: Vec<_> = self @@ -2331,8 +2329,8 @@ impl Project { let new_text = next_snapshot .text_for_range(edit.new.start.1..edit.new.end.1) .collect(); - lsp2::TextDocumentContentChangeEvent { - range: Some(lsp2::Range::new( + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( point_to_lsp(edit_start), point_to_lsp(edit_end), )), @@ -2348,19 +2346,19 @@ impl Project { .text_document_sync .as_ref() .and_then(|sync| match sync { - lsp2::TextDocumentSyncCapability::Kind(kind) => Some(*kind), - lsp2::TextDocumentSyncCapability::Options(options) => options.change, + lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), + lsp::TextDocumentSyncCapability::Options(options) => options.change, }); let content_changes: Vec<_> = match document_sync_kind { - Some(lsp2::TextDocumentSyncKind::FULL) => { - vec![lsp2::TextDocumentContentChangeEvent { + Some(lsp::TextDocumentSyncKind::FULL) => { + vec![lsp::TextDocumentContentChangeEvent { range: None, range_length: None, text: next_snapshot.text(), }] } - Some(lsp2::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), + Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), _ => { #[cfg(any(test, feature = "test-support"))] { @@ -2382,9 +2380,9 @@ impl Project { }); language_server - .notify::( - lsp2::DidChangeTextDocumentParams { - text_document: lsp2::VersionedTextDocumentIdentifier::new( + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( uri.clone(), next_version, ), @@ -2399,16 +2397,16 @@ impl Project { let file = File::from_dyn(buffer.read(cx).file())?; let worktree_id = file.worktree_id(cx); let abs_path = file.as_local()?.abs_path(cx); - let text_document = lsp2::TextDocumentIdentifier { - uri: lsp2::Url::from_file_path(abs_path).unwrap(), + let text_document = lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(abs_path).unwrap(), }; for (_, _, server) in self.language_servers_for_worktree(worktree_id) { let text = include_text(server.as_ref()).then(|| buffer.read(cx).text()); server - .notify::( - lsp2::DidSaveTextDocumentParams { + .notify::( + lsp::DidSaveTextDocumentParams { text_document: text_document.clone(), text, }, @@ -2436,7 +2434,7 @@ impl Project { Duration::from_secs(1); let task = cx.spawn(move |this, mut cx| async move { - cx.executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; + cx.background_executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.disk_based_diagnostics_finished( @@ -2621,7 +2619,7 @@ impl Project { if let Some(handle) = buffer.upgrade() { let buffer = &handle.read(cx); if buffer.language().is_none() - || buffer.language() == Some(&*language2::PLAIN_TEXT) + || buffer.language() == Some(&*language::PLAIN_TEXT) { plain_text_buffers.push(handle); } else if buffer.contains_unknown_injections() { @@ -2671,8 +2669,8 @@ impl Project { let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await; server - .notify::( - lsp2::DidChangeConfigurationParams { + .notify::( + lsp::DidChangeConfigurationParams { settings: workspace_config.clone(), }, ) @@ -2722,20 +2720,7 @@ impl Project { let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); - let task_buffer = buffer.clone(); - let prettier_installation_task = - self.install_default_formatters(worktree, &new_language, &settings, cx); - cx.spawn(move |project, mut cx| async move { - prettier_installation_task.await?; - let _ = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(&task_buffer, cx) - })? - .await; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - + self.install_default_formatters(worktree, &new_language, &settings, cx); if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { @@ -2992,7 +2977,7 @@ impl Project { let language_server = pending_server.task.await?; language_server - .on_notification::({ + .on_notification::({ let adapter = adapter.clone(); let this = this.clone(); move |mut params, mut cx| { @@ -3015,7 +3000,7 @@ impl Project { .detach(); language_server - .on_request::({ + .on_request::({ let adapter = adapter.clone(); move |params, cx| { let adapter = adapter.clone(); @@ -3045,7 +3030,7 @@ impl Project { // avoid stalling any language server like `gopls` which waits for a response // to these requests when initializing. language_server - .on_request::({ + .on_request::({ let this = this.clone(); move |params, mut cx| { let this = this.clone(); @@ -3053,7 +3038,7 @@ impl Project { this.update(&mut cx, |this, _| { if let Some(status) = this.language_server_statuses.get_mut(&server_id) { - if let lsp2::NumberOrString::String(token) = params.token { + if let lsp::NumberOrString::String(token) = params.token { status.progress_tokens.insert(token); } } @@ -3066,7 +3051,7 @@ impl Project { .detach(); language_server - .on_request::({ + .on_request::({ let this = this.clone(); move |params, mut cx| { let this = this.clone(); @@ -3090,7 +3075,7 @@ impl Project { .detach(); language_server - .on_request::({ + .on_request::({ let adapter = adapter.clone(); let this = this.clone(); move |params, cx| { @@ -3106,7 +3091,7 @@ impl Project { .detach(); language_server - .on_request::({ + .on_request::({ let this = this.clone(); move |(), mut cx| { let this = this.clone(); @@ -3128,7 +3113,7 @@ impl Project { adapter.disk_based_diagnostics_progress_token.clone(); language_server - .on_notification::(move |params, mut cx| { + .on_notification::(move |params, mut cx| { if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.on_lsp_progress( @@ -3146,8 +3131,8 @@ impl Project { let language_server = language_server.initialize(initialization_options).await?; language_server - .notify::( - lsp2::DidChangeConfigurationParams { + .notify::( + lsp::DidChangeConfigurationParams { settings: workspace_config, }, ) @@ -3250,10 +3235,10 @@ impl Project { let snapshot = versions.last().unwrap(); let version = snapshot.version; let initial_snapshot = &snapshot.snapshot; - let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap(); - language_server.notify::( - lsp2::DidOpenTextDocumentParams { - text_document: lsp2::TextDocumentItem::new( + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + language_server.notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( uri, adapter .language_ids @@ -3477,7 +3462,7 @@ impl Project { }); const PROCESS_TIMEOUT: Duration = Duration::from_secs(5); - let mut timeout = cx.executor().timer(PROCESS_TIMEOUT).fuse(); + let mut timeout = cx.background_executor().timer(PROCESS_TIMEOUT).fuse(); let mut errored = false; if let Some(mut process) = process { @@ -3516,19 +3501,19 @@ impl Project { fn on_lsp_progress( &mut self, - progress: lsp2::ProgressParams, + progress: lsp::ProgressParams, language_server_id: LanguageServerId, disk_based_diagnostics_progress_token: Option, cx: &mut ModelContext, ) { let token = match progress.token { - lsp2::NumberOrString::String(token) => token, - lsp2::NumberOrString::Number(token) => { + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { log::info!("skipping numeric progress token {}", token); return; } }; - let lsp2::ProgressParamsValue::WorkDone(progress) = progress.value; + let lsp::ProgressParamsValue::WorkDone(progress) = progress.value; let language_server_status = if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { status @@ -3547,7 +3532,7 @@ impl Project { }); match progress { - lsp2::WorkDoneProgress::Begin(report) => { + lsp::WorkDoneProgress::Begin(report) => { if is_disk_based_diagnostics_progress { language_server_status.has_pending_diagnostic_updates = true; self.disk_based_diagnostics_started(language_server_id, cx); @@ -3582,7 +3567,7 @@ impl Project { .ok(); } } - lsp2::WorkDoneProgress::Report(report) => { + lsp::WorkDoneProgress::Report(report) => { if !is_disk_based_diagnostics_progress { self.on_lsp_work_progress( language_server_id, @@ -3608,7 +3593,7 @@ impl Project { .ok(); } } - lsp2::WorkDoneProgress::End(_) => { + lsp::WorkDoneProgress::End(_) => { language_server_status.progress_tokens.remove(&token); if is_disk_based_diagnostics_progress { @@ -3707,15 +3692,15 @@ impl Project { let glob_is_inside_worktree = worktree.update(cx, |tree, _| { if let Some(abs_path) = tree.abs_path().to_str() { let relative_glob_pattern = match &watcher.glob_pattern { - lsp2::GlobPattern::String(s) => s + lsp::GlobPattern::String(s) => s .strip_prefix(abs_path) .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)), - lsp2::GlobPattern::Relative(rp) => { + lsp::GlobPattern::Relative(rp) => { let base_uri = match &rp.base_uri { - lsp2::OneOf::Left(workspace_folder) => { + lsp::OneOf::Left(workspace_folder) => { &workspace_folder.uri } - lsp2::OneOf::Right(base_uri) => base_uri, + lsp::OneOf::Right(base_uri) => base_uri, }; base_uri.to_file_path().ok().and_then(|file_path| { (file_path.to_str() == Some(abs_path)) @@ -3760,11 +3745,11 @@ impl Project { async fn on_lsp_workspace_edit( this: WeakModel, - params: lsp2::ApplyWorkspaceEditParams, + params: lsp::ApplyWorkspaceEditParams, server_id: LanguageServerId, adapter: Arc, mut cx: AsyncAppContext, - ) -> Result { + ) -> Result { let this = this .upgrade() .ok_or_else(|| anyhow!("project project closed"))?; @@ -3787,7 +3772,7 @@ impl Project { .insert(server_id, transaction); } })?; - Ok(lsp2::ApplyWorkspaceEditResponse { + Ok(lsp::ApplyWorkspaceEditResponse { applied: true, failed_change: None, failure_reason: None, @@ -3803,7 +3788,7 @@ impl Project { pub fn update_diagnostics( &mut self, language_server_id: LanguageServerId, - mut params: lsp2::PublishDiagnosticsParams, + mut params: lsp::PublishDiagnosticsParams, disk_based_sources: &[String], cx: &mut ModelContext, ) -> Result<()> { @@ -3822,8 +3807,8 @@ impl Project { for diagnostic in ¶ms.diagnostics { let source = diagnostic.source.as_ref(); let code = diagnostic.code.as_ref().map(|code| match code { - lsp2::NumberOrString::Number(code) => code.to_string(), - lsp2::NumberOrString::String(code) => code.clone(), + lsp::NumberOrString::Number(code) => code.to_string(), + lsp::NumberOrString::String(code) => code.clone(), }); let range = range_from_lsp(diagnostic.range); let is_supporting = diagnostic @@ -4098,7 +4083,7 @@ impl Project { } pub fn format( - &self, + &mut self, buffers: HashSet>, push_to_history: bool, trigger: FormatTrigger, @@ -4118,10 +4103,10 @@ impl Project { }) .collect::>(); - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |project, mut cx| async move { // Do not allow multiple concurrent formatting requests for the // same buffer. - this.update(&mut cx, |this, cx| { + project.update(&mut cx, |this, cx| { buffers_with_paths_and_servers.retain(|(buffer, _, _)| { this.buffers_being_formatted .insert(buffer.read(cx).remote_id()) @@ -4129,7 +4114,7 @@ impl Project { })?; let _cleanup = defer({ - let this = this.clone(); + let this = project.clone(); let mut cx = cx.clone(); let buffers = &buffers_with_paths_and_servers; move || { @@ -4197,7 +4182,7 @@ impl Project { { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( - &this, + &project, &buffer, buffer_abs_path, &language_server, @@ -4232,7 +4217,7 @@ impl Project { } } (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { - if let Some(prettier_task) = this + if let Some((prettier_path, prettier_task)) = project .update(&mut cx, |project, cx| { project.prettier_instance_for_buffer(buffer, cx) })?.await { @@ -4249,16 +4234,35 @@ impl Project { .context("formatting via prettier")?, )); } - Err(e) => anyhow::bail!( - "Failed to create prettier instance for buffer during autoformatting: {e:#}" - ), + Err(e) => { + project.update(&mut cx, |project, _| { + match &prettier_path { + Some(prettier_path) => { + project.prettier_instances.remove(prettier_path); + }, + None => { + if let Some(default_prettier) = project.default_prettier.as_mut() { + default_prettier.instance = None; + } + }, + } + })?; + match &prettier_path { + Some(prettier_path) => { + log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); + }, + None => { + log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); + }, + } + } } } else if let Some((language_server, buffer_abs_path)) = language_server.as_ref().zip(buffer_abs_path.as_ref()) { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( - &this, + &project, &buffer, buffer_abs_path, &language_server, @@ -4271,7 +4275,7 @@ impl Project { } } (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { - if let Some(prettier_task) = this + if let Some((prettier_path, prettier_task)) = project .update(&mut cx, |project, cx| { project.prettier_instance_for_buffer(buffer, cx) })?.await { @@ -4288,9 +4292,28 @@ impl Project { .context("formatting via prettier")?, )); } - Err(e) => anyhow::bail!( - "Failed to create prettier instance for buffer during formatting: {e:#}" - ), + Err(e) => { + project.update(&mut cx, |project, _| { + match &prettier_path { + Some(prettier_path) => { + project.prettier_instances.remove(prettier_path); + }, + None => { + if let Some(default_prettier) = project.default_prettier.as_mut() { + default_prettier.instance = None; + } + }, + } + })?; + match &prettier_path { + Some(prettier_path) => { + log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); + }, + None => { + log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); + }, + } + } } } } @@ -4378,9 +4401,9 @@ impl Project { tab_size: NonZeroU32, cx: &mut AsyncAppContext, ) -> Result, String)>> { - let uri = lsp2::Url::from_file_path(abs_path) + let uri = lsp::Url::from_file_path(abs_path) .map_err(|_| anyhow!("failed to convert abs path to uri"))?; - let text_document = lsp2::TextDocumentIdentifier::new(uri); + let text_document = lsp::TextDocumentIdentifier::new(uri); let capabilities = &language_server.capabilities(); let formatting_provider = capabilities.document_formatting_provider.as_ref(); @@ -4388,20 +4411,20 @@ impl Project { let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { language_server - .request::(lsp2::DocumentFormattingParams { + .request::(lsp::DocumentFormattingParams { text_document, options: lsp_command::lsp_formatting_options(tab_size.get()), work_done_progress_params: Default::default(), }) .await? } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let buffer_start = lsp2::Position::new(0, 0); + let buffer_start = lsp::Position::new(0, 0); let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?; language_server - .request::(lsp2::DocumentRangeFormattingParams { + .request::(lsp::DocumentRangeFormattingParams { text_document, - range: lsp2::Range::new(buffer_start, buffer_end), + range: lsp::Range::new(buffer_start, buffer_end), options: lsp_command::lsp_formatting_options(tab_size.get()), work_done_progress_params: Default::default(), }) @@ -4564,8 +4587,8 @@ impl Project { requests.push( server - .request::( - lsp2::WorkspaceSymbolParams { + .request::( + lsp::WorkspaceSymbolParams { query: query.to_string(), ..Default::default() }, @@ -4573,12 +4596,12 @@ impl Project { .log_err() .map(move |response| { let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { - lsp2::WorkspaceSymbolResponse::Flat(flat_responses) => { + lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { flat_responses.into_iter().map(|lsp_symbol| { (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) }).collect::>() } - lsp2::WorkspaceSymbolResponse::Nested(nested_responses) => { + lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { nested_responses.into_iter().filter_map(|lsp_symbol| { let location = match lsp_symbol.location { OneOf::Left(location) => location, @@ -4728,7 +4751,7 @@ impl Project { return Task::ready(Err(anyhow!("worktree not found for symbol"))); }; let symbol_abs_path = worktree_abs_path.join(&symbol.path.path); - let symbol_uri = if let Ok(uri) = lsp2::Url::from_file_path(symbol_abs_path) { + let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) { uri } else { return Task::ready(Err(anyhow!("invalid symbol path"))); @@ -4852,7 +4875,7 @@ impl Project { .unwrap_or(false); let additional_text_edits = if can_resolve { lang_server - .request::(completion.lsp_completion) + .request::(completion.lsp_completion) .await? .additional_text_edits } else { @@ -4911,12 +4934,12 @@ impl Project { .request(proto::ApplyCompletionAdditionalEdits { project_id, buffer_id, - completion: Some(language2::proto::serialize_completion(&completion)), + completion: Some(language::proto::serialize_completion(&completion)), }) .await?; if let Some(transaction) = response.transaction { - let transaction = language2::proto::deserialize_transaction(transaction)?; + let transaction = language::proto::deserialize_transaction(transaction)?; buffer_handle .update(&mut cx, |buffer, _| { buffer.wait_for_edits(transaction.edit_ids.iter().copied()) @@ -4981,7 +5004,7 @@ impl Project { { *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); action.lsp_action = lang_server - .request::(action.lsp_action) + .request::(action.lsp_action) .await?; } else { let actions = this @@ -5017,7 +5040,7 @@ impl Project { })?; let result = lang_server - .request::(lsp2::ExecuteCommandParams { + .request::(lsp::ExecuteCommandParams { command: command.command, arguments: command.arguments.unwrap_or_default(), ..Default::default() @@ -5043,7 +5066,7 @@ impl Project { let request = proto::ApplyCodeAction { project_id, buffer_id: buffer_handle.read(cx).remote_id(), - action: Some(language2::proto::serialize_code_action(&action)), + action: Some(language::proto::serialize_code_action(&action)), }; cx.spawn(move |this, mut cx| async move { let response = client @@ -5115,7 +5138,7 @@ impl Project { .request(request) .await? .transaction - .map(language2::proto::deserialize_transaction) + .map(language::proto::deserialize_transaction) .transpose() }) } else { @@ -5126,7 +5149,7 @@ impl Project { async fn deserialize_edits( this: Model, buffer_to_edit: Model, - edits: Vec, + edits: Vec, push_to_history: bool, _: Arc, language_server: Arc, @@ -5167,7 +5190,7 @@ impl Project { async fn deserialize_workspace_edit( this: Model, - edit: lsp2::WorkspaceEdit, + edit: lsp::WorkspaceEdit, push_to_history: bool, lsp_adapter: Arc, language_server: Arc, @@ -5177,15 +5200,15 @@ impl Project { let mut operations = Vec::new(); if let Some(document_changes) = edit.document_changes { match document_changes { - lsp2::DocumentChanges::Edits(edits) => { - operations.extend(edits.into_iter().map(lsp2::DocumentChangeOperation::Edit)) + lsp::DocumentChanges::Edits(edits) => { + operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) } - lsp2::DocumentChanges::Operations(ops) => operations = ops, + lsp::DocumentChanges::Operations(ops) => operations = ops, } } else if let Some(changes) = edit.changes { operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp2::DocumentChangeOperation::Edit(lsp2::TextDocumentEdit { - text_document: lsp2::OptionalVersionedTextDocumentIdentifier { + lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { + text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri, version: None, }, @@ -5197,7 +5220,7 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for operation in operations { match operation { - lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Create(op)) => { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { let abs_path = op .uri .to_file_path() @@ -5212,7 +5235,7 @@ impl Project { fs.create_file( &abs_path, op.options - .map(|options| fs2::CreateOptions { + .map(|options| fs::CreateOptions { overwrite: options.overwrite.unwrap_or(false), ignore_if_exists: options.ignore_if_exists.unwrap_or(false), }) @@ -5222,7 +5245,7 @@ impl Project { } } - lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Rename(op)) => { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { let source_abs_path = op .old_uri .to_file_path() @@ -5235,7 +5258,7 @@ impl Project { &source_abs_path, &target_abs_path, op.options - .map(|options| fs2::RenameOptions { + .map(|options| fs::RenameOptions { overwrite: options.overwrite.unwrap_or(false), ignore_if_exists: options.ignore_if_exists.unwrap_or(false), }) @@ -5244,14 +5267,14 @@ impl Project { .await?; } - lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Delete(op)) => { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { let abs_path = op .uri .to_file_path() .map_err(|_| anyhow!("can't convert URI to path"))?; let options = op .options - .map(|options| fs2::RemoveOptions { + .map(|options| fs::RemoveOptions { recursive: options.recursive.unwrap_or(false), ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), }) @@ -5263,7 +5286,7 @@ impl Project { } } - lsp2::DocumentChangeOperation::Edit(op) => { + lsp::DocumentChangeOperation::Edit(op) => { let buffer_to_edit = this .update(cx, |this, cx| { this.open_local_buffer_via_lsp( @@ -5466,7 +5489,7 @@ impl Project { let buffer_snapshot = buffer.snapshot(); cx.spawn(move |_, mut cx| async move { - let resolve_task = lang_server.request::( + let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), ); let resolved_hint = resolve_task @@ -5593,7 +5616,7 @@ impl Project { }) .collect::>(); - let background = cx.executor().clone(); + let background = cx.background_executor().clone(); let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); if path_count == 0 { let (_, rx) = smol::channel::bounded(1024); @@ -5616,11 +5639,11 @@ impl Project { } }) .collect(); - cx.executor() + cx.background_executor() .spawn(Self::background_search( unnamed_files, opened_buffers, - cx.executor().clone(), + cx.background_executor().clone(), self.fs.clone(), workers, query.clone(), @@ -5631,9 +5654,9 @@ impl Project { .detach(); let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); - let background = cx.executor().clone(); + let background = cx.background_executor().clone(); let (result_tx, result_rx) = smol::channel::bounded(1024); - cx.executor() + cx.background_executor() .spawn(async move { let Ok(buffers) = buffers.await else { return; @@ -5741,7 +5764,7 @@ impl Project { async fn background_search( unnamed_buffers: Vec>, opened_buffers: HashMap, (Model, BufferSnapshot)>, - executor: Executor, + executor: BackgroundExecutor, fs: Arc, workers: usize, query: SearchQuery, @@ -5846,8 +5869,8 @@ impl Project { cx: &mut ModelContext, ) -> Task> where - ::Result: Send, - ::Params: Send, + ::Result: Send, + ::Params: Send, { let buffer = buffer_handle.read(cx); if self.is_local() { @@ -5993,7 +6016,7 @@ impl Project { Task::ready(Ok((tree, relative_path))) } else { let worktree = self.create_local_worktree(abs_path, visible, cx); - cx.executor() + cx.background_executor() .spawn(async move { Ok((worktree.await?, PathBuf::new())) }) } } @@ -6064,7 +6087,7 @@ impl Project { .shared() }) .clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { match task.await { Ok(worktree) => Ok(worktree), Err(err) => Err(anyhow!("{}", err)), @@ -6281,7 +6304,7 @@ impl Project { }) = self.language_servers.get(server_id) { if let Some(watched_paths) = watched_paths.get(&worktree_id) { - let params = lsp2::DidChangeWatchedFilesParams { + let params = lsp::DidChangeWatchedFilesParams { changes: changes .iter() .filter_map(|(path, _, change)| { @@ -6290,13 +6313,13 @@ impl Project { } let typ = match change { PathChange::Loaded => return None, - PathChange::Added => lsp2::FileChangeType::CREATED, - PathChange::Removed => lsp2::FileChangeType::DELETED, - PathChange::Updated => lsp2::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp2::FileChangeType::CHANGED, + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, }; - Some(lsp2::FileEvent { - uri: lsp2::Url::from_file_path(abs_path.join(path)).unwrap(), + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), typ, }) }) @@ -6305,7 +6328,7 @@ impl Project { if !params.changes.is_empty() { server - .notify::(params) + .notify::(params) .log_err(); } } @@ -6376,7 +6399,7 @@ impl Project { let snapshot = worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?; let diff_bases_by_buffer = cx - .executor() + .background_executor() .spawn(async move { future_buffers .into_iter() @@ -6508,18 +6531,28 @@ impl Project { "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" ); let prettiers_to_reload = self - .prettier_instances + .prettiers_per_worktree + .get(¤t_worktree_id) .iter() - .filter_map(|((worktree_id, prettier_path), prettier_task)| { - if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) { - Some((*worktree_id, prettier_path.clone(), prettier_task.clone())) - } else { - None - } + .flat_map(|prettier_paths| prettier_paths.iter()) + .flatten() + .filter_map(|prettier_path| { + Some(( + current_worktree_id, + Some(prettier_path.clone()), + self.prettier_instances.get(prettier_path)?.clone(), + )) }) + .chain(self.default_prettier.iter().filter_map(|default_prettier| { + Some(( + current_worktree_id, + None, + default_prettier.instance.clone()?, + )) + })) .collect::>(); - cx.executor() + cx.background_executor() .spawn(async move { for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { async move { @@ -6527,9 +6560,14 @@ impl Project { .clear_cache() .await .with_context(|| { - format!( - "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" - ) + match prettier_path { + Some(prettier_path) => format!( + "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" + ), + None => format!( + "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update" + ), + } }) .map_err(Arc::new) } @@ -7080,7 +7118,7 @@ impl Project { let ops = payload .operations .into_iter() - .map(language2::proto::deserialize_operation) + .map(language::proto::deserialize_operation) .collect::, _>>()?; let is_remote = this.is_remote(); match this.opened_buffers.entry(buffer_id) { @@ -7124,7 +7162,7 @@ impl Project { anyhow!("no worktree found for id {}", file.worktree_id) })?; buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) - as Arc); + as Arc); } let buffer_id = state.id; @@ -7149,7 +7187,7 @@ impl Project { let operations = chunk .operations .into_iter() - .map(language2::proto::deserialize_operation) + .map(language::proto::deserialize_operation) .collect::>>()?; buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; @@ -7255,9 +7293,7 @@ impl Project { buffer_id, version: serialize_version(buffer.saved_version()), mtime: Some(buffer.saved_mtime().into()), - fingerprint: language2::proto::serialize_fingerprint( - buffer.saved_version_fingerprint(), - ), + fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()), })?) } @@ -7310,7 +7346,7 @@ impl Project { this.shared_buffers.entry(guest_id).or_default().clear(); for buffer in envelope.payload.buffers { let buffer_id = buffer.id; - let remote_version = language2::proto::deserialize_version(&buffer.version); + let remote_version = language::proto::deserialize_version(&buffer.version); if let Some(buffer) = this.buffer_for_id(buffer_id) { this.shared_buffers .entry(guest_id) @@ -7320,7 +7356,7 @@ impl Project { let buffer = buffer.read(cx); response.buffers.push(proto::BufferVersion { id: buffer_id, - version: language2::proto::serialize_version(&buffer.version), + version: language::proto::serialize_version(&buffer.version), }); let operations = buffer.serialize_ops(Some(remote_version), cx); @@ -7347,18 +7383,18 @@ impl Project { .send(proto::BufferReloaded { project_id, buffer_id, - version: language2::proto::serialize_version(buffer.saved_version()), + version: language::proto::serialize_version(buffer.saved_version()), mtime: Some(buffer.saved_mtime().into()), - fingerprint: language2::proto::serialize_fingerprint( + fingerprint: language::proto::serialize_fingerprint( buffer.saved_version_fingerprint(), ), - line_ending: language2::proto::serialize_line_ending( + line_ending: language::proto::serialize_line_ending( buffer.line_ending(), ) as i32, }) .log_err(); - cx.executor() + cx.background_executor() .spawn( async move { let operations = operations.await; @@ -7426,7 +7462,7 @@ impl Project { .and_then(|buffer| buffer.upgrade()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; let language = buffer.read(cx).language(); - let completion = language2::proto::deserialize_completion( + let completion = language::proto::deserialize_completion( envelope .payload .completion @@ -7446,7 +7482,7 @@ impl Project { transaction: apply_additional_edits .await? .as_ref() - .map(language2::proto::serialize_transaction), + .map(language::proto::serialize_transaction), }) } @@ -7457,7 +7493,7 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { let sender_id = envelope.original_sender_id()?; - let action = language2::proto::deserialize_code_action( + let action = language::proto::deserialize_code_action( envelope .payload .action @@ -7509,7 +7545,7 @@ impl Project { let transaction = on_type_formatting .await? .as_ref() - .map(language2::proto::serialize_transaction); + .map(language::proto::serialize_transaction); Ok(proto::OnTypeFormattingResponse { transaction }) } @@ -7616,8 +7652,8 @@ impl Project { mut cx: AsyncAppContext, ) -> Result<::Response> where - ::Params: Send, - ::Result: Send, + ::Params: Send, + ::Result: Send, { let sender_id = envelope.original_sender_id()?; let buffer_id = T::buffer_id_from_proto(&envelope.payload); @@ -7801,7 +7837,7 @@ impl Project { .push(self.create_buffer_for_peer(&buffer, peer_id, cx)); serialized_transaction .transactions - .push(language2::proto::serialize_transaction(&transaction)); + .push(language::proto::serialize_transaction(&transaction)); } serialized_transaction } @@ -7821,7 +7857,7 @@ impl Project { this.wait_for_remote_buffer(buffer_id, cx) })? .await?; - let transaction = language2::proto::deserialize_transaction(transaction)?; + let transaction = language::proto::deserialize_transaction(transaction)?; project_transaction.0.insert(buffer, transaction); } @@ -7930,7 +7966,7 @@ impl Project { let buffer = buffer.upgrade()?; Some(proto::BufferVersion { id: *id, - version: language2::proto::serialize_version(&buffer.read(cx).version), + version: language::proto::serialize_version(&buffer.read(cx).version), }) }) .collect(); @@ -7956,11 +7992,11 @@ impl Project { .map(|buffer| { let client = client.clone(); let buffer_id = buffer.id; - let remote_version = language2::proto::deserialize_version(&buffer.version); + let remote_version = language::proto::deserialize_version(&buffer.version); if let Some(buffer) = this.buffer_for_id(buffer_id) { let operations = buffer.read(cx).serialize_ops(Some(remote_version), cx); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let operations = operations.await; for chunk in split_operations(operations) { client @@ -7983,7 +8019,7 @@ impl Project { // Any incomplete buffers have open requests waiting. Request that the host sends // creates these buffers for us again to unblock any waiting futures. for id in incomplete_buffer_ids { - cx.executor() + cx.background_executor() .spawn(client.request(proto::OpenBufferById { project_id, id })) .detach(); } @@ -8192,13 +8228,13 @@ impl Project { fn edits_from_lsp( &mut self, buffer: &Model, - lsp_edits: impl 'static + Send + IntoIterator, + lsp_edits: impl 'static + Send + IntoIterator, server_id: LanguageServerId, version: Option, cx: &mut ModelContext, ) -> Task, String)>>> { let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let snapshot = snapshot?; let mut lsp_edits = lsp_edits .into_iter() @@ -8415,7 +8451,12 @@ impl Project { &mut self, buffer: &Model, cx: &mut ModelContext, - ) -> Task, Arc>>>>> { + ) -> Task< + Option<( + Option, + Shared, Arc>>>, + )>, + > { let buffer = buffer.read(cx); let buffer_file = buffer.file(); let Some(buffer_language) = buffer.language() else { @@ -8425,142 +8466,142 @@ impl Project { return Task::ready(None); } - let buffer_file = File::from_dyn(buffer_file); - let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); - let worktree_path = buffer_file - .as_ref() - .and_then(|file| Some(file.worktree.read(cx).abs_path())); - let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); - if self.is_local() || worktree_id.is_none() || worktree_path.is_none() { + if self.is_local() { let Some(node) = self.node.as_ref().map(Arc::clone) else { return Task::ready(None); }; - let fs = self.fs.clone(); - cx.spawn(move |this, mut cx| async move { - let prettier_dir = match cx - .executor() - .spawn(Prettier::locate( - worktree_path.zip(buffer_path).map( - |(worktree_root_path, starting_path)| LocateStart { - worktree_root_path, - starting_path, - }, - ), - fs, - )) - .await - { - Ok(path) => path, - Err(e) => { - return Some( - Task::ready(Err(Arc::new(e.context( - "determining prettier path for worktree {worktree_path:?}", - )))) - .shared(), - ); - } - }; - - if let Some(existing_prettier) = this - .update(&mut cx, |project, _| { - project - .prettier_instances - .get(&(worktree_id, prettier_dir.clone())) - .cloned() - }) - .ok() - .flatten() - { - return Some(existing_prettier); - } - - log::info!("Found prettier in {prettier_dir:?}, starting."); - let task_prettier_dir = prettier_dir.clone(); - let new_prettier_task = cx - .spawn({ - let this = this.clone(); - move |mut cx| async move { - let new_server_id = this.update(&mut cx, |this, _| { - this.languages.next_language_server_id() - })?; - let prettier = Prettier::start( - worktree_id.map(|id| id.to_usize()), - new_server_id, - task_prettier_dir, - node, - cx.clone(), - ) + match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) + { + Some((worktree_id, buffer_path)) => { + let fs = Arc::clone(&self.fs); + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + return cx.spawn(|project, mut cx| async move { + match cx + .background_executor() + .spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + &buffer_path, + ) + .await + }) .await - .context("prettier start") - .map_err(Arc::new)?; - log::info!("Started prettier in {:?}", prettier.prettier_dir()); - - if let Some(prettier_server) = prettier.server() { - this.update(&mut cx, |project, cx| { - let name = if prettier.is_default() { - LanguageServerName(Arc::from("prettier (default)")) - } else { - let prettier_dir = prettier.prettier_dir(); - let worktree_path = prettier - .worktree_id() - .map(WorktreeId::from_usize) - .and_then(|id| project.worktree_for_id(id, cx)) - .map(|worktree| worktree.read(cx).abs_path()); - match worktree_path { - Some(worktree_path) => { - if worktree_path.as_ref() == prettier_dir { - LanguageServerName(Arc::from(format!( - "prettier ({})", - prettier_dir - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default() - ))) - } else { - let dir_to_display = match prettier_dir - .strip_prefix(&worktree_path) - .ok() - { - Some(relative_path) => relative_path, - None => prettier_dir, - }; - LanguageServerName(Arc::from(format!( - "prettier ({})", - dir_to_display.display(), - ))) - } - } - None => LanguageServerName(Arc::from(format!( - "prettier ({})", - prettier_dir.display(), - ))), - } - }; - + { + Ok(None) => { + match project.update(&mut cx, |project, _| { project - .supplementary_language_servers - .insert(new_server_id, (name, Arc::clone(prettier_server))); - cx.emit(Event::LanguageServerAdded(new_server_id)); - })?; + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(None); + project.default_prettier.as_ref().and_then( + |default_prettier| default_prettier.instance.clone(), + ) + }) { + Ok(Some(old_task)) => Some((None, old_task)), + Ok(None) => { + match project.update(&mut cx, |_, cx| { + start_default_prettier(node, Some(worktree_id), cx) + }) { + Ok(new_default_prettier) => { + return Some((None, new_default_prettier.await)) + } + Err(e) => { + Some(( + None, + Task::ready(Err(Arc::new(e.context("project is gone during default prettier startup")))) + .shared(), + )) + } + } + } + Err(e) => Some((None, Task::ready(Err(Arc::new(e.context("project is gone during default prettier checks")))) + .shared())), + } + } + Ok(Some(prettier_dir)) => { + match project.update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(Some(prettier_dir.clone())); + project.prettier_instances.get(&prettier_dir).cloned() + }) { + Ok(Some(existing_prettier)) => { + log::debug!( + "Found already started prettier in {prettier_dir:?}" + ); + return Some((Some(prettier_dir), existing_prettier)); + } + Err(e) => { + return Some(( + Some(prettier_dir), + Task::ready(Err(Arc::new(e.context("project is gone during custom prettier checks")))) + .shared(), + )) + } + _ => {}, + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let new_prettier_task = + match project.update(&mut cx, |project, cx| { + let new_prettier_task = start_prettier( + node, + prettier_dir.clone(), + Some(worktree_id), + cx, + ); + project.prettier_instances.insert( + prettier_dir.clone(), + new_prettier_task.clone(), + ); + new_prettier_task + }) { + Ok(task) => task, + Err(e) => return Some(( + Some(prettier_dir), + Task::ready(Err(Arc::new(e.context("project is gone during custom prettier startup")))) + .shared() + )), + }; + Some((Some(prettier_dir), new_prettier_task)) + } + Err(e) => { + return Some(( + None, + Task::ready(Err(Arc::new( + e.context("determining prettier path"), + ))) + .shared(), + )); } - Ok(Arc::new(prettier)).map_err(Arc::new) } - }) - .shared(); - this.update(&mut cx, |project, _| { - project - .prettier_instances - .insert((worktree_id, prettier_dir), new_prettier_task.clone()); - }) - .ok(); - Some(new_prettier_task) - }) + }); + } + None => { + let started_default_prettier = self + .default_prettier + .as_ref() + .and_then(|default_prettier| default_prettier.instance.clone()); + match started_default_prettier { + Some(old_task) => return Task::ready(Some((None, old_task))), + None => { + let new_task = start_default_prettier(node, None, cx); + return cx.spawn(|_, _| async move { Some((None, new_task.await)) }); + } + } + } + } } else if self.remote_id().is_some() { return Task::ready(None); } else { - Task::ready(Some( + Task::ready(Some(( + None, Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), - )) + ))) } } @@ -8571,8 +8612,7 @@ impl Project { _: &Language, _: &LanguageSettings, _: &mut ModelContext, - ) -> Task> { - Task::ready(Ok(())) + ) { } #[cfg(not(any(test, feature = "test-support")))] @@ -8582,19 +8622,19 @@ impl Project { new_language: &Language, language_settings: &LanguageSettings, cx: &mut ModelContext, - ) -> Task> { + ) { match &language_settings.formatter { Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())), + Formatter::LanguageServer | Formatter::External { .. } => return, }; let Some(node) = self.node.as_ref().cloned() else { - return Task::ready(Ok(())); + return; }; let mut prettier_plugins = None; if new_language.prettier_parser_name().is_some() { prettier_plugins - .get_or_insert_with(|| HashSet::default()) + .get_or_insert_with(|| HashSet::<&'static str>::default()) .extend( new_language .lsp_adapters() @@ -8603,130 +8643,303 @@ impl Project { ) } let Some(prettier_plugins) = prettier_plugins else { - return Task::ready(Ok(())); + return; }; + let fs = Arc::clone(&self.fs); + let locate_prettier_installation = match worktree.and_then(|worktree_id| { + self.worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + }) { + Some(locate_from) => { + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + cx.background_executor().spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + locate_from.as_ref(), + ) + .await + }) + } + None => Task::ready(Ok(None)), + }; let mut plugins_to_install = prettier_plugins; - let (mut install_success_tx, mut install_success_rx) = - futures::channel::mpsc::channel::>(1); - let new_installation_process = cx - .spawn(|this, mut cx| async move { - if let Some(installed_plugins) = install_success_rx.next().await { - this.update(&mut cx, |this, _| { - let default_prettier = - this.default_prettier - .get_or_insert_with(|| DefaultPrettier { - installation_process: None, - installed_plugins: HashSet::default(), - }); - if !installed_plugins.is_empty() { - log::info!("Installed new prettier plugins: {installed_plugins:?}"); - default_prettier.installed_plugins.extend(installed_plugins); - } - }) - .ok(); - } - }) - .shared(); let previous_installation_process = if let Some(default_prettier) = &mut self.default_prettier { plugins_to_install .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); if plugins_to_install.is_empty() { - return Task::ready(Ok(())); + return; } - std::mem::replace( - &mut default_prettier.installation_process, - Some(new_installation_process.clone()), - ) + default_prettier.installation_process.clone() } else { None }; - let default_prettier_dir = util::paths::DEFAULT_PRETTIER_DIR.as_path(); - let already_running_prettier = self - .prettier_instances - .get(&(worktree, default_prettier_dir.to_path_buf())) - .cloned(); let fs = Arc::clone(&self.fs); - cx.spawn_on_main(move |this, mut cx| async move { - if let Some(previous_installation_process) = previous_installation_process { - previous_installation_process.await; - } - let mut everything_was_installed = false; - this.update(&mut cx, |this, _| { - match &mut this.default_prettier { - Some(default_prettier) => { - plugins_to_install - .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); - everything_was_installed = plugins_to_install.is_empty(); - }, - None => this.default_prettier = Some(DefaultPrettier { installation_process: Some(new_installation_process), installed_plugins: HashSet::default() }), - } - })?; - if everything_was_installed { - return Ok(()); - } - - cx.spawn(move |_| async move { - let prettier_wrapper_path = default_prettier_dir.join(prettier2::PRETTIER_SERVER_FILE); - // method creates parent directory if it doesn't exist - fs.save(&prettier_wrapper_path, &text::Rope::from(prettier2::PRETTIER_SERVER_JS), text::LineEnding::Unix).await - .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier2::PRETTIER_SERVER_FILE))?; - - let packages_to_versions = future::try_join_all( - plugins_to_install - .iter() - .chain(Some(&"prettier")) - .map(|package_name| async { - let returned_package_name = package_name.to_string(); - let latest_version = node.npm_package_latest_version(package_name) + let default_prettier = self + .default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: None, + installed_plugins: HashSet::default(), + }); + default_prettier.installation_process = Some( + cx.spawn(|this, mut cx| async move { + match locate_prettier_installation + .await + .context("locate prettier installation") + .map_err(Arc::new)? + { + Some(_non_default_prettier) => return Ok(()), + None => { + let mut needs_install = match previous_installation_process { + Some(previous_installation_process) => { + previous_installation_process.await.is_err() + } + None => true, + }; + this.update(&mut cx, |this, _| { + if let Some(default_prettier) = &mut this.default_prettier { + plugins_to_install.retain(|plugin| { + !default_prettier.installed_plugins.contains(plugin) + }); + needs_install |= !plugins_to_install.is_empty(); + } + })?; + if needs_install { + let installed_plugins = plugins_to_install.clone(); + cx.background_executor() + .spawn(async move { + install_default_prettier(plugins_to_install, node, fs).await + }) .await - .with_context(|| { - format!("fetching latest npm version for package {returned_package_name}") - })?; - anyhow::Ok((returned_package_name, latest_version)) - }), - ) - .await - .context("fetching latest npm versions")?; - - log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); - let borrowed_packages = packages_to_versions.iter().map(|(package, version)| { - (package.as_str(), version.as_str()) - }).collect::>(); - node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?; - let installed_packages = !plugins_to_install.is_empty(); - install_success_tx.try_send(plugins_to_install).ok(); - - if !installed_packages { - if let Some(prettier) = already_running_prettier { - prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?; + .context("prettier & plugins install") + .map_err(Arc::new)?; + this.update(&mut cx, |this, _| { + let default_prettier = + this.default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: Some( + Task::ready(Ok(())).shared(), + ), + installed_plugins: HashSet::default(), + }); + default_prettier.instance = None; + default_prettier.installed_plugins.extend(installed_plugins); + })?; + } } } - - anyhow::Ok(()) - }).await - }) + Ok(()) + }) + .shared(), + ); } } +fn start_default_prettier( + node: Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Task, Arc>>>> { + cx.spawn(|project, mut cx| async move { + loop { + let default_prettier_installing = match project.update(&mut cx, |project, _| { + project + .default_prettier + .as_ref() + .and_then(|default_prettier| default_prettier.installation_process.clone()) + }) { + Ok(installation) => installation, + Err(e) => { + return Task::ready(Err(Arc::new( + e.context("project is gone during default prettier installation"), + ))) + .shared() + } + }; + match default_prettier_installing { + Some(installation_task) => { + if installation_task.await.is_ok() { + break; + } + } + None => break, + } + } + + match project.update(&mut cx, |project, cx| { + match project + .default_prettier + .as_mut() + .and_then(|default_prettier| default_prettier.instance.as_mut()) + { + Some(default_prettier) => default_prettier.clone(), + None => { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project + .default_prettier + .get_or_insert_with(|| DefaultPrettier { + instance: None, + installation_process: None, + #[cfg(not(any(test, feature = "test-support")))] + installed_plugins: HashSet::default(), + }) + .instance = Some(new_default_prettier.clone()); + new_default_prettier + } + } + }) { + Ok(task) => task, + Err(e) => Task::ready(Err(Arc::new( + e.context("project is gone during default prettier startup"), + ))) + .shared(), + } + }) +} + +fn start_prettier( + node: Arc, + prettier_dir: PathBuf, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Shared, Arc>>> { + cx.spawn(|project, mut cx| async move { + let new_server_id = project.update(&mut cx, |project, _| { + project.languages.next_language_server_id() + })?; + let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; + register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); + Ok(new_prettier) + }) + .shared() +} + +fn register_new_prettier( + project: &WeakModel, + prettier: &Prettier, + worktree_id: Option, + new_server_id: LanguageServerId, + cx: &mut AsyncAppContext, +) { + let prettier_dir = prettier.prettier_dir(); + let is_default = prettier.is_default(); + if is_default { + log::info!("Started default prettier in {prettier_dir:?}"); + } else { + log::info!("Started prettier in {prettier_dir:?}"); + } + if let Some(prettier_server) = prettier.server() { + project + .update(cx, |project, cx| { + let name = if is_default { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let worktree_path = worktree_id + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); + let name = match worktree_path { + Some(worktree_path) => { + if prettier_dir == worktree_path.as_ref() { + let name = prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + format!("prettier ({name})") + } else { + let dir_to_display = prettier_dir + .strip_prefix(worktree_path.as_ref()) + .ok() + .unwrap_or(prettier_dir); + format!("prettier ({})", dir_to_display.display()) + } + } + None => format!("prettier ({})", prettier_dir.display()), + }; + LanguageServerName(Arc::from(name)) + }; + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }) + .ok(); + } +} + +#[cfg(not(any(test, feature = "test-support")))] +async fn install_default_prettier( + plugins_to_install: HashSet<&'static str>, + node: Arc, + fs: Arc, +) -> anyhow::Result<()> { + let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); + // method creates parent directory if it doesn't exist + fs.save( + &prettier_wrapper_path, + &text::Rope::from(prettier::PRETTIER_SERVER_JS), + text::LineEnding::Unix, + ) + .await + .with_context(|| { + format!( + "writing {} file at {prettier_wrapper_path:?}", + prettier::PRETTIER_SERVER_FILE + ) + })?; + + let packages_to_versions = + future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( + |package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node + .npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }, + )) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions + .iter() + .map(|(package, version)| (package.as_str(), version.as_str())) + .collect::>(); + node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) + .await + .context("fetching formatter packages")?; + anyhow::Ok(()) +} + fn subscribe_for_copilot_events( copilot: &Model, cx: &mut ModelContext<'_, Project>, -) -> gpui2::Subscription { +) -> gpui::Subscription { cx.subscribe( copilot, |project, copilot, copilot_event, cx| match copilot_event { - copilot2::Event::CopilotLanguageServerStarted => { + copilot::Event::CopilotLanguageServerStarted => { match copilot.read(cx).language_server() { Some((name, copilot_server)) => { // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. - if !copilot_server.has_notification_handler::() { + if !copilot_server.has_notification_handler::() { let new_server_id = copilot_server.server_id(); let weak_project = cx.weak_model(); let copilot_log_subscription = copilot_server - .on_notification::( + .on_notification::( move |params, mut cx| { weak_project.update(&mut cx, |_, cx| { cx.emit(Event::LanguageServerLog( @@ -8796,7 +9009,7 @@ pub struct PathMatchCandidateSet { pub include_root_name: bool, } -impl<'a> fuzzy2::PathMatchCandidateSet<'a> for PathMatchCandidateSet { +impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { type Candidates = PathMatchCandidateSetIter<'a>; fn id(&self) -> usize { @@ -8833,12 +9046,12 @@ pub struct PathMatchCandidateSetIter<'a> { } impl<'a> Iterator for PathMatchCandidateSetIter<'a> { - type Item = fuzzy2::PathMatchCandidate<'a>; + type Item = fuzzy::PathMatchCandidate<'a>; fn next(&mut self) -> Option { self.traversal.next().map(|entry| { if let EntryKind::File(char_bag) = entry.kind { - fuzzy2::PathMatchCandidate { + fuzzy::PathMatchCandidate { path: &entry.path, char_bag, } @@ -8958,18 +9171,18 @@ async fn wait_for_loading_buffer( } } -fn include_text(server: &lsp2::LanguageServer) -> bool { +fn include_text(server: &lsp::LanguageServer) -> bool { server .capabilities() .text_document_sync .as_ref() .and_then(|sync| match sync { - lsp2::TextDocumentSyncCapability::Kind(_) => None, - lsp2::TextDocumentSyncCapability::Options(options) => options.save.as_ref(), + lsp::TextDocumentSyncCapability::Kind(_) => None, + lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(), }) .and_then(|save_options| match save_options { - lsp2::TextDocumentSyncSaveOptions::Supported(_) => None, - lsp2::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text, + lsp::TextDocumentSyncSaveOptions::Supported(_) => None, + lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text, }) .unwrap_or(false) } diff --git a/crates/project2/src/project_settings.rs b/crates/project2/src/project_settings.rs index b85226f7cc..028a564b9c 100644 --- a/crates/project2/src/project_settings.rs +++ b/crates/project2/src/project_settings.rs @@ -1,8 +1,8 @@ use collections::HashMap; -use gpui2::AppContext; +use gpui::AppContext; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use std::sync::Arc; #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 4ca8bb0fa1..19485b2306 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -1,4077 +1,4029 @@ -// use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; -// use fs::{FakeFs, RealFs}; -// use futures::{future, StreamExt}; -// use gpui::{executor::Deterministic, test::subscribe, AppContext}; -// use language2::{ -// language_settings::{AllLanguageSettings, LanguageSettingsContent}, -// tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, -// LineEnding, OffsetRangeExt, Point, ToPoint, -// }; -// use lsp2::Url; -// use parking_lot::Mutex; -// use pretty_assertions::assert_eq; -// use serde_json::json; -// use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; -// use unindent::Unindent as _; -// use util::{assert_set_eq, test::temp_tree}; - -// #[cfg(test)] -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } - -// #[gpui::test] -// async fn test_symlinks(cx: &mut gpui::TestAppContext) { -// init_test(cx); -// cx.foreground().allow_parking(); - -// let dir = temp_tree(json!({ -// "root": { -// "apple": "", -// "banana": { -// "carrot": { -// "date": "", -// "endive": "", -// } -// }, -// "fennel": { -// "grape": "", -// } -// } -// })); - -// let root_link_path = dir.path().join("root_link"); -// unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); -// unix::fs::symlink( -// &dir.path().join("root/fennel"), -// &dir.path().join("root/finnochio"), -// ) -// .unwrap(); - -// let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await; -// project.read_with(cx, |project, cx| { -// let tree = project.worktrees(cx).next().unwrap().read(cx); -// assert_eq!(tree.file_count(), 5); -// assert_eq!( -// tree.inode_for_path("fennel/grape"), -// tree.inode_for_path("finnochio/grape") -// ); -// }); -// } - -// #[gpui::test] -// async fn test_managing_project_specific_settings( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/the-root", -// json!({ -// ".zed": { -// "settings.json": r#"{ "tab_size": 8 }"# -// }, -// "a": { -// "a.rs": "fn a() {\n A\n}" -// }, -// "b": { -// ".zed": { -// "settings.json": r#"{ "tab_size": 2 }"# -// }, -// "b.rs": "fn b() {\n B\n}" -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; -// let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); - -// deterministic.run_until_parked(); -// cx.read(|cx| { -// let tree = worktree.read(cx); - -// let settings_a = language_settings( -// None, -// Some( -// &(File::for_entry( -// tree.entry_for_path("a/a.rs").unwrap().clone(), -// worktree.clone(), -// ) as _), -// ), -// cx, -// ); -// let settings_b = language_settings( -// None, -// Some( -// &(File::for_entry( -// tree.entry_for_path("b/b.rs").unwrap().clone(), -// worktree.clone(), -// ) as _), -// ), -// cx, -// ); - -// assert_eq!(settings_a.tab_size.get(), 8); -// assert_eq!(settings_b.tab_size.get(), 2); -// }); -// } - -// #[gpui::test] -// async fn test_managing_language_servers( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx); - -// let mut rust_language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut json_language = Language::new( -// LanguageConfig { -// name: "JSON".into(), -// path_suffixes: vec!["json".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_rust_servers = rust_language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-rust-language-server", -// capabilities: lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), "::".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let mut fake_json_servers = json_language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-json-language-server", -// capabilities: lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/the-root", -// json!({ -// "test.rs": "const A: i32 = 1;", -// "test2.rs": "", -// "Cargo.toml": "a = 1", -// "package.json": "{\"a\": 1}", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - -// // Open a buffer without an associated language server. -// let toml_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/the-root/Cargo.toml", cx) -// }) -// .await -// .unwrap(); - -// // Open a buffer with an associated language server before the language for it has been loaded. -// let rust_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/the-root/test.rs", cx) -// }) -// .await -// .unwrap(); -// rust_buffer.read_with(cx, |buffer, _| { -// assert_eq!(buffer.language().map(|l| l.name()), None); -// }); - -// // Now we add the languages to the project, and ensure they get assigned to all -// // the relevant open buffers. -// project.update(cx, |project, _| { -// project.languages.add(Arc::new(json_language)); -// project.languages.add(Arc::new(rust_language)); -// }); -// deterministic.run_until_parked(); -// rust_buffer.read_with(cx, |buffer, _| { -// assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into())); -// }); - -// // A server is started up, and it is notified about Rust files. -// let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), -// version: 0, -// text: "const A: i32 = 1;".to_string(), -// language_id: Default::default() -// } -// ); - -// // The buffer is configured based on the language server's capabilities. -// rust_buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer.completion_triggers(), -// &[".".to_string(), "::".to_string()] -// ); -// }); -// toml_buffer.read_with(cx, |buffer, _| { -// assert!(buffer.completion_triggers().is_empty()); -// }); - -// // Edit a buffer. The changes are reported to the language server. -// rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::VersionedTextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), -// 1 -// ) -// ); - -// // Open a third buffer with a different associated language server. -// let json_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/the-root/package.json", cx) -// }) -// .await -// .unwrap(); - -// // A json language server is started up and is only notified about the json buffer. -// let mut fake_json_server = fake_json_servers.next().await.unwrap(); -// assert_eq!( -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(), -// version: 0, -// text: "{\"a\": 1}".to_string(), -// language_id: Default::default() -// } -// ); - -// // This buffer is configured based on the second language server's -// // capabilities. -// json_buffer.read_with(cx, |buffer, _| { -// assert_eq!(buffer.completion_triggers(), &[":".to_string()]); -// }); - -// // When opening another buffer whose language server is already running, -// // it is also configured based on the existing language server's capabilities. -// let rust_buffer2 = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/the-root/test2.rs", cx) -// }) -// .await -// .unwrap(); -// rust_buffer2.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer.completion_triggers(), -// &[".".to_string(), "::".to_string()] -// ); -// }); - -// // Changes are reported only to servers matching the buffer's language. -// toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); -// rust_buffer2.update(cx, |buffer, cx| { -// buffer.edit([(0..0, "let x = 1;")], None, cx) -// }); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::VersionedTextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(), -// 1 -// ) -// ); - -// // Save notifications are reported to all servers. -// project -// .update(cx, |project, cx| project.save_buffer(toml_buffer, cx)) -// .await -// .unwrap(); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap() -// ) -// ); -// assert_eq!( -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap() -// ) -// ); - -// // Renames are reported only to servers matching the buffer's language. -// fs.rename( -// Path::new("/the-root/test2.rs"), -// Path::new("/the-root/test3.rs"), -// Default::default(), -// ) -// .await -// .unwrap(); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()), -// ); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(), -// version: 0, -// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), -// language_id: Default::default() -// }, -// ); - -// rust_buffer2.update(cx, |buffer, cx| { -// buffer.update_diagnostics( -// LanguageServerId(0), -// DiagnosticSet::from_sorted_entries( -// vec![DiagnosticEntry { -// diagnostic: Default::default(), -// range: Anchor::MIN..Anchor::MAX, -// }], -// &buffer.snapshot(), -// ), -// cx, -// ); -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, usize>(0..buffer.len(), false) -// .count(), -// 1 -// ); -// }); - -// // When the rename changes the extension of the file, the buffer gets closed on the old -// // language server and gets opened on the new one. -// fs.rename( -// Path::new("/the-root/test3.rs"), -// Path::new("/the-root/test3.json"), -// Default::default(), -// ) -// .await -// .unwrap(); -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),), -// ); -// assert_eq!( -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), -// version: 0, -// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), -// language_id: Default::default() -// }, -// ); - -// // We clear the diagnostics, since the language has changed. -// rust_buffer2.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, usize>(0..buffer.len(), false) -// .count(), -// 0 -// ); -// }); - -// // The renamed file's version resets after changing language server. -// rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); -// assert_eq!( -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::VersionedTextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), -// 1 -// ) -// ); - -// // Restart language servers -// project.update(cx, |project, cx| { -// project.restart_language_servers_for_buffers( -// vec![rust_buffer.clone(), json_buffer.clone()], -// cx, -// ); -// }); - -// let mut rust_shutdown_requests = fake_rust_server -// .handle_request::(|_, _| future::ready(Ok(()))); -// let mut json_shutdown_requests = fake_json_server -// .handle_request::(|_, _| future::ready(Ok(()))); -// futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); - -// let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); -// let mut fake_json_server = fake_json_servers.next().await.unwrap(); - -// // Ensure rust document is reopened in new rust language server -// assert_eq!( -// fake_rust_server -// .receive_notification::() -// .await -// .text_document, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), -// version: 0, -// text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), -// language_id: Default::default() -// } -// ); - -// // Ensure json documents are reopened in new json language server -// assert_set_eq!( -// [ -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// fake_json_server -// .receive_notification::() -// .await -// .text_document, -// ], -// [ -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(), -// version: 0, -// text: json_buffer.read_with(cx, |buffer, _| buffer.text()), -// language_id: Default::default() -// }, -// lsp2::TextDocumentItem { -// uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), -// version: 0, -// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), -// language_id: Default::default() -// } -// ] -// ); - -// // Close notifications are reported only to servers matching the buffer's language. -// cx.update(|_| drop(json_buffer)); -// let close_message = lsp2::DidCloseTextDocumentParams { -// text_document: lsp2::TextDocumentIdentifier::new( -// lsp2::Url::from_file_path("/the-root/package.json").unwrap(), -// ), -// }; -// assert_eq!( -// fake_json_server -// .receive_notification::() -// .await, -// close_message, -// ); -// } - -// #[gpui::test] -// async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// 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_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-language-server", -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/the-root", -// json!({ -// ".gitignore": "target\n", -// "src": { -// "a.rs": "", -// "b.rs": "", -// }, -// "target": { -// "x": { -// "out": { -// "x.rs": "" -// } -// }, -// "y": { -// "out": { -// "y.rs": "", -// } -// }, -// "z": { -// "out": { -// "z.rs": "" -// } -// } -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages.add(Arc::new(language)); -// }); -// cx.foreground().run_until_parked(); - -// // Start the language server by opening a buffer with a compatible file extension. -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/the-root/src/a.rs", cx) -// }) -// .await -// .unwrap(); - -// // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them. -// project.read_with(cx, |project, cx| { -// let worktree = project.worktrees(cx).next().unwrap(); -// assert_eq!( -// worktree -// .read(cx) -// .snapshot() -// .entries(true) -// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) -// .collect::>(), -// &[ -// (Path::new(""), false), -// (Path::new(".gitignore"), false), -// (Path::new("src"), false), -// (Path::new("src/a.rs"), false), -// (Path::new("src/b.rs"), false), -// (Path::new("target"), true), -// ] -// ); -// }); - -// let prev_read_dir_count = fs.read_dir_call_count(); - -// // Keep track of the FS events reported to the language server. -// let fake_server = fake_servers.next().await.unwrap(); -// let file_changes = Arc::new(Mutex::new(Vec::new())); -// fake_server -// .request::(lsp2::RegistrationParams { -// registrations: vec![lsp2::Registration { -// id: Default::default(), -// method: "workspace/didChangeWatchedFiles".to_string(), -// register_options: serde_json::to_value( -// lsp::DidChangeWatchedFilesRegistrationOptions { -// watchers: vec![ -// lsp2::FileSystemWatcher { -// glob_pattern: lsp2::GlobPattern::String( -// "/the-root/Cargo.toml".to_string(), -// ), -// kind: None, -// }, -// lsp2::FileSystemWatcher { -// glob_pattern: lsp2::GlobPattern::String( -// "/the-root/src/*.{rs,c}".to_string(), -// ), -// kind: None, -// }, -// lsp2::FileSystemWatcher { -// glob_pattern: lsp2::GlobPattern::String( -// "/the-root/target/y/**/*.rs".to_string(), -// ), -// kind: None, -// }, -// ], -// }, -// ) -// .ok(), -// }], -// }) -// .await -// .unwrap(); -// fake_server.handle_notification::({ -// let file_changes = file_changes.clone(); -// move |params, _| { -// let mut file_changes = file_changes.lock(); -// file_changes.extend(params.changes); -// file_changes.sort_by(|a, b| a.uri.cmp(&b.uri)); -// } -// }); - -// cx.foreground().run_until_parked(); -// assert_eq!(mem::take(&mut *file_changes.lock()), &[]); -// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4); - -// // Now the language server has asked us to watch an ignored directory path, -// // so we recursively load it. -// project.read_with(cx, |project, cx| { -// let worktree = project.worktrees(cx).next().unwrap(); -// assert_eq!( -// worktree -// .read(cx) -// .snapshot() -// .entries(true) -// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) -// .collect::>(), -// &[ -// (Path::new(""), false), -// (Path::new(".gitignore"), false), -// (Path::new("src"), false), -// (Path::new("src/a.rs"), false), -// (Path::new("src/b.rs"), false), -// (Path::new("target"), true), -// (Path::new("target/x"), true), -// (Path::new("target/y"), true), -// (Path::new("target/y/out"), true), -// (Path::new("target/y/out/y.rs"), true), -// (Path::new("target/z"), true), -// ] -// ); -// }); - -// // Perform some file system mutations, two of which match the watched patterns, -// // and one of which does not. -// fs.create_file("/the-root/src/c.rs".as_ref(), Default::default()) -// .await -// .unwrap(); -// fs.create_file("/the-root/src/d.txt".as_ref(), Default::default()) -// .await -// .unwrap(); -// fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) -// .await -// .unwrap(); -// fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) -// .await -// .unwrap(); -// fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default()) -// .await -// .unwrap(); - -// // The language server receives events for the FS mutations that match its watch patterns. -// cx.foreground().run_until_parked(); -// assert_eq!( -// &*file_changes.lock(), -// &[ -// lsp2::FileEvent { -// uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(), -// typ: lsp2::FileChangeType::DELETED, -// }, -// lsp2::FileEvent { -// uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(), -// typ: lsp2::FileChangeType::CREATED, -// }, -// lsp2::FileEvent { -// uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(), -// typ: lsp2::FileChangeType::CREATED, -// }, -// ] -// ); -// } - -// #[gpui::test] -// async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": "let a = 1;", -// "b.rs": "let b = 2;" -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await; - -// let buffer_a = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); -// let buffer_b = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) -// .await -// .unwrap(); - -// project.update(cx, |project, cx| { -// project -// .update_diagnostics( -// LanguageServerId(0), -// lsp::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/a.rs").unwrap(), -// version: None, -// diagnostics: vec![lsp2::Diagnostic { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 4), -// lsp2::Position::new(0, 5), -// ), -// severity: Some(lsp2::DiagnosticSeverity::ERROR), -// message: "error 1".to_string(), -// ..Default::default() -// }], -// }, -// &[], -// cx, -// ) -// .unwrap(); -// project -// .update_diagnostics( -// LanguageServerId(0), -// lsp::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/b.rs").unwrap(), -// version: None, -// diagnostics: vec![lsp2::Diagnostic { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 4), -// lsp2::Position::new(0, 5), -// ), -// severity: Some(lsp2::DiagnosticSeverity::WARNING), -// message: "error 2".to_string(), -// ..Default::default() -// }], -// }, -// &[], -// cx, -// ) -// .unwrap(); -// }); - -// buffer_a.read_with(cx, |buffer, _| { -// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); -// assert_eq!( -// chunks -// .iter() -// .map(|(s, d)| (s.as_str(), *d)) -// .collect::>(), -// &[ -// ("let ", None), -// ("a", Some(DiagnosticSeverity::ERROR)), -// (" = 1;", None), -// ] -// ); -// }); -// buffer_b.read_with(cx, |buffer, _| { -// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); -// assert_eq!( -// chunks -// .iter() -// .map(|(s, d)| (s.as_str(), *d)) -// .collect::>(), -// &[ -// ("let ", None), -// ("b", Some(DiagnosticSeverity::WARNING)), -// (" = 2;", None), -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root", -// json!({ -// "dir": { -// "a.rs": "let a = 1;", -// }, -// "other.rs": "let b = c;" -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; - -// let (worktree, _) = project -// .update(cx, |project, cx| { -// project.find_or_create_local_worktree("/root/other.rs", false, cx) -// }) -// .await -// .unwrap(); -// let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); - -// project.update(cx, |project, cx| { -// project -// .update_diagnostics( -// LanguageServerId(0), -// lsp::PublishDiagnosticsParams { -// uri: Url::from_file_path("/root/other.rs").unwrap(), -// version: None, -// diagnostics: vec![lsp2::Diagnostic { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 8), -// lsp2::Position::new(0, 9), -// ), -// severity: Some(lsp2::DiagnosticSeverity::ERROR), -// message: "unknown variable 'c'".to_string(), -// ..Default::default() -// }], -// }, -// &[], -// cx, -// ) -// .unwrap(); -// }); - -// let buffer = project -// .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) -// .await -// .unwrap(); -// buffer.read_with(cx, |buffer, _| { -// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); -// assert_eq!( -// chunks -// .iter() -// .map(|(s, d)| (s.as_str(), *d)) -// .collect::>(), -// &[ -// ("let b = ", None), -// ("c", Some(DiagnosticSeverity::ERROR)), -// (";", None), -// ] -// ); -// }); - -// project.read_with(cx, |project, cx| { -// assert_eq!(project.diagnostic_summaries(cx).next(), None); -// assert_eq!(project.diagnostic_summary(cx).error_count, 0); -// }); -// } - -// #[gpui::test] -// async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let progress_token = "the-progress-token"; -// 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_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// disk_based_diagnostics_progress_token: Some(progress_token.into()), -// disk_based_diagnostics_sources: vec!["disk".into()], -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": "fn a() { A }", -// "b.rs": "const y: i32 = 1", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let worktree_id = project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id()); - -// // Cause worktree to start the fake language server -// let _buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) -// .await -// .unwrap(); - -// let mut events = subscribe(&project, cx); - -// let fake_server = fake_servers.next().await.unwrap(); -// assert_eq!( -// events.next().await.unwrap(), -// Event::LanguageServerAdded(LanguageServerId(0)), -// ); - -// fake_server -// .start_progress(format!("{}/0", progress_token)) -// .await; -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiskBasedDiagnosticsStarted { -// language_server_id: LanguageServerId(0), -// } -// ); - -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/a.rs").unwrap(), -// version: None, -// diagnostics: vec![lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), -// severity: Some(lsp2::DiagnosticSeverity::ERROR), -// message: "undefined variable 'A'".to_string(), -// ..Default::default() -// }], -// }); -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiagnosticsUpdated { -// language_server_id: LanguageServerId(0), -// path: (worktree_id, Path::new("a.rs")).into() -// } -// ); - -// fake_server.end_progress(format!("{}/0", progress_token)); -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiskBasedDiagnosticsFinished { -// language_server_id: LanguageServerId(0) -// } -// ); - -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// buffer.read_with(cx, |buffer, _| { -// let snapshot = buffer.snapshot(); -// let diagnostics = snapshot -// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) -// .collect::>(); -// assert_eq!( -// diagnostics, -// &[DiagnosticEntry { -// range: Point::new(0, 9)..Point::new(0, 10), -// diagnostic: Diagnostic { -// severity: lsp2::DiagnosticSeverity::ERROR, -// message: "undefined variable 'A'".to_string(), -// group_id: 0, -// is_primary: true, -// ..Default::default() -// } -// }] -// ) -// }); - -// // Ensure publishing empty diagnostics twice only results in one update event. -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/a.rs").unwrap(), -// version: None, -// diagnostics: Default::default(), -// }); -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiagnosticsUpdated { -// language_server_id: LanguageServerId(0), -// path: (worktree_id, Path::new("a.rs")).into() -// } -// ); - -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/a.rs").unwrap(), -// version: None, -// diagnostics: Default::default(), -// }); -// cx.foreground().run_until_parked(); -// assert_eq!(futures::poll!(events.next()), Poll::Pending); -// } - -// #[gpui::test] -// async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let progress_token = "the-progress-token"; -// let mut language = Language::new( -// LanguageConfig { -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// disk_based_diagnostics_sources: vec!["disk".into()], -// disk_based_diagnostics_progress_token: Some(progress_token.into()), -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": "" })).await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); - -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// // Simulate diagnostics starting to update. -// let fake_server = fake_servers.next().await.unwrap(); -// fake_server.start_progress(progress_token).await; - -// // Restart the server before the diagnostics finish updating. -// project.update(cx, |project, cx| { -// project.restart_language_servers_for_buffers([buffer], cx); -// }); -// let mut events = subscribe(&project, cx); - -// // Simulate the newly started server sending more diagnostics. -// let fake_server = fake_servers.next().await.unwrap(); -// assert_eq!( -// events.next().await.unwrap(), -// Event::LanguageServerAdded(LanguageServerId(1)) -// ); -// fake_server.start_progress(progress_token).await; -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiskBasedDiagnosticsStarted { -// language_server_id: LanguageServerId(1) -// } -// ); -// project.read_with(cx, |project, _| { -// assert_eq!( -// project -// .language_servers_running_disk_based_diagnostics() -// .collect::>(), -// [LanguageServerId(1)] -// ); -// }); - -// // All diagnostics are considered done, despite the old server's diagnostic -// // task never completing. -// fake_server.end_progress(progress_token); -// assert_eq!( -// events.next().await.unwrap(), -// Event::DiskBasedDiagnosticsFinished { -// language_server_id: LanguageServerId(1) -// } -// ); -// project.read_with(cx, |project, _| { -// assert_eq!( -// project -// .language_servers_running_disk_based_diagnostics() -// .collect::>(), -// [LanguageServerId(0); 0] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut language = Language::new( -// LanguageConfig { -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); - -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// // Publish diagnostics -// let fake_server = fake_servers.next().await.unwrap(); -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: Url::from_file_path("/dir/a.rs").unwrap(), -// version: None, -// diagnostics: vec![lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)), -// severity: Some(lsp2::DiagnosticSeverity::ERROR), -// message: "the message".to_string(), -// ..Default::default() -// }], -// }); - -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, usize>(0..1, false) -// .map(|entry| entry.diagnostic.message.clone()) -// .collect::>(), -// ["the message".to_string()] -// ); -// }); -// project.read_with(cx, |project, cx| { -// assert_eq!( -// project.diagnostic_summary(cx), -// DiagnosticSummary { -// error_count: 1, -// warning_count: 0, -// } -// ); -// }); - -// project.update(cx, |project, cx| { -// project.restart_language_servers_for_buffers([buffer.clone()], cx); -// }); - -// // The diagnostics are cleared. -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, usize>(0..1, false) -// .map(|entry| entry.diagnostic.message.clone()) -// .collect::>(), -// Vec::::new(), -// ); -// }); -// project.read_with(cx, |project, cx| { -// assert_eq!( -// project.diagnostic_summary(cx), -// DiagnosticSummary { -// error_count: 0, -// warning_count: 0, -// } -// ); -// }); -// } - -// #[gpui::test] -// async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut language = Language::new( -// LanguageConfig { -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-lsp", -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": "" })).await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); - -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// // Before restarting the server, report diagnostics with an unknown buffer version. -// let fake_server = fake_servers.next().await.unwrap(); -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), -// version: Some(10000), -// diagnostics: Vec::new(), -// }); -// cx.foreground().run_until_parked(); - -// project.update(cx, |project, cx| { -// project.restart_language_servers_for_buffers([buffer.clone()], cx); -// }); -// let mut fake_server = fake_servers.next().await.unwrap(); -// let notification = fake_server -// .receive_notification::() -// .await -// .text_document; -// assert_eq!(notification.version, 0); -// } - -// #[gpui::test] -// async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut rust = Language::new( -// LanguageConfig { -// name: Arc::from("Rust"), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_rust_servers = rust -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "rust-lsp", -// ..Default::default() -// })) -// .await; -// let mut js = Language::new( -// LanguageConfig { -// name: Arc::from("JavaScript"), -// path_suffixes: vec!["js".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_js_servers = js -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "js-lsp", -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages.add(Arc::new(rust)); -// project.languages.add(Arc::new(js)); -// }); - -// let _rs_buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); -// let _js_buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx)) -// .await -// .unwrap(); - -// let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap(); -// assert_eq!( -// fake_rust_server_1 -// .receive_notification::() -// .await -// .text_document -// .uri -// .as_str(), -// "file:///dir/a.rs" -// ); - -// let mut fake_js_server = fake_js_servers.next().await.unwrap(); -// assert_eq!( -// fake_js_server -// .receive_notification::() -// .await -// .text_document -// .uri -// .as_str(), -// "file:///dir/b.js" -// ); - -// // Disable Rust language server, ensuring only that server gets stopped. -// cx.update(|cx| { -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.languages.insert( -// Arc::from("Rust"), -// LanguageSettingsContent { -// enable_language_server: Some(false), -// ..Default::default() -// }, -// ); -// }); -// }) -// }); -// fake_rust_server_1 -// .receive_notification::() -// .await; - -// // Enable Rust and disable JavaScript language servers, ensuring that the -// // former gets started again and that the latter stops. -// cx.update(|cx| { -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.languages.insert( -// Arc::from("Rust"), -// LanguageSettingsContent { -// enable_language_server: Some(true), -// ..Default::default() -// }, -// ); -// settings.languages.insert( -// Arc::from("JavaScript"), -// LanguageSettingsContent { -// enable_language_server: Some(false), -// ..Default::default() -// }, -// ); -// }); -// }) -// }); -// let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); -// assert_eq!( -// fake_rust_server_2 -// .receive_notification::() -// .await -// .text_document -// .uri -// .as_str(), -// "file:///dir/a.rs" -// ); -// fake_js_server -// .receive_notification::() -// .await; -// } - -// #[gpui::test(iterations = 3)] -// async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// 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_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// disk_based_diagnostics_sources: vec!["disk".into()], -// ..Default::default() -// })) -// .await; - -// let text = " -// fn a() { A } -// fn b() { BB } -// fn c() { CCC } -// " -// .unindent(); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": text })).await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); - -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// let mut fake_server = fake_servers.next().await.unwrap(); -// let open_notification = fake_server -// .receive_notification::() -// .await; - -// // Edit the buffer, moving the content down -// buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); -// let change_notification_1 = fake_server -// .receive_notification::() -// .await; -// assert!(change_notification_1.text_document.version > open_notification.text_document.version); - -// // Report some diagnostics for the initial version of the buffer -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), -// version: Some(open_notification.text_document.version), -// diagnostics: vec![ -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), -// severity: Some(DiagnosticSeverity::ERROR), -// message: "undefined variable 'A'".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)), -// severity: Some(DiagnosticSeverity::ERROR), -// message: "undefined variable 'BB'".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)), -// severity: Some(DiagnosticSeverity::ERROR), -// source: Some("disk".to_string()), -// message: "undefined variable 'CCC'".to_string(), -// ..Default::default() -// }, -// ], -// }); - -// // The diagnostics have moved down since they were created. -// buffer.next_notification(cx).await; -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false) -// .collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(3, 9)..Point::new(3, 11), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::ERROR, -// message: "undefined variable 'BB'".to_string(), -// is_disk_based: true, -// group_id: 1, -// is_primary: true, -// ..Default::default() -// }, -// }, -// DiagnosticEntry { -// range: Point::new(4, 9)..Point::new(4, 12), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::ERROR, -// message: "undefined variable 'CCC'".to_string(), -// is_disk_based: true, -// group_id: 2, -// is_primary: true, -// ..Default::default() -// } -// } -// ] -// ); -// assert_eq!( -// chunks_with_diagnostics(buffer, 0..buffer.len()), -// [ -// ("\n\nfn a() { ".to_string(), None), -// ("A".to_string(), Some(DiagnosticSeverity::ERROR)), -// (" }\nfn b() { ".to_string(), None), -// ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), -// (" }\nfn c() { ".to_string(), None), -// ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), -// (" }\n".to_string(), None), -// ] -// ); -// assert_eq!( -// chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), -// [ -// ("B".to_string(), Some(DiagnosticSeverity::ERROR)), -// (" }\nfn c() { ".to_string(), None), -// ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), -// ] -// ); -// }); - -// // Ensure overlapping diagnostics are highlighted correctly. -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), -// version: Some(open_notification.text_document.version), -// diagnostics: vec![ -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), -// severity: Some(DiagnosticSeverity::ERROR), -// message: "undefined variable 'A'".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)), -// severity: Some(DiagnosticSeverity::WARNING), -// message: "unreachable statement".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// ], -// }); - -// buffer.next_notification(cx).await; -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false) -// .collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(2, 9)..Point::new(2, 12), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::WARNING, -// message: "unreachable statement".to_string(), -// is_disk_based: true, -// group_id: 4, -// is_primary: true, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(2, 9)..Point::new(2, 10), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::ERROR, -// message: "undefined variable 'A'".to_string(), -// is_disk_based: true, -// group_id: 3, -// is_primary: true, -// ..Default::default() -// }, -// } -// ] -// ); -// assert_eq!( -// chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), -// [ -// ("fn a() { ".to_string(), None), -// ("A".to_string(), Some(DiagnosticSeverity::ERROR)), -// (" }".to_string(), Some(DiagnosticSeverity::WARNING)), -// ("\n".to_string(), None), -// ] -// ); -// assert_eq!( -// chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), -// [ -// (" }".to_string(), Some(DiagnosticSeverity::WARNING)), -// ("\n".to_string(), None), -// ] -// ); -// }); - -// // Keep editing the buffer and ensure disk-based diagnostics get translated according to the -// // changes since the last save. -// buffer.update(cx, |buffer, cx| { -// buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); -// buffer.edit( -// [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], -// None, -// cx, -// ); -// buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); -// }); -// let change_notification_2 = fake_server -// .receive_notification::() -// .await; -// assert!( -// change_notification_2.text_document.version > change_notification_1.text_document.version -// ); - -// // Handle out-of-order diagnostics -// fake_server.notify::(lsp2::PublishDiagnosticsParams { -// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), -// version: Some(change_notification_2.text_document.version), -// diagnostics: vec![ -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)), -// severity: Some(DiagnosticSeverity::ERROR), -// message: "undefined variable 'BB'".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), -// severity: Some(DiagnosticSeverity::WARNING), -// message: "undefined variable 'A'".to_string(), -// source: Some("disk".to_string()), -// ..Default::default() -// }, -// ], -// }); - -// buffer.next_notification(cx).await; -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer -// .snapshot() -// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) -// .collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(2, 21)..Point::new(2, 22), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::WARNING, -// message: "undefined variable 'A'".to_string(), -// is_disk_based: true, -// group_id: 6, -// is_primary: true, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(3, 9)..Point::new(3, 14), -// diagnostic: Diagnostic { -// source: Some("disk".into()), -// severity: DiagnosticSeverity::ERROR, -// message: "undefined variable 'BB'".to_string(), -// is_disk_based: true, -// group_id: 5, -// is_primary: true, -// ..Default::default() -// }, -// } -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let text = concat!( -// "let one = ;\n", // -// "let two = \n", -// "let three = 3;\n", -// ); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": text })).await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// project.update(cx, |project, cx| { -// project -// .update_buffer_diagnostics( -// &buffer, -// LanguageServerId(0), -// None, -// vec![ -// DiagnosticEntry { -// range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// message: "syntax error 1".to_string(), -// ..Default::default() -// }, -// }, -// DiagnosticEntry { -// range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// message: "syntax error 2".to_string(), -// ..Default::default() -// }, -// }, -// ], -// cx, -// ) -// .unwrap(); -// }); - -// // An empty range is extended forward to include the following character. -// // At the end of a line, an empty range is extended backward to include -// // the preceding character. -// buffer.read_with(cx, |buffer, _| { -// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); -// assert_eq!( -// chunks -// .iter() -// .map(|(s, d)| (s.as_str(), *d)) -// .collect::>(), -// &[ -// ("let one = ", None), -// (";", Some(DiagnosticSeverity::ERROR)), -// ("\nlet two =", None), -// (" ", Some(DiagnosticSeverity::ERROR)), -// ("\nlet three = 3;\n", None) -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; - -// project.update(cx, |project, cx| { -// project -// .update_diagnostic_entries( -// LanguageServerId(0), -// Path::new("/dir/a.rs").to_owned(), -// None, -// vec![DiagnosticEntry { -// range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// is_primary: true, -// message: "syntax error a1".to_string(), -// ..Default::default() -// }, -// }], -// cx, -// ) -// .unwrap(); -// project -// .update_diagnostic_entries( -// LanguageServerId(1), -// Path::new("/dir/a.rs").to_owned(), -// None, -// vec![DiagnosticEntry { -// range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// is_primary: true, -// message: "syntax error b1".to_string(), -// ..Default::default() -// }, -// }], -// cx, -// ) -// .unwrap(); - -// assert_eq!( -// project.diagnostic_summary(cx), -// DiagnosticSummary { -// error_count: 2, -// warning_count: 0, -// } -// ); -// }); -// } - -// #[gpui::test] -// async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// 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_servers = language.set_fake_lsp_adapter(Default::default()).await; - -// let text = " -// fn a() { -// f1(); -// } -// fn b() { -// f2(); -// } -// fn c() { -// f3(); -// } -// " -// .unindent(); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": text.clone(), -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// let mut fake_server = fake_servers.next().await.unwrap(); -// let lsp_document_version = fake_server -// .receive_notification::() -// .await -// .text_document -// .version; - -// // Simulate editing the buffer after the language server computes some edits. -// buffer.update(cx, |buffer, cx| { -// buffer.edit( -// [( -// Point::new(0, 0)..Point::new(0, 0), -// "// above first function\n", -// )], -// None, -// cx, -// ); -// buffer.edit( -// [( -// Point::new(2, 0)..Point::new(2, 0), -// " // inside first function\n", -// )], -// None, -// cx, -// ); -// buffer.edit( -// [( -// Point::new(6, 4)..Point::new(6, 4), -// "// inside second function ", -// )], -// None, -// cx, -// ); - -// assert_eq!( -// buffer.text(), -// " -// // above first function -// fn a() { -// // inside first function -// f1(); -// } -// fn b() { -// // inside second function f2(); -// } -// fn c() { -// f3(); -// } -// " -// .unindent() -// ); -// }); - -// let edits = project -// .update(cx, |project, cx| { -// project.edits_from_lsp( -// &buffer, -// vec![ -// // replace body of first function -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 0), -// lsp2::Position::new(3, 0), -// ), -// new_text: " -// fn a() { -// f10(); -// } -// " -// .unindent(), -// }, -// // edit inside second function -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(4, 6), -// lsp2::Position::new(4, 6), -// ), -// new_text: "00".into(), -// }, -// // edit inside third function via two distinct edits -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(7, 5), -// lsp2::Position::new(7, 5), -// ), -// new_text: "4000".into(), -// }, -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(7, 5), -// lsp2::Position::new(7, 6), -// ), -// new_text: "".into(), -// }, -// ], -// LanguageServerId(0), -// Some(lsp_document_version), -// cx, -// ) -// }) -// .await -// .unwrap(); - -// buffer.update(cx, |buffer, cx| { -// for (range, new_text) in edits { -// buffer.edit([(range, new_text)], None, cx); -// } -// assert_eq!( -// buffer.text(), -// " -// // above first function -// fn a() { -// // inside first function -// f10(); -// } -// fn b() { -// // inside second function f200(); -// } -// fn c() { -// f4000(); -// } -// " -// .unindent() -// ); -// }); -// } - -// #[gpui::test] -// async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let text = " -// use a::b; -// use a::c; - -// fn f() { -// b(); -// c(); -// } -// " -// .unindent(); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": text.clone(), -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// // Simulate the language server sending us a small edit in the form of a very large diff. -// // Rust-analyzer does this when performing a merge-imports code action. -// let edits = project -// .update(cx, |project, cx| { -// project.edits_from_lsp( -// &buffer, -// [ -// // Replace the first use statement without editing the semicolon. -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 4), -// lsp2::Position::new(0, 8), -// ), -// new_text: "a::{b, c}".into(), -// }, -// // Reinsert the remainder of the file between the semicolon and the final -// // newline of the file. -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 9), -// lsp2::Position::new(0, 9), -// ), -// new_text: "\n\n".into(), -// }, -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 9), -// lsp2::Position::new(0, 9), -// ), -// new_text: " -// fn f() { -// b(); -// c(); -// }" -// .unindent(), -// }, -// // Delete everything after the first newline of the file. -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(1, 0), -// lsp2::Position::new(7, 0), -// ), -// new_text: "".into(), -// }, -// ], -// LanguageServerId(0), -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); - -// buffer.update(cx, |buffer, cx| { -// let edits = edits -// .into_iter() -// .map(|(range, text)| { -// ( -// range.start.to_point(buffer)..range.end.to_point(buffer), -// text, -// ) -// }) -// .collect::>(); - -// assert_eq!( -// edits, -// [ -// (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), -// (Point::new(1, 0)..Point::new(2, 0), "".into()) -// ] -// ); - -// for (range, new_text) in edits { -// buffer.edit([(range, new_text)], None, cx); -// } -// assert_eq!( -// buffer.text(), -// " -// use a::{b, c}; - -// fn f() { -// b(); -// c(); -// } -// " -// .unindent() -// ); -// }); -// } - -// #[gpui::test] -// async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let text = " -// use a::b; -// use a::c; - -// fn f() { -// b(); -// c(); -// } -// " -// .unindent(); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": text.clone(), -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) -// .await -// .unwrap(); - -// // Simulate the language server sending us edits in a non-ordered fashion, -// // with ranges sometimes being inverted or pointing to invalid locations. -// let edits = project -// .update(cx, |project, cx| { -// project.edits_from_lsp( -// &buffer, -// [ -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 9), -// lsp2::Position::new(0, 9), -// ), -// new_text: "\n\n".into(), -// }, -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 8), -// lsp2::Position::new(0, 4), -// ), -// new_text: "a::{b, c}".into(), -// }, -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(1, 0), -// lsp2::Position::new(99, 0), -// ), -// new_text: "".into(), -// }, -// lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 9), -// lsp2::Position::new(0, 9), -// ), -// new_text: " -// fn f() { -// b(); -// c(); -// }" -// .unindent(), -// }, -// ], -// LanguageServerId(0), -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); - -// buffer.update(cx, |buffer, cx| { -// let edits = edits -// .into_iter() -// .map(|(range, text)| { -// ( -// range.start.to_point(buffer)..range.end.to_point(buffer), -// text, -// ) -// }) -// .collect::>(); - -// assert_eq!( -// edits, -// [ -// (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), -// (Point::new(1, 0)..Point::new(2, 0), "".into()) -// ] -// ); - -// for (range, new_text) in edits { -// buffer.edit([(range, new_text)], None, cx); -// } -// assert_eq!( -// buffer.text(), -// " -// use a::{b, c}; - -// fn f() { -// b(); -// c(); -// } -// " -// .unindent() -// ); -// }); -// } - -// fn chunks_with_diagnostics( -// buffer: &Buffer, -// range: Range, -// ) -> Vec<(String, Option)> { -// let mut chunks: Vec<(String, Option)> = Vec::new(); -// for chunk in buffer.snapshot().chunks(range, true) { -// if chunks.last().map_or(false, |prev_chunk| { -// prev_chunk.1 == chunk.diagnostic_severity -// }) { -// chunks.last_mut().unwrap().0.push_str(chunk.text); -// } else { -// chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); -// } -// } -// chunks -// } - -// #[gpui::test(iterations = 10)] -// async fn test_definition(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// 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_servers = language.set_fake_lsp_adapter(Default::default()).await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.rs": "const fn a() { A }", -// "b.rs": "const y: i32 = crate::a()", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); - -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) -// .await -// .unwrap(); - -// let fake_server = fake_servers.next().await.unwrap(); -// fake_server.handle_request::(|params, _| async move { -// let params = params.text_document_position_params; -// assert_eq!( -// params.text_document.uri.to_file_path().unwrap(), -// Path::new("/dir/b.rs"), -// ); -// assert_eq!(params.position, lsp2::Position::new(0, 22)); - -// Ok(Some(lsp2::GotoDefinitionResponse::Scalar( -// lsp2::Location::new( -// lsp2::Url::from_file_path("/dir/a.rs").unwrap(), -// lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), -// ), -// ))) -// }); - -// let mut definitions = project -// .update(cx, |project, cx| project.definition(&buffer, 22, cx)) -// .await -// .unwrap(); - -// // Assert no new language server started -// cx.foreground().run_until_parked(); -// assert!(fake_servers.try_next().is_err()); - -// assert_eq!(definitions.len(), 1); -// let definition = definitions.pop().unwrap(); -// cx.update(|cx| { -// let target_buffer = definition.target.buffer.read(cx); -// assert_eq!( -// target_buffer -// .file() -// .unwrap() -// .as_local() -// .unwrap() -// .abs_path(cx), -// Path::new("/dir/a.rs"), -// ); -// assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); -// assert_eq!( -// list_worktrees(&project, cx), -// [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)] -// ); - -// drop(definition); -// }); -// cx.read(|cx| { -// assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); -// }); - -// fn list_worktrees<'a>( -// project: &'a ModelHandle, -// cx: &'a AppContext, -// ) -> Vec<(&'a Path, bool)> { -// project -// .read(cx) -// .worktrees(cx) -// .map(|worktree| { -// let worktree = worktree.read(cx); -// ( -// worktree.as_local().unwrap().abs_path().as_ref(), -// worktree.is_visible(), -// ) -// }) -// .collect::>() -// } -// } - -// #[gpui::test] -// async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut language = Language::new( -// LanguageConfig { -// name: "TypeScript".into(), -// path_suffixes: vec!["ts".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_typescript()), -// ); -// 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()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.ts": "", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) -// .await -// .unwrap(); - -// let fake_server = fake_language_servers.next().await.unwrap(); - -// let text = "let a = b.fqn"; -// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); -// let completions = project.update(cx, |project, cx| { -// project.completions(&buffer, text.len(), cx) -// }); - -// fake_server -// .handle_request::(|_, _| async move { -// Ok(Some(lsp2::CompletionResponse::Array(vec![ -// lsp2::CompletionItem { -// label: "fullyQualifiedName?".into(), -// insert_text: Some("fullyQualifiedName".into()), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await; -// let completions = completions.await.unwrap(); -// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); -// assert_eq!(completions.len(), 1); -// assert_eq!(completions[0].new_text, "fullyQualifiedName"); -// assert_eq!( -// completions[0].old_range.to_offset(&snapshot), -// text.len() - 3..text.len() -// ); - -// let text = "let a = \"atoms/cmp\""; -// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); -// let completions = project.update(cx, |project, cx| { -// project.completions(&buffer, text.len() - 1, cx) -// }); - -// fake_server -// .handle_request::(|_, _| async move { -// Ok(Some(lsp2::CompletionResponse::Array(vec![ -// lsp2::CompletionItem { -// label: "component".into(), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await; -// let completions = completions.await.unwrap(); -// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); -// assert_eq!(completions.len(), 1); -// assert_eq!(completions[0].new_text, "component"); -// assert_eq!( -// completions[0].old_range.to_offset(&snapshot), -// text.len() - 4..text.len() - 1 -// ); -// } - -// #[gpui::test] -// async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut language = Language::new( -// LanguageConfig { -// name: "TypeScript".into(), -// path_suffixes: vec!["ts".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_typescript()), -// ); -// 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()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.ts": "", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) -// .await -// .unwrap(); - -// let fake_server = fake_language_servers.next().await.unwrap(); - -// let text = "let a = b.fqn"; -// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); -// let completions = project.update(cx, |project, cx| { -// project.completions(&buffer, text.len(), cx) -// }); - -// fake_server -// .handle_request::(|_, _| async move { -// Ok(Some(lsp2::CompletionResponse::Array(vec![ -// lsp2::CompletionItem { -// label: "fullyQualifiedName?".into(), -// insert_text: Some("fully\rQualified\r\nName".into()), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await; -// let completions = completions.await.unwrap(); -// assert_eq!(completions.len(), 1); -// assert_eq!(completions[0].new_text, "fully\nQualified\nName"); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let mut language = Language::new( -// LanguageConfig { -// name: "TypeScript".into(), -// path_suffixes: vec!["ts".to_string()], -// ..Default::default() -// }, -// None, -// ); -// let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.ts": "a", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) -// .await -// .unwrap(); - -// let fake_server = fake_language_servers.next().await.unwrap(); - -// // Language server returns code actions that contain commands, and not edits. -// let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); -// fake_server -// .handle_request::(|_, _| async move { -// Ok(Some(vec![ -// lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction { -// title: "The code action".into(), -// command: Some(lsp::Command { -// title: "The command".into(), -// command: "_the/command".into(), -// arguments: Some(vec![json!("the-argument")]), -// }), -// ..Default::default() -// }), -// lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction { -// title: "two".into(), -// ..Default::default() -// }), -// ])) -// }) -// .next() -// .await; - -// let action = actions.await.unwrap()[0].clone(); -// let apply = project.update(cx, |project, cx| { -// project.apply_code_action(buffer.clone(), action, true, cx) -// }); - -// // Resolving the code action does not populate its edits. In absence of -// // edits, we must execute the given command. -// fake_server.handle_request::( -// |action, _| async move { Ok(action) }, -// ); - -// // While executing the command, the language server sends the editor -// // a `workspaceEdit` request. -// fake_server -// .handle_request::({ -// let fake = fake_server.clone(); -// move |params, _| { -// assert_eq!(params.command, "_the/command"); -// let fake = fake.clone(); -// async move { -// fake.server -// .request::( -// lsp2::ApplyWorkspaceEditParams { -// label: None, -// edit: lsp::WorkspaceEdit { -// changes: Some( -// [( -// lsp2::Url::from_file_path("/dir/a.ts").unwrap(), -// vec![lsp2::TextEdit { -// range: lsp2::Range::new( -// lsp2::Position::new(0, 0), -// lsp2::Position::new(0, 0), -// ), -// new_text: "X".into(), -// }], -// )] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }, -// }, -// ) -// .await -// .unwrap(); -// Ok(Some(json!(null))) -// } -// } -// }) -// .next() -// .await; - -// // Applying the code action returns a project transaction containing the edits -// // sent by the language server in its `workspaceEdit` request. -// let transaction = apply.await.unwrap(); -// assert!(transaction.0.contains_key(&buffer)); -// buffer.update(cx, |buffer, cx| { -// assert_eq!(buffer.text(), "Xa"); -// buffer.undo(cx); -// assert_eq!(buffer.text(), "a"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_save_file(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "file1": "the old contents", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) -// .await -// .unwrap(); -// buffer.update(cx, |buffer, cx| { -// assert_eq!(buffer.text(), "the old contents"); -// buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); -// }); - -// project -// .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) -// .await -// .unwrap(); - -// let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); -// assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); -// } - -// #[gpui::test] -// async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "file1": "the old contents", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await; -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) -// .await -// .unwrap(); -// buffer.update(cx, |buffer, cx| { -// buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); -// }); - -// project -// .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) -// .await -// .unwrap(); - -// let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); -// assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); -// } - -// #[gpui::test] -// async fn test_save_as(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/dir", json!({})).await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// let languages = project.read_with(cx, |project, _| project.languages().clone()); -// languages.register( -// "/some/path", -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".into()], -// ..Default::default() -// }, -// tree_sitter_rust::language(), -// vec![], -// |_| Default::default(), -// ); - -// let buffer = project.update(cx, |project, cx| { -// project.create_buffer("", None, cx).unwrap() -// }); -// buffer.update(cx, |buffer, cx| { -// buffer.edit([(0..0, "abc")], None, cx); -// assert!(buffer.is_dirty()); -// assert!(!buffer.has_conflict()); -// assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); -// }); -// project -// .update(cx, |project, cx| { -// project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); - -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, cx| { -// assert_eq!( -// buffer.file().unwrap().full_path(cx), -// Path::new("dir/file1.rs") -// ); -// assert!(!buffer.is_dirty()); -// assert!(!buffer.has_conflict()); -// assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); -// }); - -// let opened_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/dir/file1.rs", cx) -// }) -// .await -// .unwrap(); -// assert_eq!(opened_buffer, buffer); -// } - -// #[gpui::test(retries = 5)] -// async fn test_rescan_and_remote_updates( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx); -// cx.foreground().allow_parking(); - -// let dir = temp_tree(json!({ -// "a": { -// "file1": "", -// "file2": "", -// "file3": "", -// }, -// "b": { -// "c": { -// "file4": "", -// "file5": "", -// } -// } -// })); - -// let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; -// let rpc = project.read_with(cx, |p, _| p.client.clone()); - -// let buffer_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| { -// let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); -// async move { buffer.await.unwrap() } -// }; -// let id_for_path = |path: &'static str, cx: &gpui2::TestAppContext| { -// project.read_with(cx, |project, cx| { -// let tree = project.worktrees(cx).next().unwrap(); -// tree.read(cx) -// .entry_for_path(path) -// .unwrap_or_else(|| panic!("no entry for path {}", path)) -// .id -// }) -// }; - -// let buffer2 = buffer_for_path("a/file2", cx).await; -// let buffer3 = buffer_for_path("a/file3", cx).await; -// let buffer4 = buffer_for_path("b/c/file4", cx).await; -// let buffer5 = buffer_for_path("b/c/file5", cx).await; - -// let file2_id = id_for_path("a/file2", cx); -// let file3_id = id_for_path("a/file3", cx); -// let file4_id = id_for_path("b/c/file4", cx); - -// // Create a remote copy of this worktree. -// let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); - -// let metadata = tree.read_with(cx, |tree, _| tree.as_local().unwrap().metadata_proto()); - -// let updates = Arc::new(Mutex::new(Vec::new())); -// tree.update(cx, |tree, cx| { -// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { -// let updates = updates.clone(); -// move |update| { -// updates.lock().push(update); -// async { true } -// } -// }); -// }); - -// let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx)); -// deterministic.run_until_parked(); - -// cx.read(|cx| { -// assert!(!buffer2.read(cx).is_dirty()); -// assert!(!buffer3.read(cx).is_dirty()); -// assert!(!buffer4.read(cx).is_dirty()); -// assert!(!buffer5.read(cx).is_dirty()); -// }); - -// // Rename and delete files and directories. -// tree.flush_fs_events(cx).await; -// std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); -// std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); -// std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); -// std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); -// tree.flush_fs_events(cx).await; - -// let expected_paths = vec![ -// "a", -// "a/file1", -// "a/file2.new", -// "b", -// "d", -// "d/file3", -// "d/file4", -// ]; - -// cx.read(|app| { -// assert_eq!( -// tree.read(app) -// .paths() -// .map(|p| p.to_str().unwrap()) -// .collect::>(), -// expected_paths -// ); - -// assert_eq!(id_for_path("a/file2.new", cx), file2_id); -// assert_eq!(id_for_path("d/file3", cx), file3_id); -// assert_eq!(id_for_path("d/file4", cx), file4_id); - -// assert_eq!( -// buffer2.read(app).file().unwrap().path().as_ref(), -// Path::new("a/file2.new") -// ); -// assert_eq!( -// buffer3.read(app).file().unwrap().path().as_ref(), -// Path::new("d/file3") -// ); -// assert_eq!( -// buffer4.read(app).file().unwrap().path().as_ref(), -// Path::new("d/file4") -// ); -// assert_eq!( -// buffer5.read(app).file().unwrap().path().as_ref(), -// Path::new("b/c/file5") -// ); - -// assert!(!buffer2.read(app).file().unwrap().is_deleted()); -// assert!(!buffer3.read(app).file().unwrap().is_deleted()); -// assert!(!buffer4.read(app).file().unwrap().is_deleted()); -// assert!(buffer5.read(app).file().unwrap().is_deleted()); -// }); - -// // Update the remote worktree. Check that it becomes consistent with the -// // local worktree. -// deterministic.run_until_parked(); -// remote.update(cx, |remote, _| { -// for update in updates.lock().drain(..) { -// remote.as_remote_mut().unwrap().update_from_remote(update); -// } -// }); -// deterministic.run_until_parked(); -// remote.read_with(cx, |remote, _| { -// assert_eq!( -// remote -// .paths() -// .map(|p| p.to_str().unwrap()) -// .collect::>(), -// expected_paths -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_buffer_identity_across_renames( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a": { -// "file1": "", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs, [Path::new("/dir")], cx).await; -// let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); -// let tree_id = tree.read_with(cx, |tree, _| tree.id()); - -// let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { -// project.read_with(cx, |project, cx| { -// let tree = project.worktrees(cx).next().unwrap(); -// tree.read(cx) -// .entry_for_path(path) -// .unwrap_or_else(|| panic!("no entry for path {}", path)) -// .id -// }) -// }; - -// let dir_id = id_for_path("a", cx); -// let file_id = id_for_path("a/file1", cx); -// let buffer = project -// .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx)) -// .await -// .unwrap(); -// buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty())); - -// project -// .update(cx, |project, cx| { -// project.rename_entry(dir_id, Path::new("b"), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// deterministic.run_until_parked(); -// assert_eq!(id_for_path("b", cx), dir_id); -// assert_eq!(id_for_path("b/file1", cx), file_id); -// buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty())); -// } - -// #[gpui2::test] -// async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// // Spawn multiple tasks to open paths, repeating some paths. -// let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { -// ( -// p.open_local_buffer("/dir/a.txt", cx), -// p.open_local_buffer("/dir/b.txt", cx), -// p.open_local_buffer("/dir/a.txt", cx), -// ) -// }); - -// let buffer_a_1 = buffer_a_1.await.unwrap(); -// let buffer_a_2 = buffer_a_2.await.unwrap(); -// let buffer_b = buffer_b.await.unwrap(); -// assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents"); -// assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents"); - -// // There is only one buffer per path. -// let buffer_a_id = buffer_a_1.id(); -// assert_eq!(buffer_a_2.id(), buffer_a_id); - -// // Open the same path again while it is still open. -// drop(buffer_a_1); -// let buffer_a_3 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx)) -// .await -// .unwrap(); - -// // There's still only one buffer per path. -// assert_eq!(buffer_a_3.id(), buffer_a_id); -// } - -// #[gpui2::test] -// async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "file1": "abc", -// "file2": "def", -// "file3": "ghi", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// let buffer1 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) -// .await -// .unwrap(); -// let events = Rc::new(RefCell::new(Vec::new())); - -// // initially, the buffer isn't dirty. -// buffer1.update(cx, |buffer, cx| { -// cx.subscribe(&buffer1, { -// let events = events.clone(); -// move |_, _, event, _| match event { -// BufferEvent::Operation(_) => {} -// _ => events.borrow_mut().push(event.clone()), -// } -// }) -// .detach(); - -// assert!(!buffer.is_dirty()); -// assert!(events.borrow().is_empty()); - -// buffer.edit([(1..2, "")], None, cx); -// }); - -// // after the first edit, the buffer is dirty, and emits a dirtied event. -// buffer1.update(cx, |buffer, cx| { -// assert!(buffer.text() == "ac"); -// assert!(buffer.is_dirty()); -// assert_eq!( -// *events.borrow(), -// &[language2::Event::Edited, language2::Event::DirtyChanged] -// ); -// events.borrow_mut().clear(); -// buffer.did_save( -// buffer.version(), -// buffer.as_rope().fingerprint(), -// buffer.file().unwrap().mtime(), -// cx, -// ); -// }); - -// // after saving, the buffer is not dirty, and emits a saved event. -// buffer1.update(cx, |buffer, cx| { -// assert!(!buffer.is_dirty()); -// assert_eq!(*events.borrow(), &[language2::Event::Saved]); -// events.borrow_mut().clear(); - -// buffer.edit([(1..1, "B")], None, cx); -// buffer.edit([(2..2, "D")], None, cx); -// }); - -// // after editing again, the buffer is dirty, and emits another dirty event. -// buffer1.update(cx, |buffer, cx| { -// assert!(buffer.text() == "aBDc"); -// assert!(buffer.is_dirty()); -// assert_eq!( -// *events.borrow(), -// &[ -// language2::Event::Edited, -// language2::Event::DirtyChanged, -// language2::Event::Edited, -// ], -// ); -// events.borrow_mut().clear(); - -// // After restoring the buffer to its previously-saved state, -// // the buffer is not considered dirty anymore. -// buffer.edit([(1..3, "")], None, cx); -// assert!(buffer.text() == "ac"); -// assert!(!buffer.is_dirty()); -// }); - -// assert_eq!( -// *events.borrow(), -// &[language2::Event::Edited, language2::Event::DirtyChanged] -// ); - -// // When a file is deleted, the buffer is considered dirty. -// let events = Rc::new(RefCell::new(Vec::new())); -// let buffer2 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) -// .await -// .unwrap(); -// buffer2.update(cx, |_, cx| { -// cx.subscribe(&buffer2, { -// let events = events.clone(); -// move |_, _, event, _| events.borrow_mut().push(event.clone()) -// }) -// .detach(); -// }); - -// fs.remove_file("/dir/file2".as_ref(), Default::default()) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty())); -// assert_eq!( -// *events.borrow(), -// &[ -// language2::Event::DirtyChanged, -// language2::Event::FileHandleChanged -// ] -// ); - -// // When a file is already dirty when deleted, we don't emit a Dirtied event. -// let events = Rc::new(RefCell::new(Vec::new())); -// let buffer3 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) -// .await -// .unwrap(); -// buffer3.update(cx, |_, cx| { -// cx.subscribe(&buffer3, { -// let events = events.clone(); -// move |_, _, event, _| events.borrow_mut().push(event.clone()) -// }) -// .detach(); -// }); - -// buffer3.update(cx, |buffer, cx| { -// buffer.edit([(0..0, "x")], None, cx); -// }); -// events.borrow_mut().clear(); -// fs.remove_file("/dir/file3".as_ref(), Default::default()) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// assert_eq!(*events.borrow(), &[language2::Event::FileHandleChanged]); -// cx.read(|cx| assert!(buffer3.read(cx).is_dirty())); -// } - -// #[gpui::test] -// async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let initial_contents = "aaa\nbbbbb\nc\n"; -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "the-file": initial_contents, -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) -// .await -// .unwrap(); - -// let anchors = (0..3) -// .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1)))) -// .collect::>(); - -// // Change the file on disk, adding two new lines of text, and removing -// // one line. -// buffer.read_with(cx, |buffer, _| { -// assert!(!buffer.is_dirty()); -// assert!(!buffer.has_conflict()); -// }); -// let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; -// fs.save( -// "/dir/the-file".as_ref(), -// &new_contents.into(), -// LineEnding::Unix, -// ) -// .await -// .unwrap(); - -// // Because the buffer was not modified, it is reloaded from disk. Its -// // contents are edited according to the diff between the old and new -// // file contents. -// cx.foreground().run_until_parked(); -// buffer.update(cx, |buffer, _| { -// assert_eq!(buffer.text(), new_contents); -// assert!(!buffer.is_dirty()); -// assert!(!buffer.has_conflict()); - -// let anchor_positions = anchors -// .iter() -// .map(|anchor| anchor.to_point(&*buffer)) -// .collect::>(); -// assert_eq!( -// anchor_positions, -// [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)] -// ); -// }); - -// // Modify the buffer -// buffer.update(cx, |buffer, cx| { -// buffer.edit([(0..0, " ")], None, cx); -// assert!(buffer.is_dirty()); -// assert!(!buffer.has_conflict()); -// }); - -// // Change the file on disk again, adding blank lines to the beginning. -// fs.save( -// "/dir/the-file".as_ref(), -// &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), -// LineEnding::Unix, -// ) -// .await -// .unwrap(); - -// // Because the buffer is modified, it doesn't reload from disk, but is -// // marked as having a conflict. -// cx.foreground().run_until_parked(); -// buffer.read_with(cx, |buffer, _| { -// assert!(buffer.has_conflict()); -// }); -// } - -// #[gpui::test] -// async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "file1": "a\nb\nc\n", -// "file2": "one\r\ntwo\r\nthree\r\n", -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; -// let buffer1 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) -// .await -// .unwrap(); -// let buffer2 = project -// .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) -// .await -// .unwrap(); - -// buffer1.read_with(cx, |buffer, _| { -// assert_eq!(buffer.text(), "a\nb\nc\n"); -// assert_eq!(buffer.line_ending(), LineEnding::Unix); -// }); -// buffer2.read_with(cx, |buffer, _| { -// assert_eq!(buffer.text(), "one\ntwo\nthree\n"); -// assert_eq!(buffer.line_ending(), LineEnding::Windows); -// }); - -// // Change a file's line endings on disk from unix to windows. The buffer's -// // state updates correctly. -// fs.save( -// "/dir/file1".as_ref(), -// &"aaa\nb\nc\n".into(), -// LineEnding::Windows, -// ) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// buffer1.read_with(cx, |buffer, _| { -// assert_eq!(buffer.text(), "aaa\nb\nc\n"); -// assert_eq!(buffer.line_ending(), LineEnding::Windows); -// }); - -// // Save a file with windows line endings. The file is written correctly. -// buffer2.update(cx, |buffer, cx| { -// buffer.set_text("one\ntwo\nthree\nfour\n", cx); -// }); -// project -// .update(cx, |project, cx| project.save_buffer(buffer2, cx)) -// .await -// .unwrap(); -// assert_eq!( -// fs.load("/dir/file2".as_ref()).await.unwrap(), -// "one\r\ntwo\r\nthree\r\nfour\r\n", -// ); -// } - -// #[gpui::test] -// async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/the-dir", -// json!({ -// "a.rs": " -// fn foo(mut v: Vec) { -// for x in &v { -// v.push(1); -// } -// } -// " -// .unindent(), -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await; -// let buffer = project -// .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) -// .await -// .unwrap(); - -// let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap(); -// let message = lsp::PublishDiagnosticsParams { -// uri: buffer_uri.clone(), -// diagnostics: vec![ -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)), -// severity: Some(DiagnosticSeverity::WARNING), -// message: "error 1".to_string(), -// related_information: Some(vec![lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri.clone(), -// range: lsp2::Range::new( -// lsp2::Position::new(1, 8), -// lsp2::Position::new(1, 9), -// ), -// }, -// message: "error 1 hint 1".to_string(), -// }]), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)), -// severity: Some(DiagnosticSeverity::HINT), -// message: "error 1 hint 1".to_string(), -// related_information: Some(vec![lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri.clone(), -// range: lsp2::Range::new( -// lsp2::Position::new(1, 8), -// lsp2::Position::new(1, 9), -// ), -// }, -// message: "original diagnostic".to_string(), -// }]), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)), -// severity: Some(DiagnosticSeverity::ERROR), -// message: "error 2".to_string(), -// related_information: Some(vec![ -// lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri.clone(), -// range: lsp2::Range::new( -// lsp2::Position::new(1, 13), -// lsp2::Position::new(1, 15), -// ), -// }, -// message: "error 2 hint 1".to_string(), -// }, -// lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri.clone(), -// range: lsp2::Range::new( -// lsp2::Position::new(1, 13), -// lsp2::Position::new(1, 15), -// ), -// }, -// message: "error 2 hint 2".to_string(), -// }, -// ]), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)), -// severity: Some(DiagnosticSeverity::HINT), -// message: "error 2 hint 1".to_string(), -// related_information: Some(vec![lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri.clone(), -// range: lsp2::Range::new( -// lsp2::Position::new(2, 8), -// lsp2::Position::new(2, 17), -// ), -// }, -// message: "original diagnostic".to_string(), -// }]), -// ..Default::default() -// }, -// lsp2::Diagnostic { -// range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)), -// severity: Some(DiagnosticSeverity::HINT), -// message: "error 2 hint 2".to_string(), -// related_information: Some(vec![lsp::DiagnosticRelatedInformation { -// location: lsp::Location { -// uri: buffer_uri, -// range: lsp2::Range::new( -// lsp2::Position::new(2, 8), -// lsp2::Position::new(2, 17), -// ), -// }, -// message: "original diagnostic".to_string(), -// }]), -// ..Default::default() -// }, -// ], -// version: None, -// }; - -// project -// .update(cx, |p, cx| { -// p.update_diagnostics(LanguageServerId(0), message, &[], cx) -// }) -// .unwrap(); -// let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - -// assert_eq!( -// buffer -// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) -// .collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(1, 8)..Point::new(1, 9), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::WARNING, -// message: "error 1".to_string(), -// group_id: 1, -// is_primary: true, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(1, 8)..Point::new(1, 9), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 1 hint 1".to_string(), -// group_id: 1, -// is_primary: false, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(1, 13)..Point::new(1, 15), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 2 hint 1".to_string(), -// group_id: 0, -// is_primary: false, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(1, 13)..Point::new(1, 15), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 2 hint 2".to_string(), -// group_id: 0, -// is_primary: false, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(2, 8)..Point::new(2, 17), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// message: "error 2".to_string(), -// group_id: 0, -// is_primary: true, -// ..Default::default() -// } -// } -// ] -// ); - -// assert_eq!( -// buffer.diagnostic_group::(0).collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(1, 13)..Point::new(1, 15), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 2 hint 1".to_string(), -// group_id: 0, -// is_primary: false, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(1, 13)..Point::new(1, 15), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 2 hint 2".to_string(), -// group_id: 0, -// is_primary: false, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(2, 8)..Point::new(2, 17), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::ERROR, -// message: "error 2".to_string(), -// group_id: 0, -// is_primary: true, -// ..Default::default() -// } -// } -// ] -// ); - -// assert_eq!( -// buffer.diagnostic_group::(1).collect::>(), -// &[ -// DiagnosticEntry { -// range: Point::new(1, 8)..Point::new(1, 9), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::WARNING, -// message: "error 1".to_string(), -// group_id: 1, -// is_primary: true, -// ..Default::default() -// } -// }, -// DiagnosticEntry { -// range: Point::new(1, 8)..Point::new(1, 9), -// diagnostic: Diagnostic { -// severity: DiagnosticSeverity::HINT, -// message: "error 1 hint 1".to_string(), -// group_id: 1, -// is_primary: false, -// ..Default::default() -// } -// }, -// ] -// ); -// } - -// #[gpui::test] -// async fn test_rename(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// 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_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp2::ServerCapabilities { -// rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions { -// prepare_provider: Some(true), -// work_done_progress_options: Default::default(), -// })), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "one.rs": "const ONE: usize = 1;", -// "two.rs": "const TWO: usize = one::ONE + one::ONE;" -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages.add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/dir/one.rs", cx) -// }) -// .await -// .unwrap(); - -// let fake_server = fake_servers.next().await.unwrap(); - -// let response = project.update(cx, |project, cx| { -// project.prepare_rename(buffer.clone(), 7, cx) -// }); -// fake_server -// .handle_request::(|params, _| async move { -// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); -// assert_eq!(params.position, lsp2::Position::new(0, 7)); -// Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new( -// lsp2::Position::new(0, 6), -// lsp2::Position::new(0, 9), -// )))) -// }) -// .next() -// .await -// .unwrap(); -// let range = response.await.unwrap().unwrap(); -// let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer)); -// assert_eq!(range, 6..9); - -// let response = project.update(cx, |project, cx| { -// project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) -// }); -// fake_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, -// lsp2::Position::new(0, 7) -// ); -// assert_eq!(params.new_name, "THREE"); -// Ok(Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp2::Url::from_file_path("/dir/one.rs").unwrap(), -// vec![lsp2::TextEdit::new( -// lsp2::Range::new( -// lsp2::Position::new(0, 6), -// lsp2::Position::new(0, 9), -// ), -// "THREE".to_string(), -// )], -// ), -// ( -// lsp2::Url::from_file_path("/dir/two.rs").unwrap(), -// vec![ -// lsp2::TextEdit::new( -// lsp2::Range::new( -// lsp2::Position::new(0, 24), -// lsp2::Position::new(0, 27), -// ), -// "THREE".to_string(), -// ), -// lsp2::TextEdit::new( -// lsp2::Range::new( -// lsp2::Position::new(0, 35), -// lsp2::Position::new(0, 38), -// ), -// "THREE".to_string(), -// ), -// ], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// })) -// }) -// .next() -// .await -// .unwrap(); -// let mut transaction = response.await.unwrap().0; -// assert_eq!(transaction.len(), 2); -// assert_eq!( -// transaction -// .remove_entry(&buffer) -// .unwrap() -// .0 -// .read_with(cx, |buffer, _| buffer.text()), -// "const THREE: usize = 1;" -// ); -// assert_eq!( -// transaction -// .into_keys() -// .next() -// .unwrap() -// .read_with(cx, |buffer, _| buffer.text()), -// "const TWO: usize = one::THREE + one::THREE;" -// ); -// } - -// #[gpui::test] -// async fn test_search(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// 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; -// assert_eq!( -// search( -// &project, -// SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("two.rs".to_string(), vec![6..9]), -// ("three.rs".to_string(), vec![37..40]) -// ]) -// ); - -// let buffer_4 = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/dir/four.rs", cx) -// }) -// .await -// .unwrap(); -// buffer_4.update(cx, |buffer, cx| { -// let text = "two::TWO"; -// buffer.edit([(20..28, text), (31..43, text)], None, cx); -// }); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("two.rs".to_string(), vec![6..9]), -// ("three.rs".to_string(), vec![37..40]), -// ("four.rs".to_string(), vec![25..28, 36..39]) -// ]) -// ); -// } - -// #[gpui::test] -// async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let search_query = "file"; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "one.rs": r#"// Rust file one"#, -// "one.ts": r#"// TypeScript file one"#, -// "two.rs": r#"// Rust file two"#, -// "two.ts": r#"// TypeScript file two"#, -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// assert!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![PathMatcher::new("*.odd").unwrap()], -// Vec::new() -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap() -// .is_empty(), -// "If no inclusions match, no files should be returned" -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![PathMatcher::new("*.rs").unwrap()], -// Vec::new() -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.rs".to_string(), vec![8..12]), -// ("two.rs".to_string(), vec![8..12]), -// ]), -// "Rust only search should give only Rust files" -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![ -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap(), -// ], -// Vec::new() -// ).unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.ts".to_string(), vec![14..18]), -// ("two.ts".to_string(), vec![14..18]), -// ]), -// "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything" -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![ -// PathMatcher::new("*.rs").unwrap(), -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap(), -// ], -// Vec::new() -// ).unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.rs".to_string(), vec![8..12]), -// ("one.ts".to_string(), vec![14..18]), -// ("two.rs".to_string(), vec![8..12]), -// ("two.ts".to_string(), vec![14..18]), -// ]), -// "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything" -// ); -// } - -// #[gpui::test] -// async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let search_query = "file"; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "one.rs": r#"// Rust file one"#, -// "one.ts": r#"// TypeScript file one"#, -// "two.rs": r#"// Rust file two"#, -// "two.ts": r#"// TypeScript file two"#, -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// Vec::new(), -// vec![PathMatcher::new("*.odd").unwrap()], -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.rs".to_string(), vec![8..12]), -// ("one.ts".to_string(), vec![14..18]), -// ("two.rs".to_string(), vec![8..12]), -// ("two.ts".to_string(), vec![14..18]), -// ]), -// "If no exclusions match, all files should be returned" -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// Vec::new(), -// vec![PathMatcher::new("*.rs").unwrap()], -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.ts".to_string(), vec![14..18]), -// ("two.ts".to_string(), vec![14..18]), -// ]), -// "Rust exclusion search should give only TypeScript files" -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// Vec::new(), -// vec![ -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap(), -// ], -// ).unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.rs".to_string(), vec![8..12]), -// ("two.rs".to_string(), vec![8..12]), -// ]), -// "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything" -// ); - -// assert!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// Vec::new(), -// vec![ -// PathMatcher::new("*.rs").unwrap(), -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap(), -// ], -// ).unwrap(), -// cx -// ) -// .await -// .unwrap().is_empty(), -// "Rust and typescript exclusion should give no files, even if other exclusions don't match anything" -// ); -// } - -// #[gpui::test] -// async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let search_query = "file"; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/dir", -// json!({ -// "one.rs": r#"// Rust file one"#, -// "one.ts": r#"// TypeScript file one"#, -// "two.rs": r#"// Rust file two"#, -// "two.ts": r#"// TypeScript file two"#, -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - -// assert!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![PathMatcher::new("*.odd").unwrap()], -// vec![PathMatcher::new("*.odd").unwrap()], -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap() -// .is_empty(), -// "If both no exclusions and inclusions match, exclusions should win and return nothing" -// ); - -// assert!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![PathMatcher::new("*.ts").unwrap()], -// vec![PathMatcher::new("*.ts").unwrap()], -// ).unwrap(), -// cx -// ) -// .await -// .unwrap() -// .is_empty(), -// "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files." -// ); - -// assert!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![ -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap() -// ], -// vec![ -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap() -// ], -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap() -// .is_empty(), -// "Non-matching inclusions and exclusions should not change that." -// ); - -// assert_eq!( -// search( -// &project, -// SearchQuery::text( -// search_query, -// false, -// true, -// vec![ -// PathMatcher::new("*.ts").unwrap(), -// PathMatcher::new("*.odd").unwrap() -// ], -// vec![ -// PathMatcher::new("*.rs").unwrap(), -// PathMatcher::new("*.odd").unwrap() -// ], -// ) -// .unwrap(), -// cx -// ) -// .await -// .unwrap(), -// HashMap::from_iter([ -// ("one.ts".to_string(), vec![14..18]), -// ("two.ts".to_string(), vec![14..18]), -// ]), -// "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files" -// ); -// } - -// #[test] -// fn test_glob_literal_prefix() { -// assert_eq!(glob_literal_prefix("**/*.js"), ""); -// assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules"); -// assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo"); -// assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); -// } - -// async fn search( -// project: &ModelHandle, -// query: SearchQuery, -// cx: &mut gpui::TestAppContext, -// ) -> Result>>> { -// let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); -// let mut result = HashMap::default(); -// while let Some((buffer, range)) = search_rx.next().await { -// result.entry(buffer).or_insert(range); -// } -// Ok(result -// .into_iter() -// .map(|(buffer, ranges)| { -// buffer.read_with(cx, |buffer, _| { -// let path = buffer.file().unwrap().path().to_string_lossy().to_string(); -// let ranges = ranges -// .into_iter() -// .map(|range| range.to_offset(buffer)) -// .collect::>(); -// (path, ranges) -// }) -// }) -// .collect()) -// } - -// fn init_test(cx: &mut gpui::TestAppContext) { -// cx.foreground().forbid_parking(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// language2::init(cx); -// Project::init_settings(cx); -// }); -// } +use crate::{Event, *}; +use fs::FakeFs; +use futures::{future, StreamExt}; +use gpui::AppContext; +use language::{ + language_settings::{AllLanguageSettings, LanguageSettingsContent}, + tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, + LineEnding, OffsetRangeExt, Point, ToPoint, +}; +use lsp::Url; +use parking_lot::Mutex; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::{os, task::Poll}; +use unindent::Unindent as _; +use util::{assert_set_eq, paths::PathMatcher, test::temp_tree}; + +#[gpui::test] +async fn test_block_via_channel(cx: &mut gpui::TestAppContext) { + cx.executor().allow_parking(); + + let (tx, mut rx) = futures::channel::mpsc::unbounded(); + let _thread = std::thread::spawn(move || { + std::fs::metadata("/Users").unwrap(); + std::thread::sleep(Duration::from_millis(1000)); + tx.unbounded_send(1).unwrap(); + }); + rx.next().await.unwrap(); +} + +#[gpui::test] +async fn test_block_via_smol(cx: &mut gpui::TestAppContext) { + cx.executor().allow_parking(); + + let io_task = smol::unblock(move || { + println!("sleeping on thread {:?}", std::thread::current().id()); + std::thread::sleep(Duration::from_millis(10)); + 1 + }); + + let task = cx.foreground_executor().spawn(async move { + io_task.await; + }); + + task.await; +} + +#[gpui::test] +async fn test_symlinks(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let dir = temp_tree(json!({ + "root": { + "apple": "", + "banana": { + "carrot": { + "date": "", + "endive": "", + } + }, + "fennel": { + "grape": "", + } + } + })); + + let root_link_path = dir.path().join("root_link"); + os::unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); + os::unix::fs::symlink( + &dir.path().join("root/fennel"), + &dir.path().join("root/finnochio"), + ) + .unwrap(); + + let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await; + + project.update(cx, |project, cx| { + let tree = project.worktrees().next().unwrap().read(cx); + assert_eq!(tree.file_count(), 5); + assert_eq!( + tree.inode_for_path("fennel/grape"), + tree.inode_for_path("finnochio/grape") + ); + }); +} + +#[gpui::test] +async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/the-root", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "b.rs": "fn b() {\n B\n}" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap()); + + cx.executor().run_until_parked(); + cx.update(|cx| { + let tree = worktree.read(cx); + + let settings_a = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + let settings_b = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + + assert_eq!(settings_a.tab_size.get(), 8); + assert_eq!(settings_b.tab_size.get(), 2); + }); +} + +#[gpui::test] +async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut rust_language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut json_language = Language::new( + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_rust_servers = rust_language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-rust-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + let mut fake_json_servers = json_language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-json-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/the-root", + json!({ + "test.rs": "const A: i32 = 1;", + "test2.rs": "", + "Cargo.toml": "a = 1", + "package.json": "{\"a\": 1}", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + // Open a buffer without an associated language server. + let toml_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/Cargo.toml", cx) + }) + .await + .unwrap(); + + // Open a buffer with an associated language server before the language for it has been loaded. + let rust_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/test.rs", cx) + }) + .await + .unwrap(); + rust_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.language().map(|l| l.name()), None); + }); + + // Now we add the languages to the project, and ensure they get assigned to all + // the relevant open buffers. + project.update(cx, |project, _| { + project.languages.add(Arc::new(json_language)); + project.languages.add(Arc::new(rust_language)); + }); + cx.executor().run_until_parked(); + rust_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into())); + }); + + // A server is started up, and it is notified about Rust files. + let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 0, + text: "const A: i32 = 1;".to_string(), + language_id: Default::default() + } + ); + + // The buffer is configured based on the language server's capabilities. + rust_buffer.update(cx, |buffer, _| { + assert_eq!( + buffer.completion_triggers(), + &[".".to_string(), "::".to_string()] + ); + }); + toml_buffer.update(cx, |buffer, _| { + assert!(buffer.completion_triggers().is_empty()); + }); + + // Edit a buffer. The changes are reported to the language server. + rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + 1 + ) + ); + + // Open a third buffer with a different associated language server. + let json_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/package.json", cx) + }) + .await + .unwrap(); + + // A json language server is started up and is only notified about the json buffer. + let mut fake_json_server = fake_json_servers.next().await.unwrap(); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + + // This buffer is configured based on the second language server's + // capabilities. + json_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.completion_triggers(), &[":".to_string()]); + }); + + // When opening another buffer whose language server is already running, + // it is also configured based on the existing language server's capabilities. + let rust_buffer2 = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/test2.rs", cx) + }) + .await + .unwrap(); + rust_buffer2.update(cx, |buffer, _| { + assert_eq!( + buffer.completion_triggers(), + &[".".to_string(), "::".to_string()] + ); + }); + + // Changes are reported only to servers matching the buffer's language. + toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); + rust_buffer2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "let x = 1;")], None, cx) + }); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), + 1 + ) + ); + + // Save notifications are reported to all servers. + project + .update(cx, |project, cx| project.save_buffer(toml_buffer, cx)) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()) + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()) + ); + + // Renames are reported only to servers matching the buffer's language. + fs.rename( + Path::new("/the-root/test2.rs"), + Path::new("/the-root/test3.rs"), + Default::default(), + ) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test2.rs").unwrap()), + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), + version: 0, + text: rust_buffer2.update(cx, |buffer, _| buffer.text()), + language_id: Default::default() + }, + ); + + rust_buffer2.update(cx, |buffer, cx| { + buffer.update_diagnostics( + LanguageServerId(0), + DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + diagnostic: Default::default(), + range: Anchor::MIN..Anchor::MAX, + }], + &buffer.snapshot(), + ), + cx, + ); + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..buffer.len(), false) + .count(), + 1 + ); + }); + + // When the rename changes the extension of the file, the buffer gets closed on the old + // language server and gets opened on the new one. + fs.rename( + Path::new("/the-root/test3.rs"), + Path::new("/the-root/test3.json"), + Default::default(), + ) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),), + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + version: 0, + text: rust_buffer2.update(cx, |buffer, _| buffer.text()), + language_id: Default::default() + }, + ); + + // We clear the diagnostics, since the language has changed. + rust_buffer2.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..buffer.len(), false) + .count(), + 0 + ); + }); + + // The renamed file's version resets after changing language server. + rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + 1 + ) + ); + + // Restart language servers + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers( + vec![rust_buffer.clone(), json_buffer.clone()], + cx, + ); + }); + + let mut rust_shutdown_requests = fake_rust_server + .handle_request::(|_, _| future::ready(Ok(()))); + let mut json_shutdown_requests = fake_json_server + .handle_request::(|_, _| future::ready(Ok(()))); + futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); + + let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); + let mut fake_json_server = fake_json_servers.next().await.unwrap(); + + // Ensure rust document is reopened in new rust language server + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 0, + text: rust_buffer.update(cx, |buffer, _| buffer.text()), + language_id: Default::default() + } + ); + + // Ensure json documents are reopened in new json language server + assert_set_eq!( + [ + fake_json_server + .receive_notification::() + .await + .text_document, + fake_json_server + .receive_notification::() + .await + .text_document, + ], + [ + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: json_buffer.update(cx, |buffer, _| buffer.text()), + language_id: Default::default() + }, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + version: 0, + text: rust_buffer2.update(cx, |buffer, _| buffer.text()), + language_id: Default::default() + } + ] + ); + + // Close notifications are reported only to servers matching the buffer's language. + cx.update(|_| drop(json_buffer)); + let close_message = lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/package.json").unwrap(), + ), + }; + assert_eq!( + fake_json_server + .receive_notification::() + .await, + close_message, + ); +} + +#[gpui::test] +async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + 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_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-language-server", + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/the-root", + json!({ + ".gitignore": "target\n", + "src": { + "a.rs": "", + "b.rs": "", + }, + "target": { + "x": { + "out": { + "x.rs": "" + } + }, + "y": { + "out": { + "y.rs": "", + } + }, + "z": { + "out": { + "z.rs": "" + } + } + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages.add(Arc::new(language)); + }); + cx.executor().run_until_parked(); + + // Start the language server by opening a buffer with a compatible file extension. + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/src/a.rs", cx) + }) + .await + .unwrap(); + + // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them. + project.update(cx, |project, cx| { + let worktree = project.worktrees().next().unwrap(); + assert_eq!( + worktree + .read(cx) + .snapshot() + .entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + (Path::new("target"), true), + ] + ); + }); + + let prev_read_dir_count = fs.read_dir_call_count(); + + // Keep track of the FS events reported to the language server. + let fake_server = fake_servers.next().await.unwrap(); + let file_changes = Arc::new(Mutex::new(Vec::new())); + fake_server + .request::(lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: Default::default(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: serde_json::to_value( + lsp::DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/Cargo.toml".to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/src/*.{rs,c}".to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/target/y/**/*.rs".to_string(), + ), + kind: None, + }, + ], + }, + ) + .ok(), + }], + }) + .await + .unwrap(); + fake_server.handle_notification::({ + let file_changes = file_changes.clone(); + move |params, _| { + let mut file_changes = file_changes.lock(); + file_changes.extend(params.changes); + file_changes.sort_by(|a, b| a.uri.cmp(&b.uri)); + } + }); + + cx.executor().run_until_parked(); + assert_eq!(mem::take(&mut *file_changes.lock()), &[]); + assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4); + + // Now the language server has asked us to watch an ignored directory path, + // so we recursively load it. + project.update(cx, |project, cx| { + let worktree = project.worktrees().next().unwrap(); + assert_eq!( + worktree + .read(cx) + .snapshot() + .entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + (Path::new("target"), true), + (Path::new("target/x"), true), + (Path::new("target/y"), true), + (Path::new("target/y/out"), true), + (Path::new("target/y/out/y.rs"), true), + (Path::new("target/z"), true), + ] + ); + }); + + // Perform some file system mutations, two of which match the watched patterns, + // and one of which does not. + fs.create_file("/the-root/src/c.rs".as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file("/the-root/src/d.txt".as_ref(), Default::default()) + .await + .unwrap(); + fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default()) + .await + .unwrap(); + + // The language server receives events for the FS mutations that match its watch patterns. + cx.executor().run_until_parked(); + assert_eq!( + &*file_changes.lock(), + &[ + lsp::FileEvent { + uri: lsp::Url::from_file_path("/the-root/src/b.rs").unwrap(), + typ: lsp::FileChangeType::DELETED, + }, + lsp::FileEvent { + uri: lsp::Url::from_file_path("/the-root/src/c.rs").unwrap(), + typ: lsp::FileChangeType::CREATED, + }, + lsp::FileEvent { + uri: lsp::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(), + typ: lsp::FileChangeType::CREATED, + }, + ] + ); +} + +#[gpui::test] +async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "let a = 1;", + "b.rs": "let b = 2;" + }), + ) + .await; + + let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await; + + let buffer_a = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + let buffer_b = project + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) + .await + .unwrap(); + + project.update(cx, |project, cx| { + project + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "error 1".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + project + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/b.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::WARNING), + message: "error 2".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + }); + + buffer_a.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("a", Some(DiagnosticSeverity::ERROR)), + (" = 1;", None), + ] + ); + }); + buffer_b.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("b", Some(DiagnosticSeverity::WARNING)), + (" = 2;", None), + ] + ); + }); +} + +#[gpui::test] +async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir": { + "a.rs": "let a = 1;", + }, + "other.rs": "let b = c;" + }), + ) + .await; + + let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root/other.rs", false, cx) + }) + .await + .unwrap(); + let worktree_id = worktree.update(cx, |tree, _| tree.id()); + + project.update(cx, |project, cx| { + project + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/root/other.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "unknown variable 'c'".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + }); + + let buffer = project + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .await + .unwrap(); + buffer.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let b = ", None), + ("c", Some(DiagnosticSeverity::ERROR)), + (";", None), + ] + ); + }); + + project.update(cx, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).next(), None); + assert_eq!(project.diagnostic_summary(cx).error_count, 0); + }); +} + +#[gpui::test] +async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let progress_token = "the-progress-token"; + 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_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token.into()), + disk_based_diagnostics_sources: vec!["disk".into()], + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id()); + + // Cause worktree to start the fake language server + let _buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) + .await + .unwrap(); + + let mut events = cx.events(&project); + + let fake_server = fake_servers.next().await.unwrap(); + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerAdded(LanguageServerId(0)), + ); + + fake_server + .start_progress(format!("{}/0", progress_token)) + .await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsStarted { + language_server_id: LanguageServerId(0), + } + ); + + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + ..Default::default() + }], + }); + assert_eq!( + events.next().await.unwrap(), + Event::DiagnosticsUpdated { + language_server_id: LanguageServerId(0), + path: (worktree_id, Path::new("a.rs")).into() + } + ); + + fake_server.end_progress(format!("{}/0", progress_token)); + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsFinished { + language_server_id: LanguageServerId(0) + } + ); + + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let diagnostics = snapshot + .diagnostics_in_range::<_, Point>(0..buffer.len(), false) + .collect::>(); + assert_eq!( + diagnostics, + &[DiagnosticEntry { + range: Point::new(0, 9)..Point::new(0, 10), + diagnostic: Diagnostic { + severity: lsp::DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() + } + }] + ) + }); + + // Ensure publishing empty diagnostics twice only results in one update event. + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: Default::default(), + }); + assert_eq!( + events.next().await.unwrap(), + Event::DiagnosticsUpdated { + language_server_id: LanguageServerId(0), + path: (worktree_id, Path::new("a.rs")).into() + } + ); + + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: Default::default(), + }); + cx.executor().run_until_parked(); + assert_eq!(futures::poll!(events.next()), Poll::Pending); +} + +#[gpui::test] +async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let progress_token = "the-progress-token"; + let mut language = Language::new( + LanguageConfig { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_sources: vec!["disk".into()], + disk_based_diagnostics_progress_token: Some(progress_token.into()), + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + // Simulate diagnostics starting to update. + let fake_server = fake_servers.next().await.unwrap(); + fake_server.start_progress(progress_token).await; + + // Restart the server before the diagnostics finish updating. + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers([buffer], cx); + }); + let mut events = cx.events(&project); + + // Simulate the newly started server sending more diagnostics. + let fake_server = fake_servers.next().await.unwrap(); + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerAdded(LanguageServerId(1)) + ); + fake_server.start_progress(progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsStarted { + language_server_id: LanguageServerId(1) + } + ); + project.update(cx, |project, _| { + assert_eq!( + project + .language_servers_running_disk_based_diagnostics() + .collect::>(), + [LanguageServerId(1)] + ); + }); + + // All diagnostics are considered done, despite the old server's diagnostic + // task never completing. + fake_server.end_progress(progress_token); + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsFinished { + language_server_id: LanguageServerId(1) + } + ); + project.update(cx, |project, _| { + assert_eq!( + project + .language_servers_running_disk_based_diagnostics() + .collect::>(), + [LanguageServerId(0); 0] + ); + }); +} + +#[gpui::test] +async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + // Publish diagnostics + let fake_server = fake_servers.next().await.unwrap(); + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "the message".to_string(), + ..Default::default() + }], + }); + + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..1, false) + .map(|entry| entry.diagnostic.message.clone()) + .collect::>(), + ["the message".to_string()] + ); + }); + project.update(cx, |project, cx| { + assert_eq!( + project.diagnostic_summary(cx), + DiagnosticSummary { + error_count: 1, + warning_count: 0, + } + ); + }); + + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers([buffer.clone()], cx); + }); + + // The diagnostics are cleared. + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..1, false) + .map(|entry| entry.diagnostic.message.clone()) + .collect::>(), + Vec::::new(), + ); + }); + project.update(cx, |project, cx| { + assert_eq!( + project.diagnostic_summary(cx), + DiagnosticSummary { + error_count: 0, + warning_count: 0, + } + ); + }); +} + +#[gpui::test] +async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-lsp", + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + // Before restarting the server, report diagnostics with an unknown buffer version. + let fake_server = fake_servers.next().await.unwrap(); + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(10000), + diagnostics: Vec::new(), + }); + cx.executor().run_until_parked(); + + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers([buffer.clone()], cx); + }); + let mut fake_server = fake_servers.next().await.unwrap(); + let notification = fake_server + .receive_notification::() + .await + .text_document; + assert_eq!(notification.version, 0); +} + +#[gpui::test] +async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut rust = Language::new( + LanguageConfig { + name: Arc::from("Rust"), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_rust_servers = rust + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "rust-lsp", + ..Default::default() + })) + .await; + let mut js = Language::new( + LanguageConfig { + name: Arc::from("JavaScript"), + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_js_servers = js + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "js-lsp", + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages.add(Arc::new(rust)); + project.languages.add(Arc::new(js)); + }); + + let _rs_buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + let _js_buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx)) + .await + .unwrap(); + + let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server_1 + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/a.rs" + ); + + let mut fake_js_server = fake_js_servers.next().await.unwrap(); + assert_eq!( + fake_js_server + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/b.js" + ); + + // Disable Rust language server, ensuring only that server gets stopped. + cx.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); + }) + }); + fake_rust_server_1 + .receive_notification::() + .await; + + // Enable Rust and disable JavaScript language servers, ensuring that the + // former gets started again and that the latter stops. + cx.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(true), + ..Default::default() + }, + ); + settings.languages.insert( + Arc::from("JavaScript"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); + }) + }); + let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server_2 + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/a.rs" + ); + fake_js_server + .receive_notification::() + .await; +} + +#[gpui::test(iterations = 3)] +async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { + init_test(cx); + + 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_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_sources: vec!["disk".into()], + ..Default::default() + })) + .await; + + let text = " + fn a() { A } + fn b() { BB } + fn c() { CCC } + " + .unindent(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let open_notification = fake_server + .receive_notification::() + .await; + + // Edit the buffer, moving the content down + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); + let change_notification_1 = fake_server + .receive_notification::() + .await; + assert!(change_notification_1.text_document.version > open_notification.text_document.version); + + // Report some diagnostics for the initial version of the buffer + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)), + severity: Some(DiagnosticSeverity::ERROR), + source: Some("disk".to_string()), + message: "undefined variable 'CCC'".to_string(), + ..Default::default() + }, + ], + }); + + // The diagnostics have moved down since they were created. + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Point::new(4, 9)..Point::new(4, 12), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'CCC'".to_string(), + is_disk_based: true, + group_id: 2, + is_primary: true, + ..Default::default() + } + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, 0..buffer.len()), + [ + ("\n\nfn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn b() { ".to_string(), None), + ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), + [ + ("B".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), + ] + ); + }); + + // Ensure overlapping diagnostics are highlighted correctly. + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)), + severity: Some(DiagnosticSeverity::WARNING), + message: "unreachable statement".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }); + + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 12), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::WARNING, + message: "unreachable statement".to_string(), + is_disk_based: true, + group_id: 4, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 10), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 3, + is_primary: true, + ..Default::default() + }, + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), + [ + ("fn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), + [ + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + }); + + // Keep editing the buffer and ensure disk-based diagnostics get translated according to the + // changes since the last save. + buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], + None, + cx, + ); + buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); + }); + let change_notification_2 = fake_server + .receive_notification::() + .await; + assert!( + change_notification_2.text_document.version > change_notification_1.text_document.version + ); + + // Handle out-of-order diagnostics + fake_server.notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(change_notification_2.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::WARNING), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }); + + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(0..buffer.len(), false) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 21)..Point::new(2, 22), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::WARNING, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 6, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 14), + diagnostic: Diagnostic { + source: Some("disk".into()), + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 5, + is_primary: true, + ..Default::default() + }, + } + ] + ); + }); +} + +#[gpui::test] +async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let text = concat!( + "let one = ;\n", // + "let two = \n", + "let three = 3;\n", + ); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + project.update(cx, |project, cx| { + project + .update_buffer_diagnostics( + &buffer, + LanguageServerId(0), + None, + vec![ + DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 1".to_string(), + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 2".to_string(), + ..Default::default() + }, + }, + ], + cx, + ) + .unwrap(); + }); + + // An empty range is extended forward to include the following character. + // At the end of a line, an empty range is extended backward to include + // the preceding character. + buffer.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let one = ", None), + (";", Some(DiagnosticSeverity::ERROR)), + ("\nlet two =", None), + (" ", Some(DiagnosticSeverity::ERROR)), + ("\nlet three = 3;\n", None) + ] + ); + }); +} + +#[gpui::test] +async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + LanguageServerId(0), + Path::new("/dir/a.rs").to_owned(), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + is_primary: true, + message: "syntax error a1".to_string(), + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project + .update_diagnostic_entries( + LanguageServerId(1), + Path::new("/dir/a.rs").to_owned(), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + is_primary: true, + message: "syntax error b1".to_string(), + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + + assert_eq!( + project.diagnostic_summary(cx), + DiagnosticSummary { + error_count: 2, + warning_count: 0, + } + ); + }); +} + +#[gpui::test] +async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { + init_test(cx); + + 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_servers = language.set_fake_lsp_adapter(Default::default()).await; + + let text = " + fn a() { + f1(); + } + fn b() { + f2(); + } + fn c() { + f3(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let lsp_document_version = fake_server + .receive_notification::() + .await + .text_document + .version; + + // Simulate editing the buffer after the language server computes some edits. + buffer.update(cx, |buffer, cx| { + buffer.edit( + [( + Point::new(0, 0)..Point::new(0, 0), + "// above first function\n", + )], + None, + cx, + ); + buffer.edit( + [( + Point::new(2, 0)..Point::new(2, 0), + " // inside first function\n", + )], + None, + cx, + ); + buffer.edit( + [( + Point::new(6, 4)..Point::new(6, 4), + "// inside second function ", + )], + None, + cx, + ); + + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f1(); + } + fn b() { + // inside second function f2(); + } + fn c() { + f3(); + } + " + .unindent() + ); + }); + + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + vec![ + // replace body of first function + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)), + new_text: " + fn a() { + f10(); + } + " + .unindent(), + }, + // edit inside second function + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)), + new_text: "00".into(), + }, + // edit inside third function via two distinct edits + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)), + new_text: "4000".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)), + new_text: "".into(), + }, + ], + LanguageServerId(0), + Some(lsp_document_version), + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + for (range, new_text) in edits { + buffer.edit([(range, new_text)], None, cx); + } + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f10(); + } + fn b() { + // inside second function f200(); + } + fn c() { + f4000(); + } + " + .unindent() + ); + }); +} + +#[gpui::test] +async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let text = " + use a::b; + use a::c; + + fn f() { + b(); + c(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + // Simulate the language server sending us a small edit in the form of a very large diff. + // Rust-analyzer does this when performing a merge-imports code action. + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + [ + // Replace the first use statement without editing the semicolon. + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)), + new_text: "a::{b, c}".into(), + }, + // Reinsert the remainder of the file between the semicolon and the final + // newline of the file. + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), + new_text: "\n\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), + new_text: " + fn f() { + b(); + c(); + }" + .unindent(), + }, + // Delete everything after the first newline of the file. + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)), + new_text: "".into(), + }, + ], + LanguageServerId(0), + None, + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + let edits = edits + .into_iter() + .map(|(range, text)| { + ( + range.start.to_point(buffer)..range.end.to_point(buffer), + text, + ) + }) + .collect::>(); + + assert_eq!( + edits, + [ + (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), + (Point::new(1, 0)..Point::new(2, 0), "".into()) + ] + ); + + for (range, new_text) in edits { + buffer.edit([(range, new_text)], None, cx); + } + assert_eq!( + buffer.text(), + " + use a::{b, c}; + + fn f() { + b(); + c(); + } + " + .unindent() + ); + }); +} + +#[gpui::test] +async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let text = " + use a::b; + use a::c; + + fn f() { + b(); + c(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + + // Simulate the language server sending us edits in a non-ordered fashion, + // with ranges sometimes being inverted or pointing to invalid locations. + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + [ + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), + new_text: "\n\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)), + new_text: "a::{b, c}".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)), + new_text: "".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), + new_text: " + fn f() { + b(); + c(); + }" + .unindent(), + }, + ], + LanguageServerId(0), + None, + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + let edits = edits + .into_iter() + .map(|(range, text)| { + ( + range.start.to_point(buffer)..range.end.to_point(buffer), + text, + ) + }) + .collect::>(); + + assert_eq!( + edits, + [ + (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), + (Point::new(1, 0)..Point::new(2, 0), "".into()) + ] + ); + + for (range, new_text) in edits { + buffer.edit([(range, new_text)], None, cx); + } + assert_eq!( + buffer.text(), + " + use a::{b, c}; + + fn f() { + b(); + c(); + } + " + .unindent() + ); + }); +} + +fn chunks_with_diagnostics( + buffer: &Buffer, + range: Range, +) -> Vec<(String, Option)> { + let mut chunks: Vec<(String, Option)> = Vec::new(); + for chunk in buffer.snapshot().chunks(range, true) { + if chunks.last().map_or(false, |prev_chunk| { + prev_chunk.1 == chunk.diagnostic_severity + }) { + chunks.last_mut().unwrap().0.push_str(chunk.text); + } else { + chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); + } + } + chunks +} + +#[gpui::test(iterations = 10)] +async fn test_definition(cx: &mut gpui::TestAppContext) { + init_test(cx); + + 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_servers = language.set_fake_lsp_adapter(Default::default()).await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "const fn a() { A }", + "b.rs": "const y: i32 = crate::a()", + }), + ) + .await; + + let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) + .await + .unwrap(); + + let fake_server = fake_servers.next().await.unwrap(); + fake_server.handle_request::(|params, _| async move { + let params = params.text_document_position_params; + assert_eq!( + params.text_document.uri.to_file_path().unwrap(), + Path::new("/dir/b.rs"), + ); + assert_eq!(params.position, lsp::Position::new(0, 22)); + + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + ), + ))) + }); + + let mut definitions = project + .update(cx, |project, cx| project.definition(&buffer, 22, cx)) + .await + .unwrap(); + + // Assert no new language server started + cx.executor().run_until_parked(); + assert!(fake_servers.try_next().is_err()); + + assert_eq!(definitions.len(), 1); + let definition = definitions.pop().unwrap(); + cx.update(|cx| { + let target_buffer = definition.target.buffer.read(cx); + assert_eq!( + target_buffer + .file() + .unwrap() + .as_local() + .unwrap() + .abs_path(cx), + Path::new("/dir/a.rs"), + ); + assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); + assert_eq!( + list_worktrees(&project, cx), + [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)] + ); + + drop(definition); + }); + cx.update(|cx| { + assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); + }); + + fn list_worktrees<'a>( + project: &'a Model, + cx: &'a AppContext, + ) -> Vec<(&'a Path, bool)> { + project + .read(cx) + .worktrees() + .map(|worktree| { + let worktree = worktree.read(cx); + ( + worktree.as_local().unwrap().abs_path().as_ref(), + worktree.is_visible(), + ) + }) + .collect::>() + } +} + +#[gpui::test] +async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ); + 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()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let text = "let a = b.fqn"; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), cx) + }); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "fullyQualifiedName?".into(), + insert_text: Some("fullyQualifiedName".into()), + ..Default::default() + }, + ]))) + }) + .next() + .await; + let completions = completions.await.unwrap(); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "fullyQualifiedName"); + assert_eq!( + completions[0].old_range.to_offset(&snapshot), + text.len() - 3..text.len() + ); + + let text = "let a = \"atoms/cmp\""; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len() - 1, cx) + }); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "component".into(), + ..Default::default() + }, + ]))) + }) + .next() + .await; + let completions = completions.await.unwrap(); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "component"); + assert_eq!( + completions[0].old_range.to_offset(&snapshot), + text.len() - 4..text.len() - 1 + ); +} + +#[gpui::test] +async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ); + 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()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let text = "let a = b.fqn"; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), cx) + }); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "fullyQualifiedName?".into(), + insert_text: Some("fully\rQualified\r\nName".into()), + ..Default::default() + }, + ]))) + }) + .next() + .await; + let completions = completions.await.unwrap(); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "fully\nQualified\nName"); +} + +#[gpui::test(iterations = 10)] +async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + // Language server returns code actions that contain commands, and not edits. + let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + fake_server + .handle_request::(|_, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "The code action".into(), + command: Some(lsp::Command { + title: "The command".into(), + command: "_the/command".into(), + arguments: Some(vec![json!("the-argument")]), + }), + ..Default::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "two".into(), + ..Default::default() + }), + ])) + }) + .next() + .await; + + let action = actions.await.unwrap()[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. + fake_server.handle_request::( + |action, _| async move { Ok(action) }, + ); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. + fake_server + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path("/dir/a.ts").unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } + }) + .next() + .await; + + // Applying the code action returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_save_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "file1": "the old contents", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .await + .unwrap(); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "the old contents"); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + + let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); +} + +#[gpui::test] +async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "file1": "the old contents", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await; + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .await + .unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + + let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); +} + +#[gpui::test] +async fn test_save_as(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/dir", json!({})).await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let languages = project.update(cx, |project, _| project.languages().clone()); + languages.register( + "/some/path", + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + tree_sitter_rust::language(), + vec![], + |_| Default::default(), + ); + + let buffer = project.update(cx, |project, cx| { + project.create_buffer("", None, cx).unwrap() + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "abc")], None, cx); + assert!(buffer.is_dirty()); + assert!(!buffer.has_conflict()); + assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); + }); + project + .update(cx, |project, cx| { + project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) + }) + .await + .unwrap(); + assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); + + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, cx| { + assert_eq!( + buffer.file().unwrap().full_path(cx), + Path::new("dir/file1.rs") + ); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); + }); + + let opened_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/file1.rs", cx) + }) + .await + .unwrap(); + assert_eq!(opened_buffer, buffer); +} + +#[gpui::test(retries = 5)] +async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let dir = temp_tree(json!({ + "a": { + "file1": "", + "file2": "", + "file3": "", + }, + "b": { + "c": { + "file4": "", + "file5": "", + } + } + })); + + let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; + let rpc = project.update(cx, |p, _| p.client.clone()); + + let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { + let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); + async move { buffer.await.unwrap() } + }; + let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { + project.update(cx, |project, cx| { + let tree = project.worktrees().next().unwrap(); + tree.read(cx) + .entry_for_path(path) + .unwrap_or_else(|| panic!("no entry for path {}", path)) + .id + }) + }; + + let buffer2 = buffer_for_path("a/file2", cx).await; + let buffer3 = buffer_for_path("a/file3", cx).await; + let buffer4 = buffer_for_path("b/c/file4", cx).await; + let buffer5 = buffer_for_path("b/c/file5", cx).await; + + let file2_id = id_for_path("a/file2", cx); + let file3_id = id_for_path("a/file3", cx); + let file4_id = id_for_path("b/c/file4", cx); + + // Create a remote copy of this worktree. + let tree = project.update(cx, |project, _| project.worktrees().next().unwrap()); + + let metadata = tree.update(cx, |tree, _| tree.as_local().unwrap().metadata_proto()); + + let updates = Arc::new(Mutex::new(Vec::new())); + tree.update(cx, |tree, cx| { + let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { + let updates = updates.clone(); + move |update| { + updates.lock().push(update); + async { true } + } + }); + }); + + let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx)); + + cx.executor().run_until_parked(); + + cx.update(|cx| { + assert!(!buffer2.read(cx).is_dirty()); + assert!(!buffer3.read(cx).is_dirty()); + assert!(!buffer4.read(cx).is_dirty()); + assert!(!buffer5.read(cx).is_dirty()); + }); + + // Rename and delete files and directories. + tree.flush_fs_events(cx).await; + std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); + std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); + std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); + std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); + tree.flush_fs_events(cx).await; + + let expected_paths = vec![ + "a", + "a/file1", + "a/file2.new", + "b", + "d", + "d/file3", + "d/file4", + ]; + + cx.update(|app| { + assert_eq!( + tree.read(app) + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(), + expected_paths + ); + }); + + assert_eq!(id_for_path("a/file2.new", cx), file2_id); + assert_eq!(id_for_path("d/file3", cx), file3_id); + assert_eq!(id_for_path("d/file4", cx), file4_id); + + cx.update(|cx| { + assert_eq!( + buffer2.read(cx).file().unwrap().path().as_ref(), + Path::new("a/file2.new") + ); + assert_eq!( + buffer3.read(cx).file().unwrap().path().as_ref(), + Path::new("d/file3") + ); + assert_eq!( + buffer4.read(cx).file().unwrap().path().as_ref(), + Path::new("d/file4") + ); + assert_eq!( + buffer5.read(cx).file().unwrap().path().as_ref(), + Path::new("b/c/file5") + ); + + assert!(!buffer2.read(cx).file().unwrap().is_deleted()); + assert!(!buffer3.read(cx).file().unwrap().is_deleted()); + assert!(!buffer4.read(cx).file().unwrap().is_deleted()); + assert!(buffer5.read(cx).file().unwrap().is_deleted()); + }); + + // Update the remote worktree. Check that it becomes consistent with the + // local worktree. + cx.executor().run_until_parked(); + + remote.update(cx, |remote, _| { + for update in updates.lock().drain(..) { + remote.as_remote_mut().unwrap().update_from_remote(update); + } + }); + cx.executor().run_until_parked(); + remote.update(cx, |remote, _| { + assert_eq!( + remote + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(), + expected_paths + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a": { + "file1": "", + } + }), + ) + .await; + + let project = Project::test(fs, [Path::new("/dir")], cx).await; + let tree = project.update(cx, |project, _| project.worktrees().next().unwrap()); + let tree_id = tree.update(cx, |tree, _| tree.id()); + + let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { + project.update(cx, |project, cx| { + let tree = project.worktrees().next().unwrap(); + tree.read(cx) + .entry_for_path(path) + .unwrap_or_else(|| panic!("no entry for path {}", path)) + .id + }) + }; + + let dir_id = id_for_path("a", cx); + let file_id = id_for_path("a/file1", cx); + let buffer = project + .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx)) + .await + .unwrap(); + buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + + project + .update(cx, |project, cx| { + project.rename_entry(dir_id, Path::new("b"), cx) + }) + .unwrap() + .await + .unwrap(); + cx.executor().run_until_parked(); + + assert_eq!(id_for_path("b", cx), dir_id); + assert_eq!(id_for_path("b/file1", cx), file_id); + buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty())); +} + +#[gpui::test] +async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + // Spawn multiple tasks to open paths, repeating some paths. + let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { + ( + p.open_local_buffer("/dir/a.txt", cx), + p.open_local_buffer("/dir/b.txt", cx), + p.open_local_buffer("/dir/a.txt", cx), + ) + }); + + let buffer_a_1 = buffer_a_1.await.unwrap(); + let buffer_a_2 = buffer_a_2.await.unwrap(); + let buffer_b = buffer_b.await.unwrap(); + assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents"); + assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents"); + + // There is only one buffer per path. + let buffer_a_id = buffer_a_1.entity_id(); + assert_eq!(buffer_a_2.entity_id(), buffer_a_id); + + // Open the same path again while it is still open. + drop(buffer_a_1); + let buffer_a_3 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx)) + .await + .unwrap(); + + // There's still only one buffer per path. + assert_eq!(buffer_a_3.entity_id(), buffer_a_id); +} + +#[gpui::test] +async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "file1": "abc", + "file2": "def", + "file3": "ghi", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let buffer1 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .await + .unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + + // initially, the buffer isn't dirty. + buffer1.update(cx, |buffer, cx| { + cx.subscribe(&buffer1, { + let events = events.clone(); + move |_, _, event, _| match event { + BufferEvent::Operation(_) => {} + _ => events.lock().push(event.clone()), + } + }) + .detach(); + + assert!(!buffer.is_dirty()); + assert!(events.lock().is_empty()); + + buffer.edit([(1..2, "")], None, cx); + }); + + // after the first edit, the buffer is dirty, and emits a dirtied event. + buffer1.update(cx, |buffer, cx| { + assert!(buffer.text() == "ac"); + assert!(buffer.is_dirty()); + assert_eq!( + *events.lock(), + &[language::Event::Edited, language::Event::DirtyChanged] + ); + events.lock().clear(); + buffer.did_save( + buffer.version(), + buffer.as_rope().fingerprint(), + buffer.file().unwrap().mtime(), + cx, + ); + }); + + // after saving, the buffer is not dirty, and emits a saved event. + buffer1.update(cx, |buffer, cx| { + assert!(!buffer.is_dirty()); + assert_eq!(*events.lock(), &[language::Event::Saved]); + events.lock().clear(); + + buffer.edit([(1..1, "B")], None, cx); + buffer.edit([(2..2, "D")], None, cx); + }); + + // after editing again, the buffer is dirty, and emits another dirty event. + buffer1.update(cx, |buffer, cx| { + assert!(buffer.text() == "aBDc"); + assert!(buffer.is_dirty()); + assert_eq!( + *events.lock(), + &[ + language::Event::Edited, + language::Event::DirtyChanged, + language::Event::Edited, + ], + ); + events.lock().clear(); + + // After restoring the buffer to its previously-saved state, + // the buffer is not considered dirty anymore. + buffer.edit([(1..3, "")], None, cx); + assert!(buffer.text() == "ac"); + assert!(!buffer.is_dirty()); + }); + + assert_eq!( + *events.lock(), + &[language::Event::Edited, language::Event::DirtyChanged] + ); + + // When a file is deleted, the buffer is considered dirty. + let events = Arc::new(Mutex::new(Vec::new())); + let buffer2 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) + .await + .unwrap(); + buffer2.update(cx, |_, cx| { + cx.subscribe(&buffer2, { + let events = events.clone(); + move |_, _, event, _| events.lock().push(event.clone()) + }) + .detach(); + }); + + fs.remove_file("/dir/file2".as_ref(), Default::default()) + .await + .unwrap(); + cx.executor().run_until_parked(); + buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty())); + assert_eq!( + *events.lock(), + &[ + language::Event::DirtyChanged, + language::Event::FileHandleChanged + ] + ); + + // When a file is already dirty when deleted, we don't emit a Dirtied event. + let events = Arc::new(Mutex::new(Vec::new())); + let buffer3 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) + .await + .unwrap(); + buffer3.update(cx, |_, cx| { + cx.subscribe(&buffer3, { + let events = events.clone(); + move |_, _, event, _| events.lock().push(event.clone()) + }) + .detach(); + }); + + buffer3.update(cx, |buffer, cx| { + buffer.edit([(0..0, "x")], None, cx); + }); + events.lock().clear(); + fs.remove_file("/dir/file3".as_ref(), Default::default()) + .await + .unwrap(); + cx.executor().run_until_parked(); + assert_eq!(*events.lock(), &[language::Event::FileHandleChanged]); + cx.update(|cx| assert!(buffer3.read(cx).is_dirty())); +} + +#[gpui::test] +async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let initial_contents = "aaa\nbbbbb\nc\n"; + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "the-file": initial_contents, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) + .await + .unwrap(); + + let anchors = (0..3) + .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1)))) + .collect::>(); + + // Change the file on disk, adding two new lines of text, and removing + // one line. + buffer.update(cx, |buffer, _| { + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; + fs.save( + "/dir/the-file".as_ref(), + &new_contents.into(), + LineEnding::Unix, + ) + .await + .unwrap(); + + // Because the buffer was not modified, it is reloaded from disk. Its + // contents are edited according to the diff between the old and new + // file contents. + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert_eq!(buffer.text(), new_contents); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + + let anchor_positions = anchors + .iter() + .map(|anchor| anchor.to_point(&*buffer)) + .collect::>(); + assert_eq!( + anchor_positions, + [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)] + ); + }); + + // Modify the buffer + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, " ")], None, cx); + assert!(buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + + // Change the file on disk again, adding blank lines to the beginning. + fs.save( + "/dir/the-file".as_ref(), + &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), + LineEnding::Unix, + ) + .await + .unwrap(); + + // Because the buffer is modified, it doesn't reload from disk, but is + // marked as having a conflict. + cx.executor().run_until_parked(); + buffer.update(cx, |buffer, _| { + assert!(buffer.has_conflict()); + }); +} + +#[gpui::test] +async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "file1": "a\nb\nc\n", + "file2": "one\r\ntwo\r\nthree\r\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer1 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .await + .unwrap(); + let buffer2 = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) + .await + .unwrap(); + + buffer1.update(cx, |buffer, _| { + assert_eq!(buffer.text(), "a\nb\nc\n"); + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + buffer2.update(cx, |buffer, _| { + assert_eq!(buffer.text(), "one\ntwo\nthree\n"); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + + // Change a file's line endings on disk from unix to windows. The buffer's + // state updates correctly. + fs.save( + "/dir/file1".as_ref(), + &"aaa\nb\nc\n".into(), + LineEnding::Windows, + ) + .await + .unwrap(); + cx.executor().run_until_parked(); + buffer1.update(cx, |buffer, _| { + assert_eq!(buffer.text(), "aaa\nb\nc\n"); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + + // Save a file with windows line endings. The file is written correctly. + buffer2.update(cx, |buffer, cx| { + buffer.set_text("one\ntwo\nthree\nfour\n", cx); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer2, cx)) + .await + .unwrap(); + assert_eq!( + fs.load("/dir/file2".as_ref()).await.unwrap(), + "one\r\ntwo\r\nthree\r\nfour\r\n", + ); +} + +#[gpui::test] +async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/the-dir", + json!({ + "a.rs": " + fn foo(mut v: Vec) { + for x in &v { + v.push(1); + } + } + " + .unindent(), + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await; + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) + .await + .unwrap(); + + let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap(); + let message = lsp::PublishDiagnosticsParams { + uri: buffer_uri.clone(), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + severity: Some(DiagnosticSeverity::WARNING), + message: "error 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri.clone(), + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + }, + message: "error 1 hint 1".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 1 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri.clone(), + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + severity: Some(DiagnosticSeverity::ERROR), + message: "error 2".to_string(), + related_information: Some(vec![ + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri.clone(), + range: lsp::Range::new( + lsp::Position::new(1, 13), + lsp::Position::new(1, 15), + ), + }, + message: "error 2 hint 1".to_string(), + }, + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri.clone(), + range: lsp::Range::new( + lsp::Position::new(1, 13), + lsp::Position::new(1, 15), + ), + }, + message: "error 2 hint 2".to_string(), + }, + ]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri.clone(), + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 2".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: buffer_uri, + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + ], + version: None, + }; + + project + .update(cx, |p, cx| { + p.update_diagnostics(LanguageServerId(0), message, &[], cx) + }) + .unwrap(); + let buffer = buffer.update(cx, |buffer, _| buffer.snapshot()); + + assert_eq!( + buffer + .diagnostics_in_range::<_, Point>(0..buffer.len(), false) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 1, + is_primary: false, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 0, + is_primary: false, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 0, + is_primary: false, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(2, 8)..Point::new(2, 17), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() + } + } + ] + ); + + assert_eq!( + buffer.diagnostic_group::(0).collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 0, + is_primary: false, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 0, + is_primary: false, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(2, 8)..Point::new(2, 17), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() + } + } + ] + ); + + assert_eq!( + buffer.diagnostic_group::(1).collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 1, + is_primary: false, + ..Default::default() + } + }, + ] + ); +} + +#[gpui::test] +async fn test_rename(cx: &mut gpui::TestAppContext) { + init_test(cx); + + 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_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; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/one.rs", cx) + }) + .await + .unwrap(); + + let fake_server = fake_servers.next().await.unwrap(); + + let response = project.update(cx, |project, cx| { + project.prepare_rename(buffer.clone(), 7, cx) + }); + fake_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(); + let range = response.await.unwrap().unwrap(); + let range = buffer.update(cx, |buffer, _| range.to_offset(buffer)); + assert_eq!(range, 6..9); + + let response = project.update(cx, |project, cx| { + project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) + }); + fake_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, 7) + ); + 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(); + let mut transaction = response.await.unwrap().0; + assert_eq!(transaction.len(), 2); + assert_eq!( + transaction + .remove_entry(&buffer) + .unwrap() + .0 + .update(cx, |buffer, _| buffer.text()), + "const THREE: usize = 1;" + ); + assert_eq!( + transaction + .into_keys() + .next() + .unwrap() + .update(cx, |buffer, _| buffer.text()), + "const TWO: usize = one::THREE + one::THREE;" + ); +} + +#[gpui::test] +async fn test_search(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.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; + assert_eq!( + search( + &project, + SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]) + ]) + ); + + let buffer_4 = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/four.rs", cx) + }) + .await + .unwrap(); + buffer_4.update(cx, |buffer, cx| { + let text = "two::TWO"; + buffer.edit([(20..28, text), (31..43, text)], None, cx); + }); + + assert_eq!( + search( + &project, + SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]), + ("four.rs".to_string(), vec![25..28, 36..39]) + ]) + ); +} + +#[gpui::test] +async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let search_query = "file"; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": r#"// Rust file one"#, + "one.ts": r#"// TypeScript file one"#, + "two.rs": r#"// Rust file two"#, + "two.ts": r#"// TypeScript file two"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + assert!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![PathMatcher::new("*.odd").unwrap()], + Vec::new() + ) + .unwrap(), + cx + ) + .await + .unwrap() + .is_empty(), + "If no inclusions match, no files should be returned" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![PathMatcher::new("*.rs").unwrap()], + Vec::new() + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.rs".to_string(), vec![8..12]), + ("two.rs".to_string(), vec![8..12]), + ]), + "Rust only search should give only Rust files" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![ + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), + ], + Vec::new() + ).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.ts".to_string(), vec![14..18]), + ("two.ts".to_string(), vec![14..18]), + ]), + "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![ + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), + ], + Vec::new() + ).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.rs".to_string(), vec![8..12]), + ("one.ts".to_string(), vec![14..18]), + ("two.rs".to_string(), vec![8..12]), + ("two.ts".to_string(), vec![14..18]), + ]), + "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything" + ); +} + +#[gpui::test] +async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let search_query = "file"; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": r#"// Rust file one"#, + "one.ts": r#"// TypeScript file one"#, + "two.rs": r#"// Rust file two"#, + "two.ts": r#"// TypeScript file two"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + Vec::new(), + vec![PathMatcher::new("*.odd").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.rs".to_string(), vec![8..12]), + ("one.ts".to_string(), vec![14..18]), + ("two.rs".to_string(), vec![8..12]), + ("two.ts".to_string(), vec![14..18]), + ]), + "If no exclusions match, all files should be returned" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + Vec::new(), + vec![PathMatcher::new("*.rs").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.ts".to_string(), vec![14..18]), + ("two.ts".to_string(), vec![14..18]), + ]), + "Rust exclusion search should give only TypeScript files" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + Vec::new(), + vec![ + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), + ], + ).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.rs".to_string(), vec![8..12]), + ("two.rs".to_string(), vec![8..12]), + ]), + "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything" + ); + + assert!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + Vec::new(), + vec![ + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), + ], + ).unwrap(), + cx + ) + .await + .unwrap().is_empty(), + "Rust and typescript exclusion should give no files, even if other exclusions don't match anything" + ); +} + +#[gpui::test] +async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let search_query = "file"; + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": r#"// Rust file one"#, + "one.ts": r#"// TypeScript file one"#, + "two.rs": r#"// Rust file two"#, + "two.ts": r#"// TypeScript file two"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + assert!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![PathMatcher::new("*.odd").unwrap()], + vec![PathMatcher::new("*.odd").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap() + .is_empty(), + "If both no exclusions and inclusions match, exclusions should win and return nothing" + ); + + assert!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![PathMatcher::new("*.ts").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], + ).unwrap(), + cx + ) + .await + .unwrap() + .is_empty(), + "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files." + ); + + assert!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![ + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() + ], + vec![ + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() + ], + ) + .unwrap(), + cx + ) + .await + .unwrap() + .is_empty(), + "Non-matching inclusions and exclusions should not change that." + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + search_query, + false, + true, + vec![ + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() + ], + vec![ + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.odd").unwrap() + ], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("one.ts".to_string(), vec![14..18]), + ("two.ts".to_string(), vec![14..18]), + ]), + "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files" + ); +} + +#[test] +fn test_glob_literal_prefix() { + assert_eq!(glob_literal_prefix("**/*.js"), ""); + assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules"); + assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo"); + assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); +} + +async fn search( + project: &Model, + query: SearchQuery, + cx: &mut gpui::TestAppContext, +) -> Result>>> { + let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); + let mut result = HashMap::default(); + while let Some((buffer, range)) = search_rx.next().await { + result.entry(buffer).or_insert(range); + } + Ok(result + .into_iter() + .map(|(buffer, ranges)| { + buffer.update(cx, |buffer, _| { + let path = buffer.file().unwrap().path().to_string_lossy().to_string(); + let ranges = ranges + .into_iter() + .map(|range| range.to_offset(buffer)) + .collect::>(); + (path, ranges) + }) + }) + .collect()) +} + +fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); +} diff --git a/crates/project2/src/search.rs b/crates/project2/src/search.rs index 9af13c7732..7e360e22ee 100644 --- a/crates/project2/src/search.rs +++ b/crates/project2/src/search.rs @@ -1,18 +1,18 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::{Context, Result}; -use client2::proto; -use globset::{Glob, GlobMatcher}; +use client::proto; use itertools::Itertools; -use language2::{char_kind, BufferSnapshot}; +use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, - path::{Path, PathBuf}, + path::Path, sync::Arc, }; +use util::paths::PathMatcher; #[derive(Clone, Debug)] pub struct SearchInputs { @@ -52,31 +52,6 @@ pub enum SearchQuery { }, } -#[derive(Clone, Debug)] -pub struct PathMatcher { - maybe_path: PathBuf, - glob: GlobMatcher, -} - -impl std::fmt::Display for PathMatcher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.maybe_path.to_string_lossy().fmt(f) - } -} - -impl PathMatcher { - pub fn new(maybe_glob: &str) -> Result { - Ok(PathMatcher { - glob: Glob::new(&maybe_glob)?.compile_matcher(), - maybe_path: PathBuf::from(maybe_glob), - }) - } - - pub fn is_match>(&self, other: P) -> bool { - other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other) - } -} - impl SearchQuery { pub fn text( query: impl ToString, diff --git a/crates/project2/src/terminals.rs b/crates/project2/src/terminals.rs index ce89914dc6..1bf69aa8b5 100644 --- a/crates/project2/src/terminals.rs +++ b/crates/project2/src/terminals.rs @@ -1,8 +1,8 @@ use crate::Project; -use gpui2::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel}; -use settings2::Settings; +use gpui::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel}; +use settings::Settings; use std::path::{Path, PathBuf}; -use terminal2::{ +use terminal::{ terminal_settings::{self, TerminalSettings, VenvSettingsContent}, Terminal, TerminalBuilder, }; @@ -11,7 +11,7 @@ use terminal2::{ use std::os::unix::ffi::OsStrExt; pub struct Terminals { - pub(crate) local_handles: Vec>, + pub(crate) local_handles: Vec>, } impl Project { @@ -121,7 +121,7 @@ impl Project { } } - pub fn local_terminal_handles(&self) -> &Vec> { + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 045fd7953e..937a549a31 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -3,10 +3,10 @@ use crate::{ }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; -use client2::{proto, Client}; +use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use fs2::{ +use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, Fs, }; @@ -17,21 +17,22 @@ use futures::{ }, select_biased, task::Poll, - FutureExt, Stream, StreamExt, + FutureExt as _, Stream, StreamExt, }; -use fuzzy2::CharBag; +use fuzzy::CharBag; use git::{DOT_GIT, GITIGNORE}; -use gpui2::{ - AppContext, AsyncAppContext, Context, EventEmitter, Executor, Model, ModelContext, Task, +use gpui::{ + AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, + Task, }; -use language2::{ +use language::{ proto::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, }; -use lsp2::LanguageServerId; +use lsp::LanguageServerId; use parking_lot::Mutex; use postage::{ barrier, @@ -296,6 +297,7 @@ impl Worktree { // After determining whether the root entry is a file or a directory, populate the // snapshot's "root name", which will be used for the purpose of fuzzy matching. let abs_path = path.into(); + let metadata = fs .metadata(&abs_path) .await @@ -364,10 +366,10 @@ impl Worktree { }) .detach(); - let background_scanner_task = cx.executor().spawn({ + let background_scanner_task = cx.background_executor().spawn({ let fs = fs.clone(); let snapshot = snapshot.clone(); - let background = cx.executor().clone(); + let background = cx.background_executor().clone(); async move { let events = fs.watch(&abs_path, Duration::from_millis(100)).await; BackgroundScanner::new( @@ -428,7 +430,7 @@ impl Worktree { let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); - cx.executor() + cx.background_executor() .spawn({ let background_snapshot = background_snapshot.clone(); async move { @@ -600,7 +602,7 @@ impl LocalWorktree { .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))? .await?; let text_buffer = cx - .executor() + .background_executor() .spawn(async move { text::Buffer::new(0, id, contents) }) .await; cx.build_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file)))) @@ -888,7 +890,7 @@ impl LocalWorktree { if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) { let repo = repo.repo_ptr.clone(); index_task = Some( - cx.executor() + cx.background_executor() .spawn(async move { repo.lock().load_index_text(&repo_path) }), ); } @@ -1007,7 +1009,7 @@ impl LocalWorktree { let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); - let write = cx.executor().spawn(async move { + let write = cx.background_executor().spawn(async move { if is_dir { fs.create_dir(&abs_path).await } else { @@ -1057,7 +1059,7 @@ impl LocalWorktree { let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx - .executor() + .background_executor() .spawn(async move { fs.save(&abs_path, &text, line_ending).await }); cx.spawn(|this, mut cx| async move { @@ -1078,7 +1080,7 @@ impl LocalWorktree { let abs_path = self.absolutize(&entry.path); let fs = self.fs.clone(); - let delete = cx.executor().spawn(async move { + let delete = cx.background_executor().spawn(async move { if entry.is_file() { fs.remove_file(&abs_path, Default::default()).await?; } else { @@ -1118,7 +1120,7 @@ impl LocalWorktree { let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); let fs = self.fs.clone(); - let rename = cx.executor().spawn(async move { + let rename = cx.background_executor().spawn(async move { fs.rename(&abs_old_path, &abs_new_path, Default::default()) .await }); @@ -1145,7 +1147,7 @@ impl LocalWorktree { let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); let fs = self.fs.clone(); - let copy = cx.executor().spawn(async move { + let copy = cx.background_executor().spawn(async move { copy_recursive( fs.as_ref(), &abs_old_path, @@ -1173,7 +1175,7 @@ impl LocalWorktree { ) -> Option>> { let path = self.entry_for_id(entry_id)?.path.clone(); let mut refresh = self.refresh_entries_for_paths(vec![path]); - Some(cx.executor().spawn(async move { + Some(cx.background_executor().spawn(async move { refresh.next().await; Ok(()) })) @@ -1247,7 +1249,7 @@ impl LocalWorktree { .ok(); let worktree_id = cx.entity_id().as_u64(); - let _maintain_remote_snapshot = cx.executor().spawn(async move { + let _maintain_remote_snapshot = cx.background_executor().spawn(async move { let mut is_first = true; while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await { let update; @@ -1305,7 +1307,7 @@ impl LocalWorktree { let rx = self.observe_updates(project_id, cx, move |update| { client.request(update).map(|result| result.is_ok()) }); - cx.executor() + cx.background_executor() .spawn(async move { rx.await.map_err(|_| anyhow!("share ended")) }) } @@ -2585,8 +2587,8 @@ pub struct File { pub(crate) is_deleted: bool, } -impl language2::File for File { - fn as_local(&self) -> Option<&dyn language2::LocalFile> { +impl language::File for File { + fn as_local(&self) -> Option<&dyn language::LocalFile> { if self.is_local { Some(self) } else { @@ -2646,8 +2648,8 @@ impl language2::File for File { self } - fn to_proto(&self) -> rpc2::proto::File { - rpc2::proto::File { + fn to_proto(&self) -> rpc::proto::File { + rpc::proto::File { worktree_id: self.worktree.entity_id().as_u64(), entry_id: self.entry_id.to_proto(), path: self.path.to_string_lossy().into(), @@ -2657,7 +2659,7 @@ impl language2::File for File { } } -impl language2::LocalFile for File { +impl language::LocalFile for File { fn abs_path(&self, cx: &AppContext) -> PathBuf { let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path; if self.path.as_ref() == Path::new("") { @@ -2671,7 +2673,8 @@ impl language2::LocalFile for File { let worktree = self.worktree.read(cx).as_local().unwrap(); let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); - cx.executor().spawn(async move { fs.load(&abs_path).await }) + cx.background_executor() + .spawn(async move { fs.load(&abs_path).await }) } fn buffer_reloaded( @@ -2713,7 +2716,7 @@ impl File { } pub fn from_proto( - proto: rpc2::proto::File, + proto: rpc::proto::File, worktree: Model, cx: &AppContext, ) -> Result { @@ -2737,7 +2740,7 @@ impl File { }) } - pub fn from_dyn(file: Option<&Arc>) -> Option<&Self> { + pub fn from_dyn(file: Option<&Arc>) -> Option<&Self> { file.and_then(|f| f.as_any().downcast_ref()) } @@ -2815,7 +2818,7 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; impl Entry { fn new( path: Arc, - metadata: &fs2::Metadata, + metadata: &fs::Metadata, next_entry_id: &AtomicUsize, root_char_bag: CharBag, ) -> Self { @@ -3012,7 +3015,7 @@ struct BackgroundScanner { state: Mutex, fs: Arc, status_updates_tx: UnboundedSender, - executor: Executor, + executor: BackgroundExecutor, scan_requests_rx: channel::Receiver, path_prefixes_to_scan_rx: channel::Receiver>, next_entry_id: Arc, @@ -3032,7 +3035,7 @@ impl BackgroundScanner { next_entry_id: Arc, fs: Arc, status_updates_tx: UnboundedSender, - executor: Executor, + executor: BackgroundExecutor, scan_requests_rx: channel::Receiver, path_prefixes_to_scan_rx: channel::Receiver>, ) -> Self { @@ -4030,53 +4033,54 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -// todo!("re-enable when we have tests") -// pub trait WorktreeModelHandle { -// #[cfg(any(test, feature = "test-support"))] -// fn flush_fs_events<'a>( -// &self, -// cx: &'a gpui::TestAppContext, -// ) -> futures::future::LocalBoxFuture<'a, ()>; -// } +pub trait WorktreeModelHandle { + #[cfg(any(test, feature = "test-support"))] + fn flush_fs_events<'a>( + &self, + cx: &'a mut gpui::TestAppContext, + ) -> futures::future::LocalBoxFuture<'a, ()>; +} -// impl WorktreeModelHandle for Handle { -// // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that -// // occurred before the worktree was constructed. These events can cause the worktree to perform -// // extra directory scans, and emit extra scan-state notifications. -// // -// // This function mutates the worktree's directory and waits for those mutations to be picked up, -// // to ensure that all redundant FS events have already been processed. -// #[cfg(any(test, feature = "test-support"))] -// fn flush_fs_events<'a>( -// &self, -// cx: &'a gpui::TestAppContext, -// ) -> futures::future::LocalBoxFuture<'a, ()> { -// let filename = "fs-event-sentinel"; -// let tree = self.clone(); -// let (fs, root_path) = self.read_with(cx, |tree, _| { -// let tree = tree.as_local().unwrap(); -// (tree.fs.clone(), tree.abs_path().clone()) -// }); +impl WorktreeModelHandle for Model { + // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that + // occurred before the worktree was constructed. These events can cause the worktree to perform + // extra directory scans, and emit extra scan-state notifications. + // + // This function mutates the worktree's directory and waits for those mutations to be picked up, + // to ensure that all redundant FS events have already been processed. + #[cfg(any(test, feature = "test-support"))] + fn flush_fs_events<'a>( + &self, + cx: &'a mut gpui::TestAppContext, + ) -> futures::future::LocalBoxFuture<'a, ()> { + let file_name = "fs-event-sentinel"; -// async move { -// fs.create_file(&root_path.join(filename), Default::default()) -// .await -// .unwrap(); -// tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_some()) -// .await; + let tree = self.clone(); + let (fs, root_path) = self.update(cx, |tree, _| { + let tree = tree.as_local().unwrap(); + (tree.fs.clone(), tree.abs_path().clone()) + }); -// fs.remove_file(&root_path.join(filename), Default::default()) -// .await -// .unwrap(); -// tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_none()) -// .await; + async move { + fs.create_file(&root_path.join(file_name), Default::default()) + .await + .unwrap(); -// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) -// .await; -// } -// .boxed_local() -// } -// } + cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_some()) + .await; + + fs.remove_file(&root_path.join(file_name), Default::default()) + .await + .unwrap(); + cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_none()) + .await; + + cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + } + .boxed_local() + } +} #[derive(Clone, Debug)] struct TraversalProgress<'a> { diff --git a/crates/rich_text2/Cargo.toml b/crates/rich_text2/Cargo.toml new file mode 100644 index 0000000000..4eee1e107b --- /dev/null +++ b/crates/rich_text2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rich_text2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/rich_text.rs" +doctest = false + +[features] +test-support = [ + "gpui/test-support", + "util/test-support", +] + +[dependencies] +collections = { path = "../collections" } +gpui = { package = "gpui2", path = "../gpui2" } +sum_tree = { path = "../sum_tree" } +theme = { package = "theme2", path = "../theme2" } +language = { package = "language2", path = "../language2" } +util = { path = "../util" } +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +smallvec.workspace = true +smol.workspace = true diff --git a/crates/rich_text2/src/rich_text.rs b/crates/rich_text2/src/rich_text.rs new file mode 100644 index 0000000000..48b530b7c5 --- /dev/null +++ b/crates/rich_text2/src/rich_text.rs @@ -0,0 +1,373 @@ +use std::{ops::Range, sync::Arc}; + +use anyhow::bail; +use futures::FutureExt; +use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; +use language::{HighlightId, Language, LanguageRegistry}; +use util::RangeExt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Highlight { + Id(HighlightId), + Highlight(HighlightStyle), + Mention, + SelfMention, +} + +impl From for Highlight { + fn from(style: HighlightStyle) -> Self { + Self::Highlight(style) + } +} + +impl From for Highlight { + fn from(style: HighlightId) -> Self { + Self::Id(style) + } +} + +#[derive(Debug, Clone)] +pub struct RichText { + pub text: String, + pub highlights: Vec<(Range, Highlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BackgroundKind { + Code, + /// A mention background for non-self user. + Mention, + SelfMention, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RenderedRegion { + pub background_kind: Option, + pub link_url: Option, +} + +/// Allows one to specify extra links to the rendered markdown, which can be used +/// for e.g. mentions. +pub struct Mention { + pub range: Range, + pub is_self_mention: bool, +} + +impl RichText { + pub fn element( + &self, + // syntax: Arc, + // style: RichTextStyle, + // cx: &mut ViewContext, + ) -> AnyElement { + todo!(); + + // let mut region_id = 0; + // let view_id = cx.view_id(); + + // let regions = self.regions.clone(); + + // enum Markdown {} + // Text::new(self.text.clone(), style.text.clone()) + // .with_highlights( + // self.highlights + // .iter() + // .filter_map(|(range, highlight)| { + // let style = match highlight { + // Highlight::Id(id) => id.style(&syntax)?, + // Highlight::Highlight(style) => style.clone(), + // Highlight::Mention => style.mention_highlight, + // Highlight::SelfMention => style.self_mention_highlight, + // }; + // Some((range.clone(), style)) + // }) + // .collect::>(), + // ) + // .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { + // region_id += 1; + // let region = regions[ix].clone(); + // if let Some(url) = region.link_url { + // cx.scene().push_cursor_region(CursorRegion { + // bounds, + // style: CursorStyle::PointingHand, + // }); + // cx.scene().push_mouse_region( + // MouseRegion::new::(view_id, region_id, bounds) + // .on_click::(MouseButton::Left, move |_, _, cx| { + // cx.platform().open_url(&url) + // }), + // ); + // } + // if let Some(region_kind) = ®ion.background_kind { + // let background = match region_kind { + // BackgroundKind::Code => style.code_background, + // BackgroundKind::Mention => style.mention_background, + // BackgroundKind::SelfMention => style.self_mention_background, + // }; + // if background.is_some() { + // cx.scene().push_quad(gpui::Quad { + // bounds, + // background, + // border: Default::default(), + // corner_radii: (2.0).into(), + // }); + // } + // } + // }) + // .with_soft_wrap(true) + // .into_any() + } + + pub fn add_mention( + &mut self, + range: Range, + is_current_user: bool, + mention_style: HighlightStyle, + ) -> anyhow::Result<()> { + if range.end > self.text.len() { + bail!( + "Mention in range {range:?} is outside of bounds for a message of length {}", + self.text.len() + ); + } + + if is_current_user { + self.region_ranges.push(range.clone()); + self.regions.push(RenderedRegion { + background_kind: Some(BackgroundKind::Mention), + link_url: None, + }); + } + self.highlights + .push((range, Highlight::Highlight(mention_style))); + Ok(()) + } +} + +pub fn render_markdown_mut( + block: &str, + mut mentions: &[Mention], + language_registry: &Arc, + language: Option<&Arc>, + data: &mut RichText, +) { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + let options = Options::all(); + for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() { + let prev_len = data.text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); + } else { + if let Some(mention) = mentions.first() { + if source_range.contains_inclusive(&mention.range) { + mentions = &mentions[1..]; + let range = (prev_len + mention.range.start - source_range.start) + ..(prev_len + mention.range.end - source_range.start); + data.highlights.push(( + range.clone(), + if mention.is_self_mention { + Highlight::SelfMention + } else { + Highlight::Mention + }, + )); + data.region_ranges.push(range); + data.regions.push(RenderedRegion { + background_kind: Some(if mention.is_self_mention { + BackgroundKind::SelfMention + } else { + BackgroundKind::Mention + }), + link_url: None, + }); + } + } + + data.text.push_str(t.as_ref()); + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.font_weight = Some(FontWeight::BOLD); + } + if italic_depth > 0 { + style.font_style = Some(FontStyle::Italic); + } + if let Some(link_url) = link_url.clone() { + data.region_ranges.push(prev_len..data.text.len()); + data.regions.push(RenderedRegion { + link_url: Some(link_url), + background_kind: None, + }); + style.underline = Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = data.highlights.last_mut() { + if last_range.end == prev_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = data.text.len(); + new_highlight = false; + } + } + if new_highlight { + data.highlights + .push((prev_len..data.text.len(), Highlight::Highlight(style))); + } + } + } + } + Event::Code(t) => { + data.text.push_str(t.as_ref()); + data.region_ranges.push(prev_len..data.text.len()); + if link_url.is_some() { + data.highlights.push(( + prev_len..data.text.len(), + Highlight::Highlight(HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }), + )); + } + data.regions.push(RenderedRegion { + background_kind: Some(BackgroundKind::Code), + link_url: link_url.clone(), + }); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), + Tag::Heading(_, _, _) => { + new_paragraph(&mut data.text, &mut list_stack); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut data.text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.cloned() + } + } + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + Tag::List(number) => { + list_stack.push((number, false)); + } + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !data.text.is_empty() && !data.text.ends_with('\n') { + data.text.push('\n'); + } + for _ in 0..len - 1 { + data.text.push_str(" "); + } + if let Some(number) = list_number { + data.text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + data.text.push_str("- "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + Event::HardBreak => data.text.push('\n'), + Event::SoftBreak => data.text.push(' '), + _ => {} + } + } +} + +pub fn render_markdown( + block: String, + mentions: &[Mention], + language_registry: &Arc, + language: Option<&Arc>, +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; + + render_markdown_mut(&block, mentions, language_registry, language, &mut data); + + data.text = data.text.trim().to_string(); + + data +} + +pub fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, Highlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + highlights.push(( + prev_len + range.start..prev_len + range.end, + Highlight::Id(highlight_id), + )); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} diff --git a/crates/rpc2/Cargo.toml b/crates/rpc2/Cargo.toml index f108af3d3f..0995029b30 100644 --- a/crates/rpc2/Cargo.toml +++ b/crates/rpc2/Cargo.toml @@ -10,12 +10,12 @@ path = "src/rpc.rs" doctest = false [features] -test-support = ["collections/test-support", "gpui2/test-support"] +test-support = ["collections/test-support", "gpui/test-support"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -gpui2 = { path = "../gpui2", optional = true } +gpui = { package = "gpui2", path = "../gpui2", optional = true } util = { path = "../util" } anyhow.workspace = true async-lock = "2.4" @@ -37,7 +37,7 @@ prost-build = "0.9" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } smol.workspace = true tempdir.workspace = true ctor.workspace = true diff --git a/crates/rpc2/src/conn.rs b/crates/rpc2/src/conn.rs index 902e9822d5..ae5c9fd226 100644 --- a/crates/rpc2/src/conn.rs +++ b/crates/rpc2/src/conn.rs @@ -34,7 +34,7 @@ impl Connection { #[cfg(any(test, feature = "test-support"))] pub fn in_memory( - executor: gpui2::Executor, + executor: gpui::BackgroundExecutor, ) -> (Self, Self, std::sync::Arc) { use std::sync::{ atomic::{AtomicBool, Ordering::SeqCst}, @@ -53,7 +53,7 @@ impl Connection { #[allow(clippy::type_complexity)] fn channel( killed: Arc, - executor: gpui2::Executor, + executor: gpui::BackgroundExecutor, ) -> ( Box>, Box>>, diff --git a/crates/rpc2/src/peer.rs b/crates/rpc2/src/peer.rs index 6dfb170f4c..80a2ab4378 100644 --- a/crates/rpc2/src/peer.rs +++ b/crates/rpc2/src/peer.rs @@ -342,7 +342,7 @@ impl Peer { pub fn add_test_connection( self: &Arc, connection: Connection, - executor: gpui2::Executor, + executor: gpui::BackgroundExecutor, ) -> ( ConnectionId, impl Future> + Send, @@ -557,17 +557,18 @@ mod tests { use super::*; use crate::TypedEnvelope; use async_tungstenite::tungstenite::Message as WebSocketMessage; - use gpui2::TestAppContext; + use gpui::TestAppContext; - #[ctor::ctor] fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::init(); } } - #[gpui2::test(iterations = 50)] + #[gpui::test(iterations = 50)] async fn test_request_response(cx: &mut TestAppContext) { + init_logger(); + let executor = cx.executor(); // create 2 clients connected to 1 server @@ -662,7 +663,7 @@ mod tests { } } - #[gpui2::test(iterations = 50)] + #[gpui::test(iterations = 50)] async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) { let executor = cx.executor(); let server = Peer::new(0); @@ -760,7 +761,7 @@ mod tests { ); } - #[gpui2::test(iterations = 50)] + #[gpui::test(iterations = 50)] async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { let executor = cx.executor().clone(); let server = Peer::new(0); @@ -872,7 +873,7 @@ mod tests { ); } - #[gpui2::test(iterations = 50)] + #[gpui::test(iterations = 50)] async fn test_disconnect(cx: &mut TestAppContext) { let executor = cx.executor(); @@ -908,7 +909,7 @@ mod tests { .is_err()); } - #[gpui2::test(iterations = 50)] + #[gpui::test(iterations = 50)] async fn test_io_error(cx: &mut TestAppContext) { let executor = cx.executor(); let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); diff --git a/crates/rpc2/src/proto.rs b/crates/rpc2/src/proto.rs index c1a7af3e4d..f0d7937f6f 100644 --- a/crates/rpc2/src/proto.rs +++ b/crates/rpc2/src/proto.rs @@ -616,7 +616,7 @@ pub fn split_worktree_update( mod tests { use super::*; - #[gpui2::test] + #[gpui::test] async fn test_buffer_size() { let (tx, rx) = futures::channel::mpsc::unbounded(); let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow!(""))); @@ -648,7 +648,7 @@ mod tests { assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); } - #[gpui2::test] + #[gpui::test] fn test_converting_peer_id_from_and_to_u64() { let peer_id = PeerId { owner_id: 10, diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 64421f5431..4ebd31a2bc 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -29,7 +29,6 @@ serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true -globset.workspace = true serde_json.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ad8127dad8..82aa4b56ae 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -22,7 +22,7 @@ use gpui::{ }; use menu::Confirm; use project::{ - search::{PathMatcher, SearchInputs, SearchQuery}, + search::{SearchInputs, SearchQuery}, Entry, Project, }; use semantic_index::{SemanticIndex, SemanticIndexStatus}; @@ -37,7 +37,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use util::ResultExt as _; +use util::{paths::PathMatcher, ResultExt as _}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, searchable::{Direction, SearchableItem, SearchableItemHandle}, diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index 63527cea1c..5b416f7a64 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -9,7 +9,7 @@ use futures::channel::oneshot; use gpui::executor; use ndarray::{Array1, Array2}; use ordered_float::OrderedFloat; -use project::{search::PathMatcher, Fs}; +use project::Fs; use rpc::proto::Timestamp; use rusqlite::params; use rusqlite::types::Value; @@ -21,7 +21,7 @@ use std::{ sync::Arc, time::SystemTime, }; -use util::TryFutureExt; +use util::{paths::PathMatcher, TryFutureExt}; pub fn argsort(data: &[T]) -> Vec { let mut indices = (0..data.len()).collect::>(); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 818faa0444..7d1eacd7fa 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -21,7 +21,7 @@ use ordered_float::OrderedFloat; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; -use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId}; +use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId}; use smol::channel; use std::{ cmp::Reverse, @@ -33,6 +33,7 @@ use std::{ sync::{Arc, Weak}, time::{Duration, Instant, SystemTime}, }; +use util::paths::PathMatcher; use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt}; use workspace::WorkspaceCreated; diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 7a91d1e100..2145d1f9e0 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -10,13 +10,13 @@ use gpui::{executor::Deterministic, Task, TestAppContext}; use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; use parking_lot::Mutex; use pretty_assertions::assert_eq; -use project::{project_settings::ProjectSettings, search::PathMatcher, FakeFs, Fs, Project}; +use project::{project_settings::ProjectSettings, FakeFs, Fs, Project}; use rand::{rngs::StdRng, Rng}; use serde_json::json; use settings::SettingsStore; use std::{path::Path, sync::Arc, time::SystemTime}; use unindent::Unindent; -use util::RandomCharIter; +use util::{paths::PathMatcher, RandomCharIter}; #[ctor::ctor] fn init_logger() { @@ -289,12 +289,12 @@ async fn test_code_context_retrieval_rust() { impl E { // This is also a preceding comment pub fn function_1() -> Option<()> { - todo!(); + unimplemented!(); } // This is a preceding comment fn function_2() -> Result<()> { - todo!(); + unimplemented!(); } } @@ -344,7 +344,7 @@ async fn test_code_context_retrieval_rust() { " // This is also a preceding comment pub fn function_1() -> Option<()> { - todo!(); + unimplemented!(); }" .unindent(), text.find("pub fn function_1").unwrap(), @@ -353,7 +353,7 @@ async fn test_code_context_retrieval_rust() { " // This is a preceding comment fn function_2() -> Result<()> { - todo!(); + unimplemented!(); }" .unindent(), text.find("fn function_2").unwrap(), diff --git a/crates/settings2/Cargo.toml b/crates/settings2/Cargo.toml index b455b1e38a..0a4051cbb3 100644 --- a/crates/settings2/Cargo.toml +++ b/crates/settings2/Cargo.toml @@ -9,14 +9,14 @@ path = "src/settings2.rs" doctest = false [features] -test-support = ["gpui2/test-support", "fs/test-support"] +test-support = ["gpui/test-support", "fs/test-support"] [dependencies] collections = { path = "../collections" } -gpui2 = { path = "../gpui2" } +gpui = {package = "gpui2", path = "../gpui2" } sqlez = { path = "../sqlez" } -fs2 = { path = "../fs2" } -feature_flags2 = { path = "../feature_flags2" } +fs = {package = "fs2", path = "../fs2" } +feature_flags = {package = "feature_flags2", path = "../feature_flags2" } util = { path = "../util" } anyhow.workspace = true @@ -35,8 +35,8 @@ tree-sitter.workspace = true tree-sitter-json = "*" [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } -fs = { path = "../fs", features = ["test-support"] } +gpui = {package = "gpui2", path = "../gpui2", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } indoc.workspace = true pretty_assertions.workspace = true unindent.workspace = true diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index d0a32131b5..e51bd76e5e 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -1,7 +1,7 @@ use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{anyhow, Context, Result}; use collections::BTreeMap; -use gpui2::{AppContext, KeyBinding, SharedString}; +use gpui::{AppContext, KeyBinding, SharedString}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, @@ -137,7 +137,7 @@ impl KeymapFile { } } -fn no_action() -> Box { +fn no_action() -> Box { todo!() } diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index 2105035605..c623ae9caf 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -1,8 +1,8 @@ use crate::{settings_store::SettingsStore, Settings}; use anyhow::Result; -use fs2::Fs; +use fs::Fs; use futures::{channel::mpsc, StreamExt}; -use gpui2::{AppContext, Executor}; +use gpui::{AppContext, BackgroundExecutor}; use std::{io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; use util::{paths, ResultExt}; @@ -28,7 +28,7 @@ pub fn test_settings() -> String { } pub fn watch_config_file( - executor: &Executor, + executor: &BackgroundExecutor, fs: Arc, path: PathBuf, ) -> mpsc::UnboundedReceiver { @@ -63,7 +63,10 @@ pub fn handle_settings_file_changes( mut user_settings_file_rx: mpsc::UnboundedReceiver, cx: &mut AppContext, ) { - let user_settings_content = cx.executor().block(user_settings_file_rx.next()).unwrap(); + let user_settings_content = cx + .background_executor() + .block(user_settings_file_rx.next()) + .unwrap(); cx.update_global(|store: &mut SettingsStore, cx| { store .set_user_settings(&user_settings_content, cx) diff --git a/crates/settings2/src/settings_store.rs b/crates/settings2/src/settings_store.rs index e2c370bcac..3317a50f52 100644 --- a/crates/settings2/src/settings_store.rs +++ b/crates/settings2/src/settings_store.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; -use gpui2::AppContext; +use gpui::AppContext; use lazy_static::lazy_static; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; @@ -877,7 +877,7 @@ mod tests { use serde_derive::Deserialize; use unindent::Unindent; - #[gpui2::test] + #[gpui::test] fn test_settings_store_basic(cx: &mut AppContext) { let mut store = SettingsStore::default(); store.register_setting::(cx); @@ -994,7 +994,7 @@ mod tests { ); } - #[gpui2::test] + #[gpui::test] fn test_setting_store_assign_json_before_register(cx: &mut AppContext) { let mut store = SettingsStore::default(); store @@ -1037,7 +1037,7 @@ mod tests { ); } - #[gpui2::test] + #[gpui::test] fn test_setting_store_update(cx: &mut AppContext) { let mut store = SettingsStore::default(); store.register_setting::(cx); diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index ffadb3af41..54241b6d72 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -336,13 +336,13 @@ mod test { FOREIGN KEY(dock_pane) REFERENCES panes(pane_id), FOREIGN KEY(active_pane) REFERENCES panes(pane_id) ) STRICT; - + CREATE TABLE panes( pane_id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, active INTEGER NOT NULL, -- Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; "] diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs index afc29660ff..c1c65d62fa 100644 --- a/crates/storybook2/src/stories/colors.rs +++ b/crates/storybook2/src/stories/colors.rs @@ -1,5 +1,6 @@ use crate::story::Story; use gpui2::{px, Div, Render}; +use theme2::{default_color_scales, ColorScaleStep}; use ui::prelude::*; pub struct ColorsStory; @@ -8,7 +9,7 @@ impl Render for ColorsStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let color_scales = theme2::default_color_scales(); + let color_scales = default_color_scales(); Story::container(cx) .child(Story::title(cx, "Colors")) @@ -20,18 +21,23 @@ impl Render for ColorsStory { .gap_1() .overflow_y_scroll() .text_color(gpui2::white()) - .children(color_scales.into_iter().map(|(name, scale)| { + .children(color_scales.into_iter().map(|scale| { div() .flex() .child( div() .w(px(75.)) .line_height(px(24.)) - .child(name.to_string()), + .child(scale.name().to_string()), + ) + .child( + div() + .flex() + .gap_1() + .children(ColorScaleStep::ALL.map(|step| { + div().flex().size_6().bg(scale.step(cx, step)) + })), ) - .child(div().flex().gap_1().children( - (1..=12).map(|step| div().flex().size_6().bg(scale.step(cx, step))), - )) })), ) } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index f3f6a8d5fb..aa71040b47 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -3,7 +3,7 @@ use gpui2::{ StatelessInteractive, Styled, View, VisualContext, WindowContext, }; use serde::Deserialize; -use theme2::theme; +use theme2::ActiveTheme; #[derive(Clone, Default, PartialEq, Deserialize)] struct ActionA; @@ -34,13 +34,13 @@ impl Render for FocusStory { type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui2::ViewContext) -> Self::Element { - let theme = theme(cx); - let color_1 = theme.git_created; - let color_2 = theme.git_modified; - let color_3 = theme.git_deleted; - let color_4 = theme.git_conflict; - let color_5 = theme.git_ignored; - let color_6 = theme.git_renamed; + let theme = cx.theme(); + let color_1 = theme.styles.git.created; + let color_2 = theme.styles.git.modified; + let color_3 = theme.styles.git.deleted; + let color_4 = theme.styles.git.conflict; + let color_5 = theme.styles.git.ignored; + let color_6 = theme.styles.git.renamed; let child_1 = cx.focus_handle(); let child_2 = cx.focus_handle(); diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index b504a512a6..9236629c34 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -2,7 +2,7 @@ use gpui2::{ div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteraction, Styled, View, VisualContext, WindowContext, }; -use theme2::theme; +use theme2::ActiveTheme; pub struct ScrollStory; @@ -16,13 +16,13 @@ impl Render for ScrollStory { type Element = Div>; fn render(&mut self, cx: &mut gpui2::ViewContext) -> Self::Element { - let theme = theme(cx); - let color_1 = theme.git_created; - let color_2 = theme.git_modified; + let theme = cx.theme(); + let color_1 = theme.styles.git.created; + let color_2 = theme.styles.git.modified; div() .id("parent") - .bg(theme.background) + .bg(theme.colors().background) .size_full() .overflow_scroll() .children((0..10).map(|row| { diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c2903c88e1..4e2c439db0 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -48,7 +48,7 @@ fn main() { let args = Args::parse(); let story_selector = args.story.clone(); - let theme_name = args.theme.unwrap_or("One Dark".to_string()); + let theme_name = args.theme.unwrap_or("Zed Pro Moonlight".to_string()); let asset_source = Arc::new(Assets); gpui2::App::production(asset_source).run(move |cx| { @@ -71,18 +71,22 @@ fn main() { theme_settings.active_theme = theme_registry.get(&theme_name).unwrap(); ThemeSettings::override_global(theme_settings, cx); - cx.set_global(theme.clone()); ui::settings::init(cx); let window = cx.open_window( WindowOptions { bounds: WindowBounds::Fixed(Bounds { origin: Default::default(), - size: size(px(1700.), px(980.)).into(), + size: size(px(1500.), px(780.)).into(), }), ..Default::default() }, - move |cx| cx.build_view(|cx| StoryWrapper::new(selector.story(cx))), + move |cx| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); + + cx.build_view(|cx| StoryWrapper::new(selector.story(cx))) + }, ); cx.activate(true); diff --git a/crates/terminal2/Cargo.toml b/crates/terminal2/Cargo.toml index 3ca5dc9aba..e37b949881 100644 --- a/crates/terminal2/Cargo.toml +++ b/crates/terminal2/Cargo.toml @@ -10,10 +10,10 @@ doctest = false [dependencies] -gpui2 = { path = "../gpui2" } -settings2 = { path = "../settings2" } -db2 = { path = "../db2" } -theme2 = { path = "../theme2" } +gpui = { package = "gpui2", path = "../gpui2" } +settings = { package = "settings2", path = "../settings2" } +db = { package = "db2", path = "../db2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" } diff --git a/crates/terminal2/src/mappings/colors.rs b/crates/terminal2/src/mappings/colors.rs index 99b66b9e14..fc3557b4e8 100644 --- a/crates/terminal2/src/mappings/colors.rs +++ b/crates/terminal2/src/mappings/colors.rs @@ -113,7 +113,7 @@ use alacritty_terminal::term::color::Rgb as AlacRgb; // let b = (i % 36) % 6; // (r, g, b) // } -use gpui2::Rgba; +use gpui::Rgba; //Convenience method to convert from a GPUI color to an alacritty Rgb pub fn to_alac_rgb(color: impl Into) -> AlacRgb { diff --git a/crates/terminal2/src/mappings/keys.rs b/crates/terminal2/src/mappings/keys.rs index 0009d39e13..f8a26fbe2b 100644 --- a/crates/terminal2/src/mappings/keys.rs +++ b/crates/terminal2/src/mappings/keys.rs @@ -1,6 +1,6 @@ /// The mappings defined in this file where created from reading the alacritty source use alacritty_terminal::term::TermMode; -use gpui2::Keystroke; +use gpui::Keystroke; #[derive(Debug, PartialEq, Eq)] enum AlacModifiers { @@ -278,7 +278,7 @@ fn modifier_code(keystroke: &Keystroke) -> u32 { #[cfg(test)] mod test { - use gpui2::Modifiers; + use gpui::Modifiers; use super::*; diff --git a/crates/terminal2/src/mappings/mouse.rs b/crates/terminal2/src/mappings/mouse.rs index 28ad510fcd..eac6ad17ff 100644 --- a/crates/terminal2/src/mappings/mouse.rs +++ b/crates/terminal2/src/mappings/mouse.rs @@ -6,7 +6,7 @@ use alacritty_terminal::grid::Dimensions; /// with modifications for our circumstances use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side}; use alacritty_terminal::term::TermMode; -use gpui2::{px, Modifiers, MouseButton, MouseMoveEvent, Pixels, Point, ScrollWheelEvent}; +use gpui::{px, Modifiers, MouseButton, MouseMoveEvent, Pixels, Point, ScrollWheelEvent}; use crate::TerminalSize; @@ -45,10 +45,10 @@ impl AlacMouseButton { fn from_move(e: &MouseMoveEvent) -> Self { match e.pressed_button { Some(b) => match b { - gpui2::MouseButton::Left => AlacMouseButton::LeftMove, - gpui2::MouseButton::Middle => AlacMouseButton::MiddleMove, - gpui2::MouseButton::Right => AlacMouseButton::RightMove, - gpui2::MouseButton::Navigate(_) => AlacMouseButton::Other, + gpui::MouseButton::Left => AlacMouseButton::LeftMove, + gpui::MouseButton::Middle => AlacMouseButton::MiddleMove, + gpui::MouseButton::Right => AlacMouseButton::RightMove, + gpui::MouseButton::Navigate(_) => AlacMouseButton::Other, }, None => AlacMouseButton::NoneMove, } @@ -56,17 +56,17 @@ impl AlacMouseButton { fn from_button(e: MouseButton) -> Self { match e { - gpui2::MouseButton::Left => AlacMouseButton::LeftButton, - gpui2::MouseButton::Right => AlacMouseButton::MiddleButton, - gpui2::MouseButton::Middle => AlacMouseButton::RightButton, - gpui2::MouseButton::Navigate(_) => AlacMouseButton::Other, + gpui::MouseButton::Left => AlacMouseButton::LeftButton, + gpui::MouseButton::Right => AlacMouseButton::MiddleButton, + gpui::MouseButton::Middle => AlacMouseButton::RightButton, + gpui::MouseButton::Navigate(_) => AlacMouseButton::Other, } } fn from_scroll(e: &ScrollWheelEvent) -> Self { let is_positive = match e.delta { - gpui2::ScrollDelta::Pixels(pixels) => pixels.y > px(0.), - gpui2::ScrollDelta::Lines(lines) => lines.y > 0., + gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.), + gpui::ScrollDelta::Lines(lines) => lines.y > 0., }; if is_positive { @@ -118,7 +118,7 @@ pub fn alt_scroll(scroll_lines: i32) -> Vec { pub fn mouse_button_report( point: AlacPoint, - button: gpui2::MouseButton, + button: gpui::MouseButton, modifiers: Modifiers, pressed: bool, mode: TermMode, diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index 5cf73576bc..ba5c4815f2 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -33,7 +33,7 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use util::truncate_and_trailoff; @@ -49,10 +49,10 @@ use std::{ }; use thiserror::Error; -use gpui2::{ +use gpui::{ px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke, - MainThread, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Point, ScrollWheelEvent, Size, Task, TouchPhase, + ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + Point, ScrollWheelEvent, Size, Task, TouchPhase, }; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; @@ -403,7 +403,7 @@ impl TerminalBuilder { pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { //Event loop - cx.spawn_on_main(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { use futures::StreamExt; while let Some(event) = self.events_rx.next().await { @@ -414,7 +414,10 @@ impl TerminalBuilder { 'outer: loop { let mut events = vec![]; - let mut timer = cx.executor().timer(Duration::from_millis(4)).fuse(); + let mut timer = cx + .background_executor() + .timer(Duration::from_millis(4)) + .fuse(); let mut wakeup = false; loop { futures::select_biased! { @@ -551,7 +554,7 @@ pub struct Terminal { } impl Terminal { - fn process_event(&mut self, event: &AlacTermEvent, cx: &mut MainThread>) { + fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext) { match event { AlacTermEvent::Title(title) => { self.breadcrumb_text = title.to_string(); @@ -708,8 +711,7 @@ impl Terminal { InternalEvent::Copy => { if let Some(txt) = term.selection_to_string() { - cx.run_on_main(|cx| cx.write_to_clipboard(ClipboardItem::new(txt))) - .detach(); + cx.write_to_clipboard(ClipboardItem::new(txt)) } } InternalEvent::ScrollToAlacPoint(point) => { @@ -982,7 +984,7 @@ impl Terminal { term.lock_unfair() //It's been too long, force block } else if let None = self.sync_task { //Skip this frame - let delay = cx.executor().timer(Duration::from_millis(16)); + let delay = cx.background_executor().timer(Duration::from_millis(16)); self.sync_task = Some(cx.spawn(|weak_handle, mut cx| async move { delay.await; if let Some(handle) = weak_handle.upgrade() { @@ -1189,7 +1191,7 @@ impl Terminal { &mut self, e: &MouseUpEvent, origin: Point, - cx: &mut MainThread>, + cx: &mut ModelContext, ) { let setting = TerminalSettings::get_global(cx); @@ -1300,7 +1302,7 @@ impl Terminal { cx: &mut ModelContext, ) -> Task>> { let term = self.term.clone(); - cx.executor().spawn(async move { + cx.background_executor().spawn(async move { let term = term.lock(); all_search_matches(&term, &searcher).collect() @@ -1407,7 +1409,7 @@ mod tests { index::{Column, Line, Point as AlacPoint}, term::cell::Cell, }; - use gpui2::{point, size, Pixels}; + use gpui::{point, size, Pixels}; use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize}; diff --git a/crates/terminal2/src/terminal_settings.rs b/crates/terminal2/src/terminal_settings.rs index 1be9ac5000..1d1e1cea2a 100644 --- a/crates/terminal2/src/terminal_settings.rs +++ b/crates/terminal2/src/terminal_settings.rs @@ -1,4 +1,4 @@ -use gpui2::{AppContext, FontFeatures}; +use gpui::{AppContext, FontFeatures}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; @@ -98,7 +98,7 @@ impl TerminalSettings { // } } -impl settings2::Settings for TerminalSettings { +impl settings::Settings for TerminalSettings { const KEY: Option<&'static str> = Some("terminal"); type FileContent = TerminalSettingsContent; diff --git a/crates/text2/Cargo.toml b/crates/text2/Cargo.toml new file mode 100644 index 0000000000..7c12d22adf --- /dev/null +++ b/crates/text2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "text2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/text2.rs" +doctest = false + +[features] +test-support = ["rand"] + +[dependencies] +clock = { path = "../clock" } +collections = { path = "../collections" } +rope = { path = "../rope" } +sum_tree = { path = "../sum_tree" } +util = { path = "../util" } + +anyhow.workspace = true +digest = { version = "0.9", features = ["std"] } +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand = { workspace = true, optional = true } +smallvec.workspace = true +regex.workspace = true + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +ctor.workspace = true +env_logger.workspace = true +rand.workspace = true diff --git a/crates/text2/src/anchor.rs b/crates/text2/src/anchor.rs new file mode 100644 index 0000000000..084be0e336 --- /dev/null +++ b/crates/text2/src/anchor.rs @@ -0,0 +1,144 @@ +use crate::{ + locator::Locator, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, + ToPointUtf16, +}; +use anyhow::Result; +use std::{cmp::Ordering, fmt::Debug, ops::Range}; +use sum_tree::Bias; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] +pub struct Anchor { + pub timestamp: clock::Lamport, + pub offset: usize, + pub bias: Bias, + pub buffer_id: Option, +} + +impl Anchor { + pub const MIN: Self = Self { + timestamp: clock::Lamport::MIN, + offset: usize::MIN, + bias: Bias::Left, + buffer_id: None, + }; + + pub const MAX: Self = Self { + timestamp: clock::Lamport::MAX, + offset: usize::MAX, + bias: Bias::Right, + buffer_id: None, + }; + + pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { + let fragment_id_comparison = if self.timestamp == other.timestamp { + Ordering::Equal + } else { + buffer + .fragment_id_for_anchor(self) + .cmp(buffer.fragment_id_for_anchor(other)) + }; + + fragment_id_comparison + .then_with(|| self.offset.cmp(&other.offset)) + .then_with(|| self.bias.cmp(&other.bias)) + } + + pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).is_le() { + *self + } else { + *other + } + } + + pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).is_ge() { + *self + } else { + *other + } + } + + pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor { + if bias == Bias::Left { + self.bias_left(buffer) + } else { + self.bias_right(buffer) + } + } + + pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor { + if self.bias == Bias::Left { + *self + } else { + buffer.anchor_before(self) + } + } + + pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor { + if self.bias == Bias::Right { + *self + } else { + buffer.anchor_after(self) + } + } + + pub fn summary(&self, content: &BufferSnapshot) -> D + where + D: TextDimension, + { + content.summary_for_anchor(self) + } + + /// Returns true when the [Anchor] is located inside a visible fragment. + pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool { + if *self == Anchor::MIN || *self == Anchor::MAX { + true + } else { + let fragment_id = buffer.fragment_id_for_anchor(self); + let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(); + fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None); + fragment_cursor + .item() + .map_or(false, |fragment| fragment.visible) + } + } +} + +pub trait OffsetRangeExt { + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range; +} + +impl OffsetRangeExt for Range +where + T: ToOffset, +{ + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot)..self.end.to_offset(snapshot) + } + + fn to_point(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point(snapshot) + ..self.end.to_offset(snapshot).to_point(snapshot) + } + + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point_utf16(snapshot) + ..self.end.to_offset(snapshot).to_point_utf16(snapshot) + } +} + +pub trait AnchorRangeExt { + fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Result; +} + +impl AnchorRangeExt for Range { + fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Result { + Ok(match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), + ord => ord, + }) + } +} diff --git a/crates/text2/src/locator.rs b/crates/text2/src/locator.rs new file mode 100644 index 0000000000..07b73ace05 --- /dev/null +++ b/crates/text2/src/locator.rs @@ -0,0 +1,125 @@ +use lazy_static::lazy_static; +use smallvec::{smallvec, SmallVec}; +use std::iter; + +lazy_static! { + static ref MIN: Locator = Locator::min(); + static ref MAX: Locator = Locator::max(); +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Locator(SmallVec<[u64; 4]>); + +impl Locator { + pub fn min() -> Self { + Self(smallvec![u64::MIN]) + } + + pub fn max() -> Self { + Self(smallvec![u64::MAX]) + } + + pub fn min_ref() -> &'static Self { + &*MIN + } + + pub fn max_ref() -> &'static Self { + &*MAX + } + + pub fn assign(&mut self, other: &Self) { + self.0.resize(other.0.len(), 0); + self.0.copy_from_slice(&other.0); + } + + pub fn between(lhs: &Self, rhs: &Self) -> Self { + let lhs = lhs.0.iter().copied().chain(iter::repeat(u64::MIN)); + let rhs = rhs.0.iter().copied().chain(iter::repeat(u64::MAX)); + let mut location = SmallVec::new(); + for (lhs, rhs) in lhs.zip(rhs) { + let mid = lhs + ((rhs.saturating_sub(lhs)) >> 48); + location.push(mid); + if mid > lhs { + break; + } + } + Self(location) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Default for Locator { + fn default() -> Self { + Self::min() + } +} + +impl sum_tree::Item for Locator { + type Summary = Locator; + + fn summary(&self) -> Self::Summary { + self.clone() + } +} + +impl sum_tree::KeyedItem for Locator { + type Key = Locator; + + fn key(&self) -> Self::Key { + self.clone() + } +} + +impl sum_tree::Summary for Locator { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.assign(summary); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::prelude::*; + use std::mem; + + #[gpui::test(iterations = 100)] + fn test_locators(mut rng: StdRng) { + let mut lhs = Default::default(); + let mut rhs = Default::default(); + while lhs == rhs { + lhs = Locator( + (0..rng.gen_range(1..=5)) + .map(|_| rng.gen_range(0..=100)) + .collect(), + ); + rhs = Locator( + (0..rng.gen_range(1..=5)) + .map(|_| rng.gen_range(0..=100)) + .collect(), + ); + } + + if lhs > rhs { + mem::swap(&mut lhs, &mut rhs); + } + + let middle = Locator::between(&lhs, &rhs); + assert!(middle > lhs); + assert!(middle < rhs); + for ix in 0..middle.0.len() - 1 { + assert!( + middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0) + || middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0) + ); + } + } +} diff --git a/crates/text2/src/network.rs b/crates/text2/src/network.rs new file mode 100644 index 0000000000..2f49756ca3 --- /dev/null +++ b/crates/text2/src/network.rs @@ -0,0 +1,69 @@ +use clock::ReplicaId; + +pub struct Network { + inboxes: std::collections::BTreeMap>>, + all_messages: Vec, + rng: R, +} + +#[derive(Clone)] +struct Envelope { + message: T, +} + +impl Network { + pub fn new(rng: R) -> Self { + Network { + inboxes: Default::default(), + all_messages: Vec::new(), + rng, + } + } + + pub fn add_peer(&mut self, id: ReplicaId) { + self.inboxes.insert(id, Vec::new()); + } + + pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) { + self.inboxes + .insert(new_replica_id, self.inboxes[&old_replica_id].clone()); + } + + pub fn is_idle(&self) -> bool { + self.inboxes.values().all(|i| i.is_empty()) + } + + pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { + for (replica, inbox) in self.inboxes.iter_mut() { + if *replica != sender { + for message in &messages { + // Insert one or more duplicates of this message, potentially *before* the previous + // message sent by this peer to simulate out-of-order delivery. + for _ in 0..self.rng.gen_range(1..4) { + let insertion_index = self.rng.gen_range(0..inbox.len() + 1); + inbox.insert( + insertion_index, + Envelope { + message: message.clone(), + }, + ); + } + } + } + } + self.all_messages.extend(messages); + } + + pub fn has_unreceived(&self, receiver: ReplicaId) -> bool { + !self.inboxes[&receiver].is_empty() + } + + pub fn receive(&mut self, receiver: ReplicaId) -> Vec { + let inbox = self.inboxes.get_mut(&receiver).unwrap(); + let count = self.rng.gen_range(0..inbox.len() + 1); + inbox + .drain(0..count) + .map(|envelope| envelope.message) + .collect() + } +} diff --git a/crates/text2/src/operation_queue.rs b/crates/text2/src/operation_queue.rs new file mode 100644 index 0000000000..063f050665 --- /dev/null +++ b/crates/text2/src/operation_queue.rs @@ -0,0 +1,153 @@ +use std::{fmt::Debug, ops::Add}; +use sum_tree::{Dimension, Edit, Item, KeyedItem, SumTree, Summary}; + +pub trait Operation: Clone + Debug { + fn lamport_timestamp(&self) -> clock::Lamport; +} + +#[derive(Clone, Debug)] +struct OperationItem(T); + +#[derive(Clone, Debug)] +pub struct OperationQueue(SumTree>); + +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct OperationKey(clock::Lamport); + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct OperationSummary { + pub key: OperationKey, + pub len: usize, +} + +impl OperationKey { + pub fn new(timestamp: clock::Lamport) -> Self { + Self(timestamp) + } +} + +impl Default for OperationQueue { + fn default() -> Self { + OperationQueue::new() + } +} + +impl OperationQueue { + pub fn new() -> Self { + OperationQueue(SumTree::new()) + } + + pub fn len(&self) -> usize { + self.0.summary().len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn insert(&mut self, mut ops: Vec) { + ops.sort_by_key(|op| op.lamport_timestamp()); + ops.dedup_by_key(|op| op.lamport_timestamp()); + self.0.edit( + ops.into_iter() + .map(|op| Edit::Insert(OperationItem(op))) + .collect(), + &(), + ); + } + + pub fn drain(&mut self) -> Self { + let clone = self.clone(); + self.0 = SumTree::new(); + clone + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|i| &i.0) + } +} + +impl Summary for OperationSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + assert!(self.key < other.key); + self.key = other.key; + self.len += other.len; + } +} + +impl<'a> Add<&'a Self> for OperationSummary { + type Output = Self; + + fn add(self, other: &Self) -> Self { + assert!(self.key < other.key); + OperationSummary { + key: other.key, + len: self.len + other.len, + } + } +} + +impl<'a> Dimension<'a, OperationSummary> for OperationKey { + fn add_summary(&mut self, summary: &OperationSummary, _: &()) { + assert!(*self <= summary.key); + *self = summary.key; + } +} + +impl Item for OperationItem { + type Summary = OperationSummary; + + fn summary(&self) -> Self::Summary { + OperationSummary { + key: OperationKey::new(self.0.lamport_timestamp()), + len: 1, + } + } +} + +impl KeyedItem for OperationItem { + type Key = OperationKey; + + fn key(&self) -> Self::Key { + OperationKey::new(self.0.lamport_timestamp()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_len() { + let mut clock = clock::Lamport::new(0); + + let mut queue = OperationQueue::new(); + assert_eq!(queue.len(), 0); + + queue.insert(vec![ + TestOperation(clock.tick()), + TestOperation(clock.tick()), + ]); + assert_eq!(queue.len(), 2); + + queue.insert(vec![TestOperation(clock.tick())]); + assert_eq!(queue.len(), 3); + + drop(queue.drain()); + assert_eq!(queue.len(), 0); + + queue.insert(vec![TestOperation(clock.tick())]); + assert_eq!(queue.len(), 1); + } + + #[derive(Clone, Debug, Eq, PartialEq)] + struct TestOperation(clock::Lamport); + + impl Operation for TestOperation { + fn lamport_timestamp(&self) -> clock::Lamport { + self.0 + } + } +} diff --git a/crates/text2/src/patch.rs b/crates/text2/src/patch.rs new file mode 100644 index 0000000000..f10acbc2d3 --- /dev/null +++ b/crates/text2/src/patch.rs @@ -0,0 +1,594 @@ +use crate::Edit; +use std::{ + cmp, mem, + ops::{Add, AddAssign, Sub}, +}; + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Patch(Vec>); + +impl Patch +where + T: 'static + + Clone + + Copy + + Ord + + Sub + + Add + + AddAssign + + Default + + PartialEq, +{ + pub fn new(edits: Vec>) -> Self { + #[cfg(debug_assertions)] + { + let mut last_edit: Option<&Edit> = None; + for edit in &edits { + if let Some(last_edit) = last_edit { + assert!(edit.old.start > last_edit.old.end); + assert!(edit.new.start > last_edit.new.end); + } + last_edit = Some(edit); + } + } + Self(edits) + } + + pub fn edits(&self) -> &[Edit] { + &self.0 + } + + pub fn into_inner(self) -> Vec> { + self.0 + } + + pub fn compose(&self, new_edits_iter: impl IntoIterator>) -> Self { + let mut old_edits_iter = self.0.iter().cloned().peekable(); + let mut new_edits_iter = new_edits_iter.into_iter().peekable(); + let mut composed = Patch(Vec::new()); + + let mut old_start = T::default(); + let mut new_start = T::default(); + loop { + let old_edit = old_edits_iter.peek_mut(); + let new_edit = new_edits_iter.peek_mut(); + + // Push the old edit if its new end is before the new edit's old start. + if let Some(old_edit) = old_edit.as_ref() { + let new_edit = new_edit.as_ref(); + if new_edit.map_or(true, |new_edit| old_edit.new.end < new_edit.old.start) { + let catchup = old_edit.old.start - old_start; + old_start += catchup; + new_start += catchup; + + let old_end = old_start + old_edit.old_len(); + let new_end = new_start + old_edit.new_len(); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + old_start = old_end; + new_start = new_end; + old_edits_iter.next(); + continue; + } + } + + // Push the new edit if its old end is before the old edit's new start. + if let Some(new_edit) = new_edit.as_ref() { + let old_edit = old_edit.as_ref(); + if old_edit.map_or(true, |old_edit| new_edit.old.end < old_edit.new.start) { + let catchup = new_edit.new.start - new_start; + old_start += catchup; + new_start += catchup; + + let old_end = old_start + new_edit.old_len(); + let new_end = new_start + new_edit.new_len(); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + old_start = old_end; + new_start = new_end; + new_edits_iter.next(); + continue; + } + } + + // If we still have edits by this point then they must intersect, so we compose them. + if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) { + if old_edit.new.start < new_edit.old.start { + let catchup = old_edit.old.start - old_start; + old_start += catchup; + new_start += catchup; + + let overshoot = new_edit.old.start - old_edit.new.start; + let old_end = cmp::min(old_start + overshoot, old_edit.old.end); + let new_end = new_start + overshoot; + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + old_edit.old.start = old_end; + old_edit.new.start += overshoot; + old_start = old_end; + new_start = new_end; + } else { + let catchup = new_edit.new.start - new_start; + old_start += catchup; + new_start += catchup; + + let overshoot = old_edit.new.start - new_edit.old.start; + let old_end = old_start + overshoot; + let new_end = cmp::min(new_start + overshoot, new_edit.new.end); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + new_edit.old.start += overshoot; + new_edit.new.start = new_end; + old_start = old_end; + new_start = new_end; + } + + if old_edit.new.end > new_edit.old.end { + let old_end = old_start + cmp::min(old_edit.old_len(), new_edit.old_len()); + let new_end = new_start + new_edit.new_len(); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + old_edit.old.start = old_end; + old_edit.new.start = new_edit.old.end; + old_start = old_end; + new_start = new_end; + new_edits_iter.next(); + } else { + let old_end = old_start + old_edit.old_len(); + let new_end = new_start + cmp::min(old_edit.new_len(), new_edit.new_len()); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + new_edit.old.start = old_edit.new.end; + new_edit.new.start = new_end; + old_start = old_end; + new_start = new_end; + old_edits_iter.next(); + } + } else { + break; + } + } + + composed + } + + pub fn invert(&mut self) -> &mut Self { + for edit in &mut self.0 { + mem::swap(&mut edit.old, &mut edit.new); + } + self + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&mut self, edit: Edit) { + if edit.is_empty() { + return; + } + + if let Some(last) = self.0.last_mut() { + if last.old.end >= edit.old.start { + last.old.end = edit.old.end; + last.new.end = edit.new.end; + } else { + self.0.push(edit); + } + } else { + self.0.push(edit); + } + } + + pub fn old_to_new(&self, old: T) -> T { + let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { + Ok(ix) => ix, + Err(ix) => { + if ix == 0 { + return old; + } else { + ix - 1 + } + } + }; + if let Some(edit) = self.0.get(ix) { + if old >= edit.old.end { + edit.new.end + (old - edit.old.end) + } else { + edit.new.start + } + } else { + old + } + } +} + +impl IntoIterator for Patch { + type Item = Edit; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, T: Clone> IntoIterator for &'a Patch { + type Item = Edit; + type IntoIter = std::iter::Cloned>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().cloned() + } +} + +impl<'a, T: Clone> IntoIterator for &'a mut Patch { + type Item = Edit; + type IntoIter = std::iter::Cloned>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::prelude::*; + use std::env; + + #[gpui::test] + fn test_one_disjoint_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 0..0, + new: 0..4, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 1..3, + new: 5..8, + }, + ]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 5..9, + new: 5..7, + }]), + Patch(vec![ + Edit { + old: 1..3, + new: 1..4, + }, + Edit { + old: 4..8, + new: 5..7, + }, + ]), + ); + } + + #[gpui::test] + fn test_one_overlapping_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 3..5, + new: 3..6, + }]), + Patch(vec![Edit { + old: 1..4, + new: 1..6, + }]), + ); + } + + #[gpui::test] + fn test_two_disjoint_and_overlapping() { + assert_patch_composition( + Patch(vec![ + Edit { + old: 1..3, + new: 1..4, + }, + Edit { + old: 8..12, + new: 9..11, + }, + ]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 3..10, + new: 7..9, + }, + ]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 1..12, + new: 5..10, + }, + ]), + ); + } + + #[gpui::test] + fn test_two_new_edits_overlapping_one_old_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 0..0, + new: 0..3, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..1, + }, + Edit { + old: 1..2, + new: 2..2, + }, + ]), + Patch(vec![Edit { + old: 0..0, + new: 0..3, + }]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 2..3, + new: 2..4, + }]), + Patch(vec![ + Edit { + old: 0..2, + new: 0..1, + }, + Edit { + old: 3..3, + new: 2..5, + }, + ]), + Patch(vec![Edit { + old: 0..3, + new: 0..6, + }]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 0..0, + new: 0..2, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..2, + }, + Edit { + old: 2..5, + new: 4..4, + }, + ]), + Patch(vec![Edit { + old: 0..3, + new: 0..4, + }]), + ); + } + + #[gpui::test] + fn test_two_new_edits_touching_one_old_edit() { + assert_patch_composition( + Patch(vec![ + Edit { + old: 2..3, + new: 2..4, + }, + Edit { + old: 7..7, + new: 8..11, + }, + ]), + Patch(vec![ + Edit { + old: 2..3, + new: 2..2, + }, + Edit { + old: 4..4, + new: 3..4, + }, + ]), + Patch(vec![ + Edit { + old: 2..3, + new: 2..4, + }, + Edit { + old: 7..7, + new: 8..11, + }, + ]), + ); + } + + #[gpui::test] + fn test_old_to_new() { + let patch = Patch(vec![ + Edit { + old: 2..4, + new: 2..4, + }, + Edit { + old: 7..8, + new: 7..11, + }, + ]); + assert_eq!(patch.old_to_new(0), 0); + assert_eq!(patch.old_to_new(1), 1); + assert_eq!(patch.old_to_new(2), 2); + assert_eq!(patch.old_to_new(3), 2); + assert_eq!(patch.old_to_new(4), 4); + assert_eq!(patch.old_to_new(5), 5); + assert_eq!(patch.old_to_new(6), 6); + assert_eq!(patch.old_to_new(7), 7); + assert_eq!(patch.old_to_new(8), 11); + assert_eq!(patch.old_to_new(9), 12); + } + + #[gpui::test(iterations = 100)] + fn test_random_patch_compositions(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(20); + + let initial_chars = (0..rng.gen_range(0..=100)) + .map(|_| rng.gen_range(b'a'..=b'z') as char) + .collect::>(); + log::info!("initial chars: {:?}", initial_chars); + + // Generate two sequential patches + let mut patches = Vec::new(); + let mut expected_chars = initial_chars.clone(); + for i in 0..2 { + log::info!("patch {}:", i); + + let mut delta = 0i32; + let mut last_edit_end = 0; + let mut edits = Vec::new(); + + for _ in 0..operations { + if last_edit_end >= expected_chars.len() { + break; + } + + let end = rng.gen_range(last_edit_end..=expected_chars.len()); + let start = rng.gen_range(last_edit_end..=end); + let old_len = end - start; + + let mut new_len = rng.gen_range(0..=3); + if start == end && new_len == 0 { + new_len += 1; + } + + last_edit_end = start + new_len + 1; + + let new_chars = (0..new_len) + .map(|_| rng.gen_range(b'A'..=b'Z') as char) + .collect::>(); + log::info!( + " editing {:?}: {:?}", + start..end, + new_chars.iter().collect::() + ); + edits.push(Edit { + old: (start as i32 - delta) as u32..(end as i32 - delta) as u32, + new: start as u32..(start + new_len) as u32, + }); + expected_chars.splice(start..end, new_chars); + + delta += new_len as i32 - old_len as i32; + } + + patches.push(Patch(edits)); + } + + log::info!("old patch: {:?}", &patches[0]); + log::info!("new patch: {:?}", &patches[1]); + log::info!("initial chars: {:?}", initial_chars); + log::info!("final chars: {:?}", expected_chars); + + // Compose the patches, and verify that it has the same effect as applying the + // two patches separately. + let composed = patches[0].compose(&patches[1]); + log::info!("composed patch: {:?}", &composed); + + let mut actual_chars = initial_chars; + for edit in composed.0 { + actual_chars.splice( + edit.new.start as usize..edit.new.start as usize + edit.old.len(), + expected_chars[edit.new.start as usize..edit.new.end as usize] + .iter() + .copied(), + ); + } + + assert_eq!(actual_chars, expected_chars); + } + + #[track_caller] + fn assert_patch_composition(old: Patch, new: Patch, composed: Patch) { + let original = ('a'..'z').collect::>(); + let inserted = ('A'..'Z').collect::>(); + + let mut expected = original.clone(); + apply_patch(&mut expected, &old, &inserted); + apply_patch(&mut expected, &new, &inserted); + + let mut actual = original; + apply_patch(&mut actual, &composed, &expected); + assert_eq!( + actual.into_iter().collect::(), + expected.into_iter().collect::(), + "expected patch is incorrect" + ); + + assert_eq!(old.compose(&new), composed); + } + + fn apply_patch(text: &mut Vec, patch: &Patch, new_text: &[char]) { + for edit in patch.0.iter().rev() { + text.splice( + edit.old.start as usize..edit.old.end as usize, + new_text[edit.new.start as usize..edit.new.end as usize] + .iter() + .copied(), + ); + } + } +} diff --git a/crates/text2/src/selection.rs b/crates/text2/src/selection.rs new file mode 100644 index 0000000000..480cb99d74 --- /dev/null +++ b/crates/text2/src/selection.rs @@ -0,0 +1,123 @@ +use crate::{Anchor, BufferSnapshot, TextDimension}; +use std::cmp::Ordering; +use std::ops::Range; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum SelectionGoal { + None, + HorizontalPosition(f32), + HorizontalRange { start: f32, end: f32 }, + WrappedHorizontalPosition((u32, f32)), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Selection { + pub id: usize, + pub start: T, + pub end: T, + pub reversed: bool, + pub goal: SelectionGoal, +} + +impl Default for SelectionGoal { + fn default() -> Self { + Self::None + } +} + +impl Selection { + pub fn head(&self) -> T { + if self.reversed { + self.start.clone() + } else { + self.end.clone() + } + } + + pub fn tail(&self) -> T { + if self.reversed { + self.end.clone() + } else { + self.start.clone() + } + } + + pub fn map(&self, f: F) -> Selection + where + F: Fn(T) -> S, + { + Selection:: { + id: self.id, + start: f(self.start.clone()), + end: f(self.end.clone()), + reversed: self.reversed, + goal: self.goal, + } + } + + pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { + self.start = point.clone(); + self.end = point; + self.goal = new_goal; + self.reversed = false; + } +} + +impl Selection { + pub fn is_empty(&self) -> bool { + self.start == self.end + } + + pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) { + if head.cmp(&self.tail()) < Ordering::Equal { + if !self.reversed { + self.end = self.start; + self.reversed = true; + } + self.start = head; + } else { + if self.reversed { + self.start = self.end; + self.reversed = false; + } + self.end = head; + } + self.goal = new_goal; + } + + pub fn range(&self) -> Range { + self.start..self.end + } +} + +impl Selection { + #[cfg(feature = "test-support")] + pub fn from_offset(offset: usize) -> Self { + Selection { + id: 0, + start: offset, + end: offset, + goal: SelectionGoal::None, + reversed: false, + } + } + + pub fn equals(&self, offset_range: &Range) -> bool { + self.start == offset_range.start && self.end == offset_range.end + } +} + +impl Selection { + pub fn resolve<'a, D: 'a + TextDimension>( + &'a self, + snapshot: &'a BufferSnapshot, + ) -> Selection { + Selection { + id: self.id, + start: snapshot.summary_for_anchor(&self.start), + end: snapshot.summary_for_anchor(&self.end), + reversed: self.reversed, + goal: self.goal, + } + } +} diff --git a/crates/text2/src/subscription.rs b/crates/text2/src/subscription.rs new file mode 100644 index 0000000000..b636dfcc92 --- /dev/null +++ b/crates/text2/src/subscription.rs @@ -0,0 +1,48 @@ +use crate::{Edit, Patch}; +use parking_lot::Mutex; +use std::{ + mem, + sync::{Arc, Weak}, +}; + +#[derive(Default)] +pub struct Topic(Mutex>>>>); + +pub struct Subscription(Arc>>); + +impl Topic { + pub fn subscribe(&mut self) -> Subscription { + let subscription = Subscription(Default::default()); + self.0.get_mut().push(Arc::downgrade(&subscription.0)); + subscription + } + + pub fn publish(&self, edits: impl Clone + IntoIterator>) { + publish(&mut *self.0.lock(), edits); + } + + pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator>) { + publish(self.0.get_mut(), edits); + } +} + +impl Subscription { + pub fn consume(&self) -> Patch { + mem::take(&mut *self.0.lock()) + } +} + +fn publish( + subscriptions: &mut Vec>>>, + edits: impl Clone + IntoIterator>, +) { + subscriptions.retain(|subscription| { + if let Some(subscription) = subscription.upgrade() { + let mut patch = subscription.lock(); + *patch = patch.compose(edits.clone()); + true + } else { + false + } + }); +} diff --git a/crates/text2/src/tests.rs b/crates/text2/src/tests.rs new file mode 100644 index 0000000000..7e26e0a296 --- /dev/null +++ b/crates/text2/src/tests.rs @@ -0,0 +1,764 @@ +use super::{network::Network, *}; +use clock::ReplicaId; +use rand::prelude::*; +use std::{ + cmp::Ordering, + env, + iter::Iterator, + time::{Duration, Instant}, +}; + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +#[test] +fn test_edit() { + let mut buffer = Buffer::new(0, 0, "abc".into()); + assert_eq!(buffer.text(), "abc"); + buffer.edit([(3..3, "def")]); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit([(0..0, "ghi")]); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit([(5..5, "jkl")]); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit([(6..7, "")]); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit([(4..9, "mno")]); + assert_eq!(buffer.text(), "ghiamnoef"); +} + +#[gpui::test(iterations = 100)] +fn test_random_edits(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + let mut buffer = Buffer::new(0, 0, reference_string.clone()); + LineEnding::normalize(&mut reference_string); + + buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + let mut buffer_versions = Vec::new(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + for _i in 0..operations { + let (edits, _) = buffer.randomly_edit(&mut rng, 5); + for (old_range, new_text) in edits.iter().rev() { + reference_string.replace_range(old_range.clone(), new_text); + } + + assert_eq!(buffer.text(), reference_string); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(&mut rng); + reference_string = buffer.text(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + } + + let range = buffer.random_byte_range(0, &mut rng); + assert_eq!( + buffer.text_summary_for_range::(range.clone()), + TextSummary::from(&reference_string[range]) + ); + + buffer.check_invariants(); + + if rng.gen_bool(0.3) { + buffer_versions.push((buffer.clone(), buffer.subscribe())); + } + } + + for (old_buffer, subscription) in buffer_versions { + let edits = buffer + .edits_since::(&old_buffer.version) + .collect::>(); + + log::info!( + "applying edits since version {:?} to old text: {:?}: {:?}", + old_buffer.version(), + old_buffer.text(), + edits, + ); + + let mut text = old_buffer.visible_text.clone(); + for edit in edits { + let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); + text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); + } + assert_eq!(text.to_string(), buffer.text()); + + for _ in 0..5 { + let end_ix = old_buffer.clip_offset(rng.gen_range(0..=old_buffer.len()), Bias::Right); + let start_ix = old_buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix); + let mut old_text = old_buffer.text_for_range(range.clone()).collect::(); + let edits = buffer + .edits_since_in_range::(&old_buffer.version, range.clone()) + .collect::>(); + log::info!( + "applying edits since version {:?} to old text in range {:?}: {:?}: {:?}", + old_buffer.version(), + start_ix..end_ix, + old_text, + edits, + ); + + let new_text = buffer.text_for_range(range).collect::(); + for edit in edits { + old_text.replace_range( + edit.new.start..edit.new.start + edit.old_len(), + &new_text[edit.new], + ); + } + assert_eq!(old_text, new_text); + } + + let subscription_edits = subscription.consume(); + log::info!( + "applying subscription edits since version {:?} to old text: {:?}: {:?}", + old_buffer.version(), + old_buffer.text(), + subscription_edits, + ); + + let mut text = old_buffer.visible_text.clone(); + for edit in subscription_edits.into_inner() { + let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); + text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); + } + assert_eq!(text.to_string(), buffer.text()); + } +} + +#[test] +fn test_line_endings() { + assert_eq!(LineEnding::detect(&"🍐✅\n".repeat(1000)), LineEnding::Unix); + assert_eq!(LineEnding::detect(&"abcd\n".repeat(1000)), LineEnding::Unix); + assert_eq!( + LineEnding::detect(&"🍐✅\r\n".repeat(1000)), + LineEnding::Windows + ); + assert_eq!( + LineEnding::detect(&"abcd\r\n".repeat(1000)), + LineEnding::Windows + ); + + let mut buffer = Buffer::new(0, 0, "one\r\ntwo\rthree".into()); + assert_eq!(buffer.text(), "one\ntwo\nthree"); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + buffer.check_invariants(); + + buffer.edit([(buffer.len()..buffer.len(), "\r\nfour")]); + buffer.edit([(0..0, "zero\r\n")]); + assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + buffer.check_invariants(); +} + +#[test] +fn test_line_len() { + let mut buffer = Buffer::new(0, 0, "".into()); + buffer.edit([(0..0, "abcd\nefg\nhij")]); + buffer.edit([(12..12, "kl\nmno")]); + buffer.edit([(18..18, "\npqrs\n")]); + buffer.edit([(18..21, "\nPQ")]); + + assert_eq!(buffer.line_len(0), 4); + assert_eq!(buffer.line_len(1), 3); + assert_eq!(buffer.line_len(2), 5); + assert_eq!(buffer.line_len(3), 3); + assert_eq!(buffer.line_len(4), 4); + assert_eq!(buffer.line_len(5), 0); +} + +#[test] +fn test_common_prefix_at_position() { + let text = "a = str; b = δα"; + let buffer = Buffer::new(0, 0, text.into()); + + let offset1 = offset_after(text, "str"); + let offset2 = offset_after(text, "δα"); + + // the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "string"), + range_of(text, "str"), + ); + // a suffix of the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "tree"), + range_of(text, "tr"), + ); + // the preceding word is a substring of the suggestion, but not a prefix + assert_eq!( + buffer.common_prefix_at(offset1, "astro"), + empty_range_after(text, "str"), + ); + + // prefix matching is case insensitive. + assert_eq!( + buffer.common_prefix_at(offset1, "Strαngε"), + range_of(text, "str"), + ); + assert_eq!( + buffer.common_prefix_at(offset2, "ΔΑΜΝ"), + range_of(text, "δα"), + ); + + fn offset_after(text: &str, part: &str) -> usize { + text.find(part).unwrap() + part.len() + } + + fn empty_range_after(text: &str, part: &str) -> Range { + let offset = offset_after(text, part); + offset..offset + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } +} + +#[test] +fn test_text_summary_for_range() { + let buffer = Buffer::new(0, 0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz".into()); + assert_eq!( + buffer.text_summary_for_range::(1..3), + TextSummary { + len: 2, + len_utf16: OffsetUtf16(2), + lines: Point::new(1, 0), + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 0, + longest_row_chars: 1, + } + ); + assert_eq!( + buffer.text_summary_for_range::(1..12), + TextSummary { + len: 11, + len_utf16: OffsetUtf16(11), + lines: Point::new(3, 0), + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 2, + longest_row_chars: 4, + } + ); + assert_eq!( + buffer.text_summary_for_range::(0..20), + TextSummary { + len: 20, + len_utf16: OffsetUtf16(20), + lines: Point::new(4, 1), + first_line_chars: 2, + last_line_chars: 1, + last_line_len_utf16: 1, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range::(0..22), + TextSummary { + len: 22, + len_utf16: OffsetUtf16(22), + lines: Point::new(4, 3), + first_line_chars: 2, + last_line_chars: 3, + last_line_len_utf16: 3, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range::(7..22), + TextSummary { + len: 15, + len_utf16: OffsetUtf16(15), + lines: Point::new(2, 3), + first_line_chars: 4, + last_line_chars: 3, + last_line_len_utf16: 3, + longest_row: 1, + longest_row_chars: 6, + } + ); +} + +#[test] +fn test_chars_at() { + let mut buffer = Buffer::new(0, 0, "".into()); + buffer.edit([(0..0, "abcd\nefgh\nij")]); + buffer.edit([(12..12, "kl\nmno")]); + buffer.edit([(18..18, "\npqrs")]); + buffer.edit([(18..21, "\nPQ")]); + + let chars = buffer.chars_at(Point::new(0, 0)); + assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(1, 0)); + assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(2, 0)); + assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(3, 0)); + assert_eq!(chars.collect::(), "mno\nPQrs"); + + let chars = buffer.chars_at(Point::new(4, 0)); + assert_eq!(chars.collect::(), "PQrs"); + + // Regression test: + let mut buffer = Buffer::new(0, 0, "".into()); + buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]); + buffer.edit([(60..60, "\n")]); + + let chars = buffer.chars_at(Point::new(6, 0)); + assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); +} + +#[test] +fn test_anchors() { + let mut buffer = Buffer::new(0, 0, "".into()); + buffer.edit([(0..0, "abc")]); + let left_anchor = buffer.anchor_before(2); + let right_anchor = buffer.anchor_after(2); + + buffer.edit([(1..1, "def\n")]); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 6); + assert_eq!(right_anchor.to_offset(&buffer), 6); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit([(2..3, "")]); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 5); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit([(5..5, "ghi\n")]); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 9); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); + + buffer.edit([(7..9, "")]); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 7); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); + + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }), + buffer.anchor_before(0) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }), + buffer.anchor_before(1) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }), + buffer.anchor_before(2) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }), + buffer.anchor_before(3) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }), + buffer.anchor_before(4) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }), + buffer.anchor_before(5) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }), + buffer.anchor_before(6) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }), + buffer.anchor_before(7) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }), + buffer.anchor_before(8) + ); + + // Comparison between anchors. + let anchor_at_offset_0 = buffer.anchor_before(0); + let anchor_at_offset_1 = buffer.anchor_before(1); + let anchor_at_offset_2 = buffer.anchor_before(2); + + assert_eq!( + anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer), + Ordering::Equal + ); + + assert_eq!( + anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer), + Ordering::Less + ); + + assert_eq!( + anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer), + Ordering::Greater + ); +} + +#[test] +fn test_anchors_at_start_and_end() { + let mut buffer = Buffer::new(0, 0, "".into()); + let before_start_anchor = buffer.anchor_before(0); + let after_end_anchor = buffer.anchor_after(0); + + buffer.edit([(0..0, "abc")]); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_end_anchor.to_offset(&buffer), 3); + + let after_start_anchor = buffer.anchor_after(0); + let before_end_anchor = buffer.anchor_before(3); + + buffer.edit([(3..3, "def")]); + buffer.edit([(0..0, "ghi")]); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_start_anchor.to_offset(&buffer), 3); + assert_eq!(before_end_anchor.to_offset(&buffer), 6); + assert_eq!(after_end_anchor.to_offset(&buffer), 9); +} + +#[test] +fn test_undo_redo() { + let mut buffer = Buffer::new(0, 0, "1234".into()); + // Set group interval to zero so as to not group edits in the undo stack. + buffer.set_group_interval(Duration::from_secs(0)); + + buffer.edit([(1..1, "abx")]); + buffer.edit([(3..4, "yzef")]); + buffer.edit([(3..5, "cd")]); + assert_eq!(buffer.text(), "1abcdef234"); + + let entries = buffer.history.undo_stack.clone(); + assert_eq!(entries.len(), 3); + + buffer.undo_or_redo(entries[0].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(entries[0].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(entries[1].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(entries[2].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(entries[1].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(entries[2].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(entries[2].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(entries[0].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(entries[1].transaction.clone()).unwrap(); + assert_eq!(buffer.text(), "1234"); +} + +#[test] +fn test_history() { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, 0, "123456".into()); + buffer.set_group_interval(Duration::from_millis(300)); + + let transaction_1 = buffer.start_transaction_at(now).unwrap(); + buffer.edit([(2..4, "cd")]); + buffer.end_transaction_at(now); + assert_eq!(buffer.text(), "12cd56"); + + buffer.start_transaction_at(now); + buffer.edit([(4..5, "e")]); + buffer.end_transaction_at(now).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + + now += buffer.transaction_group_interval() + Duration::from_millis(1); + buffer.start_transaction_at(now); + buffer.edit([(0..1, "a")]); + buffer.edit([(1..1, "b")]); + buffer.end_transaction_at(now).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + + // Last transaction happened past the group interval, undo it on its own. + buffer.undo(); + assert_eq!(buffer.text(), "12cde6"); + + // First two transactions happened within the group interval, undo them together. + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + + // Redo the first two transactions together. + buffer.redo(); + assert_eq!(buffer.text(), "12cde6"); + + // Redo the last transaction on its own. + buffer.redo(); + assert_eq!(buffer.text(), "ab2cde6"); + + buffer.start_transaction_at(now); + assert!(buffer.end_transaction_at(now).is_none()); + buffer.undo(); + assert_eq!(buffer.text(), "12cde6"); + + // Redo stack gets cleared after performing an edit. + buffer.start_transaction_at(now); + buffer.edit([(0..0, "X")]); + buffer.end_transaction_at(now); + assert_eq!(buffer.text(), "X12cde6"); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); + buffer.undo(); + assert_eq!(buffer.text(), "12cde6"); + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + + // Transactions can be grouped manually. + buffer.redo(); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); + buffer.group_until_transaction(transaction_1); + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); +} + +#[test] +fn test_finalize_last_transaction() { + let now = Instant::now(); + let mut buffer = Buffer::new(0, 0, "123456".into()); + + buffer.start_transaction_at(now); + buffer.edit([(2..4, "cd")]); + buffer.end_transaction_at(now); + assert_eq!(buffer.text(), "12cd56"); + + buffer.finalize_last_transaction(); + buffer.start_transaction_at(now); + buffer.edit([(4..5, "e")]); + buffer.end_transaction_at(now).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + + buffer.start_transaction_at(now); + buffer.edit([(0..1, "a")]); + buffer.edit([(1..1, "b")]); + buffer.end_transaction_at(now).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + + buffer.undo(); + assert_eq!(buffer.text(), "12cd56"); + + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + + buffer.redo(); + assert_eq!(buffer.text(), "12cd56"); + + buffer.redo(); + assert_eq!(buffer.text(), "ab2cde6"); +} + +#[test] +fn test_edited_ranges_for_transaction() { + let now = Instant::now(); + let mut buffer = Buffer::new(0, 0, "1234567".into()); + + buffer.start_transaction_at(now); + buffer.edit([(2..4, "cd")]); + buffer.edit([(6..6, "efg")]); + buffer.end_transaction_at(now); + assert_eq!(buffer.text(), "12cd56efg7"); + + let tx = buffer.finalize_last_transaction().unwrap().clone(); + assert_eq!( + buffer + .edited_ranges_for_transaction::(&tx) + .collect::>(), + [2..4, 6..9] + ); + + buffer.edit([(5..5, "hijk")]); + assert_eq!(buffer.text(), "12cd5hijk6efg7"); + assert_eq!( + buffer + .edited_ranges_for_transaction::(&tx) + .collect::>(), + [2..4, 10..13] + ); + + buffer.edit([(4..4, "l")]); + assert_eq!(buffer.text(), "12cdl5hijk6efg7"); + assert_eq!( + buffer + .edited_ranges_for_transaction::(&tx) + .collect::>(), + [2..4, 11..14] + ); +} + +#[test] +fn test_concurrent_edits() { + let text = "abcdef"; + + let mut buffer1 = Buffer::new(1, 0, text.into()); + let mut buffer2 = Buffer::new(2, 0, text.into()); + let mut buffer3 = Buffer::new(3, 0, text.into()); + + let buf1_op = buffer1.edit([(1..2, "12")]); + assert_eq!(buffer1.text(), "a12cdef"); + let buf2_op = buffer2.edit([(3..4, "34")]); + assert_eq!(buffer2.text(), "abc34ef"); + let buf3_op = buffer3.edit([(5..6, "56")]); + assert_eq!(buffer3.text(), "abcde56"); + + buffer1.apply_op(buf2_op.clone()).unwrap(); + buffer1.apply_op(buf3_op.clone()).unwrap(); + buffer2.apply_op(buf1_op.clone()).unwrap(); + buffer2.apply_op(buf3_op).unwrap(); + buffer3.apply_op(buf1_op).unwrap(); + buffer3.apply_op(buf2_op).unwrap(); + + assert_eq!(buffer1.text(), "a12c34e56"); + assert_eq!(buffer2.text(), "a12c34e56"); + assert_eq!(buffer3.text(), "a12c34e56"); +} + +#[gpui::test(iterations = 100)] +fn test_random_concurrent_edits(mut rng: StdRng) { + let peers = env::var("PEERS") + .map(|i| i.parse().expect("invalid `PEERS` variable")) + .unwrap_or(5); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let base_text_len = rng.gen_range(0..10); + let base_text = RandomCharIter::new(&mut rng) + .take(base_text_len) + .collect::(); + let mut replica_ids = Vec::new(); + let mut buffers = Vec::new(); + let mut network = Network::new(rng.clone()); + + for i in 0..peers { + let mut buffer = Buffer::new(i as ReplicaId, 0, base_text.clone()); + buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); + } + + log::info!("initial text: {:?}", base_text); + + let mut mutation_count = operations; + loop { + let replica_index = rng.gen_range(0..peers); + let replica_id = replica_ids[replica_index]; + let buffer = &mut buffers[replica_index]; + match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + let op = buffer.randomly_edit(&mut rng, 5).1; + network.broadcast(buffer.replica_id, vec![op]); + log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); + mutation_count -= 1; + } + 51..=70 if mutation_count != 0 => { + let ops = buffer.randomly_undo_redo(&mut rng); + network.broadcast(buffer.replica_id, ops); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + let ops = network.receive(replica_id); + if !ops.is_empty() { + log::info!( + "peer {} applying {} ops from the network.", + replica_id, + ops.len() + ); + buffer.apply_ops(ops).unwrap(); + } + } + _ => {} + } + buffer.check_invariants(); + + if mutation_count == 0 && network.is_idle() { + break; + } + } + + let first_buffer = &buffers[0]; + for buffer in &buffers[1..] { + assert_eq!( + buffer.text(), + first_buffer.text(), + "Replica {} text != Replica 0 text", + buffer.replica_id + ); + buffer.check_invariants(); + } +} diff --git a/crates/text2/src/text2.rs b/crates/text2/src/text2.rs new file mode 100644 index 0000000000..c05ea1109c --- /dev/null +++ b/crates/text2/src/text2.rs @@ -0,0 +1,2682 @@ +mod anchor; +pub mod locator; +#[cfg(any(test, feature = "test-support"))] +pub mod network; +pub mod operation_queue; +mod patch; +mod selection; +pub mod subscription; +#[cfg(test)] +mod tests; +mod undo_map; + +pub use anchor::*; +use anyhow::{anyhow, Result}; +pub use clock::ReplicaId; +use collections::{HashMap, HashSet}; +use locator::Locator; +use operation_queue::OperationQueue; +pub use patch::Patch; +use postage::{oneshot, prelude::*}; + +use lazy_static::lazy_static; +use regex::Regex; +pub use rope::*; +pub use selection::*; +use std::{ + borrow::Cow, + cmp::{self, Ordering, Reverse}, + future::Future, + iter::Iterator, + ops::{self, Deref, Range, Sub}, + str, + sync::Arc, + time::{Duration, Instant}, +}; +pub use subscription::*; +pub use sum_tree::Bias; +use sum_tree::{FilterCursor, SumTree, TreeMap}; +use undo_map::UndoMap; +use util::ResultExt; + +#[cfg(any(test, feature = "test-support"))] +use util::RandomCharIter; + +lazy_static! { + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); +} + +pub type TransactionId = clock::Lamport; + +pub struct Buffer { + snapshot: BufferSnapshot, + history: History, + deferred_ops: OperationQueue, + deferred_replicas: HashSet, + pub lamport_clock: clock::Lamport, + subscriptions: Topic, + edit_id_resolvers: HashMap>>, + wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, +} + +#[derive(Clone)] +pub struct BufferSnapshot { + replica_id: ReplicaId, + remote_id: u64, + visible_text: Rope, + deleted_text: Rope, + line_ending: LineEnding, + undo_map: UndoMap, + fragments: SumTree, + insertions: SumTree, + pub version: clock::Global, +} + +#[derive(Clone, Debug)] +pub struct HistoryEntry { + transaction: Transaction, + first_edit_at: Instant, + last_edit_at: Instant, + suppress_grouping: bool, +} + +#[derive(Clone, Debug)] +pub struct Transaction { + pub id: TransactionId, + pub edit_ids: Vec, + pub start: clock::Global, +} + +impl HistoryEntry { + pub fn transaction_id(&self) -> TransactionId { + self.transaction.id + } +} + +struct History { + base_text: Rope, + operations: TreeMap, + insertion_slices: HashMap>, + undo_stack: Vec, + redo_stack: Vec, + transaction_depth: usize, + group_interval: Duration, +} + +#[derive(Clone, Debug)] +struct InsertionSlice { + insertion_id: clock::Lamport, + range: Range, +} + +impl History { + pub fn new(base_text: Rope) -> Self { + Self { + base_text, + operations: Default::default(), + insertion_slices: Default::default(), + undo_stack: Vec::new(), + redo_stack: Vec::new(), + transaction_depth: 0, + // Don't group transactions in tests unless we opt in, because it's a footgun. + #[cfg(any(test, feature = "test-support"))] + group_interval: Duration::ZERO, + #[cfg(not(any(test, feature = "test-support")))] + group_interval: Duration::from_millis(300), + } + } + + fn push(&mut self, op: Operation) { + self.operations.insert(op.timestamp(), op); + } + + fn start_transaction( + &mut self, + start: clock::Global, + now: Instant, + clock: &mut clock::Lamport, + ) -> Option { + self.transaction_depth += 1; + if self.transaction_depth == 1 { + let id = clock.tick(); + self.undo_stack.push(HistoryEntry { + transaction: Transaction { + id, + start, + edit_ids: Default::default(), + }, + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + Some(id) + } else { + None + } + } + + fn end_transaction(&mut self, now: Instant) -> Option<&HistoryEntry> { + assert_ne!(self.transaction_depth, 0); + self.transaction_depth -= 1; + if self.transaction_depth == 0 { + if self + .undo_stack + .last() + .unwrap() + .transaction + .edit_ids + .is_empty() + { + self.undo_stack.pop(); + None + } else { + self.redo_stack.clear(); + let entry = self.undo_stack.last_mut().unwrap(); + entry.last_edit_at = now; + Some(entry) + } + } else { + None + } + } + + fn group(&mut self) -> Option { + let mut count = 0; + let mut entries = self.undo_stack.iter(); + if let Some(mut entry) = entries.next_back() { + while let Some(prev_entry) = entries.next_back() { + if !prev_entry.suppress_grouping + && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval + { + entry = prev_entry; + count += 1; + } else { + break; + } + } + } + self.group_trailing(count) + } + + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for entry in self.undo_stack.iter().rev() { + if entry.transaction_id() == transaction_id { + self.group_trailing(count); + break; + } else if entry.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; + let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len); + if let Some(last_entry) = entries_to_keep.last_mut() { + for entry in &*entries_to_merge { + for edit_id in &entry.transaction.edit_ids { + last_entry.transaction.edit_ids.push(*edit_id); + } + } + + if let Some(entry) = entries_to_merge.last_mut() { + last_entry.last_edit_at = entry.last_edit_at; + } + } + + self.undo_stack.truncate(new_len); + self.undo_stack.last().map(|e| e.transaction.id) + } + + fn finalize_last_transaction(&mut self) -> Option<&Transaction> { + self.undo_stack.last_mut().map(|entry| { + entry.suppress_grouping = true; + &entry.transaction + }) + } + + fn push_transaction(&mut self, transaction: Transaction, now: Instant) { + assert_eq!(self.transaction_depth, 0); + self.undo_stack.push(HistoryEntry { + transaction, + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + self.redo_stack.clear(); + } + + fn push_undo(&mut self, op_id: clock::Lamport) { + assert_ne!(self.transaction_depth, 0); + if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { + let last_transaction = self.undo_stack.last_mut().unwrap(); + last_transaction.transaction.edit_ids.push(op_id); + } + } + + fn pop_undo(&mut self) -> Option<&HistoryEntry> { + assert_eq!(self.transaction_depth, 0); + if let Some(entry) = self.undo_stack.pop() { + self.redo_stack.push(entry); + self.redo_stack.last() + } else { + None + } + } + + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> { + assert_eq!(self.transaction_depth, 0); + + let entry_ix = self + .undo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id)?; + let entry = self.undo_stack.remove(entry_ix); + self.redo_stack.push(entry); + self.redo_stack.last() + } + + fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + assert_eq!(self.transaction_depth, 0); + + let redo_stack_start_len = self.redo_stack.len(); + if let Some(entry_ix) = self + .undo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id) + { + self.redo_stack + .extend(self.undo_stack.drain(entry_ix..).rev()); + } + &self.redo_stack[redo_stack_start_len..] + } + + fn forget(&mut self, transaction_id: TransactionId) -> Option { + assert_eq!(self.transaction_depth, 0); + if let Some(entry_ix) = self + .undo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id) + { + Some(self.undo_stack.remove(entry_ix).transaction) + } else if let Some(entry_ix) = self + .redo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id) + { + Some(self.redo_stack.remove(entry_ix).transaction) + } else { + None + } + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + let entry = self + .undo_stack + .iter_mut() + .rfind(|entry| entry.transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .rfind(|entry| entry.transaction.id == transaction_id) + })?; + Some(&mut entry.transaction) + } + + fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + if let Some(transaction) = self.forget(transaction) { + if let Some(destination) = self.transaction_mut(destination) { + destination.edit_ids.extend(transaction.edit_ids); + } + } + } + + fn pop_redo(&mut self) -> Option<&HistoryEntry> { + assert_eq!(self.transaction_depth, 0); + if let Some(entry) = self.redo_stack.pop() { + self.undo_stack.push(entry); + self.undo_stack.last() + } else { + None + } + } + + fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + assert_eq!(self.transaction_depth, 0); + + let undo_stack_start_len = self.undo_stack.len(); + if let Some(entry_ix) = self + .redo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id) + { + self.undo_stack + .extend(self.redo_stack.drain(entry_ix..).rev()); + } + &self.undo_stack[undo_stack_start_len..] + } +} + +struct Edits<'a, D: TextDimension, F: FnMut(&FragmentSummary) -> bool> { + visible_cursor: rope::Cursor<'a>, + deleted_cursor: rope::Cursor<'a>, + fragments_cursor: Option>, + undos: &'a UndoMap, + since: &'a clock::Global, + old_end: D, + new_end: D, + range: Range<(&'a Locator, usize)>, + buffer_id: u64, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Edit { + pub old: Range, + pub new: Range, +} + +impl Edit +where + D: Sub + PartialEq + Copy, +{ + pub fn old_len(&self) -> D { + self.old.end - self.old.start + } + + pub fn new_len(&self) -> D { + self.new.end - self.new.start + } + + pub fn is_empty(&self) -> bool { + self.old.start == self.old.end && self.new.start == self.new.end + } +} + +impl Edit<(D1, D2)> { + pub fn flatten(self) -> (Edit, Edit) { + ( + Edit { + old: self.old.start.0..self.old.end.0, + new: self.new.start.0..self.new.end.0, + }, + Edit { + old: self.old.start.1..self.old.end.1, + new: self.new.start.1..self.new.end.1, + }, + ) + } +} + +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct Fragment { + pub id: Locator, + pub timestamp: clock::Lamport, + pub insertion_offset: usize, + pub len: usize, + pub visible: bool, + pub deletions: HashSet, + pub max_undos: clock::Global, +} + +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct FragmentSummary { + text: FragmentTextSummary, + max_id: Locator, + max_version: clock::Global, + min_insertion_version: clock::Global, + max_insertion_version: clock::Global, +} + +#[derive(Copy, Default, Clone, Debug, PartialEq, Eq)] +struct FragmentTextSummary { + visible: usize, + deleted: usize, +} + +impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { + fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { + self.visible += summary.text.visible; + self.deleted += summary.text.deleted; + } +} + +#[derive(Eq, PartialEq, Clone, Debug)] +struct InsertionFragment { + timestamp: clock::Lamport, + split_offset: usize, + fragment_id: Locator, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct InsertionFragmentKey { + timestamp: clock::Lamport, + split_offset: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Operation { + Edit(EditOperation), + Undo(UndoOperation), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EditOperation { + pub timestamp: clock::Lamport, + pub version: clock::Global, + pub ranges: Vec>, + pub new_text: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UndoOperation { + pub timestamp: clock::Lamport, + pub version: clock::Global, + pub counts: HashMap, +} + +impl Buffer { + pub fn new(replica_id: u16, remote_id: u64, mut base_text: String) -> Buffer { + let line_ending = LineEnding::detect(&base_text); + LineEnding::normalize(&mut base_text); + + let history = History::new(Rope::from(base_text.as_ref())); + let mut fragments = SumTree::new(); + let mut insertions = SumTree::new(); + + let mut lamport_clock = clock::Lamport::new(replica_id); + let mut version = clock::Global::new(); + + let visible_text = history.base_text.clone(); + if !visible_text.is_empty() { + let insertion_timestamp = clock::Lamport { + replica_id: 0, + value: 1, + }; + lamport_clock.observe(insertion_timestamp); + version.observe(insertion_timestamp); + let fragment_id = Locator::between(&Locator::min(), &Locator::max()); + let fragment = Fragment { + id: fragment_id, + timestamp: insertion_timestamp, + insertion_offset: 0, + len: visible_text.len(), + visible: true, + deletions: Default::default(), + max_undos: Default::default(), + }; + insertions.push(InsertionFragment::new(&fragment), &()); + fragments.push(fragment, &None); + } + + Buffer { + snapshot: BufferSnapshot { + replica_id, + remote_id, + visible_text, + deleted_text: Rope::new(), + line_ending, + fragments, + insertions, + version, + undo_map: Default::default(), + }, + history, + deferred_ops: OperationQueue::new(), + deferred_replicas: HashSet::default(), + lamport_clock, + subscriptions: Default::default(), + edit_id_resolvers: Default::default(), + wait_for_version_txs: Default::default(), + } + } + + pub fn version(&self) -> clock::Global { + self.version.clone() + } + + pub fn snapshot(&self) -> BufferSnapshot { + self.snapshot.clone() + } + + pub fn replica_id(&self) -> ReplicaId { + self.lamport_clock.replica_id + } + + pub fn remote_id(&self) -> u64 { + self.remote_id + } + + pub fn deferred_ops_len(&self) -> usize { + self.deferred_ops.len() + } + + pub fn transaction_group_interval(&self) -> Duration { + self.history.group_interval + } + + pub fn edit(&mut self, edits: R) -> Operation + where + R: IntoIterator, + I: ExactSizeIterator, T)>, + S: ToOffset, + T: Into>, + { + let edits = edits + .into_iter() + .map(|(range, new_text)| (range, new_text.into())); + + self.start_transaction(); + let timestamp = self.lamport_clock.tick(); + let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); + + self.history.push(operation.clone()); + self.history.push_undo(operation.timestamp()); + self.snapshot.version.observe(operation.timestamp()); + self.end_transaction(); + operation + } + + fn apply_local_edit>>( + &mut self, + edits: impl ExactSizeIterator, T)>, + timestamp: clock::Lamport, + ) -> EditOperation { + let mut edits_patch = Patch::default(); + let mut edit_op = EditOperation { + timestamp, + version: self.version(), + ranges: Vec::with_capacity(edits.len()), + new_text: Vec::with_capacity(edits.len()), + }; + let mut new_insertions = Vec::new(); + let mut insertion_offset = 0; + let mut insertion_slices = Vec::new(); + + let mut edits = edits + .map(|(range, new_text)| (range.to_offset(&*self), new_text)) + .peekable(); + + let mut new_ropes = + RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); + let mut old_fragments = self.fragments.cursor::(); + let mut new_fragments = + old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None); + new_ropes.append(new_fragments.summary().text); + + let mut fragment_start = old_fragments.start().visible; + for (range, new_text) in edits { + let new_text = LineEnding::normalize_arc(new_text.into()); + let fragment_end = old_fragments.end(&None).visible; + + // If the current fragment ends before this range, then jump ahead to the first fragment + // that extends past the start of this range, reusing any intervening fragments. + if fragment_end < range.start { + // If the current fragment has been partially consumed, then consume the rest of it + // and advance to the next fragment before slicing. + if fragment_start > old_fragments.start().visible { + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = fragment_end - fragment_start; + suffix.insertion_offset += fragment_start - old_fragments.start().visible; + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(&None); + } + + let slice = old_fragments.slice(&range.start, Bias::Right, &None); + new_ropes.append(slice.summary().text); + new_fragments.append(slice, &None); + fragment_start = old_fragments.start().visible; + } + + let full_range_start = FullOffset(range.start + old_fragments.start().deleted); + + // Preserve any portion of the current fragment that precedes this range. + if fragment_start < range.start { + let mut prefix = old_fragments.item().unwrap().clone(); + prefix.len = range.start - fragment_start; + prefix.insertion_offset += fragment_start - old_fragments.start().visible; + prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); + new_insertions.push(InsertionFragment::insert_new(&prefix)); + new_ropes.push_fragment(&prefix, prefix.visible); + new_fragments.push(prefix, &None); + fragment_start = range.start; + } + + // Insert the new text before any existing fragments within the range. + if !new_text.is_empty() { + let new_start = new_fragments.summary().text.visible; + + let fragment = Fragment { + id: Locator::between( + &new_fragments.summary().max_id, + old_fragments + .item() + .map_or(&Locator::max(), |old_fragment| &old_fragment.id), + ), + timestamp, + insertion_offset, + len: new_text.len(), + deletions: Default::default(), + max_undos: Default::default(), + visible: true, + }; + edits_patch.push(Edit { + old: fragment_start..fragment_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); + new_insertions.push(InsertionFragment::insert_new(&fragment)); + new_ropes.push_str(new_text.as_ref()); + new_fragments.push(fragment, &None); + insertion_offset += new_text.len(); + } + + // Advance through every fragment that intersects this range, marking the intersecting + // portions as deleted. + while fragment_start < range.end { + let fragment = old_fragments.item().unwrap(); + let fragment_end = old_fragments.end(&None).visible; + let mut intersection = fragment.clone(); + let intersection_end = cmp::min(range.end, fragment_end); + if fragment.visible { + intersection.len = intersection_end - fragment_start; + intersection.insertion_offset += fragment_start - old_fragments.start().visible; + intersection.id = + Locator::between(&new_fragments.summary().max_id, &intersection.id); + intersection.deletions.insert(timestamp); + intersection.visible = false; + } + if intersection.len > 0 { + if fragment.visible && !intersection.visible { + let new_start = new_fragments.summary().text.visible; + edits_patch.push(Edit { + old: fragment_start..intersection_end, + new: new_start..new_start, + }); + insertion_slices.push(intersection.insertion_slice()); + } + new_insertions.push(InsertionFragment::insert_new(&intersection)); + new_ropes.push_fragment(&intersection, fragment.visible); + new_fragments.push(intersection, &None); + fragment_start = intersection_end; + } + if fragment_end <= range.end { + old_fragments.next(&None); + } + } + + let full_range_end = FullOffset(range.end + old_fragments.start().deleted); + edit_op.ranges.push(full_range_start..full_range_end); + edit_op.new_text.push(new_text); + } + + // If the current fragment has been partially consumed, then consume the rest of it + // and advance to the next fragment before slicing. + if fragment_start > old_fragments.start().visible { + let fragment_end = old_fragments.end(&None).visible; + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = fragment_end - fragment_start; + suffix.insertion_offset += fragment_start - old_fragments.start().visible; + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(&None); + } + + let suffix = old_fragments.suffix(&None); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); + let (visible_text, deleted_text) = new_ropes.finish(); + drop(old_fragments); + + self.snapshot.fragments = new_fragments; + self.snapshot.insertions.edit(new_insertions, &()); + self.snapshot.visible_text = visible_text; + self.snapshot.deleted_text = deleted_text; + self.subscriptions.publish_mut(&edits_patch); + self.history + .insertion_slices + .insert(timestamp, insertion_slices); + edit_op + } + + pub fn set_line_ending(&mut self, line_ending: LineEnding) { + self.snapshot.line_ending = line_ending; + } + + pub fn apply_ops>(&mut self, ops: I) -> Result<()> { + let mut deferred_ops = Vec::new(); + for op in ops { + self.history.push(op.clone()); + if self.can_apply_op(&op) { + self.apply_op(op)?; + } else { + self.deferred_replicas.insert(op.replica_id()); + deferred_ops.push(op); + } + } + self.deferred_ops.insert(deferred_ops); + self.flush_deferred_ops()?; + Ok(()) + } + + fn apply_op(&mut self, op: Operation) -> Result<()> { + match op { + Operation::Edit(edit) => { + if !self.version.observed(edit.timestamp) { + self.apply_remote_edit( + &edit.version, + &edit.ranges, + &edit.new_text, + edit.timestamp, + ); + self.snapshot.version.observe(edit.timestamp); + self.lamport_clock.observe(edit.timestamp); + self.resolve_edit(edit.timestamp); + } + } + Operation::Undo(undo) => { + if !self.version.observed(undo.timestamp) { + self.apply_undo(&undo)?; + self.snapshot.version.observe(undo.timestamp); + self.lamport_clock.observe(undo.timestamp); + } + } + } + self.wait_for_version_txs.retain_mut(|(version, tx)| { + if self.snapshot.version().observed_all(version) { + tx.try_send(()).ok(); + false + } else { + true + } + }); + Ok(()) + } + + fn apply_remote_edit( + &mut self, + version: &clock::Global, + ranges: &[Range], + new_text: &[Arc], + timestamp: clock::Lamport, + ) { + if ranges.is_empty() { + return; + } + + let edits = ranges.iter().zip(new_text.iter()); + let mut edits_patch = Patch::default(); + let mut insertion_slices = Vec::new(); + let cx = Some(version.clone()); + let mut new_insertions = Vec::new(); + let mut insertion_offset = 0; + let mut new_ropes = + RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); + let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>(); + let mut new_fragments = old_fragments.slice( + &VersionedFullOffset::Offset(ranges[0].start), + Bias::Left, + &cx, + ); + new_ropes.append(new_fragments.summary().text); + + let mut fragment_start = old_fragments.start().0.full_offset(); + for (range, new_text) in edits { + let fragment_end = old_fragments.end(&cx).0.full_offset(); + + // If the current fragment ends before this range, then jump ahead to the first fragment + // that extends past the start of this range, reusing any intervening fragments. + if fragment_end < range.start { + // If the current fragment has been partially consumed, then consume the rest of it + // and advance to the next fragment before slicing. + if fragment_start > old_fragments.start().0.full_offset() { + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = fragment_end.0 - fragment_start.0; + suffix.insertion_offset += + fragment_start - old_fragments.start().0.full_offset(); + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(&cx); + } + + let slice = + old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left, &cx); + new_ropes.append(slice.summary().text); + new_fragments.append(slice, &None); + fragment_start = old_fragments.start().0.full_offset(); + } + + // If we are at the end of a non-concurrent fragment, advance to the next one. + let fragment_end = old_fragments.end(&cx).0.full_offset(); + if fragment_end == range.start && fragment_end > fragment_start { + let mut fragment = old_fragments.item().unwrap().clone(); + fragment.len = fragment_end.0 - fragment_start.0; + fragment.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); + new_insertions.push(InsertionFragment::insert_new(&fragment)); + new_ropes.push_fragment(&fragment, fragment.visible); + new_fragments.push(fragment, &None); + old_fragments.next(&cx); + fragment_start = old_fragments.start().0.full_offset(); + } + + // Skip over insertions that are concurrent to this edit, but have a lower lamport + // timestamp. + while let Some(fragment) = old_fragments.item() { + if fragment_start == range.start && fragment.timestamp > timestamp { + new_ropes.push_fragment(fragment, fragment.visible); + new_fragments.push(fragment.clone(), &None); + old_fragments.next(&cx); + debug_assert_eq!(fragment_start, range.start); + } else { + break; + } + } + debug_assert!(fragment_start <= range.start); + + // Preserve any portion of the current fragment that precedes this range. + if fragment_start < range.start { + let mut prefix = old_fragments.item().unwrap().clone(); + prefix.len = range.start.0 - fragment_start.0; + prefix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); + prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); + new_insertions.push(InsertionFragment::insert_new(&prefix)); + fragment_start = range.start; + new_ropes.push_fragment(&prefix, prefix.visible); + new_fragments.push(prefix, &None); + } + + // Insert the new text before any existing fragments within the range. + if !new_text.is_empty() { + let mut old_start = old_fragments.start().1; + if old_fragments.item().map_or(false, |f| f.visible) { + old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; + } + let new_start = new_fragments.summary().text.visible; + let fragment = Fragment { + id: Locator::between( + &new_fragments.summary().max_id, + old_fragments + .item() + .map_or(&Locator::max(), |old_fragment| &old_fragment.id), + ), + timestamp, + insertion_offset, + len: new_text.len(), + deletions: Default::default(), + max_undos: Default::default(), + visible: true, + }; + edits_patch.push(Edit { + old: old_start..old_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); + new_insertions.push(InsertionFragment::insert_new(&fragment)); + new_ropes.push_str(new_text); + new_fragments.push(fragment, &None); + insertion_offset += new_text.len(); + } + + // Advance through every fragment that intersects this range, marking the intersecting + // portions as deleted. + while fragment_start < range.end { + let fragment = old_fragments.item().unwrap(); + let fragment_end = old_fragments.end(&cx).0.full_offset(); + let mut intersection = fragment.clone(); + let intersection_end = cmp::min(range.end, fragment_end); + if fragment.was_visible(version, &self.undo_map) { + intersection.len = intersection_end.0 - fragment_start.0; + intersection.insertion_offset += + fragment_start - old_fragments.start().0.full_offset(); + intersection.id = + Locator::between(&new_fragments.summary().max_id, &intersection.id); + intersection.deletions.insert(timestamp); + intersection.visible = false; + insertion_slices.push(intersection.insertion_slice()); + } + if intersection.len > 0 { + if fragment.visible && !intersection.visible { + let old_start = old_fragments.start().1 + + (fragment_start.0 - old_fragments.start().0.full_offset().0); + let new_start = new_fragments.summary().text.visible; + edits_patch.push(Edit { + old: old_start..old_start + intersection.len, + new: new_start..new_start, + }); + } + new_insertions.push(InsertionFragment::insert_new(&intersection)); + new_ropes.push_fragment(&intersection, fragment.visible); + new_fragments.push(intersection, &None); + fragment_start = intersection_end; + } + if fragment_end <= range.end { + old_fragments.next(&cx); + } + } + } + + // If the current fragment has been partially consumed, then consume the rest of it + // and advance to the next fragment before slicing. + if fragment_start > old_fragments.start().0.full_offset() { + let fragment_end = old_fragments.end(&cx).0.full_offset(); + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = fragment_end.0 - fragment_start.0; + suffix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(&cx); + } + + let suffix = old_fragments.suffix(&cx); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); + let (visible_text, deleted_text) = new_ropes.finish(); + drop(old_fragments); + + self.snapshot.fragments = new_fragments; + self.snapshot.visible_text = visible_text; + self.snapshot.deleted_text = deleted_text; + self.snapshot.insertions.edit(new_insertions, &()); + self.history + .insertion_slices + .insert(timestamp, insertion_slices); + self.subscriptions.publish_mut(&edits_patch) + } + + fn fragment_ids_for_edits<'a>( + &'a self, + edit_ids: impl Iterator, + ) -> Vec<&'a Locator> { + // Get all of the insertion slices changed by the given edits. + let mut insertion_slices = Vec::new(); + for edit_id in edit_ids { + if let Some(slices) = self.history.insertion_slices.get(edit_id) { + insertion_slices.extend_from_slice(slices) + } + } + insertion_slices + .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end))); + + // Get all of the fragments corresponding to these insertion slices. + let mut fragment_ids = Vec::new(); + let mut insertions_cursor = self.insertions.cursor::(); + for insertion_slice in &insertion_slices { + if insertion_slice.insertion_id != insertions_cursor.start().timestamp + || insertion_slice.range.start > insertions_cursor.start().split_offset + { + insertions_cursor.seek_forward( + &InsertionFragmentKey { + timestamp: insertion_slice.insertion_id, + split_offset: insertion_slice.range.start, + }, + Bias::Left, + &(), + ); + } + while let Some(item) = insertions_cursor.item() { + if item.timestamp != insertion_slice.insertion_id + || item.split_offset >= insertion_slice.range.end + { + break; + } + fragment_ids.push(&item.fragment_id); + insertions_cursor.next(&()); + } + } + fragment_ids.sort_unstable(); + fragment_ids + } + + fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> { + self.snapshot.undo_map.insert(undo); + + let mut edits = Patch::default(); + let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let mut new_fragments = SumTree::new(); + let mut new_ropes = + RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); + + for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { + let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); + new_ropes.append(preceding_fragments.summary().text); + new_fragments.append(preceding_fragments, &None); + + if let Some(fragment) = old_fragments.item() { + let mut fragment = fragment.clone(); + let fragment_was_visible = fragment.visible; + + fragment.visible = fragment.is_visible(&self.undo_map); + fragment.max_undos.observe(undo.timestamp); + + let old_start = old_fragments.start().1; + let new_start = new_fragments.summary().text.visible; + if fragment_was_visible && !fragment.visible { + edits.push(Edit { + old: old_start..old_start + fragment.len, + new: new_start..new_start, + }); + } else if !fragment_was_visible && fragment.visible { + edits.push(Edit { + old: old_start..old_start, + new: new_start..new_start + fragment.len, + }); + } + new_ropes.push_fragment(&fragment, fragment_was_visible); + new_fragments.push(fragment, &None); + + old_fragments.next(&None); + } + } + + let suffix = old_fragments.suffix(&None); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); + + drop(old_fragments); + let (visible_text, deleted_text) = new_ropes.finish(); + self.snapshot.fragments = new_fragments; + self.snapshot.visible_text = visible_text; + self.snapshot.deleted_text = deleted_text; + self.subscriptions.publish_mut(&edits); + Ok(()) + } + + fn flush_deferred_ops(&mut self) -> Result<()> { + self.deferred_replicas.clear(); + let mut deferred_ops = Vec::new(); + for op in self.deferred_ops.drain().iter().cloned() { + if self.can_apply_op(&op) { + self.apply_op(op)?; + } else { + self.deferred_replicas.insert(op.replica_id()); + deferred_ops.push(op); + } + } + self.deferred_ops.insert(deferred_ops); + Ok(()) + } + + fn can_apply_op(&self, op: &Operation) -> bool { + if self.deferred_replicas.contains(&op.replica_id()) { + false + } else { + self.version.observed_all(match op { + Operation::Edit(edit) => &edit.version, + Operation::Undo(undo) => &undo.version, + }) + } + } + + pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> { + self.history.undo_stack.last() + } + + pub fn peek_redo_stack(&self) -> Option<&HistoryEntry> { + self.history.redo_stack.last() + } + + pub fn start_transaction(&mut self) -> Option { + self.start_transaction_at(Instant::now()) + } + + pub fn start_transaction_at(&mut self, now: Instant) -> Option { + self.history + .start_transaction(self.version.clone(), now, &mut self.lamport_clock) + } + + pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { + self.end_transaction_at(Instant::now()) + } + + pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> { + if let Some(entry) = self.history.end_transaction(now) { + let since = entry.transaction.start.clone(); + let id = self.history.group().unwrap(); + Some((id, since)) + } else { + None + } + } + + pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> { + self.history.finalize_last_transaction() + } + + pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { + self.history.group_until(transaction_id); + } + + pub fn base_text(&self) -> &Rope { + &self.history.base_text + } + + pub fn operations(&self) -> &TreeMap { + &self.history.operations + } + + pub fn undo(&mut self) -> Option<(TransactionId, Operation)> { + if let Some(entry) = self.history.pop_undo() { + let transaction = entry.transaction.clone(); + let transaction_id = transaction.id; + let op = self.undo_or_redo(transaction).unwrap(); + Some((transaction_id, op)) + } else { + None + } + } + + pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { + let transaction = self + .history + .remove_from_undo(transaction_id)? + .transaction + .clone(); + self.undo_or_redo(transaction).log_err() + } + + #[allow(clippy::needless_collect)] + pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { + let transactions = self + .history + .remove_from_undo_until(transaction_id) + .iter() + .map(|entry| entry.transaction.clone()) + .collect::>(); + + transactions + .into_iter() + .map(|transaction| self.undo_or_redo(transaction).unwrap()) + .collect() + } + + pub fn forget_transaction(&mut self, transaction_id: TransactionId) { + self.history.forget(transaction_id); + } + + pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + self.history.merge_transactions(transaction, destination); + } + + pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { + if let Some(entry) = self.history.pop_redo() { + let transaction = entry.transaction.clone(); + let transaction_id = transaction.id; + let op = self.undo_or_redo(transaction).unwrap(); + Some((transaction_id, op)) + } else { + None + } + } + + #[allow(clippy::needless_collect)] + pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { + let transactions = self + .history + .remove_from_redo(transaction_id) + .iter() + .map(|entry| entry.transaction.clone()) + .collect::>(); + + transactions + .into_iter() + .map(|transaction| self.undo_or_redo(transaction).unwrap()) + .collect() + } + + fn undo_or_redo(&mut self, transaction: Transaction) -> Result { + let mut counts = HashMap::default(); + for edit_id in transaction.edit_ids { + counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1); + } + + let undo = UndoOperation { + timestamp: self.lamport_clock.tick(), + version: self.version(), + counts, + }; + self.apply_undo(&undo)?; + self.snapshot.version.observe(undo.timestamp); + let operation = Operation::Undo(undo); + self.history.push(operation.clone()); + Ok(operation) + } + + pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) { + self.history.push_transaction(transaction, now); + self.history.finalize_last_transaction(); + } + + pub fn edited_ranges_for_transaction<'a, D>( + &'a self, + transaction: &'a Transaction, + ) -> impl 'a + Iterator> + where + D: TextDimension, + { + // get fragment ranges + let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let offset_ranges = self + .fragment_ids_for_edits(transaction.edit_ids.iter()) + .into_iter() + .filter_map(move |fragment_id| { + cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); + let fragment = cursor.item()?; + let start_offset = cursor.start().1; + let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; + Some(start_offset..end_offset) + }); + + // combine adjacent ranges + let mut prev_range: Option> = None; + let disjoint_ranges = offset_ranges + .map(Some) + .chain([None]) + .filter_map(move |range| { + if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) { + if prev_range.end == range.start { + prev_range.end = range.end; + return None; + } + } + let result = prev_range.clone(); + prev_range = range; + result + }); + + // convert to the desired text dimension. + let mut position = D::default(); + let mut rope_cursor = self.visible_text.cursor(0); + disjoint_ranges.map(move |range| { + position.add_assign(&rope_cursor.summary(range.start)); + let start = position.clone(); + position.add_assign(&rope_cursor.summary(range.end)); + let end = position.clone(); + start..end + }) + } + + pub fn subscribe(&mut self) -> Subscription { + self.subscriptions.subscribe() + } + + pub fn wait_for_edits( + &mut self, + edit_ids: impl IntoIterator, + ) -> impl 'static + Future> { + let mut futures = Vec::new(); + for edit_id in edit_ids { + if !self.version.observed(edit_id) { + let (tx, rx) = oneshot::channel(); + self.edit_id_resolvers.entry(edit_id).or_default().push(tx); + futures.push(rx); + } + } + + async move { + for mut future in futures { + if future.recv().await.is_none() { + Err(anyhow!("gave up waiting for edits"))?; + } + } + Ok(()) + } + } + + pub fn wait_for_anchors( + &mut self, + anchors: impl IntoIterator, + ) -> impl 'static + Future> { + let mut futures = Vec::new(); + for anchor in anchors { + if !self.version.observed(anchor.timestamp) + && anchor != Anchor::MAX + && anchor != Anchor::MIN + { + let (tx, rx) = oneshot::channel(); + self.edit_id_resolvers + .entry(anchor.timestamp) + .or_default() + .push(tx); + futures.push(rx); + } + } + + async move { + for mut future in futures { + if future.recv().await.is_none() { + Err(anyhow!("gave up waiting for anchors"))?; + } + } + Ok(()) + } + } + + pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future> { + let mut rx = None; + if !self.snapshot.version.observed_all(&version) { + let channel = oneshot::channel(); + self.wait_for_version_txs.push((version, channel.0)); + rx = Some(channel.1); + } + async move { + if let Some(mut rx) = rx { + if rx.recv().await.is_none() { + Err(anyhow!("gave up waiting for version"))?; + } + } + Ok(()) + } + } + + pub fn give_up_waiting(&mut self) { + self.edit_id_resolvers.clear(); + self.wait_for_version_txs.clear(); + } + + fn resolve_edit(&mut self, edit_id: clock::Lamport) { + for mut tx in self + .edit_id_resolvers + .remove(&edit_id) + .into_iter() + .flatten() + { + tx.try_send(()).ok(); + } + } +} + +#[cfg(any(test, feature = "test-support"))] +impl Buffer { + pub fn edit_via_marked_text(&mut self, marked_string: &str) { + let edits = self.edits_for_marked_text(marked_string); + self.edit(edits); + } + + pub fn edits_for_marked_text(&self, marked_string: &str) -> Vec<(Range, String)> { + let old_text = self.text(); + let (new_text, mut ranges) = util::test::marked_text_ranges(marked_string, false); + if ranges.is_empty() { + ranges.push(0..new_text.len()); + } + + assert_eq!( + old_text[..ranges[0].start], + new_text[..ranges[0].start], + "invalid edit" + ); + + let mut delta = 0; + let mut edits = Vec::new(); + let mut ranges = ranges.into_iter().peekable(); + + while let Some(inserted_range) = ranges.next() { + let new_start = inserted_range.start; + let old_start = (new_start as isize - delta) as usize; + + let following_text = if let Some(next_range) = ranges.peek() { + &new_text[inserted_range.end..next_range.start] + } else { + &new_text[inserted_range.end..] + }; + + let inserted_len = inserted_range.len(); + let deleted_len = old_text[old_start..] + .find(following_text) + .expect("invalid edit"); + + let old_range = old_start..old_start + deleted_len; + edits.push((old_range, new_text[inserted_range].to_string())); + delta += inserted_len as isize - deleted_len as isize; + } + + assert_eq!( + old_text.len() as isize + delta, + new_text.len() as isize, + "invalid edit" + ); + + edits + } + + pub fn check_invariants(&self) { + // Ensure every fragment is ordered by locator in the fragment tree and corresponds + // to an insertion fragment in the insertions tree. + let mut prev_fragment_id = Locator::min(); + for fragment in self.snapshot.fragments.items(&None) { + assert!(fragment.id > prev_fragment_id); + prev_fragment_id = fragment.id.clone(); + + let insertion_fragment = self + .snapshot + .insertions + .get( + &InsertionFragmentKey { + timestamp: fragment.timestamp, + split_offset: fragment.insertion_offset, + }, + &(), + ) + .unwrap(); + assert_eq!( + insertion_fragment.fragment_id, fragment.id, + "fragment: {:?}\ninsertion: {:?}", + fragment, insertion_fragment + ); + } + + let mut cursor = self.snapshot.fragments.cursor::>(); + for insertion_fragment in self.snapshot.insertions.cursor::<()>() { + cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left, &None); + let fragment = cursor.item().unwrap(); + assert_eq!(insertion_fragment.fragment_id, fragment.id); + assert_eq!(insertion_fragment.split_offset, fragment.insertion_offset); + } + + let fragment_summary = self.snapshot.fragments.summary(); + assert_eq!( + fragment_summary.text.visible, + self.snapshot.visible_text.len() + ); + assert_eq!( + fragment_summary.text.deleted, + self.snapshot.deleted_text.len() + ); + + assert!(!self.text().contains("\r\n")); + } + + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.history.group_interval = group_interval; + } + + pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { + let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); + start..end + } + + pub fn get_random_edits( + &self, + rng: &mut T, + edit_count: usize, + ) -> Vec<(Range, Arc)> + where + T: rand::Rng, + { + let mut edits: Vec<(Range, Arc)> = Vec::new(); + let mut last_end = None; + for _ in 0..edit_count { + if last_end.map_or(false, |last_end| last_end >= self.len()) { + break; + } + let new_start = last_end.map_or(0, |last_end| last_end + 1); + let range = self.random_byte_range(new_start, rng); + last_end = Some(range.end); + + let new_text_len = rng.gen_range(0..10); + let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); + + edits.push((range, new_text.into())); + } + edits + } + + #[allow(clippy::type_complexity)] + pub fn randomly_edit( + &mut self, + rng: &mut T, + edit_count: usize, + ) -> (Vec<(Range, Arc)>, Operation) + where + T: rand::Rng, + { + let mut edits = self.get_random_edits(rng, edit_count); + log::info!("mutating buffer {} with {:?}", self.replica_id, edits); + + let op = self.edit(edits.iter().cloned()); + if let Operation::Edit(edit) = &op { + assert_eq!(edits.len(), edit.new_text.len()); + for (edit, new_text) in edits.iter_mut().zip(&edit.new_text) { + edit.1 = new_text.clone(); + } + } else { + unreachable!() + } + + (edits, op) + } + + pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng) -> Vec { + use rand::prelude::*; + + let mut ops = Vec::new(); + for _ in 0..rng.gen_range(1..=5) { + if let Some(entry) = self.history.undo_stack.choose(rng) { + let transaction = entry.transaction.clone(); + log::info!( + "undoing buffer {} transaction {:?}", + self.replica_id, + transaction + ); + ops.push(self.undo_or_redo(transaction).unwrap()); + } + } + ops + } +} + +impl Deref for Buffer { + type Target = BufferSnapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl BufferSnapshot { + pub fn as_rope(&self) -> &Rope { + &self.visible_text + } + + pub fn remote_id(&self) -> u64 { + self.remote_id + } + + pub fn replica_id(&self) -> ReplicaId { + self.replica_id + } + + pub fn row_count(&self) -> u32 { + self.max_point().row + 1 + } + + pub fn len(&self) -> usize { + self.visible_text.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn chars(&self) -> impl Iterator + '_ { + self.chars_at(0) + } + + pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { + self.text_for_range(range).flat_map(str::chars) + } + + pub fn reversed_chars_for_range( + &self, + range: Range, + ) -> impl Iterator + '_ { + self.reversed_chunks_in_range(range) + .flat_map(|chunk| chunk.chars().rev()) + } + + pub fn contains_str_at(&self, position: T, needle: &str) -> bool + where + T: ToOffset, + { + let position = position.to_offset(self); + position == self.clip_offset(position, Bias::Left) + && self + .bytes_in_range(position..self.len()) + .flatten() + .copied() + .take(needle.len()) + .eq(needle.bytes()) + } + + pub fn common_prefix_at(&self, position: T, needle: &str) -> Range + where + T: ToOffset + TextDimension, + { + let offset = position.to_offset(self); + let common_prefix_len = needle + .char_indices() + .map(|(index, _)| index) + .chain([needle.len()]) + .take_while(|&len| len <= offset) + .filter(|&len| { + let left = self + .chars_for_range(offset - len..offset) + .flat_map(char::to_lowercase); + let right = needle[..len].chars().flat_map(char::to_lowercase); + left.eq(right) + }) + .last() + .unwrap_or(0); + let start_offset = offset - common_prefix_len; + let start = self.text_summary_for_range(0..start_offset); + start..position + } + + pub fn text(&self) -> String { + self.visible_text.to_string() + } + + pub fn line_ending(&self) -> LineEnding { + self.line_ending + } + + pub fn deleted_text(&self) -> String { + self.deleted_text.to_string() + } + + pub fn fragments(&self) -> impl Iterator { + self.fragments.iter() + } + + pub fn text_summary(&self) -> TextSummary { + self.visible_text.summary() + } + + pub fn max_point(&self) -> Point { + self.visible_text.max_point() + } + + pub fn max_point_utf16(&self) -> PointUtf16 { + self.visible_text.max_point_utf16() + } + + pub fn point_to_offset(&self, point: Point) -> usize { + self.visible_text.point_to_offset(point) + } + + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + self.visible_text.point_utf16_to_offset(point) + } + + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { + self.visible_text.unclipped_point_utf16_to_offset(point) + } + + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { + self.visible_text.unclipped_point_utf16_to_point(point) + } + + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + self.visible_text.offset_utf16_to_offset(offset) + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + self.visible_text.offset_to_offset_utf16(offset) + } + + pub fn offset_to_point(&self, offset: usize) -> Point { + self.visible_text.offset_to_point(offset) + } + + pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { + self.visible_text.offset_to_point_utf16(offset) + } + + pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { + self.visible_text.point_to_point_utf16(point) + } + + pub fn version(&self) -> &clock::Global { + &self.version + } + + pub fn chars_at(&self, position: T) -> impl Iterator + '_ { + let offset = position.to_offset(self); + self.visible_text.chars_at(offset) + } + + pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { + let offset = position.to_offset(self); + self.visible_text.reversed_chars_at(offset) + } + + pub fn reversed_chunks_in_range(&self, range: Range) -> rope::Chunks { + let range = range.start.to_offset(self)..range.end.to_offset(self); + self.visible_text.reversed_chunks_in_range(range) + } + + pub fn bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.bytes_in_range(start..end) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.reversed_bytes_in_range(start..end) + } + + pub fn text_for_range(&self, range: Range) -> Chunks<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.chunks_in_range(start..end) + } + + pub fn line_len(&self, row: u32) -> u32 { + let row_start_offset = Point::new(row, 0).to_offset(self); + let row_end_offset = if row >= self.max_point().row { + self.len() + } else { + Point::new(row + 1, 0).to_offset(self) - 1 + }; + (row_end_offset - row_start_offset) as u32 + } + + pub fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + + pub fn text_summary_for_range(&self, range: Range) -> D + where + D: TextDimension, + { + self.visible_text + .cursor(range.start.to_offset(self)) + .summary(range.end.to_offset(self)) + } + + pub fn summaries_for_anchors<'a, D, A>(&'a self, anchors: A) -> impl 'a + Iterator + where + D: 'a + TextDimension, + A: 'a + IntoIterator, + { + let anchors = anchors.into_iter(); + self.summaries_for_anchors_with_payload::(anchors.map(|a| (a, ()))) + .map(|d| d.0) + } + + pub fn summaries_for_anchors_with_payload<'a, D, A, T>( + &'a self, + anchors: A, + ) -> impl 'a + Iterator + where + D: 'a + TextDimension, + A: 'a + IntoIterator, + { + let anchors = anchors.into_iter(); + let mut insertion_cursor = self.insertions.cursor::(); + let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let mut text_cursor = self.visible_text.cursor(0); + let mut position = D::default(); + + anchors.map(move |(anchor, payload)| { + if *anchor == Anchor::MIN { + return (D::default(), payload); + } else if *anchor == Anchor::MAX { + return (D::from_text_summary(&self.visible_text.summary()), payload); + } + + let anchor_key = InsertionFragmentKey { + timestamp: anchor.timestamp, + split_offset: anchor.offset, + }; + insertion_cursor.seek(&anchor_key, anchor.bias, &()); + if let Some(insertion) = insertion_cursor.item() { + let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); + if comparison == Ordering::Greater + || (anchor.bias == Bias::Left + && comparison == Ordering::Equal + && anchor.offset > 0) + { + insertion_cursor.prev(&()); + } + } else { + insertion_cursor.prev(&()); + } + let insertion = insertion_cursor.item().expect("invalid insertion"); + assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); + + fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None); + let fragment = fragment_cursor.item().unwrap(); + let mut fragment_offset = fragment_cursor.start().1; + if fragment.visible { + fragment_offset += anchor.offset - insertion.split_offset; + } + + position.add_assign(&text_cursor.summary(fragment_offset)); + (position.clone(), payload) + }) + } + + fn summary_for_anchor(&self, anchor: &Anchor) -> D + where + D: TextDimension, + { + if *anchor == Anchor::MIN { + D::default() + } else if *anchor == Anchor::MAX { + D::from_text_summary(&self.visible_text.summary()) + } else { + let anchor_key = InsertionFragmentKey { + timestamp: anchor.timestamp, + split_offset: anchor.offset, + }; + let mut insertion_cursor = self.insertions.cursor::(); + insertion_cursor.seek(&anchor_key, anchor.bias, &()); + if let Some(insertion) = insertion_cursor.item() { + let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); + if comparison == Ordering::Greater + || (anchor.bias == Bias::Left + && comparison == Ordering::Equal + && anchor.offset > 0) + { + insertion_cursor.prev(&()); + } + } else { + insertion_cursor.prev(&()); + } + let insertion = insertion_cursor.item().expect("invalid insertion"); + assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); + + let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); + fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left, &None); + let fragment = fragment_cursor.item().unwrap(); + let mut fragment_offset = fragment_cursor.start().1; + if fragment.visible { + fragment_offset += anchor.offset - insertion.split_offset; + } + self.text_summary_for_range(0..fragment_offset) + } + } + + fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { + if *anchor == Anchor::MIN { + Locator::min_ref() + } else if *anchor == Anchor::MAX { + Locator::max_ref() + } else { + let anchor_key = InsertionFragmentKey { + timestamp: anchor.timestamp, + split_offset: anchor.offset, + }; + let mut insertion_cursor = self.insertions.cursor::(); + insertion_cursor.seek(&anchor_key, anchor.bias, &()); + if let Some(insertion) = insertion_cursor.item() { + let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); + if comparison == Ordering::Greater + || (anchor.bias == Bias::Left + && comparison == Ordering::Equal + && anchor.offset > 0) + { + insertion_cursor.prev(&()); + } + } else { + insertion_cursor.prev(&()); + } + let insertion = insertion_cursor.item().expect("invalid insertion"); + debug_assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); + &insertion.fragment_id + } + } + + pub fn anchor_before(&self, position: T) -> Anchor { + self.anchor_at(position, Bias::Left) + } + + pub fn anchor_after(&self, position: T) -> Anchor { + self.anchor_at(position, Bias::Right) + } + + pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { + self.anchor_at_offset(position.to_offset(self), bias) + } + + fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor { + if bias == Bias::Left && offset == 0 { + Anchor::MIN + } else if bias == Bias::Right && offset == self.len() { + Anchor::MAX + } else { + let mut fragment_cursor = self.fragments.cursor::(); + fragment_cursor.seek(&offset, bias, &None); + let fragment = fragment_cursor.item().unwrap(); + let overshoot = offset - *fragment_cursor.start(); + Anchor { + timestamp: fragment.timestamp, + offset: fragment.insertion_offset + overshoot, + bias, + buffer_id: Some(self.remote_id), + } + } + } + + pub fn can_resolve(&self, anchor: &Anchor) -> bool { + *anchor == Anchor::MIN + || *anchor == Anchor::MAX + || (Some(self.remote_id) == anchor.buffer_id && self.version.observed(anchor.timestamp)) + } + + pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + self.visible_text.clip_offset(offset, bias) + } + + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + self.visible_text.clip_point(point, bias) + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + self.visible_text.clip_offset_utf16(offset, bias) + } + + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { + self.visible_text.clip_point_utf16(point, bias) + } + + pub fn edits_since<'a, D>( + &'a self, + since: &'a clock::Global, + ) -> impl 'a + Iterator> + where + D: TextDimension + Ord, + { + self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) + } + + pub fn anchored_edits_since<'a, D>( + &'a self, + since: &'a clock::Global, + ) -> impl 'a + Iterator, Range)> + where + D: TextDimension + Ord, + { + self.anchored_edits_since_in_range(since, Anchor::MIN..Anchor::MAX) + } + + pub fn edits_since_in_range<'a, D>( + &'a self, + since: &'a clock::Global, + range: Range, + ) -> impl 'a + Iterator> + where + D: TextDimension + Ord, + { + self.anchored_edits_since_in_range(since, range) + .map(|item| item.0) + } + + pub fn anchored_edits_since_in_range<'a, D>( + &'a self, + since: &'a clock::Global, + range: Range, + ) -> impl 'a + Iterator, Range)> + where + D: TextDimension + Ord, + { + let fragments_cursor = if *since == self.version { + None + } else { + let mut cursor = self + .fragments + .filter(move |summary| !since.observed_all(&summary.max_version)); + cursor.next(&None); + Some(cursor) + }; + let mut cursor = self + .fragments + .cursor::<(Option<&Locator>, FragmentTextSummary)>(); + + let start_fragment_id = self.fragment_id_for_anchor(&range.start); + cursor.seek(&Some(start_fragment_id), Bias::Left, &None); + let mut visible_start = cursor.start().1.visible; + let mut deleted_start = cursor.start().1.deleted; + if let Some(fragment) = cursor.item() { + let overshoot = range.start.offset - fragment.insertion_offset; + if fragment.visible { + visible_start += overshoot; + } else { + deleted_start += overshoot; + } + } + let end_fragment_id = self.fragment_id_for_anchor(&range.end); + + Edits { + visible_cursor: self.visible_text.cursor(visible_start), + deleted_cursor: self.deleted_text.cursor(deleted_start), + fragments_cursor, + undos: &self.undo_map, + since, + old_end: Default::default(), + new_end: Default::default(), + range: (start_fragment_id, range.start.offset)..(end_fragment_id, range.end.offset), + buffer_id: self.remote_id, + } + } +} + +struct RopeBuilder<'a> { + old_visible_cursor: rope::Cursor<'a>, + old_deleted_cursor: rope::Cursor<'a>, + new_visible: Rope, + new_deleted: Rope, +} + +impl<'a> RopeBuilder<'a> { + fn new(old_visible_cursor: rope::Cursor<'a>, old_deleted_cursor: rope::Cursor<'a>) -> Self { + Self { + old_visible_cursor, + old_deleted_cursor, + new_visible: Rope::new(), + new_deleted: Rope::new(), + } + } + + fn append(&mut self, len: FragmentTextSummary) { + self.push(len.visible, true, true); + self.push(len.deleted, false, false); + } + + fn push_fragment(&mut self, fragment: &Fragment, was_visible: bool) { + debug_assert!(fragment.len > 0); + self.push(fragment.len, was_visible, fragment.visible) + } + + fn push(&mut self, len: usize, was_visible: bool, is_visible: bool) { + let text = if was_visible { + self.old_visible_cursor + .slice(self.old_visible_cursor.offset() + len) + } else { + self.old_deleted_cursor + .slice(self.old_deleted_cursor.offset() + len) + }; + if is_visible { + self.new_visible.append(text); + } else { + self.new_deleted.append(text); + } + } + + fn push_str(&mut self, text: &str) { + self.new_visible.push(text); + } + + fn finish(mut self) -> (Rope, Rope) { + self.new_visible.append(self.old_visible_cursor.suffix()); + self.new_deleted.append(self.old_deleted_cursor.suffix()); + (self.new_visible, self.new_deleted) + } +} + +impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator for Edits<'a, D, F> { + type Item = (Edit, Range); + + fn next(&mut self) -> Option { + let mut pending_edit: Option = None; + let cursor = self.fragments_cursor.as_mut()?; + + while let Some(fragment) = cursor.item() { + if fragment.id < *self.range.start.0 { + cursor.next(&None); + continue; + } else if fragment.id > *self.range.end.0 { + break; + } + + if cursor.start().visible > self.visible_cursor.offset() { + let summary = self.visible_cursor.summary(cursor.start().visible); + self.old_end.add_assign(&summary); + self.new_end.add_assign(&summary); + } + + if pending_edit + .as_ref() + .map_or(false, |(change, _)| change.new.end < self.new_end) + { + break; + } + + let start_anchor = Anchor { + timestamp: fragment.timestamp, + offset: fragment.insertion_offset, + bias: Bias::Right, + buffer_id: Some(self.buffer_id), + }; + let end_anchor = Anchor { + timestamp: fragment.timestamp, + offset: fragment.insertion_offset + fragment.len, + bias: Bias::Left, + buffer_id: Some(self.buffer_id), + }; + + if !fragment.was_visible(self.since, self.undos) && fragment.visible { + let mut visible_end = cursor.end(&None).visible; + if fragment.id == *self.range.end.0 { + visible_end = cmp::min( + visible_end, + cursor.start().visible + (self.range.end.1 - fragment.insertion_offset), + ); + } + + let fragment_summary = self.visible_cursor.summary(visible_end); + let mut new_end = self.new_end.clone(); + new_end.add_assign(&fragment_summary); + if let Some((edit, range)) = pending_edit.as_mut() { + edit.new.end = new_end.clone(); + range.end = end_anchor; + } else { + pending_edit = Some(( + Edit { + old: self.old_end.clone()..self.old_end.clone(), + new: self.new_end.clone()..new_end.clone(), + }, + start_anchor..end_anchor, + )); + } + + self.new_end = new_end; + } else if fragment.was_visible(self.since, self.undos) && !fragment.visible { + let mut deleted_end = cursor.end(&None).deleted; + if fragment.id == *self.range.end.0 { + deleted_end = cmp::min( + deleted_end, + cursor.start().deleted + (self.range.end.1 - fragment.insertion_offset), + ); + } + + if cursor.start().deleted > self.deleted_cursor.offset() { + self.deleted_cursor.seek_forward(cursor.start().deleted); + } + let fragment_summary = self.deleted_cursor.summary(deleted_end); + let mut old_end = self.old_end.clone(); + old_end.add_assign(&fragment_summary); + if let Some((edit, range)) = pending_edit.as_mut() { + edit.old.end = old_end.clone(); + range.end = end_anchor; + } else { + pending_edit = Some(( + Edit { + old: self.old_end.clone()..old_end.clone(), + new: self.new_end.clone()..self.new_end.clone(), + }, + start_anchor..end_anchor, + )); + } + + self.old_end = old_end; + } + + cursor.next(&None); + } + + pending_edit + } +} + +impl Fragment { + fn insertion_slice(&self) -> InsertionSlice { + InsertionSlice { + insertion_id: self.timestamp, + range: self.insertion_offset..self.insertion_offset + self.len, + } + } + + fn is_visible(&self, undos: &UndoMap) -> bool { + !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) + } + + fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { + (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) + && self + .deletions + .iter() + .all(|d| !version.observed(*d) || undos.was_undone(*d, version)) + } +} + +impl sum_tree::Item for Fragment { + type Summary = FragmentSummary; + + fn summary(&self) -> Self::Summary { + let mut max_version = clock::Global::new(); + max_version.observe(self.timestamp); + for deletion in &self.deletions { + max_version.observe(*deletion); + } + max_version.join(&self.max_undos); + + let mut min_insertion_version = clock::Global::new(); + min_insertion_version.observe(self.timestamp); + let max_insertion_version = min_insertion_version.clone(); + if self.visible { + FragmentSummary { + max_id: self.id.clone(), + text: FragmentTextSummary { + visible: self.len, + deleted: 0, + }, + max_version, + min_insertion_version, + max_insertion_version, + } + } else { + FragmentSummary { + max_id: self.id.clone(), + text: FragmentTextSummary { + visible: 0, + deleted: self.len, + }, + max_version, + min_insertion_version, + max_insertion_version, + } + } + } +} + +impl sum_tree::Summary for FragmentSummary { + type Context = Option; + + fn add_summary(&mut self, other: &Self, _: &Self::Context) { + self.max_id.assign(&other.max_id); + self.text.visible += &other.text.visible; + self.text.deleted += &other.text.deleted; + self.max_version.join(&other.max_version); + self.min_insertion_version + .meet(&other.min_insertion_version); + self.max_insertion_version + .join(&other.max_insertion_version); + } +} + +impl Default for FragmentSummary { + fn default() -> Self { + FragmentSummary { + max_id: Locator::min(), + text: FragmentTextSummary::default(), + max_version: clock::Global::new(), + min_insertion_version: clock::Global::new(), + max_insertion_version: clock::Global::new(), + } + } +} + +impl sum_tree::Item for InsertionFragment { + type Summary = InsertionFragmentKey; + + fn summary(&self) -> Self::Summary { + InsertionFragmentKey { + timestamp: self.timestamp, + split_offset: self.split_offset, + } + } +} + +impl sum_tree::KeyedItem for InsertionFragment { + type Key = InsertionFragmentKey; + + fn key(&self) -> Self::Key { + sum_tree::Item::summary(self) + } +} + +impl InsertionFragment { + fn new(fragment: &Fragment) -> Self { + Self { + timestamp: fragment.timestamp, + split_offset: fragment.insertion_offset, + fragment_id: fragment.id.clone(), + } + } + + fn insert_new(fragment: &Fragment) -> sum_tree::Edit { + sum_tree::Edit::Insert(Self::new(fragment)) + } +} + +impl sum_tree::Summary for InsertionFragmentKey { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + *self = *summary; + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FullOffset(pub usize); + +impl ops::AddAssign for FullOffset { + fn add_assign(&mut self, rhs: usize) { + self.0 += rhs; + } +} + +impl ops::Add for FullOffset { + type Output = Self; + + fn add(mut self, rhs: usize) -> Self::Output { + self += rhs; + self + } +} + +impl ops::Sub for FullOffset { + type Output = usize; + + fn sub(self, rhs: Self) -> Self::Output { + self.0 - rhs.0 + } +} + +impl<'a> sum_tree::Dimension<'a, FragmentSummary> for usize { + fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { + *self += summary.text.visible; + } +} + +impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FullOffset { + fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { + self.0 += summary.text.visible + summary.text.deleted; + } +} + +impl<'a> sum_tree::Dimension<'a, FragmentSummary> for Option<&'a Locator> { + fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { + *self = Some(&summary.max_id); + } +} + +impl<'a> sum_tree::SeekTarget<'a, FragmentSummary, FragmentTextSummary> for usize { + fn cmp( + &self, + cursor_location: &FragmentTextSummary, + _: &Option, + ) -> cmp::Ordering { + Ord::cmp(self, &cursor_location.visible) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum VersionedFullOffset { + Offset(FullOffset), + Invalid, +} + +impl VersionedFullOffset { + fn full_offset(&self) -> FullOffset { + if let Self::Offset(position) = self { + *position + } else { + panic!("invalid version") + } + } +} + +impl Default for VersionedFullOffset { + fn default() -> Self { + Self::Offset(Default::default()) + } +} + +impl<'a> sum_tree::Dimension<'a, FragmentSummary> for VersionedFullOffset { + fn add_summary(&mut self, summary: &'a FragmentSummary, cx: &Option) { + if let Self::Offset(offset) = self { + let version = cx.as_ref().unwrap(); + if version.observed_all(&summary.max_insertion_version) { + *offset += summary.text.visible + summary.text.deleted; + } else if version.observed_any(&summary.min_insertion_version) { + *self = Self::Invalid; + } + } + } +} + +impl<'a> sum_tree::SeekTarget<'a, FragmentSummary, Self> for VersionedFullOffset { + fn cmp(&self, cursor_position: &Self, _: &Option) -> cmp::Ordering { + match (self, cursor_position) { + (Self::Offset(a), Self::Offset(b)) => Ord::cmp(a, b), + (Self::Offset(_), Self::Invalid) => cmp::Ordering::Less, + (Self::Invalid, _) => unreachable!(), + } + } +} + +impl Operation { + fn replica_id(&self) -> ReplicaId { + operation_queue::Operation::lamport_timestamp(self).replica_id + } + + pub fn timestamp(&self) -> clock::Lamport { + match self { + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, + } + } + + pub fn as_edit(&self) -> Option<&EditOperation> { + match self { + Operation::Edit(edit) => Some(edit), + _ => None, + } + } + + pub fn is_edit(&self) -> bool { + matches!(self, Operation::Edit { .. }) + } +} + +impl operation_queue::Operation for Operation { + fn lamport_timestamp(&self) -> clock::Lamport { + match self { + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, + } + } +} + +pub trait ToOffset { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize; +} + +impl ToOffset for Point { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.point_to_offset(*self) + } +} + +impl ToOffset for usize { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + assert!( + *self <= snapshot.len(), + "offset {} is out of range, max allowed is {}", + self, + snapshot.len() + ); + *self + } +} + +impl ToOffset for Anchor { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.summary_for_anchor(self) + } +} + +impl<'a, T: ToOffset> ToOffset for &'a T { + fn to_offset(&self, content: &BufferSnapshot) -> usize { + (*self).to_offset(content) + } +} + +impl ToOffset for PointUtf16 { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.point_utf16_to_offset(*self) + } +} + +impl ToOffset for Unclipped { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.unclipped_point_utf16_to_offset(*self) + } +} + +pub trait ToPoint { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point; +} + +impl ToPoint for Anchor { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { + snapshot.summary_for_anchor(self) + } +} + +impl ToPoint for usize { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { + snapshot.offset_to_point(*self) + } +} + +impl ToPoint for Point { + fn to_point(&self, _: &BufferSnapshot) -> Point { + *self + } +} + +impl ToPoint for Unclipped { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { + snapshot.unclipped_point_utf16_to_point(*self) + } +} + +pub trait ToPointUtf16 { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16; +} + +impl ToPointUtf16 for Anchor { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + snapshot.summary_for_anchor(self) + } +} + +impl ToPointUtf16 for usize { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + snapshot.offset_to_point_utf16(*self) + } +} + +impl ToPointUtf16 for PointUtf16 { + fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 { + *self + } +} + +impl ToPointUtf16 for Point { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + snapshot.point_to_point_utf16(*self) + } +} + +pub trait ToOffsetUtf16 { + fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16; +} + +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.summary_for_anchor(self) + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { + *self + } +} + +pub trait FromAnchor { + fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self; +} + +impl FromAnchor for Point { + fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { + snapshot.summary_for_anchor(anchor) + } +} + +impl FromAnchor for PointUtf16 { + fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { + snapshot.summary_for_anchor(anchor) + } +} + +impl FromAnchor for usize { + fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { + snapshot.summary_for_anchor(anchor) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineEnding { + Unix, + Windows, +} + +impl Default for LineEnding { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::CRLF; + } +} + +impl LineEnding { + pub fn as_str(&self) -> &'static str { + match self { + LineEnding::Unix => "\n", + LineEnding::Windows => "\r\n", + } + } + + pub fn detect(text: &str) -> Self { + let mut max_ix = cmp::min(text.len(), 1000); + while !text.is_char_boundary(max_ix) { + max_ix -= 1; + } + + if let Some(ix) = text[..max_ix].find(&['\n']) { + if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { + Self::Windows + } else { + Self::Unix + } + } else { + Self::default() + } + } + + pub fn normalize(text: &mut String) { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { + *text = replaced; + } + } + + pub fn normalize_arc(text: Arc) -> Arc { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { + replaced.into() + } else { + text + } + } +} diff --git a/crates/text2/src/undo_map.rs b/crates/text2/src/undo_map.rs new file mode 100644 index 0000000000..f95809c02e --- /dev/null +++ b/crates/text2/src/undo_map.rs @@ -0,0 +1,112 @@ +use crate::UndoOperation; +use std::cmp; +use sum_tree::{Bias, SumTree}; + +#[derive(Copy, Clone, Debug)] +struct UndoMapEntry { + key: UndoMapKey, + undo_count: u32, +} + +impl sum_tree::Item for UndoMapEntry { + type Summary = UndoMapKey; + + fn summary(&self) -> Self::Summary { + self.key + } +} + +impl sum_tree::KeyedItem for UndoMapEntry { + type Key = UndoMapKey; + + fn key(&self) -> Self::Key { + self.key + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct UndoMapKey { + edit_id: clock::Lamport, + undo_id: clock::Lamport, +} + +impl sum_tree::Summary for UndoMapKey { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + *self = cmp::max(*self, *summary); + } +} + +#[derive(Clone, Default)] +pub struct UndoMap(SumTree); + +impl UndoMap { + pub fn insert(&mut self, undo: &UndoOperation) { + let edits = undo + .counts + .iter() + .map(|(edit_id, count)| { + sum_tree::Edit::Insert(UndoMapEntry { + key: UndoMapKey { + edit_id: *edit_id, + undo_id: undo.timestamp, + }, + undo_count: *count, + }) + }) + .collect::>(); + self.0.edit(edits, &()); + } + + pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { + self.undo_count(edit_id) % 2 == 1 + } + + pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { + let mut cursor = self.0.cursor::(); + cursor.seek( + &UndoMapKey { + edit_id, + undo_id: Default::default(), + }, + Bias::Left, + &(), + ); + + let mut undo_count = 0; + for entry in cursor { + if entry.key.edit_id != edit_id { + break; + } + + if version.observed(entry.key.undo_id) { + undo_count = cmp::max(undo_count, entry.undo_count); + } + } + + undo_count % 2 == 1 + } + + pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { + let mut cursor = self.0.cursor::(); + cursor.seek( + &UndoMapKey { + edit_id, + undo_id: Default::default(), + }, + Bias::Left, + &(), + ); + + let mut undo_count = 0; + for entry in cursor { + if entry.key.edit_id != edit_id { + break; + } + + undo_count = cmp::max(undo_count, entry.undo_count); + } + undo_count + } +} diff --git a/crates/theme2/Cargo.toml b/crates/theme2/Cargo.toml index 2f89425d21..5a8448372c 100644 --- a/crates/theme2/Cargo.toml +++ b/crates/theme2/Cargo.toml @@ -6,9 +6,9 @@ publish = false [features] test-support = [ - "gpui2/test-support", + "gpui/test-support", "fs/test-support", - "settings2/test-support" + "settings/test-support" ] [lib] @@ -16,21 +16,21 @@ path = "src/theme2.rs" doctest = false [dependencies] -gpui2 = { path = "../gpui2" } -fs = { path = "../fs" } -schemars.workspace = true -settings2 = { path = "../settings2" } -util = { path = "../util" } - anyhow.workspace = true +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } indexmap = "1.6.2" parking_lot.workspace = true +refineable.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true +settings = { package = "settings2", path = "../settings2" } toml.workspace = true +util = { path = "../util" } [dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } -fs = { path = "../fs", features = ["test-support"] } -settings2 = { path = "../settings2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs new file mode 100644 index 0000000000..422e33e4f8 --- /dev/null +++ b/crates/theme2/src/colors.rs @@ -0,0 +1,147 @@ +use gpui::Hsla; +use refineable::Refineable; + +use crate::SyntaxTheme; + +#[derive(Clone)] +pub struct SystemColors { + pub transparent: Hsla, + pub mac_os_traffic_light_red: Hsla, + pub mac_os_traffic_light_yellow: Hsla, + pub mac_os_traffic_light_green: Hsla, +} + +#[derive(Debug, Clone, Copy)] +pub struct PlayerColor { + pub cursor: Hsla, + pub background: Hsla, + pub selection: Hsla, +} + +#[derive(Clone)] +pub struct PlayerColors(pub Vec); + +#[derive(Refineable, Clone, Debug)] +#[refineable(debug)] +pub struct StatusColors { + pub conflict: Hsla, + pub created: Hsla, + pub deleted: Hsla, + pub error: Hsla, + pub hidden: Hsla, + pub ignored: Hsla, + pub info: Hsla, + pub modified: Hsla, + pub renamed: Hsla, + pub success: Hsla, + pub warning: Hsla, +} + +#[derive(Refineable, Clone, Debug)] +#[refineable(debug)] +pub struct GitStatusColors { + pub conflict: Hsla, + pub created: Hsla, + pub deleted: Hsla, + pub ignored: Hsla, + pub modified: Hsla, + pub renamed: Hsla, +} + +#[derive(Refineable, Clone, Debug, Default)] +#[refineable(debug)] +pub struct ThemeColors { + pub border: Hsla, + pub border_variant: Hsla, + pub border_focused: Hsla, + pub border_transparent: Hsla, + pub elevated_surface: Hsla, + pub surface: Hsla, + pub background: Hsla, + pub element: Hsla, + pub element_hover: Hsla, + pub element_active: Hsla, + pub element_selected: Hsla, + pub element_disabled: Hsla, + pub element_placeholder: Hsla, + pub element_drop_target: Hsla, + pub ghost_element: Hsla, + pub ghost_element_hover: Hsla, + pub ghost_element_active: Hsla, + pub ghost_element_selected: Hsla, + pub ghost_element_disabled: Hsla, + pub text: Hsla, + pub text_muted: Hsla, + pub text_placeholder: Hsla, + pub text_disabled: Hsla, + pub text_accent: Hsla, + pub icon: Hsla, + pub icon_muted: Hsla, + pub icon_disabled: Hsla, + pub icon_placeholder: Hsla, + pub icon_accent: Hsla, + pub status_bar: Hsla, + pub title_bar: Hsla, + pub toolbar: Hsla, + pub tab_bar: Hsla, + pub tab_inactive: Hsla, + pub tab_active: Hsla, + pub editor: Hsla, + pub editor_subheader: Hsla, + pub editor_active_line: Hsla, +} + +#[derive(Refineable, Clone)] +pub struct ThemeStyles { + pub system: SystemColors, + pub colors: ThemeColors, + pub status: StatusColors, + pub git: GitStatusColors, + pub player: PlayerColors, + pub syntax: SyntaxTheme, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn override_a_single_theme_color() { + let mut colors = ThemeColors::default_light(); + + let magenta: Hsla = gpui::rgb(0xff00ff); + + assert_ne!(colors.text, magenta); + + let overrides = ThemeColorsRefinement { + text: Some(magenta), + ..Default::default() + }; + + colors.refine(&overrides); + + assert_eq!(colors.text, magenta); + } + + #[test] + fn override_multiple_theme_colors() { + let mut colors = ThemeColors::default_light(); + + let magenta: Hsla = gpui::rgb(0xff00ff); + let green: Hsla = gpui::rgb(0x00ff00); + + assert_ne!(colors.text, magenta); + assert_ne!(colors.background, green); + + let overrides = ThemeColorsRefinement { + text: Some(magenta), + background: Some(green), + ..Default::default() + }; + + colors.refine(&overrides); + + assert_eq!(colors.text, magenta); + assert_eq!(colors.background, green); + } +} diff --git a/crates/theme2/src/default.rs b/crates/theme2/src/default_colors.rs similarity index 70% rename from crates/theme2/src/default.rs rename to crates/theme2/src/default_colors.rs index 41d408f980..802392d296 100644 --- a/crates/theme2/src/default.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,79 +1,367 @@ -use gpui2::Rgba; -use indexmap::IndexMap; +use std::num::ParseIntError; -use crate::scale::{ColorScaleName, ColorScaleSet, ColorScales}; +use gpui::{hsla, Hsla, Rgba}; -struct DefaultColorScaleSet { - scale: ColorScaleName, - light: [&'static str; 12], - light_alpha: [&'static str; 12], - dark: [&'static str; 12], - dark_alpha: [&'static str; 12], +use crate::{ + colors::{GitStatusColors, PlayerColor, PlayerColors, StatusColors, SystemColors, ThemeColors}, + scale::{ColorScaleSet, ColorScales}, + syntax::SyntaxTheme, + ColorScale, +}; + +fn neutral() -> ColorScaleSet { + slate() } -impl From for ColorScaleSet { - fn from(default: DefaultColorScaleSet) -> Self { - Self::new( - default.scale, - default - .light - .map(|color| Rgba::try_from(color).unwrap().into()), - default - .light_alpha - .map(|color| Rgba::try_from(color).unwrap().into()), - default - .dark - .map(|color| Rgba::try_from(color).unwrap().into()), - default - .dark_alpha - .map(|color| Rgba::try_from(color).unwrap().into()), - ) +impl Default for SystemColors { + fn default() -> Self { + Self { + transparent: hsla(0.0, 0.0, 0.0, 0.0), + mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), + mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), + mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), + } + } +} + +impl Default for StatusColors { + fn default() -> Self { + Self { + conflict: red().dark().step_11(), + created: grass().dark().step_11(), + deleted: red().dark().step_11(), + error: red().dark().step_11(), + hidden: neutral().dark().step_11(), + ignored: neutral().dark().step_11(), + info: blue().dark().step_11(), + modified: yellow().dark().step_11(), + renamed: blue().dark().step_11(), + success: grass().dark().step_11(), + warning: yellow().dark().step_11(), + } + } +} + +impl Default for GitStatusColors { + fn default() -> Self { + Self { + conflict: orange().dark().step_11(), + created: grass().dark().step_11(), + deleted: red().dark().step_11(), + ignored: neutral().dark().step_11(), + modified: yellow().dark().step_11(), + renamed: blue().dark().step_11(), + } + } +} + +impl Default for PlayerColors { + fn default() -> Self { + Self(vec![ + PlayerColor { + cursor: hsla(0.0, 0.0, 0.0, 0.0), + background: hsla(0.0, 0.0, 0.0, 0.0), + selection: hsla(0.0, 0.0, 0.0, 0.0), + }, + PlayerColor { + cursor: hsla(0.0, 0.0, 0.0, 0.0), + background: hsla(0.0, 0.0, 0.0, 0.0), + selection: hsla(0.0, 0.0, 0.0, 0.0), + }, + PlayerColor { + cursor: hsla(0.0, 0.0, 0.0, 0.0), + background: hsla(0.0, 0.0, 0.0, 0.0), + selection: hsla(0.0, 0.0, 0.0, 0.0), + }, + PlayerColor { + cursor: hsla(0.0, 0.0, 0.0, 0.0), + background: hsla(0.0, 0.0, 0.0, 0.0), + selection: hsla(0.0, 0.0, 0.0, 0.0), + }, + ]) + } +} + +impl SyntaxTheme { + pub fn default_light() -> Self { + Self { + highlights: vec![ + ("attribute".into(), cyan().light().step_11().into()), + ("boolean".into(), tomato().light().step_11().into()), + ("comment".into(), neutral().light().step_11().into()), + ("comment.doc".into(), iris().light().step_12().into()), + ("constant".into(), red().light().step_7().into()), + ("constructor".into(), red().light().step_7().into()), + ("embedded".into(), red().light().step_7().into()), + ("emphasis".into(), red().light().step_7().into()), + ("emphasis.strong".into(), red().light().step_7().into()), + ("enum".into(), red().light().step_7().into()), + ("function".into(), red().light().step_7().into()), + ("hint".into(), red().light().step_7().into()), + ("keyword".into(), orange().light().step_11().into()), + ("label".into(), red().light().step_7().into()), + ("link_text".into(), red().light().step_7().into()), + ("link_uri".into(), red().light().step_7().into()), + ("number".into(), red().light().step_7().into()), + ("operator".into(), red().light().step_7().into()), + ("predictive".into(), red().light().step_7().into()), + ("preproc".into(), red().light().step_7().into()), + ("primary".into(), red().light().step_7().into()), + ("property".into(), red().light().step_7().into()), + ("punctuation".into(), neutral().light().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().light().step_11().into(), + ), + ("punctuation.special".into(), red().light().step_7().into()), + ("string".into(), jade().light().step_11().into()), + ("string.escape".into(), red().light().step_7().into()), + ("string.regex".into(), tomato().light().step_11().into()), + ("string.special".into(), red().light().step_7().into()), + ( + "string.special.symbol".into(), + red().light().step_7().into(), + ), + ("tag".into(), red().light().step_7().into()), + ("text.literal".into(), red().light().step_7().into()), + ("title".into(), red().light().step_7().into()), + ("type".into(), red().light().step_7().into()), + ("variable".into(), red().light().step_7().into()), + ("variable.special".into(), red().light().step_7().into()), + ("variant".into(), red().light().step_7().into()), + ], + } + } + + pub fn default_dark() -> Self { + Self { + highlights: vec![ + ("attribute".into(), cyan().dark().step_11().into()), + ("boolean".into(), tomato().dark().step_11().into()), + ("comment".into(), neutral().dark().step_11().into()), + ("comment.doc".into(), iris().dark().step_12().into()), + ("constant".into(), red().dark().step_7().into()), + ("constructor".into(), red().dark().step_7().into()), + ("embedded".into(), red().dark().step_7().into()), + ("emphasis".into(), red().dark().step_7().into()), + ("emphasis.strong".into(), red().dark().step_7().into()), + ("enum".into(), red().dark().step_7().into()), + ("function".into(), red().dark().step_7().into()), + ("hint".into(), red().dark().step_7().into()), + ("keyword".into(), orange().dark().step_11().into()), + ("label".into(), red().dark().step_7().into()), + ("link_text".into(), red().dark().step_7().into()), + ("link_uri".into(), red().dark().step_7().into()), + ("number".into(), red().dark().step_7().into()), + ("operator".into(), red().dark().step_7().into()), + ("predictive".into(), red().dark().step_7().into()), + ("preproc".into(), red().dark().step_7().into()), + ("primary".into(), red().dark().step_7().into()), + ("property".into(), red().dark().step_7().into()), + ("punctuation".into(), neutral().dark().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().dark().step_11().into(), + ), + ("punctuation.special".into(), red().dark().step_7().into()), + ("string".into(), jade().dark().step_11().into()), + ("string.escape".into(), red().dark().step_7().into()), + ("string.regex".into(), tomato().dark().step_11().into()), + ("string.special".into(), red().dark().step_7().into()), + ("string.special.symbol".into(), red().dark().step_7().into()), + ("tag".into(), red().dark().step_7().into()), + ("text.literal".into(), red().dark().step_7().into()), + ("title".into(), red().dark().step_7().into()), + ("type".into(), red().dark().step_7().into()), + ("variable".into(), red().dark().step_7().into()), + ("variable.special".into(), red().dark().step_7().into()), + ("variant".into(), red().dark().step_7().into()), + ], + } + } +} + +impl ThemeColors { + pub fn default_light() -> Self { + let system = SystemColors::default(); + + Self { + border: neutral().light().step_6(), + border_variant: neutral().light().step_5(), + border_focused: blue().light().step_5(), + border_transparent: system.transparent, + elevated_surface: neutral().light().step_2(), + surface: neutral().light().step_2(), + background: neutral().light().step_1(), + element: neutral().light().step_3(), + element_hover: neutral().light().step_4(), + element_active: neutral().light().step_5(), + element_selected: neutral().light().step_5(), + element_disabled: neutral().light_alpha().step_3(), + element_placeholder: neutral().light().step_11(), + element_drop_target: blue().light_alpha().step_2(), + ghost_element: system.transparent, + ghost_element_hover: neutral().light().step_4(), + ghost_element_active: neutral().light().step_5(), + ghost_element_selected: neutral().light().step_5(), + ghost_element_disabled: neutral().light_alpha().step_3(), + text: neutral().light().step_12(), + text_muted: neutral().light().step_11(), + text_placeholder: neutral().light().step_10(), + text_disabled: neutral().light().step_9(), + text_accent: blue().light().step_11(), + icon: neutral().light().step_11(), + icon_muted: neutral().light().step_10(), + icon_disabled: neutral().light().step_9(), + icon_placeholder: neutral().light().step_10(), + icon_accent: blue().light().step_11(), + status_bar: neutral().light().step_2(), + title_bar: neutral().light().step_2(), + toolbar: neutral().light().step_1(), + tab_bar: neutral().light().step_2(), + tab_active: neutral().light().step_1(), + tab_inactive: neutral().light().step_2(), + editor: neutral().light().step_1(), + editor_subheader: neutral().light().step_2(), + editor_active_line: neutral().light_alpha().step_3(), + } + } + + pub fn default_dark() -> Self { + let system = SystemColors::default(); + + Self { + border: neutral().dark().step_6(), + border_variant: neutral().dark().step_5(), + border_focused: blue().dark().step_5(), + border_transparent: system.transparent, + elevated_surface: neutral().dark().step_2(), + surface: neutral().dark().step_2(), + background: neutral().dark().step_1(), + element: neutral().dark().step_3(), + element_hover: neutral().dark().step_4(), + element_active: neutral().dark().step_5(), + element_selected: neutral().dark().step_5(), + element_disabled: neutral().dark_alpha().step_3(), + element_placeholder: neutral().dark().step_11(), + element_drop_target: blue().dark_alpha().step_2(), + ghost_element: system.transparent, + ghost_element_hover: neutral().dark().step_4(), + ghost_element_active: neutral().dark().step_5(), + ghost_element_selected: neutral().dark().step_5(), + ghost_element_disabled: neutral().dark_alpha().step_3(), + text: neutral().dark().step_12(), + text_muted: neutral().dark().step_11(), + text_placeholder: neutral().dark().step_10(), + text_disabled: neutral().dark().step_9(), + text_accent: blue().dark().step_11(), + icon: neutral().dark().step_11(), + icon_muted: neutral().dark().step_10(), + icon_disabled: neutral().dark().step_9(), + icon_placeholder: neutral().dark().step_10(), + icon_accent: blue().dark().step_11(), + status_bar: neutral().dark().step_2(), + title_bar: neutral().dark().step_2(), + toolbar: neutral().dark().step_1(), + tab_bar: neutral().dark().step_2(), + tab_active: neutral().dark().step_1(), + tab_inactive: neutral().dark().step_2(), + editor: neutral().dark().step_1(), + editor_subheader: neutral().dark().step_2(), + editor_active_line: neutral().dark_alpha().step_3(), + } + } +} + +type StaticColorScale = [&'static str; 12]; + +struct StaticColorScaleSet { + scale: &'static str, + light: StaticColorScale, + light_alpha: StaticColorScale, + dark: StaticColorScale, + dark_alpha: StaticColorScale, +} + +impl TryFrom for ColorScaleSet { + type Error = ParseIntError; + + fn try_from(value: StaticColorScaleSet) -> Result { + fn to_color_scale(scale: StaticColorScale) -> Result { + scale + .into_iter() + .map(|color| Rgba::try_from(color).map(Hsla::from)) + .collect::, _>>() + .map(ColorScale::from_iter) + } + + Ok(Self::new( + value.scale, + to_color_scale(value.light)?, + to_color_scale(value.light_alpha)?, + to_color_scale(value.dark)?, + to_color_scale(value.dark_alpha)?, + )) } } pub fn default_color_scales() -> ColorScales { - use ColorScaleName::*; - - IndexMap::from_iter([ - (Gray, gray().into()), - (Mauve, mauve().into()), - (Slate, slate().into()), - (Sage, sage().into()), - (Olive, olive().into()), - (Sand, sand().into()), - (Gold, gold().into()), - (Bronze, bronze().into()), - (Brown, brown().into()), - (Yellow, yellow().into()), - (Amber, amber().into()), - (Orange, orange().into()), - (Tomato, tomato().into()), - (Red, red().into()), - (Ruby, ruby().into()), - (Crimson, crimson().into()), - (Pink, pink().into()), - (Plum, plum().into()), - (Purple, purple().into()), - (Violet, violet().into()), - (Iris, iris().into()), - (Indigo, indigo().into()), - (Blue, blue().into()), - (Cyan, cyan().into()), - (Teal, teal().into()), - (Jade, jade().into()), - (Green, green().into()), - (Grass, grass().into()), - (Lime, lime().into()), - (Mint, mint().into()), - (Sky, sky().into()), - (Black, black().into()), - (White, white().into()), - ]) + ColorScales { + gray: gray(), + mauve: mauve(), + slate: slate(), + sage: sage(), + olive: olive(), + sand: sand(), + gold: gold(), + bronze: bronze(), + brown: brown(), + yellow: yellow(), + amber: amber(), + orange: orange(), + tomato: tomato(), + red: red(), + ruby: ruby(), + crimson: crimson(), + pink: pink(), + plum: plum(), + purple: purple(), + violet: violet(), + iris: iris(), + indigo: indigo(), + blue: blue(), + cyan: cyan(), + teal: teal(), + jade: jade(), + green: green(), + grass: grass(), + lime: lime(), + mint: mint(), + sky: sky(), + black: black(), + white: white(), + } } -fn gray() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Gray, +fn gray() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Gray", light: [ "#fcfcfcff", "#f9f9f9ff", @@ -131,11 +419,13 @@ fn gray() -> DefaultColorScaleSet { "#ffffffed", ], } + .try_into() + .unwrap() } -fn mauve() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Mauve, +fn mauve() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Mauve", light: [ "#fdfcfdff", "#faf9fbff", @@ -193,11 +483,13 @@ fn mauve() -> DefaultColorScaleSet { "#fdfdffef", ], } + .try_into() + .unwrap() } -fn slate() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Slate, +fn slate() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Slate", light: [ "#fcfcfdff", "#f9f9fbff", @@ -255,11 +547,13 @@ fn slate() -> DefaultColorScaleSet { "#fcfdffef", ], } + .try_into() + .unwrap() } -fn sage() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Sage, +fn sage() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Sage", light: [ "#fbfdfcff", "#f7f9f8ff", @@ -317,11 +611,13 @@ fn sage() -> DefaultColorScaleSet { "#fdfffeed", ], } + .try_into() + .unwrap() } -fn olive() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Olive, +fn olive() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Olive", light: [ "#fcfdfcff", "#f8faf8ff", @@ -379,11 +675,13 @@ fn olive() -> DefaultColorScaleSet { "#fdfffded", ], } + .try_into() + .unwrap() } -fn sand() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Sand, +fn sand() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Sand", light: [ "#fdfdfcff", "#f9f9f8ff", @@ -441,11 +739,13 @@ fn sand() -> DefaultColorScaleSet { "#fffffded", ], } + .try_into() + .unwrap() } -fn gold() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Gold, +fn gold() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Gold", light: [ "#fdfdfcff", "#faf9f2ff", @@ -503,11 +803,13 @@ fn gold() -> DefaultColorScaleSet { "#fef7ede7", ], } + .try_into() + .unwrap() } -fn bronze() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Bronze, +fn bronze() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Bronze", light: [ "#fdfcfcff", "#fdf7f5ff", @@ -565,11 +867,13 @@ fn bronze() -> DefaultColorScaleSet { "#fff1e9ec", ], } + .try_into() + .unwrap() } -fn brown() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Brown, +fn brown() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Brown", light: [ "#fefdfcff", "#fcf9f6ff", @@ -627,11 +931,13 @@ fn brown() -> DefaultColorScaleSet { "#feecd4f2", ], } + .try_into() + .unwrap() } -fn yellow() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Yellow, +fn yellow() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Yellow", light: [ "#fdfdf9ff", "#fefce9ff", @@ -689,11 +995,13 @@ fn yellow() -> DefaultColorScaleSet { "#fef6baf6", ], } + .try_into() + .unwrap() } -fn amber() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Amber, +fn amber() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Amber", light: [ "#fefdfbff", "#fefbe9ff", @@ -751,11 +1059,13 @@ fn amber() -> DefaultColorScaleSet { "#ffe7b3ff", ], } + .try_into() + .unwrap() } -fn orange() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Orange, +fn orange() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Orange", light: [ "#fefcfbff", "#fff7edff", @@ -813,11 +1123,13 @@ fn orange() -> DefaultColorScaleSet { "#ffe0c2ff", ], } + .try_into() + .unwrap() } -fn tomato() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Tomato, +fn tomato() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Tomato", light: [ "#fffcfcff", "#fff8f7ff", @@ -875,11 +1187,13 @@ fn tomato() -> DefaultColorScaleSet { "#ffd6cefb", ], } + .try_into() + .unwrap() } -fn red() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Red, +fn red() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Red", light: [ "#fffcfcff", "#fff7f7ff", @@ -937,11 +1251,13 @@ fn red() -> DefaultColorScaleSet { "#ffd1d9ff", ], } + .try_into() + .unwrap() } -fn ruby() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Ruby, +fn ruby() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Ruby", light: [ "#fffcfdff", "#fff7f8ff", @@ -999,11 +1315,13 @@ fn ruby() -> DefaultColorScaleSet { "#ffd3e2fe", ], } + .try_into() + .unwrap() } -fn crimson() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Crimson, +fn crimson() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Crimson", light: [ "#fffcfdff", "#fef7f9ff", @@ -1061,11 +1379,13 @@ fn crimson() -> DefaultColorScaleSet { "#ffd5eafd", ], } + .try_into() + .unwrap() } -fn pink() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Pink, +fn pink() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Pink", light: [ "#fffcfeff", "#fef7fbff", @@ -1123,11 +1443,13 @@ fn pink() -> DefaultColorScaleSet { "#ffd3ecfd", ], } + .try_into() + .unwrap() } -fn plum() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Plum, +fn plum() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Plum", light: [ "#fefcffff", "#fdf7fdff", @@ -1185,11 +1507,13 @@ fn plum() -> DefaultColorScaleSet { "#feddfef4", ], } + .try_into() + .unwrap() } -fn purple() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Purple, +fn purple() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Purple", light: [ "#fefcfeff", "#fbf7feff", @@ -1247,11 +1571,13 @@ fn purple() -> DefaultColorScaleSet { "#f1ddfffa", ], } + .try_into() + .unwrap() } -fn violet() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Violet, +fn violet() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Violet", light: [ "#fdfcfeff", "#faf8ffff", @@ -1309,11 +1635,13 @@ fn violet() -> DefaultColorScaleSet { "#e3defffe", ], } + .try_into() + .unwrap() } -fn iris() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Iris, +fn iris() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Iris", light: [ "#fdfdffff", "#f8f8ffff", @@ -1371,11 +1699,13 @@ fn iris() -> DefaultColorScaleSet { "#e1e0fffe", ], } + .try_into() + .unwrap() } -fn indigo() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Indigo, +fn indigo() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Indigo", light: [ "#fdfdfeff", "#f7f9ffff", @@ -1433,11 +1763,13 @@ fn indigo() -> DefaultColorScaleSet { "#d6e1ffff", ], } + .try_into() + .unwrap() } -fn blue() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Blue, +fn blue() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Blue", light: [ "#fbfdffff", "#f4faffff", @@ -1495,11 +1827,13 @@ fn blue() -> DefaultColorScaleSet { "#c2e6ffff", ], } + .try_into() + .unwrap() } -fn cyan() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Cyan, +fn cyan() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Cyan", light: [ "#fafdfeff", "#f2fafbff", @@ -1557,11 +1891,13 @@ fn cyan() -> DefaultColorScaleSet { "#bbf3fef7", ], } + .try_into() + .unwrap() } -fn teal() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Teal, +fn teal() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Teal", light: [ "#fafefdff", "#f3fbf9ff", @@ -1619,11 +1955,13 @@ fn teal() -> DefaultColorScaleSet { "#b8ffebef", ], } + .try_into() + .unwrap() } -fn jade() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Jade, +fn jade() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Jade", light: [ "#fbfefdff", "#f4fbf7ff", @@ -1681,11 +2019,13 @@ fn jade() -> DefaultColorScaleSet { "#b8ffe1ef", ], } + .try_into() + .unwrap() } -fn green() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Green, +fn green() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Green", light: [ "#fbfefcff", "#f4fbf6ff", @@ -1743,11 +2083,13 @@ fn green() -> DefaultColorScaleSet { "#bbffd7f0", ], } + .try_into() + .unwrap() } -fn grass() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Grass, +fn grass() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Grass", light: [ "#fbfefbff", "#f5fbf5ff", @@ -1805,11 +2147,13 @@ fn grass() -> DefaultColorScaleSet { "#ceffceef", ], } + .try_into() + .unwrap() } -fn lime() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Lime, +fn lime() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Lime", light: [ "#fcfdfaff", "#f8faf3ff", @@ -1867,11 +2211,13 @@ fn lime() -> DefaultColorScaleSet { "#e9febff7", ], } + .try_into() + .unwrap() } -fn mint() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Mint, +fn mint() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Mint", light: [ "#f9fefdff", "#f2fbf9ff", @@ -1929,11 +2275,13 @@ fn mint() -> DefaultColorScaleSet { "#cbfee9f5", ], } + .try_into() + .unwrap() } -fn sky() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Sky, +fn sky() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Sky", light: [ "#f9feffff", "#f1fafdff", @@ -1991,11 +2339,13 @@ fn sky() -> DefaultColorScaleSet { "#c2f3ffff", ], } + .try_into() + .unwrap() } -fn black() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::Black, +fn black() -> ColorScaleSet { + StaticColorScaleSet { + scale: "Black", light: [ "#0000000d", "#0000001a", @@ -2053,11 +2403,13 @@ fn black() -> DefaultColorScaleSet { "#000000f2", ], } + .try_into() + .unwrap() } -fn white() -> DefaultColorScaleSet { - DefaultColorScaleSet { - scale: ColorScaleName::White, +fn white() -> ColorScaleSet { + StaticColorScaleSet { + scale: "White", light: [ "#ffffff0d", "#ffffff1a", @@ -2115,4 +2467,6 @@ fn white() -> DefaultColorScaleSet { "#fffffff2", ], } + .try_into() + .unwrap() } diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs new file mode 100644 index 0000000000..d7360b6f71 --- /dev/null +++ b/crates/theme2/src/default_theme.rs @@ -0,0 +1,58 @@ +use crate::{ + colors::{GitStatusColors, PlayerColors, StatusColors, SystemColors, ThemeColors, ThemeStyles}, + default_color_scales, Appearance, SyntaxTheme, ThemeFamily, ThemeVariant, +}; + +fn zed_pro_daylight() -> ThemeVariant { + ThemeVariant { + id: "zed_pro_daylight".to_string(), + name: "Zed Pro Daylight".into(), + appearance: Appearance::Light, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::default_light(), + status: StatusColors::default(), + git: GitStatusColors::default(), + player: PlayerColors::default(), + syntax: SyntaxTheme::default_light(), + }, + } +} + +pub(crate) fn zed_pro_moonlight() -> ThemeVariant { + ThemeVariant { + id: "zed_pro_moonlight".to_string(), + name: "Zed Pro Moonlight".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::default_dark(), + status: StatusColors::default(), + git: GitStatusColors::default(), + player: PlayerColors::default(), + syntax: SyntaxTheme::default_dark(), + }, + } +} + +pub fn zed_pro_family() -> ThemeFamily { + ThemeFamily { + id: "zed_pro".to_string(), + name: "Zed Pro".into(), + author: "Zed Team".into(), + themes: vec![zed_pro_daylight(), zed_pro_moonlight()], + scales: default_color_scales(), + } +} + +impl Default for ThemeFamily { + fn default() -> Self { + zed_pro_family() + } +} + +impl Default for ThemeVariant { + fn default() -> Self { + zed_pro_daylight() + } +} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index eec82ef5a7..c1bba121e1 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -1,17 +1,22 @@ -use crate::{themes, Theme, ThemeMetadata}; +use crate::{zed_pro_family, ThemeFamily, ThemeVariant}; use anyhow::{anyhow, Result}; -use gpui2::SharedString; +use gpui::SharedString; use std::{collections::HashMap, sync::Arc}; pub struct ThemeRegistry { - themes: HashMap>, + themes: HashMap>, } impl ThemeRegistry { - fn insert_themes(&mut self, themes: impl IntoIterator) { + fn insert_theme_families(&mut self, families: impl IntoIterator) { + for family in families.into_iter() { + self.insert_themes(family.themes); + } + } + + fn insert_themes(&mut self, themes: impl IntoIterator) { for theme in themes.into_iter() { - self.themes - .insert(theme.metadata.name.clone(), Arc::new(theme)); + self.themes.insert(theme.name.clone(), Arc::new(theme)); } } @@ -19,11 +24,11 @@ impl ThemeRegistry { self.themes.keys().cloned() } - pub fn list(&self, _staff: bool) -> impl Iterator + '_ { - self.themes.values().map(|theme| theme.metadata.clone()) + pub fn list(&self, _staff: bool) -> impl Iterator + '_ { + self.themes.values().map(|theme| theme.name.clone()) } - pub fn get(&self, name: &str) -> Result> { + pub fn get(&self, name: &str) -> Result> { self.themes .get(name) .ok_or_else(|| anyhow!("theme not found: {}", name)) @@ -37,47 +42,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_themes([ - themes::andromeda(), - themes::atelier_cave_dark(), - themes::atelier_cave_light(), - themes::atelier_dune_dark(), - themes::atelier_dune_light(), - themes::atelier_estuary_dark(), - themes::atelier_estuary_light(), - themes::atelier_forest_dark(), - themes::atelier_forest_light(), - themes::atelier_heath_dark(), - themes::atelier_heath_light(), - themes::atelier_lakeside_dark(), - themes::atelier_lakeside_light(), - themes::atelier_plateau_dark(), - themes::atelier_plateau_light(), - themes::atelier_savanna_dark(), - themes::atelier_savanna_light(), - themes::atelier_seaside_dark(), - themes::atelier_seaside_light(), - themes::atelier_sulphurpool_dark(), - themes::atelier_sulphurpool_light(), - themes::ayu_dark(), - themes::ayu_light(), - themes::ayu_mirage(), - themes::gruvbox_dark(), - themes::gruvbox_dark_hard(), - themes::gruvbox_dark_soft(), - themes::gruvbox_light(), - themes::gruvbox_light_hard(), - themes::gruvbox_light_soft(), - themes::one_dark(), - themes::one_light(), - themes::rose_pine(), - themes::rose_pine_dawn(), - themes::rose_pine_moon(), - themes::sandcastle(), - themes::solarized_dark(), - themes::solarized_light(), - themes::summercamp(), - ]); + this.insert_theme_families([zed_pro_family()]); this } diff --git a/crates/theme2/src/scale.rs b/crates/theme2/src/scale.rs index 22a607bf07..22d191bd4a 100644 --- a/crates/theme2/src/scale.rs +++ b/crates/theme2/src/scale.rs @@ -1,98 +1,237 @@ -use gpui2::{AppContext, Hsla}; -use indexmap::IndexMap; +use gpui::{AppContext, Hsla, SharedString}; -use crate::{theme, Appearance}; +use crate::{ActiveTheme, Appearance}; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ColorScaleName { - Gray, - Mauve, - Slate, - Sage, - Olive, - Sand, - Gold, - Bronze, - Brown, - Yellow, - Amber, - Orange, - Tomato, - Red, - Ruby, - Crimson, - Pink, - Plum, - Purple, - Violet, - Iris, - Indigo, - Blue, - Cyan, - Teal, - Jade, - Green, - Grass, - Lime, - Mint, - Sky, - Black, - White, +/// A one-based step in a [`ColorScale`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct ColorScaleStep(usize); + +impl ColorScaleStep { + /// The first step in a [`ColorScale`]. + pub const ONE: Self = Self(1); + + /// The second step in a [`ColorScale`]. + pub const TWO: Self = Self(2); + + /// The third step in a [`ColorScale`]. + pub const THREE: Self = Self(3); + + /// The fourth step in a [`ColorScale`]. + pub const FOUR: Self = Self(4); + + /// The fifth step in a [`ColorScale`]. + pub const FIVE: Self = Self(5); + + /// The sixth step in a [`ColorScale`]. + pub const SIX: Self = Self(6); + + /// The seventh step in a [`ColorScale`]. + pub const SEVEN: Self = Self(7); + + /// The eighth step in a [`ColorScale`]. + pub const EIGHT: Self = Self(8); + + /// The ninth step in a [`ColorScale`]. + pub const NINE: Self = Self(9); + + /// The tenth step in a [`ColorScale`]. + pub const TEN: Self = Self(10); + + /// The eleventh step in a [`ColorScale`]. + pub const ELEVEN: Self = Self(11); + + /// The twelfth step in a [`ColorScale`]. + pub const TWELVE: Self = Self(12); + + /// All of the steps in a [`ColorScale`]. + pub const ALL: [ColorScaleStep; 12] = [ + Self::ONE, + Self::TWO, + Self::THREE, + Self::FOUR, + Self::FIVE, + Self::SIX, + Self::SEVEN, + Self::EIGHT, + Self::NINE, + Self::TEN, + Self::ELEVEN, + Self::TWELVE, + ]; } -impl std::fmt::Display for ColorScaleName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Gray => "Gray", - Self::Mauve => "Mauve", - Self::Slate => "Slate", - Self::Sage => "Sage", - Self::Olive => "Olive", - Self::Sand => "Sand", - Self::Gold => "Gold", - Self::Bronze => "Bronze", - Self::Brown => "Brown", - Self::Yellow => "Yellow", - Self::Amber => "Amber", - Self::Orange => "Orange", - Self::Tomato => "Tomato", - Self::Red => "Red", - Self::Ruby => "Ruby", - Self::Crimson => "Crimson", - Self::Pink => "Pink", - Self::Plum => "Plum", - Self::Purple => "Purple", - Self::Violet => "Violet", - Self::Iris => "Iris", - Self::Indigo => "Indigo", - Self::Blue => "Blue", - Self::Cyan => "Cyan", - Self::Teal => "Teal", - Self::Jade => "Jade", - Self::Green => "Green", - Self::Grass => "Grass", - Self::Lime => "Lime", - Self::Mint => "Mint", - Self::Sky => "Sky", - Self::Black => "Black", - Self::White => "White", - } - ) +pub struct ColorScale(Vec); + +impl FromIterator for ColorScale { + fn from_iter>(iter: T) -> Self { + Self(Vec::from_iter(iter)) } } -pub type ColorScale = [Hsla; 12]; +impl ColorScale { + /// Returns the specified step in the [`ColorScale`]. + #[inline] + pub fn step(&self, step: ColorScaleStep) -> Hsla { + // Steps are one-based, so we need convert to the zero-based vec index. + self.0[step.0 - 1] + } -pub type ColorScales = IndexMap; + /// Returns the first step in the [`ColorScale`]. + #[inline] + pub fn step_1(&self) -> Hsla { + self.step(ColorScaleStep::ONE) + } -/// A one-based step in a [`ColorScale`]. -pub type ColorScaleStep = usize; + /// Returns the second step in the [`ColorScale`]. + #[inline] + pub fn step_2(&self) -> Hsla { + self.step(ColorScaleStep::TWO) + } + + /// Returns the third step in the [`ColorScale`]. + #[inline] + pub fn step_3(&self) -> Hsla { + self.step(ColorScaleStep::THREE) + } + + /// Returns the fourth step in the [`ColorScale`]. + #[inline] + pub fn step_4(&self) -> Hsla { + self.step(ColorScaleStep::FOUR) + } + + /// Returns the fifth step in the [`ColorScale`]. + #[inline] + pub fn step_5(&self) -> Hsla { + self.step(ColorScaleStep::FIVE) + } + + /// Returns the sixth step in the [`ColorScale`]. + #[inline] + pub fn step_6(&self) -> Hsla { + self.step(ColorScaleStep::SIX) + } + + /// Returns the seventh step in the [`ColorScale`]. + #[inline] + pub fn step_7(&self) -> Hsla { + self.step(ColorScaleStep::SEVEN) + } + + /// Returns the eighth step in the [`ColorScale`]. + #[inline] + pub fn step_8(&self) -> Hsla { + self.step(ColorScaleStep::EIGHT) + } + + /// Returns the ninth step in the [`ColorScale`]. + #[inline] + pub fn step_9(&self) -> Hsla { + self.step(ColorScaleStep::NINE) + } + + /// Returns the tenth step in the [`ColorScale`]. + #[inline] + pub fn step_10(&self) -> Hsla { + self.step(ColorScaleStep::TEN) + } + + /// Returns the eleventh step in the [`ColorScale`]. + #[inline] + pub fn step_11(&self) -> Hsla { + self.step(ColorScaleStep::ELEVEN) + } + + /// Returns the twelfth step in the [`ColorScale`]. + #[inline] + pub fn step_12(&self) -> Hsla { + self.step(ColorScaleStep::TWELVE) + } +} + +pub struct ColorScales { + pub gray: ColorScaleSet, + pub mauve: ColorScaleSet, + pub slate: ColorScaleSet, + pub sage: ColorScaleSet, + pub olive: ColorScaleSet, + pub sand: ColorScaleSet, + pub gold: ColorScaleSet, + pub bronze: ColorScaleSet, + pub brown: ColorScaleSet, + pub yellow: ColorScaleSet, + pub amber: ColorScaleSet, + pub orange: ColorScaleSet, + pub tomato: ColorScaleSet, + pub red: ColorScaleSet, + pub ruby: ColorScaleSet, + pub crimson: ColorScaleSet, + pub pink: ColorScaleSet, + pub plum: ColorScaleSet, + pub purple: ColorScaleSet, + pub violet: ColorScaleSet, + pub iris: ColorScaleSet, + pub indigo: ColorScaleSet, + pub blue: ColorScaleSet, + pub cyan: ColorScaleSet, + pub teal: ColorScaleSet, + pub jade: ColorScaleSet, + pub green: ColorScaleSet, + pub grass: ColorScaleSet, + pub lime: ColorScaleSet, + pub mint: ColorScaleSet, + pub sky: ColorScaleSet, + pub black: ColorScaleSet, + pub white: ColorScaleSet, +} + +impl IntoIterator for ColorScales { + type Item = ColorScaleSet; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![ + self.gray, + self.mauve, + self.slate, + self.sage, + self.olive, + self.sand, + self.gold, + self.bronze, + self.brown, + self.yellow, + self.amber, + self.orange, + self.tomato, + self.red, + self.ruby, + self.crimson, + self.pink, + self.plum, + self.purple, + self.violet, + self.iris, + self.indigo, + self.blue, + self.cyan, + self.teal, + self.jade, + self.green, + self.grass, + self.lime, + self.mint, + self.sky, + self.black, + self.white, + ] + .into_iter() + } +} pub struct ColorScaleSet { - name: ColorScaleName, + name: SharedString, light: ColorScale, dark: ColorScale, light_alpha: ColorScale, @@ -101,14 +240,14 @@ pub struct ColorScaleSet { impl ColorScaleSet { pub fn new( - name: ColorScaleName, + name: impl Into, light: ColorScale, light_alpha: ColorScale, dark: ColorScale, dark_alpha: ColorScale, ) -> Self { Self { - name, + name: name.into(), light, light_alpha, dark, @@ -116,49 +255,37 @@ impl ColorScaleSet { } } - pub fn name(&self) -> String { - self.name.to_string() + pub fn name(&self) -> &SharedString { + &self.name } - pub fn light(&self, step: ColorScaleStep) -> Hsla { - self.light[step - 1] + pub fn light(&self) -> &ColorScale { + &self.light } - pub fn light_alpha(&self, step: ColorScaleStep) -> Hsla { - self.light_alpha[step - 1] + pub fn light_alpha(&self) -> &ColorScale { + &self.light_alpha } - pub fn dark(&self, step: ColorScaleStep) -> Hsla { - self.dark[step - 1] + pub fn dark(&self) -> &ColorScale { + &self.dark } - pub fn dark_alpha(&self, step: ColorScaleStep) -> Hsla { - self.dark_alpha[step - 1] - } - - fn current_appearance(cx: &AppContext) -> Appearance { - let theme = theme(cx); - if theme.metadata.is_light { - Appearance::Light - } else { - Appearance::Dark - } + pub fn dark_alpha(&self) -> &ColorScale { + &self.dark_alpha } pub fn step(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla { - let appearance = Self::current_appearance(cx); - - match appearance { - Appearance::Light => self.light(step), - Appearance::Dark => self.dark(step), + match cx.theme().appearance { + Appearance::Light => self.light().step(step), + Appearance::Dark => self.dark().step(step), } } pub fn step_alpha(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla { - let appearance = Self::current_appearance(cx); - match appearance { - Appearance::Light => self.light_alpha(step), - Appearance::Dark => self.dark_alpha(step), + match cx.theme().appearance { + Appearance::Light => self.light_alpha.step(step), + Appearance::Dark => self.dark_alpha.step(step), } } } diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 379b01dd4b..5e8f9de873 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -1,6 +1,6 @@ -use crate::{Theme, ThemeRegistry}; +use crate::{ThemeRegistry, ThemeVariant}; use anyhow::Result; -use gpui2::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; +use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; use schemars::{ gen::SchemaGenerator, schema::{InstanceType, Schema, SchemaObject}, @@ -8,7 +8,7 @@ use schemars::{ }; use serde::{Deserialize, Serialize}; use serde_json::Value; -use settings2::{Settings, SettingsJsonSchemaParams}; +use settings::{Settings, SettingsJsonSchemaParams}; use std::sync::Arc; use util::ResultExt as _; @@ -17,10 +17,11 @@ const MIN_LINE_HEIGHT: f32 = 1.0; #[derive(Clone)] pub struct ThemeSettings { + pub ui_font_size: Pixels, pub buffer_font: Font, pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, - pub active_theme: Arc, + pub active_theme: Arc, } #[derive(Default)] @@ -28,6 +29,8 @@ pub struct AdjustedBufferFontSize(Option); #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { + #[serde(default)] + pub ui_font_size: Option, #[serde(default)] pub buffer_font_family: Option, #[serde(default)] @@ -102,7 +105,7 @@ pub fn reset_font_size(cx: &mut AppContext) { } } -impl settings2::Settings for ThemeSettings { +impl settings::Settings for ThemeSettings { const KEY: Option<&'static str> = None; type FileContent = ThemeSettingsContent; @@ -115,6 +118,7 @@ impl settings2::Settings for ThemeSettings { let themes = cx.default_global::>(); let mut this = Self { + ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), features: defaults.buffer_font_features.clone().unwrap(), @@ -123,7 +127,10 @@ impl settings2::Settings for ThemeSettings { }, buffer_font_size: defaults.buffer_font_size.unwrap().into(), buffer_line_height: defaults.buffer_line_height.unwrap(), - active_theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(), + active_theme: themes + .get(defaults.theme.as_ref().unwrap()) + .or(themes.get("Zed Pro Moonlight")) + .unwrap(), }; for value in user_values.into_iter().copied().cloned() { @@ -140,6 +147,7 @@ impl settings2::Settings for ThemeSettings { } } + merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); merge( &mut this.buffer_font_size, value.buffer_font_size.map(Into::into), diff --git a/crates/theme2/src/syntax.rs b/crates/theme2/src/syntax.rs new file mode 100644 index 0000000000..3a068349fb --- /dev/null +++ b/crates/theme2/src/syntax.rs @@ -0,0 +1,37 @@ +use gpui::{HighlightStyle, Hsla}; + +#[derive(Clone, Default)] +pub struct SyntaxTheme { + pub highlights: Vec<(String, HighlightStyle)>, +} + +impl SyntaxTheme { + // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? + pub fn new_test(colors: impl IntoIterator) -> Self { + SyntaxTheme { + highlights: colors + .into_iter() + .map(|(key, color)| { + ( + key.to_owned(), + HighlightStyle { + color: Some(color), + ..Default::default() + }, + ) + }) + .collect(), + } + } + + pub fn get(&self, name: &str) -> HighlightStyle { + self.highlights + .iter() + .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) + .unwrap_or_default() + } + + pub fn color(&self, name: &str) -> Hsla { + self.get(name).color.unwrap_or_default() + } +} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index b96a23c338..faf252e2e5 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,17 +1,21 @@ -mod default; +mod colors; +mod default_colors; +mod default_theme; mod registry; mod scale; mod settings; -mod themes; +mod syntax; -pub use default::*; +use ::settings::Settings; +pub use colors::*; +pub use default_colors::*; +pub use default_theme::*; pub use registry::*; pub use scale::*; pub use settings::*; +pub use syntax::*; -use gpui2::{AppContext, HighlightStyle, Hsla, SharedString}; -use settings2::Settings; -use std::sync::Arc; +use gpui::{AppContext, Hsla, SharedString}; #[derive(Debug, Clone, PartialEq)] pub enum Appearance { @@ -24,132 +28,63 @@ pub fn init(cx: &mut AppContext) { ThemeSettings::register(cx); } -pub fn active_theme<'a>(cx: &'a AppContext) -> &'a Arc { - &ThemeSettings::get_global(cx).active_theme +pub trait ActiveTheme { + fn theme(&self) -> &ThemeVariant; } -pub fn theme(cx: &AppContext) -> Arc { - active_theme(cx).clone() -} - -pub struct Theme { - pub metadata: ThemeMetadata, - - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, - pub border: Hsla, - pub border_variant: Hsla, - pub border_focused: Hsla, - pub border_transparent: Hsla, - /// The background color of an elevated surface, like a modal, tooltip or toast. - pub elevated_surface: Hsla, - pub surface: Hsla, - /// Window background color of the base app - pub background: Hsla, - /// Default background for elements like filled buttons, - /// text fields, checkboxes, radio buttons, etc. - /// - TODO: Map to step 3. - pub filled_element: Hsla, - /// The background color of a hovered element, like a button being hovered - /// with a mouse, or hovered on a touch screen. - /// - TODO: Map to step 4. - pub filled_element_hover: Hsla, - /// The background color of an active element, like a button being pressed, - /// or tapped on a touch screen. - /// - TODO: Map to step 5. - pub filled_element_active: Hsla, - /// The background color of a selected element, like a selected tab, - /// a button toggled on, or a checkbox that is checked. - pub filled_element_selected: Hsla, - pub filled_element_disabled: Hsla, - pub ghost_element: Hsla, - /// The background color of a hovered element with no default background, - /// like a ghost-style button or an interactable list item. - /// - TODO: Map to step 3. - pub ghost_element_hover: Hsla, - /// - TODO: Map to step 4. - pub ghost_element_active: Hsla, - pub ghost_element_selected: Hsla, - pub ghost_element_disabled: Hsla, - pub text: Hsla, - pub text_muted: Hsla, - pub text_placeholder: Hsla, - pub text_disabled: Hsla, - pub text_accent: Hsla, - pub icon_muted: Hsla, - pub syntax: SyntaxTheme, - - pub status_bar: Hsla, - pub title_bar: Hsla, - pub toolbar: Hsla, - pub tab_bar: Hsla, - /// The background of the editor - pub editor: Hsla, - pub editor_subheader: Hsla, - pub editor_active_line: Hsla, - pub terminal: Hsla, - pub image_fallback_background: Hsla, - - pub git_created: Hsla, - pub git_modified: Hsla, - pub git_deleted: Hsla, - pub git_conflict: Hsla, - pub git_ignored: Hsla, - pub git_renamed: Hsla, - - pub players: [PlayerTheme; 8], -} - -#[derive(Clone)] -pub struct SyntaxTheme { - pub highlights: Vec<(String, HighlightStyle)>, -} - -impl SyntaxTheme { - // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? - pub fn new_test(colors: impl IntoIterator) -> Self { - SyntaxTheme { - highlights: colors - .into_iter() - .map(|(key, color)| { - ( - key.to_owned(), - HighlightStyle { - color: Some(color), - ..Default::default() - }, - ) - }) - .collect(), - } - } - - pub fn get(&self, name: &str) -> HighlightStyle { - self.highlights - .iter() - .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) - .unwrap_or_default() - } - - pub fn color(&self, name: &str) -> Hsla { - self.get(name).color.unwrap_or_default() +impl ActiveTheme for AppContext { + fn theme(&self) -> &ThemeVariant { + &ThemeSettings::get_global(self).active_theme } } -#[derive(Clone, Copy)] -pub struct PlayerTheme { - pub cursor: Hsla, - pub selection: Hsla, -} - -#[derive(Clone)] -pub struct ThemeMetadata { +pub struct ThemeFamily { + #[allow(dead_code)] + pub(crate) id: String, pub name: SharedString, - pub is_light: bool, + pub author: SharedString, + pub themes: Vec, + pub scales: ColorScales, } -pub struct Editor { - pub syntax: Arc, +impl ThemeFamily {} + +pub struct ThemeVariant { + #[allow(dead_code)] + pub(crate) id: String, + pub name: SharedString, + pub appearance: Appearance, + pub styles: ThemeStyles, +} + +impl ThemeVariant { + /// Returns the [`ThemeColors`] for the theme. + #[inline(always)] + pub fn colors(&self) -> &ThemeColors { + &self.styles.colors + } + + /// Returns the [`SyntaxTheme`] for the theme. + #[inline(always)] + pub fn syntax(&self) -> &SyntaxTheme { + &self.styles.syntax + } + + /// Returns the [`StatusColors`] for the theme. + #[inline(always)] + pub fn status(&self) -> &StatusColors { + &self.styles.status + } + + /// Returns the [`GitStatusColors`] for the theme. + #[inline(always)] + pub fn git(&self) -> &GitStatusColors { + &self.styles.git + } + + /// Returns the color for the syntax node with the given name. + #[inline(always)] + pub fn syntax_color(&self, name: &str) -> Hsla { + self.syntax().color(name) + } } diff --git a/crates/theme2/src/themes/andromeda.rs b/crates/theme2/src/themes/andromeda.rs deleted file mode 100644 index 6afd7edd4d..0000000000 --- a/crates/theme2/src/themes/andromeda.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn andromeda() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Andromeda".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x2b2f38ff).into(), - border_variant: rgba(0x2b2f38ff).into(), - border_focused: rgba(0x183934ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x262933ff).into(), - surface: rgba(0x21242bff).into(), - background: rgba(0x262933ff).into(), - filled_element: rgba(0x262933ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x12231fff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x12231fff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf7f7f8ff).into(), - text_muted: rgba(0xaca8aeff).into(), - text_placeholder: rgba(0xf82871ff).into(), - text_disabled: rgba(0x6b6b73ff).into(), - text_accent: rgba(0x10a793ff).into(), - icon_muted: rgba(0xaca8aeff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("emphasis".into(), rgba(0x10a793ff).into()), - ("punctuation.bracket".into(), rgba(0xd8d5dbff).into()), - ("attribute".into(), rgba(0x10a793ff).into()), - ("variable".into(), rgba(0xf7f7f8ff).into()), - ("predictive".into(), rgba(0x315f70ff).into()), - ("property".into(), rgba(0x10a793ff).into()), - ("variant".into(), rgba(0x10a793ff).into()), - ("embedded".into(), rgba(0xf7f7f8ff).into()), - ("string.special".into(), rgba(0xf29c14ff).into()), - ("keyword".into(), rgba(0x10a793ff).into()), - ("tag".into(), rgba(0x10a793ff).into()), - ("enum".into(), rgba(0xf29c14ff).into()), - ("link_text".into(), rgba(0xf29c14ff).into()), - ("primary".into(), rgba(0xf7f7f8ff).into()), - ("punctuation".into(), rgba(0xd8d5dbff).into()), - ("punctuation.special".into(), rgba(0xd8d5dbff).into()), - ("function".into(), rgba(0xfee56cff).into()), - ("number".into(), rgba(0x96df71ff).into()), - ("preproc".into(), rgba(0xf7f7f8ff).into()), - ("operator".into(), rgba(0xf29c14ff).into()), - ("constructor".into(), rgba(0x10a793ff).into()), - ("string.escape".into(), rgba(0xafabb1ff).into()), - ("string.special.symbol".into(), rgba(0xf29c14ff).into()), - ("string".into(), rgba(0xf29c14ff).into()), - ("comment".into(), rgba(0xafabb1ff).into()), - ("hint".into(), rgba(0x618399ff).into()), - ("type".into(), rgba(0x08e7c5ff).into()), - ("label".into(), rgba(0x10a793ff).into()), - ("comment.doc".into(), rgba(0xafabb1ff).into()), - ("text.literal".into(), rgba(0xf29c14ff).into()), - ("constant".into(), rgba(0x96df71ff).into()), - ("string.regex".into(), rgba(0xf29c14ff).into()), - ("emphasis.strong".into(), rgba(0x10a793ff).into()), - ("title".into(), rgba(0xf7f7f8ff).into()), - ("punctuation.delimiter".into(), rgba(0xd8d5dbff).into()), - ("link_uri".into(), rgba(0x96df71ff).into()), - ("boolean".into(), rgba(0x96df71ff).into()), - ("punctuation.list_marker".into(), rgba(0xd8d5dbff).into()), - ], - }, - status_bar: rgba(0x262933ff).into(), - title_bar: rgba(0x262933ff).into(), - toolbar: rgba(0x1e2025ff).into(), - tab_bar: rgba(0x21242bff).into(), - editor: rgba(0x1e2025ff).into(), - editor_subheader: rgba(0x21242bff).into(), - editor_active_line: rgba(0x21242bff).into(), - terminal: rgba(0x1e2025ff).into(), - image_fallback_background: rgba(0x262933ff).into(), - git_created: rgba(0x96df71ff).into(), - git_modified: rgba(0x10a793ff).into(), - git_deleted: rgba(0xf82871ff).into(), - git_conflict: rgba(0xfee56cff).into(), - git_ignored: rgba(0x6b6b73ff).into(), - git_renamed: rgba(0xfee56cff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x10a793ff).into(), - selection: rgba(0x10a7933d).into(), - }, - PlayerTheme { - cursor: rgba(0x96df71ff).into(), - selection: rgba(0x96df713d).into(), - }, - PlayerTheme { - cursor: rgba(0xc74cecff).into(), - selection: rgba(0xc74cec3d).into(), - }, - PlayerTheme { - cursor: rgba(0xf29c14ff).into(), - selection: rgba(0xf29c143d).into(), - }, - PlayerTheme { - cursor: rgba(0x893ea6ff).into(), - selection: rgba(0x893ea63d).into(), - }, - PlayerTheme { - cursor: rgba(0x08e7c5ff).into(), - selection: rgba(0x08e7c53d).into(), - }, - PlayerTheme { - cursor: rgba(0xf82871ff).into(), - selection: rgba(0xf828713d).into(), - }, - PlayerTheme { - cursor: rgba(0xfee56cff).into(), - selection: rgba(0xfee56c3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_cave_dark.rs b/crates/theme2/src/themes/atelier_cave_dark.rs deleted file mode 100644 index c5190f4e98..0000000000 --- a/crates/theme2/src/themes/atelier_cave_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_cave_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Cave Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x56505eff).into(), - border_variant: rgba(0x56505eff).into(), - border_focused: rgba(0x222953ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x3a353fff).into(), - surface: rgba(0x221f26ff).into(), - background: rgba(0x3a353fff).into(), - filled_element: rgba(0x3a353fff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x161a35ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x161a35ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xefecf4ff).into(), - text_muted: rgba(0x898591ff).into(), - text_placeholder: rgba(0xbe4677ff).into(), - text_disabled: rgba(0x756f7eff).into(), - text_accent: rgba(0x566ddaff).into(), - icon_muted: rgba(0x898591ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("comment.doc".into(), rgba(0x8b8792ff).into()), - ("tag".into(), rgba(0x566ddaff).into()), - ("link_text".into(), rgba(0xaa563bff).into()), - ("constructor".into(), rgba(0x566ddaff).into()), - ("punctuation".into(), rgba(0xe2dfe7ff).into()), - ("punctuation.special".into(), rgba(0xbf3fbfff).into()), - ("string.special.symbol".into(), rgba(0x299292ff).into()), - ("string.escape".into(), rgba(0x8b8792ff).into()), - ("emphasis".into(), rgba(0x566ddaff).into()), - ("type".into(), rgba(0xa06d3aff).into()), - ("punctuation.delimiter".into(), rgba(0x8b8792ff).into()), - ("variant".into(), rgba(0xa06d3aff).into()), - ("variable.special".into(), rgba(0x9559e7ff).into()), - ("text.literal".into(), rgba(0xaa563bff).into()), - ("punctuation.list_marker".into(), rgba(0xe2dfe7ff).into()), - ("comment".into(), rgba(0x655f6dff).into()), - ("function.method".into(), rgba(0x576cdbff).into()), - ("property".into(), rgba(0xbe4677ff).into()), - ("operator".into(), rgba(0x8b8792ff).into()), - ("emphasis.strong".into(), rgba(0x566ddaff).into()), - ("label".into(), rgba(0x566ddaff).into()), - ("enum".into(), rgba(0xaa563bff).into()), - ("number".into(), rgba(0xaa563bff).into()), - ("primary".into(), rgba(0xe2dfe7ff).into()), - ("keyword".into(), rgba(0x9559e7ff).into()), - ( - "function.special.definition".into(), - rgba(0xa06d3aff).into(), - ), - ("punctuation.bracket".into(), rgba(0x8b8792ff).into()), - ("constant".into(), rgba(0x2b9292ff).into()), - ("string.special".into(), rgba(0xbf3fbfff).into()), - ("title".into(), rgba(0xefecf4ff).into()), - ("preproc".into(), rgba(0xefecf4ff).into()), - ("link_uri".into(), rgba(0x2b9292ff).into()), - ("string".into(), rgba(0x299292ff).into()), - ("embedded".into(), rgba(0xefecf4ff).into()), - ("hint".into(), rgba(0x706897ff).into()), - ("boolean".into(), rgba(0x2b9292ff).into()), - ("variable".into(), rgba(0xe2dfe7ff).into()), - ("predictive".into(), rgba(0x615787ff).into()), - ("string.regex".into(), rgba(0x388bc6ff).into()), - ("function".into(), rgba(0x576cdbff).into()), - ("attribute".into(), rgba(0x566ddaff).into()), - ], - }, - status_bar: rgba(0x3a353fff).into(), - title_bar: rgba(0x3a353fff).into(), - toolbar: rgba(0x19171cff).into(), - tab_bar: rgba(0x221f26ff).into(), - editor: rgba(0x19171cff).into(), - editor_subheader: rgba(0x221f26ff).into(), - editor_active_line: rgba(0x221f26ff).into(), - terminal: rgba(0x19171cff).into(), - image_fallback_background: rgba(0x3a353fff).into(), - git_created: rgba(0x2b9292ff).into(), - git_modified: rgba(0x566ddaff).into(), - git_deleted: rgba(0xbe4677ff).into(), - git_conflict: rgba(0xa06d3aff).into(), - git_ignored: rgba(0x756f7eff).into(), - git_renamed: rgba(0xa06d3aff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x566ddaff).into(), - selection: rgba(0x566dda3d).into(), - }, - PlayerTheme { - cursor: rgba(0x2b9292ff).into(), - selection: rgba(0x2b92923d).into(), - }, - PlayerTheme { - cursor: rgba(0xbf41bfff).into(), - selection: rgba(0xbf41bf3d).into(), - }, - PlayerTheme { - cursor: rgba(0xaa563bff).into(), - selection: rgba(0xaa563b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x955ae6ff).into(), - selection: rgba(0x955ae63d).into(), - }, - PlayerTheme { - cursor: rgba(0x3a8bc6ff).into(), - selection: rgba(0x3a8bc63d).into(), - }, - PlayerTheme { - cursor: rgba(0xbe4677ff).into(), - selection: rgba(0xbe46773d).into(), - }, - PlayerTheme { - cursor: rgba(0xa06d3aff).into(), - selection: rgba(0xa06d3a3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_cave_light.rs b/crates/theme2/src/themes/atelier_cave_light.rs deleted file mode 100644 index ae2e912f14..0000000000 --- a/crates/theme2/src/themes/atelier_cave_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_cave_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Cave Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x8f8b96ff).into(), - border_variant: rgba(0x8f8b96ff).into(), - border_focused: rgba(0xc8c7f2ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xbfbcc5ff).into(), - surface: rgba(0xe6e3ebff).into(), - background: rgba(0xbfbcc5ff).into(), - filled_element: rgba(0xbfbcc5ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe1e0f9ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe1e0f9ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x19171cff).into(), - text_muted: rgba(0x5a5462ff).into(), - text_placeholder: rgba(0xbd4677ff).into(), - text_disabled: rgba(0x6e6876ff).into(), - text_accent: rgba(0x586cdaff).into(), - icon_muted: rgba(0x5a5462ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("link_text".into(), rgba(0xaa573cff).into()), - ("string".into(), rgba(0x299292ff).into()), - ("emphasis".into(), rgba(0x586cdaff).into()), - ("label".into(), rgba(0x586cdaff).into()), - ("property".into(), rgba(0xbe4677ff).into()), - ("emphasis.strong".into(), rgba(0x586cdaff).into()), - ("constant".into(), rgba(0x2b9292ff).into()), - ( - "function.special.definition".into(), - rgba(0xa06d3aff).into(), - ), - ("embedded".into(), rgba(0x19171cff).into()), - ("punctuation.special".into(), rgba(0xbf3fbfff).into()), - ("function".into(), rgba(0x576cdbff).into()), - ("tag".into(), rgba(0x586cdaff).into()), - ("number".into(), rgba(0xaa563bff).into()), - ("primary".into(), rgba(0x26232aff).into()), - ("text.literal".into(), rgba(0xaa573cff).into()), - ("variant".into(), rgba(0xa06d3aff).into()), - ("type".into(), rgba(0xa06d3aff).into()), - ("punctuation".into(), rgba(0x26232aff).into()), - ("string.escape".into(), rgba(0x585260ff).into()), - ("keyword".into(), rgba(0x9559e7ff).into()), - ("title".into(), rgba(0x19171cff).into()), - ("constructor".into(), rgba(0x586cdaff).into()), - ("punctuation.list_marker".into(), rgba(0x26232aff).into()), - ("string.special".into(), rgba(0xbf3fbfff).into()), - ("operator".into(), rgba(0x585260ff).into()), - ("function.method".into(), rgba(0x576cdbff).into()), - ("link_uri".into(), rgba(0x2b9292ff).into()), - ("variable.special".into(), rgba(0x9559e7ff).into()), - ("hint".into(), rgba(0x776d9dff).into()), - ("punctuation.bracket".into(), rgba(0x585260ff).into()), - ("string.special.symbol".into(), rgba(0x299292ff).into()), - ("predictive".into(), rgba(0x887fafff).into()), - ("attribute".into(), rgba(0x586cdaff).into()), - ("enum".into(), rgba(0xaa573cff).into()), - ("preproc".into(), rgba(0x19171cff).into()), - ("boolean".into(), rgba(0x2b9292ff).into()), - ("variable".into(), rgba(0x26232aff).into()), - ("comment.doc".into(), rgba(0x585260ff).into()), - ("string.regex".into(), rgba(0x388bc6ff).into()), - ("punctuation.delimiter".into(), rgba(0x585260ff).into()), - ("comment".into(), rgba(0x7d7787ff).into()), - ], - }, - status_bar: rgba(0xbfbcc5ff).into(), - title_bar: rgba(0xbfbcc5ff).into(), - toolbar: rgba(0xefecf4ff).into(), - tab_bar: rgba(0xe6e3ebff).into(), - editor: rgba(0xefecf4ff).into(), - editor_subheader: rgba(0xe6e3ebff).into(), - editor_active_line: rgba(0xe6e3ebff).into(), - terminal: rgba(0xefecf4ff).into(), - image_fallback_background: rgba(0xbfbcc5ff).into(), - git_created: rgba(0x2b9292ff).into(), - git_modified: rgba(0x586cdaff).into(), - git_deleted: rgba(0xbd4677ff).into(), - git_conflict: rgba(0xa06e3bff).into(), - git_ignored: rgba(0x6e6876ff).into(), - git_renamed: rgba(0xa06e3bff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x586cdaff).into(), - selection: rgba(0x586cda3d).into(), - }, - PlayerTheme { - cursor: rgba(0x2b9292ff).into(), - selection: rgba(0x2b92923d).into(), - }, - PlayerTheme { - cursor: rgba(0xbf41bfff).into(), - selection: rgba(0xbf41bf3d).into(), - }, - PlayerTheme { - cursor: rgba(0xaa573cff).into(), - selection: rgba(0xaa573c3d).into(), - }, - PlayerTheme { - cursor: rgba(0x955ae6ff).into(), - selection: rgba(0x955ae63d).into(), - }, - PlayerTheme { - cursor: rgba(0x3a8bc6ff).into(), - selection: rgba(0x3a8bc63d).into(), - }, - PlayerTheme { - cursor: rgba(0xbd4677ff).into(), - selection: rgba(0xbd46773d).into(), - }, - PlayerTheme { - cursor: rgba(0xa06e3bff).into(), - selection: rgba(0xa06e3b3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_dune_dark.rs b/crates/theme2/src/themes/atelier_dune_dark.rs deleted file mode 100644 index 03d0c5eea0..0000000000 --- a/crates/theme2/src/themes/atelier_dune_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_dune_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Dune Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x6c695cff).into(), - border_variant: rgba(0x6c695cff).into(), - border_focused: rgba(0x262f56ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x45433bff).into(), - surface: rgba(0x262622ff).into(), - background: rgba(0x45433bff).into(), - filled_element: rgba(0x45433bff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x171e38ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x171e38ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfefbecff).into(), - text_muted: rgba(0xa4a08bff).into(), - text_placeholder: rgba(0xd73837ff).into(), - text_disabled: rgba(0x8f8b77ff).into(), - text_accent: rgba(0x6684e0ff).into(), - icon_muted: rgba(0xa4a08bff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("constructor".into(), rgba(0x6684e0ff).into()), - ("punctuation".into(), rgba(0xe8e4cfff).into()), - ("punctuation.delimiter".into(), rgba(0xa6a28cff).into()), - ("string.special".into(), rgba(0xd43451ff).into()), - ("string.escape".into(), rgba(0xa6a28cff).into()), - ("comment".into(), rgba(0x7d7a68ff).into()), - ("enum".into(), rgba(0xb65611ff).into()), - ("variable.special".into(), rgba(0xb854d4ff).into()), - ("primary".into(), rgba(0xe8e4cfff).into()), - ("comment.doc".into(), rgba(0xa6a28cff).into()), - ("label".into(), rgba(0x6684e0ff).into()), - ("operator".into(), rgba(0xa6a28cff).into()), - ("string".into(), rgba(0x5fac38ff).into()), - ("variant".into(), rgba(0xae9512ff).into()), - ("variable".into(), rgba(0xe8e4cfff).into()), - ("function.method".into(), rgba(0x6583e1ff).into()), - ( - "function.special.definition".into(), - rgba(0xae9512ff).into(), - ), - ("string.regex".into(), rgba(0x1ead82ff).into()), - ("emphasis.strong".into(), rgba(0x6684e0ff).into()), - ("punctuation.special".into(), rgba(0xd43451ff).into()), - ("punctuation.bracket".into(), rgba(0xa6a28cff).into()), - ("link_text".into(), rgba(0xb65611ff).into()), - ("link_uri".into(), rgba(0x5fac39ff).into()), - ("boolean".into(), rgba(0x5fac39ff).into()), - ("hint".into(), rgba(0xb17272ff).into()), - ("tag".into(), rgba(0x6684e0ff).into()), - ("function".into(), rgba(0x6583e1ff).into()), - ("title".into(), rgba(0xfefbecff).into()), - ("property".into(), rgba(0xd73737ff).into()), - ("type".into(), rgba(0xae9512ff).into()), - ("constant".into(), rgba(0x5fac39ff).into()), - ("attribute".into(), rgba(0x6684e0ff).into()), - ("predictive".into(), rgba(0x9c6262ff).into()), - ("string.special.symbol".into(), rgba(0x5fac38ff).into()), - ("punctuation.list_marker".into(), rgba(0xe8e4cfff).into()), - ("emphasis".into(), rgba(0x6684e0ff).into()), - ("keyword".into(), rgba(0xb854d4ff).into()), - ("text.literal".into(), rgba(0xb65611ff).into()), - ("number".into(), rgba(0xb65610ff).into()), - ("preproc".into(), rgba(0xfefbecff).into()), - ("embedded".into(), rgba(0xfefbecff).into()), - ], - }, - status_bar: rgba(0x45433bff).into(), - title_bar: rgba(0x45433bff).into(), - toolbar: rgba(0x20201dff).into(), - tab_bar: rgba(0x262622ff).into(), - editor: rgba(0x20201dff).into(), - editor_subheader: rgba(0x262622ff).into(), - editor_active_line: rgba(0x262622ff).into(), - terminal: rgba(0x20201dff).into(), - image_fallback_background: rgba(0x45433bff).into(), - git_created: rgba(0x5fac39ff).into(), - git_modified: rgba(0x6684e0ff).into(), - git_deleted: rgba(0xd73837ff).into(), - git_conflict: rgba(0xae9414ff).into(), - git_ignored: rgba(0x8f8b77ff).into(), - git_renamed: rgba(0xae9414ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x6684e0ff).into(), - selection: rgba(0x6684e03d).into(), - }, - PlayerTheme { - cursor: rgba(0x5fac39ff).into(), - selection: rgba(0x5fac393d).into(), - }, - PlayerTheme { - cursor: rgba(0xd43651ff).into(), - selection: rgba(0xd436513d).into(), - }, - PlayerTheme { - cursor: rgba(0xb65611ff).into(), - selection: rgba(0xb656113d).into(), - }, - PlayerTheme { - cursor: rgba(0xb854d3ff).into(), - selection: rgba(0xb854d33d).into(), - }, - PlayerTheme { - cursor: rgba(0x20ad83ff).into(), - selection: rgba(0x20ad833d).into(), - }, - PlayerTheme { - cursor: rgba(0xd73837ff).into(), - selection: rgba(0xd738373d).into(), - }, - PlayerTheme { - cursor: rgba(0xae9414ff).into(), - selection: rgba(0xae94143d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_dune_light.rs b/crates/theme2/src/themes/atelier_dune_light.rs deleted file mode 100644 index 1d0f944916..0000000000 --- a/crates/theme2/src/themes/atelier_dune_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_dune_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Dune Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xa8a48eff).into(), - border_variant: rgba(0xa8a48eff).into(), - border_focused: rgba(0xcdd1f5ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xcecab4ff).into(), - surface: rgba(0xeeebd7ff).into(), - background: rgba(0xcecab4ff).into(), - filled_element: rgba(0xcecab4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe3e5faff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe3e5faff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x20201dff).into(), - text_muted: rgba(0x706d5fff).into(), - text_placeholder: rgba(0xd73737ff).into(), - text_disabled: rgba(0x878471ff).into(), - text_accent: rgba(0x6684dfff).into(), - icon_muted: rgba(0x706d5fff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("primary".into(), rgba(0x292824ff).into()), - ("comment".into(), rgba(0x999580ff).into()), - ("type".into(), rgba(0xae9512ff).into()), - ("variant".into(), rgba(0xae9512ff).into()), - ("label".into(), rgba(0x6684dfff).into()), - ("function.method".into(), rgba(0x6583e1ff).into()), - ("variable.special".into(), rgba(0xb854d4ff).into()), - ("string.regex".into(), rgba(0x1ead82ff).into()), - ("property".into(), rgba(0xd73737ff).into()), - ("keyword".into(), rgba(0xb854d4ff).into()), - ("number".into(), rgba(0xb65610ff).into()), - ("punctuation.list_marker".into(), rgba(0x292824ff).into()), - ( - "function.special.definition".into(), - rgba(0xae9512ff).into(), - ), - ("punctuation.special".into(), rgba(0xd43451ff).into()), - ("punctuation".into(), rgba(0x292824ff).into()), - ("punctuation.delimiter".into(), rgba(0x6e6b5eff).into()), - ("tag".into(), rgba(0x6684dfff).into()), - ("link_text".into(), rgba(0xb65712ff).into()), - ("boolean".into(), rgba(0x61ac39ff).into()), - ("hint".into(), rgba(0xb37979ff).into()), - ("operator".into(), rgba(0x6e6b5eff).into()), - ("constant".into(), rgba(0x61ac39ff).into()), - ("function".into(), rgba(0x6583e1ff).into()), - ("text.literal".into(), rgba(0xb65712ff).into()), - ("string.special.symbol".into(), rgba(0x5fac38ff).into()), - ("attribute".into(), rgba(0x6684dfff).into()), - ("emphasis".into(), rgba(0x6684dfff).into()), - ("preproc".into(), rgba(0x20201dff).into()), - ("comment.doc".into(), rgba(0x6e6b5eff).into()), - ("punctuation.bracket".into(), rgba(0x6e6b5eff).into()), - ("string".into(), rgba(0x5fac38ff).into()), - ("enum".into(), rgba(0xb65712ff).into()), - ("variable".into(), rgba(0x292824ff).into()), - ("string.special".into(), rgba(0xd43451ff).into()), - ("embedded".into(), rgba(0x20201dff).into()), - ("emphasis.strong".into(), rgba(0x6684dfff).into()), - ("predictive".into(), rgba(0xc88a8aff).into()), - ("title".into(), rgba(0x20201dff).into()), - ("constructor".into(), rgba(0x6684dfff).into()), - ("link_uri".into(), rgba(0x61ac39ff).into()), - ("string.escape".into(), rgba(0x6e6b5eff).into()), - ], - }, - status_bar: rgba(0xcecab4ff).into(), - title_bar: rgba(0xcecab4ff).into(), - toolbar: rgba(0xfefbecff).into(), - tab_bar: rgba(0xeeebd7ff).into(), - editor: rgba(0xfefbecff).into(), - editor_subheader: rgba(0xeeebd7ff).into(), - editor_active_line: rgba(0xeeebd7ff).into(), - terminal: rgba(0xfefbecff).into(), - image_fallback_background: rgba(0xcecab4ff).into(), - git_created: rgba(0x61ac39ff).into(), - git_modified: rgba(0x6684dfff).into(), - git_deleted: rgba(0xd73737ff).into(), - git_conflict: rgba(0xae9414ff).into(), - git_ignored: rgba(0x878471ff).into(), - git_renamed: rgba(0xae9414ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x6684dfff).into(), - selection: rgba(0x6684df3d).into(), - }, - PlayerTheme { - cursor: rgba(0x61ac39ff).into(), - selection: rgba(0x61ac393d).into(), - }, - PlayerTheme { - cursor: rgba(0xd43652ff).into(), - selection: rgba(0xd436523d).into(), - }, - PlayerTheme { - cursor: rgba(0xb65712ff).into(), - selection: rgba(0xb657123d).into(), - }, - PlayerTheme { - cursor: rgba(0xb755d3ff).into(), - selection: rgba(0xb755d33d).into(), - }, - PlayerTheme { - cursor: rgba(0x21ad82ff).into(), - selection: rgba(0x21ad823d).into(), - }, - PlayerTheme { - cursor: rgba(0xd73737ff).into(), - selection: rgba(0xd737373d).into(), - }, - PlayerTheme { - cursor: rgba(0xae9414ff).into(), - selection: rgba(0xae94143d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_estuary_dark.rs b/crates/theme2/src/themes/atelier_estuary_dark.rs deleted file mode 100644 index ad5c9fbc1e..0000000000 --- a/crates/theme2/src/themes/atelier_estuary_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_estuary_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Estuary Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5d5c4cff).into(), - border_variant: rgba(0x5d5c4cff).into(), - border_focused: rgba(0x1c3927ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x424136ff).into(), - surface: rgba(0x2c2b23ff).into(), - background: rgba(0x424136ff).into(), - filled_element: rgba(0x424136ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x142319ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x142319ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf4f3ecff).into(), - text_muted: rgba(0x91907fff).into(), - text_placeholder: rgba(0xba6136ff).into(), - text_disabled: rgba(0x7d7c6aff).into(), - text_accent: rgba(0x36a165ff).into(), - icon_muted: rgba(0x91907fff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.special.symbol".into(), rgba(0x7c9725ff).into()), - ("comment".into(), rgba(0x6c6b5aff).into()), - ("operator".into(), rgba(0x929181ff).into()), - ("punctuation.delimiter".into(), rgba(0x929181ff).into()), - ("keyword".into(), rgba(0x5f9182ff).into()), - ("punctuation.special".into(), rgba(0x9d6b7bff).into()), - ("preproc".into(), rgba(0xf4f3ecff).into()), - ("title".into(), rgba(0xf4f3ecff).into()), - ("string.escape".into(), rgba(0x929181ff).into()), - ("boolean".into(), rgba(0x7d9726ff).into()), - ("punctuation.bracket".into(), rgba(0x929181ff).into()), - ("emphasis.strong".into(), rgba(0x36a165ff).into()), - ("string".into(), rgba(0x7c9725ff).into()), - ("constant".into(), rgba(0x7d9726ff).into()), - ("link_text".into(), rgba(0xae7214ff).into()), - ("tag".into(), rgba(0x36a165ff).into()), - ("hint".into(), rgba(0x6f815aff).into()), - ("punctuation".into(), rgba(0xe7e6dfff).into()), - ("string.regex".into(), rgba(0x5a9d47ff).into()), - ("variant".into(), rgba(0xa5980cff).into()), - ("type".into(), rgba(0xa5980cff).into()), - ("attribute".into(), rgba(0x36a165ff).into()), - ("emphasis".into(), rgba(0x36a165ff).into()), - ("enum".into(), rgba(0xae7214ff).into()), - ("number".into(), rgba(0xae7312ff).into()), - ("property".into(), rgba(0xba6135ff).into()), - ("predictive".into(), rgba(0x5f724cff).into()), - ( - "function.special.definition".into(), - rgba(0xa5980cff).into(), - ), - ("link_uri".into(), rgba(0x7d9726ff).into()), - ("variable.special".into(), rgba(0x5f9182ff).into()), - ("text.literal".into(), rgba(0xae7214ff).into()), - ("label".into(), rgba(0x36a165ff).into()), - ("primary".into(), rgba(0xe7e6dfff).into()), - ("variable".into(), rgba(0xe7e6dfff).into()), - ("embedded".into(), rgba(0xf4f3ecff).into()), - ("function.method".into(), rgba(0x35a166ff).into()), - ("comment.doc".into(), rgba(0x929181ff).into()), - ("string.special".into(), rgba(0x9d6b7bff).into()), - ("constructor".into(), rgba(0x36a165ff).into()), - ("punctuation.list_marker".into(), rgba(0xe7e6dfff).into()), - ("function".into(), rgba(0x35a166ff).into()), - ], - }, - status_bar: rgba(0x424136ff).into(), - title_bar: rgba(0x424136ff).into(), - toolbar: rgba(0x22221bff).into(), - tab_bar: rgba(0x2c2b23ff).into(), - editor: rgba(0x22221bff).into(), - editor_subheader: rgba(0x2c2b23ff).into(), - editor_active_line: rgba(0x2c2b23ff).into(), - terminal: rgba(0x22221bff).into(), - image_fallback_background: rgba(0x424136ff).into(), - git_created: rgba(0x7d9726ff).into(), - git_modified: rgba(0x36a165ff).into(), - git_deleted: rgba(0xba6136ff).into(), - git_conflict: rgba(0xa5980fff).into(), - git_ignored: rgba(0x7d7c6aff).into(), - git_renamed: rgba(0xa5980fff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x36a165ff).into(), - selection: rgba(0x36a1653d).into(), - }, - PlayerTheme { - cursor: rgba(0x7d9726ff).into(), - selection: rgba(0x7d97263d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d6b7bff).into(), - selection: rgba(0x9d6b7b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xae7214ff).into(), - selection: rgba(0xae72143d).into(), - }, - PlayerTheme { - cursor: rgba(0x5f9182ff).into(), - selection: rgba(0x5f91823d).into(), - }, - PlayerTheme { - cursor: rgba(0x5a9d47ff).into(), - selection: rgba(0x5a9d473d).into(), - }, - PlayerTheme { - cursor: rgba(0xba6136ff).into(), - selection: rgba(0xba61363d).into(), - }, - PlayerTheme { - cursor: rgba(0xa5980fff).into(), - selection: rgba(0xa5980f3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_estuary_light.rs b/crates/theme2/src/themes/atelier_estuary_light.rs deleted file mode 100644 index 91eaa88fab..0000000000 --- a/crates/theme2/src/themes/atelier_estuary_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_estuary_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Estuary Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x969585ff).into(), - border_variant: rgba(0x969585ff).into(), - border_focused: rgba(0xbbddc6ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xc5c4b9ff).into(), - surface: rgba(0xebeae3ff).into(), - background: rgba(0xc5c4b9ff).into(), - filled_element: rgba(0xc5c4b9ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xd9ecdfff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xd9ecdfff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x22221bff).into(), - text_muted: rgba(0x61604fff).into(), - text_placeholder: rgba(0xba6336ff).into(), - text_disabled: rgba(0x767463ff).into(), - text_accent: rgba(0x37a165ff).into(), - icon_muted: rgba(0x61604fff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.special".into(), rgba(0x9d6b7bff).into()), - ("link_text".into(), rgba(0xae7214ff).into()), - ("emphasis.strong".into(), rgba(0x37a165ff).into()), - ("tag".into(), rgba(0x37a165ff).into()), - ("primary".into(), rgba(0x302f27ff).into()), - ("emphasis".into(), rgba(0x37a165ff).into()), - ("hint".into(), rgba(0x758961ff).into()), - ("title".into(), rgba(0x22221bff).into()), - ("string.regex".into(), rgba(0x5a9d47ff).into()), - ("attribute".into(), rgba(0x37a165ff).into()), - ("string.escape".into(), rgba(0x5f5e4eff).into()), - ("embedded".into(), rgba(0x22221bff).into()), - ("punctuation.bracket".into(), rgba(0x5f5e4eff).into()), - ( - "function.special.definition".into(), - rgba(0xa5980cff).into(), - ), - ("operator".into(), rgba(0x5f5e4eff).into()), - ("constant".into(), rgba(0x7c9728ff).into()), - ("comment.doc".into(), rgba(0x5f5e4eff).into()), - ("label".into(), rgba(0x37a165ff).into()), - ("variable".into(), rgba(0x302f27ff).into()), - ("punctuation".into(), rgba(0x302f27ff).into()), - ("punctuation.delimiter".into(), rgba(0x5f5e4eff).into()), - ("comment".into(), rgba(0x878573ff).into()), - ("punctuation.special".into(), rgba(0x9d6b7bff).into()), - ("string.special.symbol".into(), rgba(0x7c9725ff).into()), - ("enum".into(), rgba(0xae7214ff).into()), - ("variable.special".into(), rgba(0x5f9182ff).into()), - ("link_uri".into(), rgba(0x7c9728ff).into()), - ("punctuation.list_marker".into(), rgba(0x302f27ff).into()), - ("number".into(), rgba(0xae7312ff).into()), - ("function".into(), rgba(0x35a166ff).into()), - ("text.literal".into(), rgba(0xae7214ff).into()), - ("boolean".into(), rgba(0x7c9728ff).into()), - ("predictive".into(), rgba(0x879a72ff).into()), - ("type".into(), rgba(0xa5980cff).into()), - ("constructor".into(), rgba(0x37a165ff).into()), - ("property".into(), rgba(0xba6135ff).into()), - ("keyword".into(), rgba(0x5f9182ff).into()), - ("function.method".into(), rgba(0x35a166ff).into()), - ("variant".into(), rgba(0xa5980cff).into()), - ("string".into(), rgba(0x7c9725ff).into()), - ("preproc".into(), rgba(0x22221bff).into()), - ], - }, - status_bar: rgba(0xc5c4b9ff).into(), - title_bar: rgba(0xc5c4b9ff).into(), - toolbar: rgba(0xf4f3ecff).into(), - tab_bar: rgba(0xebeae3ff).into(), - editor: rgba(0xf4f3ecff).into(), - editor_subheader: rgba(0xebeae3ff).into(), - editor_active_line: rgba(0xebeae3ff).into(), - terminal: rgba(0xf4f3ecff).into(), - image_fallback_background: rgba(0xc5c4b9ff).into(), - git_created: rgba(0x7c9728ff).into(), - git_modified: rgba(0x37a165ff).into(), - git_deleted: rgba(0xba6336ff).into(), - git_conflict: rgba(0xa5980fff).into(), - git_ignored: rgba(0x767463ff).into(), - git_renamed: rgba(0xa5980fff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x37a165ff).into(), - selection: rgba(0x37a1653d).into(), - }, - PlayerTheme { - cursor: rgba(0x7c9728ff).into(), - selection: rgba(0x7c97283d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d6b7bff).into(), - selection: rgba(0x9d6b7b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xae7214ff).into(), - selection: rgba(0xae72143d).into(), - }, - PlayerTheme { - cursor: rgba(0x5f9182ff).into(), - selection: rgba(0x5f91823d).into(), - }, - PlayerTheme { - cursor: rgba(0x5c9d49ff).into(), - selection: rgba(0x5c9d493d).into(), - }, - PlayerTheme { - cursor: rgba(0xba6336ff).into(), - selection: rgba(0xba63363d).into(), - }, - PlayerTheme { - cursor: rgba(0xa5980fff).into(), - selection: rgba(0xa5980f3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_forest_dark.rs b/crates/theme2/src/themes/atelier_forest_dark.rs deleted file mode 100644 index 83228e671f..0000000000 --- a/crates/theme2/src/themes/atelier_forest_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_forest_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Forest Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x665f5cff).into(), - border_variant: rgba(0x665f5cff).into(), - border_focused: rgba(0x182d5bff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x443c39ff).into(), - surface: rgba(0x27211eff).into(), - background: rgba(0x443c39ff).into(), - filled_element: rgba(0x443c39ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x0f1c3dff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x0f1c3dff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf0eeedff).into(), - text_muted: rgba(0xa79f9dff).into(), - text_placeholder: rgba(0xf22c3fff).into(), - text_disabled: rgba(0x8e8683ff).into(), - text_accent: rgba(0x407ee6ff).into(), - icon_muted: rgba(0xa79f9dff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("link_uri".into(), rgba(0x7a9726ff).into()), - ("punctuation.list_marker".into(), rgba(0xe6e2e0ff).into()), - ("type".into(), rgba(0xc38417ff).into()), - ("punctuation.bracket".into(), rgba(0xa8a19fff).into()), - ("punctuation".into(), rgba(0xe6e2e0ff).into()), - ("preproc".into(), rgba(0xf0eeedff).into()), - ("punctuation.special".into(), rgba(0xc33ff3ff).into()), - ("variable.special".into(), rgba(0x6666eaff).into()), - ("tag".into(), rgba(0x407ee6ff).into()), - ("constructor".into(), rgba(0x407ee6ff).into()), - ("title".into(), rgba(0xf0eeedff).into()), - ("hint".into(), rgba(0xa77087ff).into()), - ("constant".into(), rgba(0x7a9726ff).into()), - ("number".into(), rgba(0xdf521fff).into()), - ("emphasis.strong".into(), rgba(0x407ee6ff).into()), - ("boolean".into(), rgba(0x7a9726ff).into()), - ("comment".into(), rgba(0x766e6bff).into()), - ("string.special".into(), rgba(0xc33ff3ff).into()), - ("text.literal".into(), rgba(0xdf5321ff).into()), - ("string.regex".into(), rgba(0x3c96b8ff).into()), - ("enum".into(), rgba(0xdf5321ff).into()), - ("operator".into(), rgba(0xa8a19fff).into()), - ("embedded".into(), rgba(0xf0eeedff).into()), - ("string.special.symbol".into(), rgba(0x7a9725ff).into()), - ("predictive".into(), rgba(0x8f5b70ff).into()), - ("comment.doc".into(), rgba(0xa8a19fff).into()), - ("variant".into(), rgba(0xc38417ff).into()), - ("label".into(), rgba(0x407ee6ff).into()), - ("property".into(), rgba(0xf22c40ff).into()), - ("keyword".into(), rgba(0x6666eaff).into()), - ("function".into(), rgba(0x3f7ee7ff).into()), - ("string.escape".into(), rgba(0xa8a19fff).into()), - ("string".into(), rgba(0x7a9725ff).into()), - ("primary".into(), rgba(0xe6e2e0ff).into()), - ("function.method".into(), rgba(0x3f7ee7ff).into()), - ("link_text".into(), rgba(0xdf5321ff).into()), - ("attribute".into(), rgba(0x407ee6ff).into()), - ("emphasis".into(), rgba(0x407ee6ff).into()), - ( - "function.special.definition".into(), - rgba(0xc38417ff).into(), - ), - ("variable".into(), rgba(0xe6e2e0ff).into()), - ("punctuation.delimiter".into(), rgba(0xa8a19fff).into()), - ], - }, - status_bar: rgba(0x443c39ff).into(), - title_bar: rgba(0x443c39ff).into(), - toolbar: rgba(0x1b1918ff).into(), - tab_bar: rgba(0x27211eff).into(), - editor: rgba(0x1b1918ff).into(), - editor_subheader: rgba(0x27211eff).into(), - editor_active_line: rgba(0x27211eff).into(), - terminal: rgba(0x1b1918ff).into(), - image_fallback_background: rgba(0x443c39ff).into(), - git_created: rgba(0x7a9726ff).into(), - git_modified: rgba(0x407ee6ff).into(), - git_deleted: rgba(0xf22c3fff).into(), - git_conflict: rgba(0xc38418ff).into(), - git_ignored: rgba(0x8e8683ff).into(), - git_renamed: rgba(0xc38418ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x407ee6ff).into(), - selection: rgba(0x407ee63d).into(), - }, - PlayerTheme { - cursor: rgba(0x7a9726ff).into(), - selection: rgba(0x7a97263d).into(), - }, - PlayerTheme { - cursor: rgba(0xc340f2ff).into(), - selection: rgba(0xc340f23d).into(), - }, - PlayerTheme { - cursor: rgba(0xdf5321ff).into(), - selection: rgba(0xdf53213d).into(), - }, - PlayerTheme { - cursor: rgba(0x6565e9ff).into(), - selection: rgba(0x6565e93d).into(), - }, - PlayerTheme { - cursor: rgba(0x3d97b8ff).into(), - selection: rgba(0x3d97b83d).into(), - }, - PlayerTheme { - cursor: rgba(0xf22c3fff).into(), - selection: rgba(0xf22c3f3d).into(), - }, - PlayerTheme { - cursor: rgba(0xc38418ff).into(), - selection: rgba(0xc384183d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_forest_light.rs b/crates/theme2/src/themes/atelier_forest_light.rs deleted file mode 100644 index 882d5c2fcb..0000000000 --- a/crates/theme2/src/themes/atelier_forest_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_forest_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Forest Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xaaa3a1ff).into(), - border_variant: rgba(0xaaa3a1ff).into(), - border_focused: rgba(0xc6cef7ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xccc7c5ff).into(), - surface: rgba(0xe9e6e4ff).into(), - background: rgba(0xccc7c5ff).into(), - filled_element: rgba(0xccc7c5ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdfe3fbff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdfe3fbff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x1b1918ff).into(), - text_muted: rgba(0x6a6360ff).into(), - text_placeholder: rgba(0xf22e40ff).into(), - text_disabled: rgba(0x837b78ff).into(), - text_accent: rgba(0x407ee6ff).into(), - icon_muted: rgba(0x6a6360ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("punctuation.special".into(), rgba(0xc33ff3ff).into()), - ("text.literal".into(), rgba(0xdf5421ff).into()), - ("string.escape".into(), rgba(0x68615eff).into()), - ("string.regex".into(), rgba(0x3c96b8ff).into()), - ("number".into(), rgba(0xdf521fff).into()), - ("preproc".into(), rgba(0x1b1918ff).into()), - ("keyword".into(), rgba(0x6666eaff).into()), - ("variable.special".into(), rgba(0x6666eaff).into()), - ("punctuation.delimiter".into(), rgba(0x68615eff).into()), - ("emphasis.strong".into(), rgba(0x407ee6ff).into()), - ("boolean".into(), rgba(0x7a9728ff).into()), - ("variant".into(), rgba(0xc38417ff).into()), - ("predictive".into(), rgba(0xbe899eff).into()), - ("tag".into(), rgba(0x407ee6ff).into()), - ("property".into(), rgba(0xf22c40ff).into()), - ("enum".into(), rgba(0xdf5421ff).into()), - ("attribute".into(), rgba(0x407ee6ff).into()), - ("function.method".into(), rgba(0x3f7ee7ff).into()), - ("function".into(), rgba(0x3f7ee7ff).into()), - ("emphasis".into(), rgba(0x407ee6ff).into()), - ("primary".into(), rgba(0x2c2421ff).into()), - ("variable".into(), rgba(0x2c2421ff).into()), - ("constant".into(), rgba(0x7a9728ff).into()), - ("title".into(), rgba(0x1b1918ff).into()), - ("comment.doc".into(), rgba(0x68615eff).into()), - ("constructor".into(), rgba(0x407ee6ff).into()), - ("type".into(), rgba(0xc38417ff).into()), - ("punctuation.list_marker".into(), rgba(0x2c2421ff).into()), - ("punctuation".into(), rgba(0x2c2421ff).into()), - ("string".into(), rgba(0x7a9725ff).into()), - ("label".into(), rgba(0x407ee6ff).into()), - ("string.special".into(), rgba(0xc33ff3ff).into()), - ("embedded".into(), rgba(0x1b1918ff).into()), - ("link_text".into(), rgba(0xdf5421ff).into()), - ("punctuation.bracket".into(), rgba(0x68615eff).into()), - ("comment".into(), rgba(0x9c9491ff).into()), - ( - "function.special.definition".into(), - rgba(0xc38417ff).into(), - ), - ("link_uri".into(), rgba(0x7a9728ff).into()), - ("operator".into(), rgba(0x68615eff).into()), - ("hint".into(), rgba(0xa67287ff).into()), - ("string.special.symbol".into(), rgba(0x7a9725ff).into()), - ], - }, - status_bar: rgba(0xccc7c5ff).into(), - title_bar: rgba(0xccc7c5ff).into(), - toolbar: rgba(0xf0eeedff).into(), - tab_bar: rgba(0xe9e6e4ff).into(), - editor: rgba(0xf0eeedff).into(), - editor_subheader: rgba(0xe9e6e4ff).into(), - editor_active_line: rgba(0xe9e6e4ff).into(), - terminal: rgba(0xf0eeedff).into(), - image_fallback_background: rgba(0xccc7c5ff).into(), - git_created: rgba(0x7a9728ff).into(), - git_modified: rgba(0x407ee6ff).into(), - git_deleted: rgba(0xf22e40ff).into(), - git_conflict: rgba(0xc38419ff).into(), - git_ignored: rgba(0x837b78ff).into(), - git_renamed: rgba(0xc38419ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x407ee6ff).into(), - selection: rgba(0x407ee63d).into(), - }, - PlayerTheme { - cursor: rgba(0x7a9728ff).into(), - selection: rgba(0x7a97283d).into(), - }, - PlayerTheme { - cursor: rgba(0xc340f2ff).into(), - selection: rgba(0xc340f23d).into(), - }, - PlayerTheme { - cursor: rgba(0xdf5421ff).into(), - selection: rgba(0xdf54213d).into(), - }, - PlayerTheme { - cursor: rgba(0x6765e9ff).into(), - selection: rgba(0x6765e93d).into(), - }, - PlayerTheme { - cursor: rgba(0x3e96b8ff).into(), - selection: rgba(0x3e96b83d).into(), - }, - PlayerTheme { - cursor: rgba(0xf22e40ff).into(), - selection: rgba(0xf22e403d).into(), - }, - PlayerTheme { - cursor: rgba(0xc38419ff).into(), - selection: rgba(0xc384193d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_heath_dark.rs b/crates/theme2/src/themes/atelier_heath_dark.rs deleted file mode 100644 index 354c98069f..0000000000 --- a/crates/theme2/src/themes/atelier_heath_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_heath_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Heath Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x675b67ff).into(), - border_variant: rgba(0x675b67ff).into(), - border_focused: rgba(0x192961ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x433a43ff).into(), - surface: rgba(0x252025ff).into(), - background: rgba(0x433a43ff).into(), - filled_element: rgba(0x433a43ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x0d1a43ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x0d1a43ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf7f3f7ff).into(), - text_muted: rgba(0xa899a8ff).into(), - text_placeholder: rgba(0xca3f2bff).into(), - text_disabled: rgba(0x908190ff).into(), - text_accent: rgba(0x5169ebff).into(), - icon_muted: rgba(0xa899a8ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("preproc".into(), rgba(0xf7f3f7ff).into()), - ("number".into(), rgba(0xa65825ff).into()), - ("boolean".into(), rgba(0x918b3aff).into()), - ("embedded".into(), rgba(0xf7f3f7ff).into()), - ("variable.special".into(), rgba(0x7b58bfff).into()), - ("operator".into(), rgba(0xab9babff).into()), - ("punctuation.delimiter".into(), rgba(0xab9babff).into()), - ("primary".into(), rgba(0xd8cad8ff).into()), - ("punctuation.bracket".into(), rgba(0xab9babff).into()), - ("comment.doc".into(), rgba(0xab9babff).into()), - ("variant".into(), rgba(0xbb8a34ff).into()), - ("attribute".into(), rgba(0x5169ebff).into()), - ("property".into(), rgba(0xca3f2aff).into()), - ("keyword".into(), rgba(0x7b58bfff).into()), - ("hint".into(), rgba(0x8d70a8ff).into()), - ("string.special.symbol".into(), rgba(0x918b3aff).into()), - ("punctuation.special".into(), rgba(0xcc32ccff).into()), - ("link_uri".into(), rgba(0x918b3aff).into()), - ("link_text".into(), rgba(0xa65827ff).into()), - ("enum".into(), rgba(0xa65827ff).into()), - ("function".into(), rgba(0x506aecff).into()), - ( - "function.special.definition".into(), - rgba(0xbb8a34ff).into(), - ), - ("constant".into(), rgba(0x918b3aff).into()), - ("title".into(), rgba(0xf7f3f7ff).into()), - ("string.regex".into(), rgba(0x149393ff).into()), - ("variable".into(), rgba(0xd8cad8ff).into()), - ("comment".into(), rgba(0x776977ff).into()), - ("predictive".into(), rgba(0x75588fff).into()), - ("function.method".into(), rgba(0x506aecff).into()), - ("type".into(), rgba(0xbb8a34ff).into()), - ("punctuation".into(), rgba(0xd8cad8ff).into()), - ("emphasis".into(), rgba(0x5169ebff).into()), - ("emphasis.strong".into(), rgba(0x5169ebff).into()), - ("tag".into(), rgba(0x5169ebff).into()), - ("text.literal".into(), rgba(0xa65827ff).into()), - ("string".into(), rgba(0x918b3aff).into()), - ("string.escape".into(), rgba(0xab9babff).into()), - ("constructor".into(), rgba(0x5169ebff).into()), - ("label".into(), rgba(0x5169ebff).into()), - ("punctuation.list_marker".into(), rgba(0xd8cad8ff).into()), - ("string.special".into(), rgba(0xcc32ccff).into()), - ], - }, - status_bar: rgba(0x433a43ff).into(), - title_bar: rgba(0x433a43ff).into(), - toolbar: rgba(0x1b181bff).into(), - tab_bar: rgba(0x252025ff).into(), - editor: rgba(0x1b181bff).into(), - editor_subheader: rgba(0x252025ff).into(), - editor_active_line: rgba(0x252025ff).into(), - terminal: rgba(0x1b181bff).into(), - image_fallback_background: rgba(0x433a43ff).into(), - git_created: rgba(0x918b3aff).into(), - git_modified: rgba(0x5169ebff).into(), - git_deleted: rgba(0xca3f2bff).into(), - git_conflict: rgba(0xbb8a35ff).into(), - git_ignored: rgba(0x908190ff).into(), - git_renamed: rgba(0xbb8a35ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x5169ebff).into(), - selection: rgba(0x5169eb3d).into(), - }, - PlayerTheme { - cursor: rgba(0x918b3aff).into(), - selection: rgba(0x918b3a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xcc34ccff).into(), - selection: rgba(0xcc34cc3d).into(), - }, - PlayerTheme { - cursor: rgba(0xa65827ff).into(), - selection: rgba(0xa658273d).into(), - }, - PlayerTheme { - cursor: rgba(0x7b58bfff).into(), - selection: rgba(0x7b58bf3d).into(), - }, - PlayerTheme { - cursor: rgba(0x189393ff).into(), - selection: rgba(0x1893933d).into(), - }, - PlayerTheme { - cursor: rgba(0xca3f2bff).into(), - selection: rgba(0xca3f2b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xbb8a35ff).into(), - selection: rgba(0xbb8a353d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_heath_light.rs b/crates/theme2/src/themes/atelier_heath_light.rs deleted file mode 100644 index f1a9e4d8c6..0000000000 --- a/crates/theme2/src/themes/atelier_heath_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_heath_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Heath Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xad9dadff).into(), - border_variant: rgba(0xad9dadff).into(), - border_focused: rgba(0xcac7faff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xc6b8c6ff).into(), - surface: rgba(0xe0d5e0ff).into(), - background: rgba(0xc6b8c6ff).into(), - filled_element: rgba(0xc6b8c6ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe2dffcff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe2dffcff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x1b181bff).into(), - text_muted: rgba(0x6b5e6bff).into(), - text_placeholder: rgba(0xca402bff).into(), - text_disabled: rgba(0x857785ff).into(), - text_accent: rgba(0x5169ebff).into(), - icon_muted: rgba(0x6b5e6bff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("enum".into(), rgba(0xa65927ff).into()), - ("string.escape".into(), rgba(0x695d69ff).into()), - ("link_uri".into(), rgba(0x918b3bff).into()), - ("function.method".into(), rgba(0x506aecff).into()), - ("comment.doc".into(), rgba(0x695d69ff).into()), - ("property".into(), rgba(0xca3f2aff).into()), - ("string.special".into(), rgba(0xcc32ccff).into()), - ("tag".into(), rgba(0x5169ebff).into()), - ("embedded".into(), rgba(0x1b181bff).into()), - ("primary".into(), rgba(0x292329ff).into()), - ("punctuation".into(), rgba(0x292329ff).into()), - ("punctuation.special".into(), rgba(0xcc32ccff).into()), - ("type".into(), rgba(0xbb8a34ff).into()), - ("number".into(), rgba(0xa65825ff).into()), - ("function".into(), rgba(0x506aecff).into()), - ("preproc".into(), rgba(0x1b181bff).into()), - ("punctuation.bracket".into(), rgba(0x695d69ff).into()), - ("punctuation.delimiter".into(), rgba(0x695d69ff).into()), - ("variable".into(), rgba(0x292329ff).into()), - ( - "function.special.definition".into(), - rgba(0xbb8a34ff).into(), - ), - ("label".into(), rgba(0x5169ebff).into()), - ("constructor".into(), rgba(0x5169ebff).into()), - ("emphasis.strong".into(), rgba(0x5169ebff).into()), - ("constant".into(), rgba(0x918b3bff).into()), - ("keyword".into(), rgba(0x7b58bfff).into()), - ("variable.special".into(), rgba(0x7b58bfff).into()), - ("variant".into(), rgba(0xbb8a34ff).into()), - ("title".into(), rgba(0x1b181bff).into()), - ("attribute".into(), rgba(0x5169ebff).into()), - ("comment".into(), rgba(0x9e8f9eff).into()), - ("string.special.symbol".into(), rgba(0x918b3aff).into()), - ("predictive".into(), rgba(0xa487bfff).into()), - ("link_text".into(), rgba(0xa65927ff).into()), - ("punctuation.list_marker".into(), rgba(0x292329ff).into()), - ("boolean".into(), rgba(0x918b3bff).into()), - ("text.literal".into(), rgba(0xa65927ff).into()), - ("emphasis".into(), rgba(0x5169ebff).into()), - ("string.regex".into(), rgba(0x149393ff).into()), - ("hint".into(), rgba(0x8c70a6ff).into()), - ("string".into(), rgba(0x918b3aff).into()), - ("operator".into(), rgba(0x695d69ff).into()), - ], - }, - status_bar: rgba(0xc6b8c6ff).into(), - title_bar: rgba(0xc6b8c6ff).into(), - toolbar: rgba(0xf7f3f7ff).into(), - tab_bar: rgba(0xe0d5e0ff).into(), - editor: rgba(0xf7f3f7ff).into(), - editor_subheader: rgba(0xe0d5e0ff).into(), - editor_active_line: rgba(0xe0d5e0ff).into(), - terminal: rgba(0xf7f3f7ff).into(), - image_fallback_background: rgba(0xc6b8c6ff).into(), - git_created: rgba(0x918b3bff).into(), - git_modified: rgba(0x5169ebff).into(), - git_deleted: rgba(0xca402bff).into(), - git_conflict: rgba(0xbb8a35ff).into(), - git_ignored: rgba(0x857785ff).into(), - git_renamed: rgba(0xbb8a35ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x5169ebff).into(), - selection: rgba(0x5169eb3d).into(), - }, - PlayerTheme { - cursor: rgba(0x918b3bff).into(), - selection: rgba(0x918b3b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xcc34ccff).into(), - selection: rgba(0xcc34cc3d).into(), - }, - PlayerTheme { - cursor: rgba(0xa65927ff).into(), - selection: rgba(0xa659273d).into(), - }, - PlayerTheme { - cursor: rgba(0x7a5ac0ff).into(), - selection: rgba(0x7a5ac03d).into(), - }, - PlayerTheme { - cursor: rgba(0x189393ff).into(), - selection: rgba(0x1893933d).into(), - }, - PlayerTheme { - cursor: rgba(0xca402bff).into(), - selection: rgba(0xca402b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xbb8a35ff).into(), - selection: rgba(0xbb8a353d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_lakeside_dark.rs b/crates/theme2/src/themes/atelier_lakeside_dark.rs deleted file mode 100644 index 61b78864b7..0000000000 --- a/crates/theme2/src/themes/atelier_lakeside_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_lakeside_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Lakeside Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x4f6a78ff).into(), - border_variant: rgba(0x4f6a78ff).into(), - border_focused: rgba(0x1a2f3cff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x33444dff).into(), - surface: rgba(0x1c2529ff).into(), - background: rgba(0x33444dff).into(), - filled_element: rgba(0x33444dff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x121c24ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x121c24ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xebf8ffff).into(), - text_muted: rgba(0x7c9fb3ff).into(), - text_placeholder: rgba(0xd22e72ff).into(), - text_disabled: rgba(0x688c9dff).into(), - text_accent: rgba(0x267eadff).into(), - icon_muted: rgba(0x7c9fb3ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("punctuation.bracket".into(), rgba(0x7ea2b4ff).into()), - ("punctuation.special".into(), rgba(0xb72cd2ff).into()), - ("property".into(), rgba(0xd22c72ff).into()), - ("function.method".into(), rgba(0x247eadff).into()), - ("comment".into(), rgba(0x5a7b8cff).into()), - ("constructor".into(), rgba(0x267eadff).into()), - ("boolean".into(), rgba(0x558c3aff).into()), - ("hint".into(), rgba(0x52809aff).into()), - ("label".into(), rgba(0x267eadff).into()), - ("string.special".into(), rgba(0xb72cd2ff).into()), - ("title".into(), rgba(0xebf8ffff).into()), - ("punctuation.list_marker".into(), rgba(0xc1e4f6ff).into()), - ("emphasis.strong".into(), rgba(0x267eadff).into()), - ("enum".into(), rgba(0x935b25ff).into()), - ("type".into(), rgba(0x8a8a0eff).into()), - ("tag".into(), rgba(0x267eadff).into()), - ("punctuation.delimiter".into(), rgba(0x7ea2b4ff).into()), - ("primary".into(), rgba(0xc1e4f6ff).into()), - ("link_text".into(), rgba(0x935b25ff).into()), - ("variable".into(), rgba(0xc1e4f6ff).into()), - ("variable.special".into(), rgba(0x6a6ab7ff).into()), - ("string.special.symbol".into(), rgba(0x558c3aff).into()), - ("link_uri".into(), rgba(0x558c3aff).into()), - ("function".into(), rgba(0x247eadff).into()), - ("predictive".into(), rgba(0x426f88ff).into()), - ("punctuation".into(), rgba(0xc1e4f6ff).into()), - ("string.escape".into(), rgba(0x7ea2b4ff).into()), - ("keyword".into(), rgba(0x6a6ab7ff).into()), - ("attribute".into(), rgba(0x267eadff).into()), - ("string.regex".into(), rgba(0x2c8f6eff).into()), - ("embedded".into(), rgba(0xebf8ffff).into()), - ("emphasis".into(), rgba(0x267eadff).into()), - ("string".into(), rgba(0x558c3aff).into()), - ("operator".into(), rgba(0x7ea2b4ff).into()), - ("text.literal".into(), rgba(0x935b25ff).into()), - ("constant".into(), rgba(0x558c3aff).into()), - ("comment.doc".into(), rgba(0x7ea2b4ff).into()), - ("number".into(), rgba(0x935c24ff).into()), - ("preproc".into(), rgba(0xebf8ffff).into()), - ( - "function.special.definition".into(), - rgba(0x8a8a0eff).into(), - ), - ("variant".into(), rgba(0x8a8a0eff).into()), - ], - }, - status_bar: rgba(0x33444dff).into(), - title_bar: rgba(0x33444dff).into(), - toolbar: rgba(0x161b1dff).into(), - tab_bar: rgba(0x1c2529ff).into(), - editor: rgba(0x161b1dff).into(), - editor_subheader: rgba(0x1c2529ff).into(), - editor_active_line: rgba(0x1c2529ff).into(), - terminal: rgba(0x161b1dff).into(), - image_fallback_background: rgba(0x33444dff).into(), - git_created: rgba(0x558c3aff).into(), - git_modified: rgba(0x267eadff).into(), - git_deleted: rgba(0xd22e72ff).into(), - git_conflict: rgba(0x8a8a10ff).into(), - git_ignored: rgba(0x688c9dff).into(), - git_renamed: rgba(0x8a8a10ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x267eadff).into(), - selection: rgba(0x267ead3d).into(), - }, - PlayerTheme { - cursor: rgba(0x558c3aff).into(), - selection: rgba(0x558c3a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xb72ed2ff).into(), - selection: rgba(0xb72ed23d).into(), - }, - PlayerTheme { - cursor: rgba(0x935b25ff).into(), - selection: rgba(0x935b253d).into(), - }, - PlayerTheme { - cursor: rgba(0x6a6ab7ff).into(), - selection: rgba(0x6a6ab73d).into(), - }, - PlayerTheme { - cursor: rgba(0x2d8f6fff).into(), - selection: rgba(0x2d8f6f3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd22e72ff).into(), - selection: rgba(0xd22e723d).into(), - }, - PlayerTheme { - cursor: rgba(0x8a8a10ff).into(), - selection: rgba(0x8a8a103d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_lakeside_light.rs b/crates/theme2/src/themes/atelier_lakeside_light.rs deleted file mode 100644 index 64fb70dadb..0000000000 --- a/crates/theme2/src/themes/atelier_lakeside_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_lakeside_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Lakeside Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x80a4b6ff).into(), - border_variant: rgba(0x80a4b6ff).into(), - border_focused: rgba(0xb9cee0ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xa6cadcff).into(), - surface: rgba(0xcdeaf9ff).into(), - background: rgba(0xa6cadcff).into(), - filled_element: rgba(0xa6cadcff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xd8e4eeff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xd8e4eeff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x161b1dff).into(), - text_muted: rgba(0x526f7dff).into(), - text_placeholder: rgba(0xd22e71ff).into(), - text_disabled: rgba(0x628496ff).into(), - text_accent: rgba(0x267eadff).into(), - icon_muted: rgba(0x526f7dff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("emphasis".into(), rgba(0x267eadff).into()), - ("number".into(), rgba(0x935c24ff).into()), - ("embedded".into(), rgba(0x161b1dff).into()), - ("link_text".into(), rgba(0x935c25ff).into()), - ("string".into(), rgba(0x558c3aff).into()), - ("constructor".into(), rgba(0x267eadff).into()), - ("punctuation.list_marker".into(), rgba(0x1f292eff).into()), - ("string.special".into(), rgba(0xb72cd2ff).into()), - ("title".into(), rgba(0x161b1dff).into()), - ("variant".into(), rgba(0x8a8a0eff).into()), - ("tag".into(), rgba(0x267eadff).into()), - ("attribute".into(), rgba(0x267eadff).into()), - ("keyword".into(), rgba(0x6a6ab7ff).into()), - ("enum".into(), rgba(0x935c25ff).into()), - ("function".into(), rgba(0x247eadff).into()), - ("string.escape".into(), rgba(0x516d7bff).into()), - ("operator".into(), rgba(0x516d7bff).into()), - ("function.method".into(), rgba(0x247eadff).into()), - ( - "function.special.definition".into(), - rgba(0x8a8a0eff).into(), - ), - ("punctuation.delimiter".into(), rgba(0x516d7bff).into()), - ("comment".into(), rgba(0x7094a7ff).into()), - ("primary".into(), rgba(0x1f292eff).into()), - ("punctuation.bracket".into(), rgba(0x516d7bff).into()), - ("variable".into(), rgba(0x1f292eff).into()), - ("emphasis.strong".into(), rgba(0x267eadff).into()), - ("predictive".into(), rgba(0x6a97b2ff).into()), - ("punctuation.special".into(), rgba(0xb72cd2ff).into()), - ("hint".into(), rgba(0x5a87a0ff).into()), - ("text.literal".into(), rgba(0x935c25ff).into()), - ("string.special.symbol".into(), rgba(0x558c3aff).into()), - ("comment.doc".into(), rgba(0x516d7bff).into()), - ("constant".into(), rgba(0x568c3bff).into()), - ("boolean".into(), rgba(0x568c3bff).into()), - ("preproc".into(), rgba(0x161b1dff).into()), - ("variable.special".into(), rgba(0x6a6ab7ff).into()), - ("link_uri".into(), rgba(0x568c3bff).into()), - ("string.regex".into(), rgba(0x2c8f6eff).into()), - ("punctuation".into(), rgba(0x1f292eff).into()), - ("property".into(), rgba(0xd22c72ff).into()), - ("label".into(), rgba(0x267eadff).into()), - ("type".into(), rgba(0x8a8a0eff).into()), - ], - }, - status_bar: rgba(0xa6cadcff).into(), - title_bar: rgba(0xa6cadcff).into(), - toolbar: rgba(0xebf8ffff).into(), - tab_bar: rgba(0xcdeaf9ff).into(), - editor: rgba(0xebf8ffff).into(), - editor_subheader: rgba(0xcdeaf9ff).into(), - editor_active_line: rgba(0xcdeaf9ff).into(), - terminal: rgba(0xebf8ffff).into(), - image_fallback_background: rgba(0xa6cadcff).into(), - git_created: rgba(0x568c3bff).into(), - git_modified: rgba(0x267eadff).into(), - git_deleted: rgba(0xd22e71ff).into(), - git_conflict: rgba(0x8a8a10ff).into(), - git_ignored: rgba(0x628496ff).into(), - git_renamed: rgba(0x8a8a10ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x267eadff).into(), - selection: rgba(0x267ead3d).into(), - }, - PlayerTheme { - cursor: rgba(0x568c3bff).into(), - selection: rgba(0x568c3b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xb72ed2ff).into(), - selection: rgba(0xb72ed23d).into(), - }, - PlayerTheme { - cursor: rgba(0x935c25ff).into(), - selection: rgba(0x935c253d).into(), - }, - PlayerTheme { - cursor: rgba(0x6c6ab7ff).into(), - selection: rgba(0x6c6ab73d).into(), - }, - PlayerTheme { - cursor: rgba(0x2e8f6eff).into(), - selection: rgba(0x2e8f6e3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd22e71ff).into(), - selection: rgba(0xd22e713d).into(), - }, - PlayerTheme { - cursor: rgba(0x8a8a10ff).into(), - selection: rgba(0x8a8a103d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_plateau_dark.rs b/crates/theme2/src/themes/atelier_plateau_dark.rs deleted file mode 100644 index 0ba5a1659d..0000000000 --- a/crates/theme2/src/themes/atelier_plateau_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_plateau_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Plateau Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x564e4eff).into(), - border_variant: rgba(0x564e4eff).into(), - border_focused: rgba(0x2c2b45ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x3b3535ff).into(), - surface: rgba(0x252020ff).into(), - background: rgba(0x3b3535ff).into(), - filled_element: rgba(0x3b3535ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x1c1b29ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x1c1b29ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf4ececff).into(), - text_muted: rgba(0x898383ff).into(), - text_placeholder: rgba(0xca4848ff).into(), - text_disabled: rgba(0x756e6eff).into(), - text_accent: rgba(0x7272caff).into(), - icon_muted: rgba(0x898383ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("variant".into(), rgba(0xa06d3aff).into()), - ("label".into(), rgba(0x7272caff).into()), - ("punctuation.delimiter".into(), rgba(0x8a8585ff).into()), - ("string.regex".into(), rgba(0x5485b6ff).into()), - ("variable.special".into(), rgba(0x8464c4ff).into()), - ("string".into(), rgba(0x4b8b8bff).into()), - ("property".into(), rgba(0xca4848ff).into()), - ("hint".into(), rgba(0x8a647aff).into()), - ("comment.doc".into(), rgba(0x8a8585ff).into()), - ("attribute".into(), rgba(0x7272caff).into()), - ("tag".into(), rgba(0x7272caff).into()), - ("constructor".into(), rgba(0x7272caff).into()), - ("boolean".into(), rgba(0x4b8b8bff).into()), - ("preproc".into(), rgba(0xf4ececff).into()), - ("constant".into(), rgba(0x4b8b8bff).into()), - ("punctuation.special".into(), rgba(0xbd5187ff).into()), - ("function.method".into(), rgba(0x7272caff).into()), - ("comment".into(), rgba(0x655d5dff).into()), - ("variable".into(), rgba(0xe7dfdfff).into()), - ("primary".into(), rgba(0xe7dfdfff).into()), - ("title".into(), rgba(0xf4ececff).into()), - ("emphasis".into(), rgba(0x7272caff).into()), - ("emphasis.strong".into(), rgba(0x7272caff).into()), - ("function".into(), rgba(0x7272caff).into()), - ("type".into(), rgba(0xa06d3aff).into()), - ("operator".into(), rgba(0x8a8585ff).into()), - ("embedded".into(), rgba(0xf4ececff).into()), - ("predictive".into(), rgba(0x795369ff).into()), - ("punctuation".into(), rgba(0xe7dfdfff).into()), - ("link_text".into(), rgba(0xb4593bff).into()), - ("enum".into(), rgba(0xb4593bff).into()), - ("string.special".into(), rgba(0xbd5187ff).into()), - ("text.literal".into(), rgba(0xb4593bff).into()), - ("string.escape".into(), rgba(0x8a8585ff).into()), - ( - "function.special.definition".into(), - rgba(0xa06d3aff).into(), - ), - ("keyword".into(), rgba(0x8464c4ff).into()), - ("link_uri".into(), rgba(0x4b8b8bff).into()), - ("number".into(), rgba(0xb4593bff).into()), - ("punctuation.bracket".into(), rgba(0x8a8585ff).into()), - ("string.special.symbol".into(), rgba(0x4b8b8bff).into()), - ("punctuation.list_marker".into(), rgba(0xe7dfdfff).into()), - ], - }, - status_bar: rgba(0x3b3535ff).into(), - title_bar: rgba(0x3b3535ff).into(), - toolbar: rgba(0x1b1818ff).into(), - tab_bar: rgba(0x252020ff).into(), - editor: rgba(0x1b1818ff).into(), - editor_subheader: rgba(0x252020ff).into(), - editor_active_line: rgba(0x252020ff).into(), - terminal: rgba(0x1b1818ff).into(), - image_fallback_background: rgba(0x3b3535ff).into(), - git_created: rgba(0x4b8b8bff).into(), - git_modified: rgba(0x7272caff).into(), - git_deleted: rgba(0xca4848ff).into(), - git_conflict: rgba(0xa06d3aff).into(), - git_ignored: rgba(0x756e6eff).into(), - git_renamed: rgba(0xa06d3aff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x7272caff).into(), - selection: rgba(0x7272ca3d).into(), - }, - PlayerTheme { - cursor: rgba(0x4b8b8bff).into(), - selection: rgba(0x4b8b8b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xbd5187ff).into(), - selection: rgba(0xbd51873d).into(), - }, - PlayerTheme { - cursor: rgba(0xb4593bff).into(), - selection: rgba(0xb4593b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x8464c4ff).into(), - selection: rgba(0x8464c43d).into(), - }, - PlayerTheme { - cursor: rgba(0x5485b6ff).into(), - selection: rgba(0x5485b63d).into(), - }, - PlayerTheme { - cursor: rgba(0xca4848ff).into(), - selection: rgba(0xca48483d).into(), - }, - PlayerTheme { - cursor: rgba(0xa06d3aff).into(), - selection: rgba(0xa06d3a3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_plateau_light.rs b/crates/theme2/src/themes/atelier_plateau_light.rs deleted file mode 100644 index 68f100dd85..0000000000 --- a/crates/theme2/src/themes/atelier_plateau_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_plateau_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Plateau Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x8e8989ff).into(), - border_variant: rgba(0x8e8989ff).into(), - border_focused: rgba(0xcecaecff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xc1bbbbff).into(), - surface: rgba(0xebe3e3ff).into(), - background: rgba(0xc1bbbbff).into(), - filled_element: rgba(0xc1bbbbff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe4e1f5ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe4e1f5ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x1b1818ff).into(), - text_muted: rgba(0x5a5252ff).into(), - text_placeholder: rgba(0xca4a4aff).into(), - text_disabled: rgba(0x6e6666ff).into(), - text_accent: rgba(0x7272caff).into(), - icon_muted: rgba(0x5a5252ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("text.literal".into(), rgba(0xb45a3cff).into()), - ("punctuation.special".into(), rgba(0xbd5187ff).into()), - ("variant".into(), rgba(0xa06d3aff).into()), - ("punctuation".into(), rgba(0x292424ff).into()), - ("string.escape".into(), rgba(0x585050ff).into()), - ("emphasis".into(), rgba(0x7272caff).into()), - ("title".into(), rgba(0x1b1818ff).into()), - ("constructor".into(), rgba(0x7272caff).into()), - ("variable".into(), rgba(0x292424ff).into()), - ("predictive".into(), rgba(0xa27a91ff).into()), - ("label".into(), rgba(0x7272caff).into()), - ("function.method".into(), rgba(0x7272caff).into()), - ("link_uri".into(), rgba(0x4c8b8bff).into()), - ("punctuation.delimiter".into(), rgba(0x585050ff).into()), - ("link_text".into(), rgba(0xb45a3cff).into()), - ("hint".into(), rgba(0x91697fff).into()), - ("emphasis.strong".into(), rgba(0x7272caff).into()), - ("attribute".into(), rgba(0x7272caff).into()), - ("boolean".into(), rgba(0x4c8b8bff).into()), - ("string.special.symbol".into(), rgba(0x4b8b8bff).into()), - ("string".into(), rgba(0x4b8b8bff).into()), - ("type".into(), rgba(0xa06d3aff).into()), - ("string.regex".into(), rgba(0x5485b6ff).into()), - ("comment.doc".into(), rgba(0x585050ff).into()), - ("string.special".into(), rgba(0xbd5187ff).into()), - ("property".into(), rgba(0xca4848ff).into()), - ("preproc".into(), rgba(0x1b1818ff).into()), - ("embedded".into(), rgba(0x1b1818ff).into()), - ("comment".into(), rgba(0x7e7777ff).into()), - ("primary".into(), rgba(0x292424ff).into()), - ("number".into(), rgba(0xb4593bff).into()), - ("function".into(), rgba(0x7272caff).into()), - ("punctuation.bracket".into(), rgba(0x585050ff).into()), - ("tag".into(), rgba(0x7272caff).into()), - ("punctuation.list_marker".into(), rgba(0x292424ff).into()), - ( - "function.special.definition".into(), - rgba(0xa06d3aff).into(), - ), - ("enum".into(), rgba(0xb45a3cff).into()), - ("keyword".into(), rgba(0x8464c4ff).into()), - ("operator".into(), rgba(0x585050ff).into()), - ("variable.special".into(), rgba(0x8464c4ff).into()), - ("constant".into(), rgba(0x4c8b8bff).into()), - ], - }, - status_bar: rgba(0xc1bbbbff).into(), - title_bar: rgba(0xc1bbbbff).into(), - toolbar: rgba(0xf4ececff).into(), - tab_bar: rgba(0xebe3e3ff).into(), - editor: rgba(0xf4ececff).into(), - editor_subheader: rgba(0xebe3e3ff).into(), - editor_active_line: rgba(0xebe3e3ff).into(), - terminal: rgba(0xf4ececff).into(), - image_fallback_background: rgba(0xc1bbbbff).into(), - git_created: rgba(0x4c8b8bff).into(), - git_modified: rgba(0x7272caff).into(), - git_deleted: rgba(0xca4a4aff).into(), - git_conflict: rgba(0xa06e3bff).into(), - git_ignored: rgba(0x6e6666ff).into(), - git_renamed: rgba(0xa06e3bff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x7272caff).into(), - selection: rgba(0x7272ca3d).into(), - }, - PlayerTheme { - cursor: rgba(0x4c8b8bff).into(), - selection: rgba(0x4c8b8b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xbd5186ff).into(), - selection: rgba(0xbd51863d).into(), - }, - PlayerTheme { - cursor: rgba(0xb45a3cff).into(), - selection: rgba(0xb45a3c3d).into(), - }, - PlayerTheme { - cursor: rgba(0x8464c4ff).into(), - selection: rgba(0x8464c43d).into(), - }, - PlayerTheme { - cursor: rgba(0x5485b5ff).into(), - selection: rgba(0x5485b53d).into(), - }, - PlayerTheme { - cursor: rgba(0xca4a4aff).into(), - selection: rgba(0xca4a4a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xa06e3bff).into(), - selection: rgba(0xa06e3b3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_savanna_dark.rs b/crates/theme2/src/themes/atelier_savanna_dark.rs deleted file mode 100644 index d4040db958..0000000000 --- a/crates/theme2/src/themes/atelier_savanna_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_savanna_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Savanna Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x505e55ff).into(), - border_variant: rgba(0x505e55ff).into(), - border_focused: rgba(0x1f3233ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x353f39ff).into(), - surface: rgba(0x1f2621ff).into(), - background: rgba(0x353f39ff).into(), - filled_element: rgba(0x353f39ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x151e20ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x151e20ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xecf4eeff).into(), - text_muted: rgba(0x859188ff).into(), - text_placeholder: rgba(0xb16038ff).into(), - text_disabled: rgba(0x6f7e74ff).into(), - text_accent: rgba(0x468b8fff).into(), - icon_muted: rgba(0x859188ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("function.method".into(), rgba(0x468b8fff).into()), - ("title".into(), rgba(0xecf4eeff).into()), - ("label".into(), rgba(0x468b8fff).into()), - ("text.literal".into(), rgba(0x9f703bff).into()), - ("boolean".into(), rgba(0x479962ff).into()), - ("punctuation.list_marker".into(), rgba(0xdfe7e2ff).into()), - ("string.escape".into(), rgba(0x87928aff).into()), - ("string.special".into(), rgba(0x857368ff).into()), - ("punctuation.delimiter".into(), rgba(0x87928aff).into()), - ("tag".into(), rgba(0x468b8fff).into()), - ("property".into(), rgba(0xb16038ff).into()), - ("preproc".into(), rgba(0xecf4eeff).into()), - ("primary".into(), rgba(0xdfe7e2ff).into()), - ("link_uri".into(), rgba(0x479962ff).into()), - ("comment".into(), rgba(0x5f6d64ff).into()), - ("type".into(), rgba(0xa07d3aff).into()), - ("hint".into(), rgba(0x607e76ff).into()), - ("punctuation".into(), rgba(0xdfe7e2ff).into()), - ("string.special.symbol".into(), rgba(0x479962ff).into()), - ("emphasis.strong".into(), rgba(0x468b8fff).into()), - ("keyword".into(), rgba(0x55859bff).into()), - ("comment.doc".into(), rgba(0x87928aff).into()), - ("punctuation.bracket".into(), rgba(0x87928aff).into()), - ("constant".into(), rgba(0x479962ff).into()), - ("link_text".into(), rgba(0x9f703bff).into()), - ("number".into(), rgba(0x9f703bff).into()), - ("function".into(), rgba(0x468b8fff).into()), - ("variable".into(), rgba(0xdfe7e2ff).into()), - ("emphasis".into(), rgba(0x468b8fff).into()), - ("punctuation.special".into(), rgba(0x857368ff).into()), - ("constructor".into(), rgba(0x468b8fff).into()), - ("variable.special".into(), rgba(0x55859bff).into()), - ("operator".into(), rgba(0x87928aff).into()), - ("enum".into(), rgba(0x9f703bff).into()), - ("string.regex".into(), rgba(0x1b9aa0ff).into()), - ("attribute".into(), rgba(0x468b8fff).into()), - ("predictive".into(), rgba(0x506d66ff).into()), - ("string".into(), rgba(0x479962ff).into()), - ("embedded".into(), rgba(0xecf4eeff).into()), - ("variant".into(), rgba(0xa07d3aff).into()), - ( - "function.special.definition".into(), - rgba(0xa07d3aff).into(), - ), - ], - }, - status_bar: rgba(0x353f39ff).into(), - title_bar: rgba(0x353f39ff).into(), - toolbar: rgba(0x171c19ff).into(), - tab_bar: rgba(0x1f2621ff).into(), - editor: rgba(0x171c19ff).into(), - editor_subheader: rgba(0x1f2621ff).into(), - editor_active_line: rgba(0x1f2621ff).into(), - terminal: rgba(0x171c19ff).into(), - image_fallback_background: rgba(0x353f39ff).into(), - git_created: rgba(0x479962ff).into(), - git_modified: rgba(0x468b8fff).into(), - git_deleted: rgba(0xb16038ff).into(), - git_conflict: rgba(0xa07d3aff).into(), - git_ignored: rgba(0x6f7e74ff).into(), - git_renamed: rgba(0xa07d3aff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x468b8fff).into(), - selection: rgba(0x468b8f3d).into(), - }, - PlayerTheme { - cursor: rgba(0x479962ff).into(), - selection: rgba(0x4799623d).into(), - }, - PlayerTheme { - cursor: rgba(0x857368ff).into(), - selection: rgba(0x8573683d).into(), - }, - PlayerTheme { - cursor: rgba(0x9f703bff).into(), - selection: rgba(0x9f703b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x55859bff).into(), - selection: rgba(0x55859b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x1d9aa0ff).into(), - selection: rgba(0x1d9aa03d).into(), - }, - PlayerTheme { - cursor: rgba(0xb16038ff).into(), - selection: rgba(0xb160383d).into(), - }, - PlayerTheme { - cursor: rgba(0xa07d3aff).into(), - selection: rgba(0xa07d3a3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_savanna_light.rs b/crates/theme2/src/themes/atelier_savanna_light.rs deleted file mode 100644 index 08722cd91c..0000000000 --- a/crates/theme2/src/themes/atelier_savanna_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_savanna_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Savanna Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x8b968eff).into(), - border_variant: rgba(0x8b968eff).into(), - border_focused: rgba(0xbed4d6ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xbcc5bfff).into(), - surface: rgba(0xe3ebe6ff).into(), - background: rgba(0xbcc5bfff).into(), - filled_element: rgba(0xbcc5bfff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdae7e8ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdae7e8ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x171c19ff).into(), - text_muted: rgba(0x546259ff).into(), - text_placeholder: rgba(0xb16139ff).into(), - text_disabled: rgba(0x68766dff).into(), - text_accent: rgba(0x488b90ff).into(), - icon_muted: rgba(0x546259ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("text.literal".into(), rgba(0x9f713cff).into()), - ("string".into(), rgba(0x479962ff).into()), - ("punctuation.special".into(), rgba(0x857368ff).into()), - ("type".into(), rgba(0xa07d3aff).into()), - ("enum".into(), rgba(0x9f713cff).into()), - ("title".into(), rgba(0x171c19ff).into()), - ("comment".into(), rgba(0x77877cff).into()), - ("predictive".into(), rgba(0x75958bff).into()), - ("punctuation.list_marker".into(), rgba(0x232a25ff).into()), - ("string.special.symbol".into(), rgba(0x479962ff).into()), - ("constructor".into(), rgba(0x488b90ff).into()), - ("variable".into(), rgba(0x232a25ff).into()), - ("label".into(), rgba(0x488b90ff).into()), - ("attribute".into(), rgba(0x488b90ff).into()), - ("constant".into(), rgba(0x499963ff).into()), - ("function".into(), rgba(0x468b8fff).into()), - ("variable.special".into(), rgba(0x55859bff).into()), - ("keyword".into(), rgba(0x55859bff).into()), - ("number".into(), rgba(0x9f703bff).into()), - ("boolean".into(), rgba(0x499963ff).into()), - ("embedded".into(), rgba(0x171c19ff).into()), - ("string.special".into(), rgba(0x857368ff).into()), - ("emphasis.strong".into(), rgba(0x488b90ff).into()), - ("string.regex".into(), rgba(0x1b9aa0ff).into()), - ("hint".into(), rgba(0x66847cff).into()), - ("preproc".into(), rgba(0x171c19ff).into()), - ("link_uri".into(), rgba(0x499963ff).into()), - ("variant".into(), rgba(0xa07d3aff).into()), - ("function.method".into(), rgba(0x468b8fff).into()), - ("punctuation.bracket".into(), rgba(0x526057ff).into()), - ("punctuation.delimiter".into(), rgba(0x526057ff).into()), - ("punctuation".into(), rgba(0x232a25ff).into()), - ("primary".into(), rgba(0x232a25ff).into()), - ("string.escape".into(), rgba(0x526057ff).into()), - ("property".into(), rgba(0xb16038ff).into()), - ("operator".into(), rgba(0x526057ff).into()), - ("comment.doc".into(), rgba(0x526057ff).into()), - ( - "function.special.definition".into(), - rgba(0xa07d3aff).into(), - ), - ("link_text".into(), rgba(0x9f713cff).into()), - ("tag".into(), rgba(0x488b90ff).into()), - ("emphasis".into(), rgba(0x488b90ff).into()), - ], - }, - status_bar: rgba(0xbcc5bfff).into(), - title_bar: rgba(0xbcc5bfff).into(), - toolbar: rgba(0xecf4eeff).into(), - tab_bar: rgba(0xe3ebe6ff).into(), - editor: rgba(0xecf4eeff).into(), - editor_subheader: rgba(0xe3ebe6ff).into(), - editor_active_line: rgba(0xe3ebe6ff).into(), - terminal: rgba(0xecf4eeff).into(), - image_fallback_background: rgba(0xbcc5bfff).into(), - git_created: rgba(0x499963ff).into(), - git_modified: rgba(0x488b90ff).into(), - git_deleted: rgba(0xb16139ff).into(), - git_conflict: rgba(0xa07d3bff).into(), - git_ignored: rgba(0x68766dff).into(), - git_renamed: rgba(0xa07d3bff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x488b90ff).into(), - selection: rgba(0x488b903d).into(), - }, - PlayerTheme { - cursor: rgba(0x499963ff).into(), - selection: rgba(0x4999633d).into(), - }, - PlayerTheme { - cursor: rgba(0x857368ff).into(), - selection: rgba(0x8573683d).into(), - }, - PlayerTheme { - cursor: rgba(0x9f713cff).into(), - selection: rgba(0x9f713c3d).into(), - }, - PlayerTheme { - cursor: rgba(0x55859bff).into(), - selection: rgba(0x55859b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x1e9aa0ff).into(), - selection: rgba(0x1e9aa03d).into(), - }, - PlayerTheme { - cursor: rgba(0xb16139ff).into(), - selection: rgba(0xb161393d).into(), - }, - PlayerTheme { - cursor: rgba(0xa07d3bff).into(), - selection: rgba(0xa07d3b3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_seaside_dark.rs b/crates/theme2/src/themes/atelier_seaside_dark.rs deleted file mode 100644 index 475115e0d1..0000000000 --- a/crates/theme2/src/themes/atelier_seaside_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_seaside_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Seaside Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5c6c5cff).into(), - border_variant: rgba(0x5c6c5cff).into(), - border_focused: rgba(0x102667ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x3b453bff).into(), - surface: rgba(0x1f231fff).into(), - background: rgba(0x3b453bff).into(), - filled_element: rgba(0x3b453bff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x051949ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x051949ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf3faf3ff).into(), - text_muted: rgba(0x8ba48bff).into(), - text_placeholder: rgba(0xe61c3bff).into(), - text_disabled: rgba(0x778f77ff).into(), - text_accent: rgba(0x3e62f4ff).into(), - icon_muted: rgba(0x8ba48bff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("comment".into(), rgba(0x687d68ff).into()), - ("predictive".into(), rgba(0x00788bff).into()), - ("string.special".into(), rgba(0xe618c3ff).into()), - ("string.regex".into(), rgba(0x1899b3ff).into()), - ("boolean".into(), rgba(0x2aa329ff).into()), - ("string".into(), rgba(0x28a328ff).into()), - ("operator".into(), rgba(0x8ca68cff).into()), - ("primary".into(), rgba(0xcfe8cfff).into()), - ("number".into(), rgba(0x87711cff).into()), - ("punctuation.special".into(), rgba(0xe618c3ff).into()), - ("link_text".into(), rgba(0x87711dff).into()), - ("title".into(), rgba(0xf3faf3ff).into()), - ("comment.doc".into(), rgba(0x8ca68cff).into()), - ("label".into(), rgba(0x3e62f4ff).into()), - ("preproc".into(), rgba(0xf3faf3ff).into()), - ("punctuation.bracket".into(), rgba(0x8ca68cff).into()), - ("punctuation.delimiter".into(), rgba(0x8ca68cff).into()), - ("function.method".into(), rgba(0x3d62f5ff).into()), - ("tag".into(), rgba(0x3e62f4ff).into()), - ("embedded".into(), rgba(0xf3faf3ff).into()), - ("text.literal".into(), rgba(0x87711dff).into()), - ("punctuation".into(), rgba(0xcfe8cfff).into()), - ("string.special.symbol".into(), rgba(0x28a328ff).into()), - ("link_uri".into(), rgba(0x2aa329ff).into()), - ("keyword".into(), rgba(0xac2aeeff).into()), - ("function".into(), rgba(0x3d62f5ff).into()), - ("string.escape".into(), rgba(0x8ca68cff).into()), - ("variant".into(), rgba(0x98981bff).into()), - ( - "function.special.definition".into(), - rgba(0x98981bff).into(), - ), - ("constructor".into(), rgba(0x3e62f4ff).into()), - ("constant".into(), rgba(0x2aa329ff).into()), - ("hint".into(), rgba(0x008b9fff).into()), - ("type".into(), rgba(0x98981bff).into()), - ("emphasis".into(), rgba(0x3e62f4ff).into()), - ("variable".into(), rgba(0xcfe8cfff).into()), - ("emphasis.strong".into(), rgba(0x3e62f4ff).into()), - ("attribute".into(), rgba(0x3e62f4ff).into()), - ("enum".into(), rgba(0x87711dff).into()), - ("property".into(), rgba(0xe6183bff).into()), - ("punctuation.list_marker".into(), rgba(0xcfe8cfff).into()), - ("variable.special".into(), rgba(0xac2aeeff).into()), - ], - }, - status_bar: rgba(0x3b453bff).into(), - title_bar: rgba(0x3b453bff).into(), - toolbar: rgba(0x131513ff).into(), - tab_bar: rgba(0x1f231fff).into(), - editor: rgba(0x131513ff).into(), - editor_subheader: rgba(0x1f231fff).into(), - editor_active_line: rgba(0x1f231fff).into(), - terminal: rgba(0x131513ff).into(), - image_fallback_background: rgba(0x3b453bff).into(), - git_created: rgba(0x2aa329ff).into(), - git_modified: rgba(0x3e62f4ff).into(), - git_deleted: rgba(0xe61c3bff).into(), - git_conflict: rgba(0x98981bff).into(), - git_ignored: rgba(0x778f77ff).into(), - git_renamed: rgba(0x98981bff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x3e62f4ff).into(), - selection: rgba(0x3e62f43d).into(), - }, - PlayerTheme { - cursor: rgba(0x2aa329ff).into(), - selection: rgba(0x2aa3293d).into(), - }, - PlayerTheme { - cursor: rgba(0xe61cc3ff).into(), - selection: rgba(0xe61cc33d).into(), - }, - PlayerTheme { - cursor: rgba(0x87711dff).into(), - selection: rgba(0x87711d3d).into(), - }, - PlayerTheme { - cursor: rgba(0xac2dedff).into(), - selection: rgba(0xac2ded3d).into(), - }, - PlayerTheme { - cursor: rgba(0x1b99b3ff).into(), - selection: rgba(0x1b99b33d).into(), - }, - PlayerTheme { - cursor: rgba(0xe61c3bff).into(), - selection: rgba(0xe61c3b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x98981bff).into(), - selection: rgba(0x98981b3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_seaside_light.rs b/crates/theme2/src/themes/atelier_seaside_light.rs deleted file mode 100644 index 557134b540..0000000000 --- a/crates/theme2/src/themes/atelier_seaside_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_seaside_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Seaside Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x8ea88eff).into(), - border_variant: rgba(0x8ea88eff).into(), - border_focused: rgba(0xc9c4fdff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xb4ceb4ff).into(), - surface: rgba(0xdaeedaff).into(), - background: rgba(0xb4ceb4ff).into(), - filled_element: rgba(0xb4ceb4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe1ddfeff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe1ddfeff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x131513ff).into(), - text_muted: rgba(0x5f705fff).into(), - text_placeholder: rgba(0xe61c3dff).into(), - text_disabled: rgba(0x718771ff).into(), - text_accent: rgba(0x3e61f4ff).into(), - icon_muted: rgba(0x5f705fff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.escape".into(), rgba(0x5e6e5eff).into()), - ("boolean".into(), rgba(0x2aa32aff).into()), - ("string.special".into(), rgba(0xe618c3ff).into()), - ("comment".into(), rgba(0x809980ff).into()), - ("number".into(), rgba(0x87711cff).into()), - ("comment.doc".into(), rgba(0x5e6e5eff).into()), - ("tag".into(), rgba(0x3e61f4ff).into()), - ("string.special.symbol".into(), rgba(0x28a328ff).into()), - ("primary".into(), rgba(0x242924ff).into()), - ("string".into(), rgba(0x28a328ff).into()), - ("enum".into(), rgba(0x87711fff).into()), - ("operator".into(), rgba(0x5e6e5eff).into()), - ("string.regex".into(), rgba(0x1899b3ff).into()), - ("keyword".into(), rgba(0xac2aeeff).into()), - ("emphasis".into(), rgba(0x3e61f4ff).into()), - ("link_uri".into(), rgba(0x2aa32aff).into()), - ("constant".into(), rgba(0x2aa32aff).into()), - ("constructor".into(), rgba(0x3e61f4ff).into()), - ("link_text".into(), rgba(0x87711fff).into()), - ("emphasis.strong".into(), rgba(0x3e61f4ff).into()), - ("punctuation.list_marker".into(), rgba(0x242924ff).into()), - ("punctuation.delimiter".into(), rgba(0x5e6e5eff).into()), - ("punctuation.special".into(), rgba(0xe618c3ff).into()), - ("variant".into(), rgba(0x98981bff).into()), - ("predictive".into(), rgba(0x00a2b5ff).into()), - ("attribute".into(), rgba(0x3e61f4ff).into()), - ("preproc".into(), rgba(0x131513ff).into()), - ("embedded".into(), rgba(0x131513ff).into()), - ("punctuation".into(), rgba(0x242924ff).into()), - ("label".into(), rgba(0x3e61f4ff).into()), - ("function.method".into(), rgba(0x3d62f5ff).into()), - ("property".into(), rgba(0xe6183bff).into()), - ("title".into(), rgba(0x131513ff).into()), - ("variable".into(), rgba(0x242924ff).into()), - ("function".into(), rgba(0x3d62f5ff).into()), - ("variable.special".into(), rgba(0xac2aeeff).into()), - ("type".into(), rgba(0x98981bff).into()), - ("text.literal".into(), rgba(0x87711fff).into()), - ("hint".into(), rgba(0x008fa1ff).into()), - ( - "function.special.definition".into(), - rgba(0x98981bff).into(), - ), - ("punctuation.bracket".into(), rgba(0x5e6e5eff).into()), - ], - }, - status_bar: rgba(0xb4ceb4ff).into(), - title_bar: rgba(0xb4ceb4ff).into(), - toolbar: rgba(0xf3faf3ff).into(), - tab_bar: rgba(0xdaeedaff).into(), - editor: rgba(0xf3faf3ff).into(), - editor_subheader: rgba(0xdaeedaff).into(), - editor_active_line: rgba(0xdaeedaff).into(), - terminal: rgba(0xf3faf3ff).into(), - image_fallback_background: rgba(0xb4ceb4ff).into(), - git_created: rgba(0x2aa32aff).into(), - git_modified: rgba(0x3e61f4ff).into(), - git_deleted: rgba(0xe61c3dff).into(), - git_conflict: rgba(0x98981cff).into(), - git_ignored: rgba(0x718771ff).into(), - git_renamed: rgba(0x98981cff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x3e61f4ff).into(), - selection: rgba(0x3e61f43d).into(), - }, - PlayerTheme { - cursor: rgba(0x2aa32aff).into(), - selection: rgba(0x2aa32a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xe61cc2ff).into(), - selection: rgba(0xe61cc23d).into(), - }, - PlayerTheme { - cursor: rgba(0x87711fff).into(), - selection: rgba(0x87711f3d).into(), - }, - PlayerTheme { - cursor: rgba(0xac2dedff).into(), - selection: rgba(0xac2ded3d).into(), - }, - PlayerTheme { - cursor: rgba(0x1c99b3ff).into(), - selection: rgba(0x1c99b33d).into(), - }, - PlayerTheme { - cursor: rgba(0xe61c3dff).into(), - selection: rgba(0xe61c3d3d).into(), - }, - PlayerTheme { - cursor: rgba(0x98981cff).into(), - selection: rgba(0x98981c3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_sulphurpool_dark.rs b/crates/theme2/src/themes/atelier_sulphurpool_dark.rs deleted file mode 100644 index 8be8451740..0000000000 --- a/crates/theme2/src/themes/atelier_sulphurpool_dark.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_sulphurpool_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Sulphurpool Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5b6385ff).into(), - border_variant: rgba(0x5b6385ff).into(), - border_focused: rgba(0x203348ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x3e4769ff).into(), - surface: rgba(0x262f51ff).into(), - background: rgba(0x3e4769ff).into(), - filled_element: rgba(0x3e4769ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x161f2bff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x161f2bff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf5f7ffff).into(), - text_muted: rgba(0x959bb2ff).into(), - text_placeholder: rgba(0xc94922ff).into(), - text_disabled: rgba(0x7e849eff).into(), - text_accent: rgba(0x3e8ed0ff).into(), - icon_muted: rgba(0x959bb2ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("title".into(), rgba(0xf5f7ffff).into()), - ("constructor".into(), rgba(0x3e8ed0ff).into()), - ("type".into(), rgba(0xc08b2fff).into()), - ("punctuation.list_marker".into(), rgba(0xdfe2f1ff).into()), - ("property".into(), rgba(0xc94821ff).into()), - ("link_uri".into(), rgba(0xac9739ff).into()), - ("string.escape".into(), rgba(0x979db4ff).into()), - ("constant".into(), rgba(0xac9739ff).into()), - ("embedded".into(), rgba(0xf5f7ffff).into()), - ("punctuation.special".into(), rgba(0x9b6279ff).into()), - ("punctuation.bracket".into(), rgba(0x979db4ff).into()), - ("preproc".into(), rgba(0xf5f7ffff).into()), - ("emphasis.strong".into(), rgba(0x3e8ed0ff).into()), - ("emphasis".into(), rgba(0x3e8ed0ff).into()), - ("enum".into(), rgba(0xc76a29ff).into()), - ("boolean".into(), rgba(0xac9739ff).into()), - ("primary".into(), rgba(0xdfe2f1ff).into()), - ("function.method".into(), rgba(0x3d8fd1ff).into()), - ( - "function.special.definition".into(), - rgba(0xc08b2fff).into(), - ), - ("comment.doc".into(), rgba(0x979db4ff).into()), - ("string".into(), rgba(0xac9738ff).into()), - ("text.literal".into(), rgba(0xc76a29ff).into()), - ("operator".into(), rgba(0x979db4ff).into()), - ("number".into(), rgba(0xc76a28ff).into()), - ("string.special".into(), rgba(0x9b6279ff).into()), - ("punctuation.delimiter".into(), rgba(0x979db4ff).into()), - ("tag".into(), rgba(0x3e8ed0ff).into()), - ("string.special.symbol".into(), rgba(0xac9738ff).into()), - ("variable".into(), rgba(0xdfe2f1ff).into()), - ("attribute".into(), rgba(0x3e8ed0ff).into()), - ("punctuation".into(), rgba(0xdfe2f1ff).into()), - ("string.regex".into(), rgba(0x21a2c9ff).into()), - ("keyword".into(), rgba(0x6679ccff).into()), - ("label".into(), rgba(0x3e8ed0ff).into()), - ("hint".into(), rgba(0x6c81a5ff).into()), - ("function".into(), rgba(0x3d8fd1ff).into()), - ("link_text".into(), rgba(0xc76a29ff).into()), - ("variant".into(), rgba(0xc08b2fff).into()), - ("variable.special".into(), rgba(0x6679ccff).into()), - ("predictive".into(), rgba(0x58709aff).into()), - ("comment".into(), rgba(0x6a7293ff).into()), - ], - }, - status_bar: rgba(0x3e4769ff).into(), - title_bar: rgba(0x3e4769ff).into(), - toolbar: rgba(0x202646ff).into(), - tab_bar: rgba(0x262f51ff).into(), - editor: rgba(0x202646ff).into(), - editor_subheader: rgba(0x262f51ff).into(), - editor_active_line: rgba(0x262f51ff).into(), - terminal: rgba(0x202646ff).into(), - image_fallback_background: rgba(0x3e4769ff).into(), - git_created: rgba(0xac9739ff).into(), - git_modified: rgba(0x3e8ed0ff).into(), - git_deleted: rgba(0xc94922ff).into(), - git_conflict: rgba(0xc08b30ff).into(), - git_ignored: rgba(0x7e849eff).into(), - git_renamed: rgba(0xc08b30ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x3e8ed0ff).into(), - selection: rgba(0x3e8ed03d).into(), - }, - PlayerTheme { - cursor: rgba(0xac9739ff).into(), - selection: rgba(0xac97393d).into(), - }, - PlayerTheme { - cursor: rgba(0x9b6279ff).into(), - selection: rgba(0x9b62793d).into(), - }, - PlayerTheme { - cursor: rgba(0xc76a29ff).into(), - selection: rgba(0xc76a293d).into(), - }, - PlayerTheme { - cursor: rgba(0x6679ccff).into(), - selection: rgba(0x6679cc3d).into(), - }, - PlayerTheme { - cursor: rgba(0x24a1c9ff).into(), - selection: rgba(0x24a1c93d).into(), - }, - PlayerTheme { - cursor: rgba(0xc94922ff).into(), - selection: rgba(0xc949223d).into(), - }, - PlayerTheme { - cursor: rgba(0xc08b30ff).into(), - selection: rgba(0xc08b303d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/atelier_sulphurpool_light.rs b/crates/theme2/src/themes/atelier_sulphurpool_light.rs deleted file mode 100644 index dba723331a..0000000000 --- a/crates/theme2/src/themes/atelier_sulphurpool_light.rs +++ /dev/null @@ -1,136 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn atelier_sulphurpool_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Atelier Sulphurpool Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x9a9fb6ff).into(), - border_variant: rgba(0x9a9fb6ff).into(), - border_focused: rgba(0xc2d5efff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xc1c5d8ff).into(), - surface: rgba(0xe5e8f5ff).into(), - background: rgba(0xc1c5d8ff).into(), - filled_element: rgba(0xc1c5d8ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdde7f6ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdde7f6ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x202646ff).into(), - text_muted: rgba(0x5f6789ff).into(), - text_placeholder: rgba(0xc94922ff).into(), - text_disabled: rgba(0x767d9aff).into(), - text_accent: rgba(0x3e8fd0ff).into(), - icon_muted: rgba(0x5f6789ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.special".into(), rgba(0x9b6279ff).into()), - ("string.regex".into(), rgba(0x21a2c9ff).into()), - ("embedded".into(), rgba(0x202646ff).into()), - ("string".into(), rgba(0xac9738ff).into()), - ( - "function.special.definition".into(), - rgba(0xc08b2fff).into(), - ), - ("hint".into(), rgba(0x7087b2ff).into()), - ("function.method".into(), rgba(0x3d8fd1ff).into()), - ("punctuation.list_marker".into(), rgba(0x293256ff).into()), - ("punctuation".into(), rgba(0x293256ff).into()), - ("constant".into(), rgba(0xac9739ff).into()), - ("label".into(), rgba(0x3e8fd0ff).into()), - ("comment.doc".into(), rgba(0x5d6587ff).into()), - ("property".into(), rgba(0xc94821ff).into()), - ("punctuation.bracket".into(), rgba(0x5d6587ff).into()), - ("constructor".into(), rgba(0x3e8fd0ff).into()), - ("variable.special".into(), rgba(0x6679ccff).into()), - ("emphasis".into(), rgba(0x3e8fd0ff).into()), - ("link_text".into(), rgba(0xc76a29ff).into()), - ("keyword".into(), rgba(0x6679ccff).into()), - ("primary".into(), rgba(0x293256ff).into()), - ("comment".into(), rgba(0x898ea4ff).into()), - ("title".into(), rgba(0x202646ff).into()), - ("link_uri".into(), rgba(0xac9739ff).into()), - ("text.literal".into(), rgba(0xc76a29ff).into()), - ("operator".into(), rgba(0x5d6587ff).into()), - ("number".into(), rgba(0xc76a28ff).into()), - ("preproc".into(), rgba(0x202646ff).into()), - ("attribute".into(), rgba(0x3e8fd0ff).into()), - ("emphasis.strong".into(), rgba(0x3e8fd0ff).into()), - ("string.escape".into(), rgba(0x5d6587ff).into()), - ("tag".into(), rgba(0x3e8fd0ff).into()), - ("variable".into(), rgba(0x293256ff).into()), - ("predictive".into(), rgba(0x8599beff).into()), - ("enum".into(), rgba(0xc76a29ff).into()), - ("string.special.symbol".into(), rgba(0xac9738ff).into()), - ("punctuation.delimiter".into(), rgba(0x5d6587ff).into()), - ("function".into(), rgba(0x3d8fd1ff).into()), - ("type".into(), rgba(0xc08b2fff).into()), - ("punctuation.special".into(), rgba(0x9b6279ff).into()), - ("variant".into(), rgba(0xc08b2fff).into()), - ("boolean".into(), rgba(0xac9739ff).into()), - ], - }, - status_bar: rgba(0xc1c5d8ff).into(), - title_bar: rgba(0xc1c5d8ff).into(), - toolbar: rgba(0xf5f7ffff).into(), - tab_bar: rgba(0xe5e8f5ff).into(), - editor: rgba(0xf5f7ffff).into(), - editor_subheader: rgba(0xe5e8f5ff).into(), - editor_active_line: rgba(0xe5e8f5ff).into(), - terminal: rgba(0xf5f7ffff).into(), - image_fallback_background: rgba(0xc1c5d8ff).into(), - git_created: rgba(0xac9739ff).into(), - git_modified: rgba(0x3e8fd0ff).into(), - git_deleted: rgba(0xc94922ff).into(), - git_conflict: rgba(0xc08b30ff).into(), - git_ignored: rgba(0x767d9aff).into(), - git_renamed: rgba(0xc08b30ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x3e8fd0ff).into(), - selection: rgba(0x3e8fd03d).into(), - }, - PlayerTheme { - cursor: rgba(0xac9739ff).into(), - selection: rgba(0xac97393d).into(), - }, - PlayerTheme { - cursor: rgba(0x9b6279ff).into(), - selection: rgba(0x9b62793d).into(), - }, - PlayerTheme { - cursor: rgba(0xc76a29ff).into(), - selection: rgba(0xc76a293d).into(), - }, - PlayerTheme { - cursor: rgba(0x6679cbff).into(), - selection: rgba(0x6679cb3d).into(), - }, - PlayerTheme { - cursor: rgba(0x24a1c9ff).into(), - selection: rgba(0x24a1c93d).into(), - }, - PlayerTheme { - cursor: rgba(0xc94922ff).into(), - selection: rgba(0xc949223d).into(), - }, - PlayerTheme { - cursor: rgba(0xc08b30ff).into(), - selection: rgba(0xc08b303d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/ayu_dark.rs b/crates/theme2/src/themes/ayu_dark.rs deleted file mode 100644 index 35d3a43154..0000000000 --- a/crates/theme2/src/themes/ayu_dark.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn ayu_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Ayu Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x3f4043ff).into(), - border_variant: rgba(0x3f4043ff).into(), - border_focused: rgba(0x1b4a6eff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x313337ff).into(), - surface: rgba(0x1f2127ff).into(), - background: rgba(0x313337ff).into(), - filled_element: rgba(0x313337ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x0d2f4eff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x0d2f4eff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xbfbdb6ff).into(), - text_muted: rgba(0x8a8986ff).into(), - text_placeholder: rgba(0xef7177ff).into(), - text_disabled: rgba(0x696a6aff).into(), - text_accent: rgba(0x5ac1feff).into(), - icon_muted: rgba(0x8a8986ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("emphasis".into(), rgba(0x5ac1feff).into()), - ("punctuation.bracket".into(), rgba(0xa6a5a0ff).into()), - ("constructor".into(), rgba(0x5ac1feff).into()), - ("predictive".into(), rgba(0x5a728bff).into()), - ("emphasis.strong".into(), rgba(0x5ac1feff).into()), - ("string.regex".into(), rgba(0x95e6cbff).into()), - ("tag".into(), rgba(0x5ac1feff).into()), - ("punctuation".into(), rgba(0xa6a5a0ff).into()), - ("number".into(), rgba(0xd2a6ffff).into()), - ("punctuation.special".into(), rgba(0xd2a6ffff).into()), - ("primary".into(), rgba(0xbfbdb6ff).into()), - ("boolean".into(), rgba(0xd2a6ffff).into()), - ("variant".into(), rgba(0x5ac1feff).into()), - ("link_uri".into(), rgba(0xaad84cff).into()), - ("comment.doc".into(), rgba(0x8c8b88ff).into()), - ("title".into(), rgba(0xbfbdb6ff).into()), - ("text.literal".into(), rgba(0xfe8f40ff).into()), - ("link_text".into(), rgba(0xfe8f40ff).into()), - ("punctuation.delimiter".into(), rgba(0xa6a5a0ff).into()), - ("string.escape".into(), rgba(0x8c8b88ff).into()), - ("hint".into(), rgba(0x628b80ff).into()), - ("type".into(), rgba(0x59c2ffff).into()), - ("variable".into(), rgba(0xbfbdb6ff).into()), - ("label".into(), rgba(0x5ac1feff).into()), - ("enum".into(), rgba(0xfe8f40ff).into()), - ("operator".into(), rgba(0xf29668ff).into()), - ("function".into(), rgba(0xffb353ff).into()), - ("preproc".into(), rgba(0xbfbdb6ff).into()), - ("embedded".into(), rgba(0xbfbdb6ff).into()), - ("string".into(), rgba(0xa9d94bff).into()), - ("attribute".into(), rgba(0x5ac1feff).into()), - ("keyword".into(), rgba(0xff8f3fff).into()), - ("string.special.symbol".into(), rgba(0xfe8f40ff).into()), - ("comment".into(), rgba(0xabb5be8c).into()), - ("property".into(), rgba(0x5ac1feff).into()), - ("punctuation.list_marker".into(), rgba(0xa6a5a0ff).into()), - ("constant".into(), rgba(0xd2a6ffff).into()), - ("string.special".into(), rgba(0xe5b572ff).into()), - ], - }, - status_bar: rgba(0x313337ff).into(), - title_bar: rgba(0x313337ff).into(), - toolbar: rgba(0x0d1016ff).into(), - tab_bar: rgba(0x1f2127ff).into(), - editor: rgba(0x0d1016ff).into(), - editor_subheader: rgba(0x1f2127ff).into(), - editor_active_line: rgba(0x1f2127ff).into(), - terminal: rgba(0x0d1016ff).into(), - image_fallback_background: rgba(0x313337ff).into(), - git_created: rgba(0xaad84cff).into(), - git_modified: rgba(0x5ac1feff).into(), - git_deleted: rgba(0xef7177ff).into(), - git_conflict: rgba(0xfeb454ff).into(), - git_ignored: rgba(0x696a6aff).into(), - git_renamed: rgba(0xfeb454ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x5ac1feff).into(), - selection: rgba(0x5ac1fe3d).into(), - }, - PlayerTheme { - cursor: rgba(0xaad84cff).into(), - selection: rgba(0xaad84c3d).into(), - }, - PlayerTheme { - cursor: rgba(0x39bae5ff).into(), - selection: rgba(0x39bae53d).into(), - }, - PlayerTheme { - cursor: rgba(0xfe8f40ff).into(), - selection: rgba(0xfe8f403d).into(), - }, - PlayerTheme { - cursor: rgba(0xd2a6feff).into(), - selection: rgba(0xd2a6fe3d).into(), - }, - PlayerTheme { - cursor: rgba(0x95e5cbff).into(), - selection: rgba(0x95e5cb3d).into(), - }, - PlayerTheme { - cursor: rgba(0xef7177ff).into(), - selection: rgba(0xef71773d).into(), - }, - PlayerTheme { - cursor: rgba(0xfeb454ff).into(), - selection: rgba(0xfeb4543d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/ayu_light.rs b/crates/theme2/src/themes/ayu_light.rs deleted file mode 100644 index 887282e564..0000000000 --- a/crates/theme2/src/themes/ayu_light.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn ayu_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Ayu Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xcfd1d2ff).into(), - border_variant: rgba(0xcfd1d2ff).into(), - border_focused: rgba(0xc4daf6ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xdcdddeff).into(), - surface: rgba(0xececedff).into(), - background: rgba(0xdcdddeff).into(), - filled_element: rgba(0xdcdddeff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdeebfaff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdeebfaff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x5c6166ff).into(), - text_muted: rgba(0x8b8e92ff).into(), - text_placeholder: rgba(0xef7271ff).into(), - text_disabled: rgba(0xa9acaeff).into(), - text_accent: rgba(0x3b9ee5ff).into(), - icon_muted: rgba(0x8b8e92ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string".into(), rgba(0x86b300ff).into()), - ("enum".into(), rgba(0xf98d3fff).into()), - ("comment".into(), rgba(0x787b8099).into()), - ("comment.doc".into(), rgba(0x898d90ff).into()), - ("emphasis".into(), rgba(0x3b9ee5ff).into()), - ("keyword".into(), rgba(0xfa8d3eff).into()), - ("string.regex".into(), rgba(0x4bbf98ff).into()), - ("text.literal".into(), rgba(0xf98d3fff).into()), - ("string.escape".into(), rgba(0x898d90ff).into()), - ("link_text".into(), rgba(0xf98d3fff).into()), - ("punctuation".into(), rgba(0x73777bff).into()), - ("constructor".into(), rgba(0x3b9ee5ff).into()), - ("constant".into(), rgba(0xa37accff).into()), - ("variable".into(), rgba(0x5c6166ff).into()), - ("primary".into(), rgba(0x5c6166ff).into()), - ("emphasis.strong".into(), rgba(0x3b9ee5ff).into()), - ("string.special".into(), rgba(0xe6ba7eff).into()), - ("number".into(), rgba(0xa37accff).into()), - ("preproc".into(), rgba(0x5c6166ff).into()), - ("punctuation.delimiter".into(), rgba(0x73777bff).into()), - ("string.special.symbol".into(), rgba(0xf98d3fff).into()), - ("boolean".into(), rgba(0xa37accff).into()), - ("property".into(), rgba(0x3b9ee5ff).into()), - ("title".into(), rgba(0x5c6166ff).into()), - ("hint".into(), rgba(0x8ca7c2ff).into()), - ("predictive".into(), rgba(0x9eb9d3ff).into()), - ("operator".into(), rgba(0xed9365ff).into()), - ("type".into(), rgba(0x389ee6ff).into()), - ("function".into(), rgba(0xf2ad48ff).into()), - ("variant".into(), rgba(0x3b9ee5ff).into()), - ("label".into(), rgba(0x3b9ee5ff).into()), - ("punctuation.list_marker".into(), rgba(0x73777bff).into()), - ("punctuation.bracket".into(), rgba(0x73777bff).into()), - ("embedded".into(), rgba(0x5c6166ff).into()), - ("punctuation.special".into(), rgba(0xa37accff).into()), - ("attribute".into(), rgba(0x3b9ee5ff).into()), - ("tag".into(), rgba(0x3b9ee5ff).into()), - ("link_uri".into(), rgba(0x85b304ff).into()), - ], - }, - status_bar: rgba(0xdcdddeff).into(), - title_bar: rgba(0xdcdddeff).into(), - toolbar: rgba(0xfcfcfcff).into(), - tab_bar: rgba(0xececedff).into(), - editor: rgba(0xfcfcfcff).into(), - editor_subheader: rgba(0xececedff).into(), - editor_active_line: rgba(0xececedff).into(), - terminal: rgba(0xfcfcfcff).into(), - image_fallback_background: rgba(0xdcdddeff).into(), - git_created: rgba(0x85b304ff).into(), - git_modified: rgba(0x3b9ee5ff).into(), - git_deleted: rgba(0xef7271ff).into(), - git_conflict: rgba(0xf1ad49ff).into(), - git_ignored: rgba(0xa9acaeff).into(), - git_renamed: rgba(0xf1ad49ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x3b9ee5ff).into(), - selection: rgba(0x3b9ee53d).into(), - }, - PlayerTheme { - cursor: rgba(0x85b304ff).into(), - selection: rgba(0x85b3043d).into(), - }, - PlayerTheme { - cursor: rgba(0x55b4d3ff).into(), - selection: rgba(0x55b4d33d).into(), - }, - PlayerTheme { - cursor: rgba(0xf98d3fff).into(), - selection: rgba(0xf98d3f3d).into(), - }, - PlayerTheme { - cursor: rgba(0xa37accff).into(), - selection: rgba(0xa37acc3d).into(), - }, - PlayerTheme { - cursor: rgba(0x4dbf99ff).into(), - selection: rgba(0x4dbf993d).into(), - }, - PlayerTheme { - cursor: rgba(0xef7271ff).into(), - selection: rgba(0xef72713d).into(), - }, - PlayerTheme { - cursor: rgba(0xf1ad49ff).into(), - selection: rgba(0xf1ad493d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/ayu_mirage.rs b/crates/theme2/src/themes/ayu_mirage.rs deleted file mode 100644 index 2974881a18..0000000000 --- a/crates/theme2/src/themes/ayu_mirage.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn ayu_mirage() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Ayu Mirage".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x53565dff).into(), - border_variant: rgba(0x53565dff).into(), - border_focused: rgba(0x24556fff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x464a52ff).into(), - surface: rgba(0x353944ff).into(), - background: rgba(0x464a52ff).into(), - filled_element: rgba(0x464a52ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x123950ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x123950ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xcccac2ff).into(), - text_muted: rgba(0x9a9a98ff).into(), - text_placeholder: rgba(0xf18779ff).into(), - text_disabled: rgba(0x7b7d7fff).into(), - text_accent: rgba(0x72cffeff).into(), - icon_muted: rgba(0x9a9a98ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("text.literal".into(), rgba(0xfead66ff).into()), - ("link_text".into(), rgba(0xfead66ff).into()), - ("function".into(), rgba(0xffd173ff).into()), - ("punctuation.delimiter".into(), rgba(0xb4b3aeff).into()), - ("property".into(), rgba(0x72cffeff).into()), - ("title".into(), rgba(0xcccac2ff).into()), - ("boolean".into(), rgba(0xdfbfffff).into()), - ("link_uri".into(), rgba(0xd5fe80ff).into()), - ("label".into(), rgba(0x72cffeff).into()), - ("primary".into(), rgba(0xcccac2ff).into()), - ("number".into(), rgba(0xdfbfffff).into()), - ("variant".into(), rgba(0x72cffeff).into()), - ("enum".into(), rgba(0xfead66ff).into()), - ("string.special.symbol".into(), rgba(0xfead66ff).into()), - ("operator".into(), rgba(0xf29e74ff).into()), - ("punctuation.special".into(), rgba(0xdfbfffff).into()), - ("constructor".into(), rgba(0x72cffeff).into()), - ("type".into(), rgba(0x73cfffff).into()), - ("emphasis.strong".into(), rgba(0x72cffeff).into()), - ("embedded".into(), rgba(0xcccac2ff).into()), - ("comment".into(), rgba(0xb8cfe680).into()), - ("tag".into(), rgba(0x72cffeff).into()), - ("keyword".into(), rgba(0xffad65ff).into()), - ("punctuation".into(), rgba(0xb4b3aeff).into()), - ("preproc".into(), rgba(0xcccac2ff).into()), - ("hint".into(), rgba(0x7399a3ff).into()), - ("string.special".into(), rgba(0xffdfb3ff).into()), - ("attribute".into(), rgba(0x72cffeff).into()), - ("string.regex".into(), rgba(0x95e6cbff).into()), - ("predictive".into(), rgba(0x6d839bff).into()), - ("comment.doc".into(), rgba(0x9b9b99ff).into()), - ("emphasis".into(), rgba(0x72cffeff).into()), - ("string".into(), rgba(0xd4fe7fff).into()), - ("constant".into(), rgba(0xdfbfffff).into()), - ("string.escape".into(), rgba(0x9b9b99ff).into()), - ("variable".into(), rgba(0xcccac2ff).into()), - ("punctuation.bracket".into(), rgba(0xb4b3aeff).into()), - ("punctuation.list_marker".into(), rgba(0xb4b3aeff).into()), - ], - }, - status_bar: rgba(0x464a52ff).into(), - title_bar: rgba(0x464a52ff).into(), - toolbar: rgba(0x242835ff).into(), - tab_bar: rgba(0x353944ff).into(), - editor: rgba(0x242835ff).into(), - editor_subheader: rgba(0x353944ff).into(), - editor_active_line: rgba(0x353944ff).into(), - terminal: rgba(0x242835ff).into(), - image_fallback_background: rgba(0x464a52ff).into(), - git_created: rgba(0xd5fe80ff).into(), - git_modified: rgba(0x72cffeff).into(), - git_deleted: rgba(0xf18779ff).into(), - git_conflict: rgba(0xfecf72ff).into(), - git_ignored: rgba(0x7b7d7fff).into(), - git_renamed: rgba(0xfecf72ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x72cffeff).into(), - selection: rgba(0x72cffe3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd5fe80ff).into(), - selection: rgba(0xd5fe803d).into(), - }, - PlayerTheme { - cursor: rgba(0x5bcde5ff).into(), - selection: rgba(0x5bcde53d).into(), - }, - PlayerTheme { - cursor: rgba(0xfead66ff).into(), - selection: rgba(0xfead663d).into(), - }, - PlayerTheme { - cursor: rgba(0xdebffeff).into(), - selection: rgba(0xdebffe3d).into(), - }, - PlayerTheme { - cursor: rgba(0x95e5cbff).into(), - selection: rgba(0x95e5cb3d).into(), - }, - PlayerTheme { - cursor: rgba(0xf18779ff).into(), - selection: rgba(0xf187793d).into(), - }, - PlayerTheme { - cursor: rgba(0xfecf72ff).into(), - selection: rgba(0xfecf723d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_dark.rs b/crates/theme2/src/themes/gruvbox_dark.rs deleted file mode 100644 index 6e982808cf..0000000000 --- a/crates/theme2/src/themes/gruvbox_dark.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5b534dff).into(), - border_variant: rgba(0x5b534dff).into(), - border_focused: rgba(0x303a36ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x4c4642ff).into(), - surface: rgba(0x3a3735ff).into(), - background: rgba(0x4c4642ff).into(), - filled_element: rgba(0x4c4642ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x1e2321ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x1e2321ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfbf1c7ff).into(), - text_muted: rgba(0xc5b597ff).into(), - text_placeholder: rgba(0xfb4a35ff).into(), - text_disabled: rgba(0x998b78ff).into(), - text_accent: rgba(0x83a598ff).into(), - icon_muted: rgba(0xc5b597ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("operator".into(), rgba(0x8ec07cff).into()), - ("string.special.symbol".into(), rgba(0x8ec07cff).into()), - ("emphasis.strong".into(), rgba(0x83a598ff).into()), - ("attribute".into(), rgba(0x83a598ff).into()), - ("property".into(), rgba(0xebdbb2ff).into()), - ("comment.doc".into(), rgba(0xc6b697ff).into()), - ("emphasis".into(), rgba(0x83a598ff).into()), - ("variant".into(), rgba(0x83a598ff).into()), - ("text.literal".into(), rgba(0x83a598ff).into()), - ("keyword".into(), rgba(0xfb4833ff).into()), - ("primary".into(), rgba(0xebdbb2ff).into()), - ("variable".into(), rgba(0x83a598ff).into()), - ("enum".into(), rgba(0xfe7f18ff).into()), - ("constructor".into(), rgba(0x83a598ff).into()), - ("punctuation".into(), rgba(0xd5c4a1ff).into()), - ("link_uri".into(), rgba(0xd3869bff).into()), - ("hint".into(), rgba(0x8c957dff).into()), - ("string.regex".into(), rgba(0xfe7f18ff).into()), - ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()), - ("string".into(), rgba(0xb8bb25ff).into()), - ("punctuation.special".into(), rgba(0xe5d5adff).into()), - ("link_text".into(), rgba(0x8ec07cff).into()), - ("tag".into(), rgba(0x8ec07cff).into()), - ("string.escape".into(), rgba(0xc6b697ff).into()), - ("label".into(), rgba(0x83a598ff).into()), - ("constant".into(), rgba(0xfabd2eff).into()), - ("type".into(), rgba(0xfabd2eff).into()), - ("number".into(), rgba(0xd3869bff).into()), - ("string.special".into(), rgba(0xd3869bff).into()), - ("function.builtin".into(), rgba(0xfb4833ff).into()), - ("boolean".into(), rgba(0xd3869bff).into()), - ("embedded".into(), rgba(0x8ec07cff).into()), - ("title".into(), rgba(0xb8bb25ff).into()), - ("function".into(), rgba(0xb8bb25ff).into()), - ("punctuation.bracket".into(), rgba(0xa89984ff).into()), - ("comment".into(), rgba(0xa89984ff).into()), - ("preproc".into(), rgba(0xfbf1c7ff).into()), - ("predictive".into(), rgba(0x717363ff).into()), - ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()), - ], - }, - status_bar: rgba(0x4c4642ff).into(), - title_bar: rgba(0x4c4642ff).into(), - toolbar: rgba(0x282828ff).into(), - tab_bar: rgba(0x3a3735ff).into(), - editor: rgba(0x282828ff).into(), - editor_subheader: rgba(0x3a3735ff).into(), - editor_active_line: rgba(0x3a3735ff).into(), - terminal: rgba(0x282828ff).into(), - image_fallback_background: rgba(0x4c4642ff).into(), - git_created: rgba(0xb7bb26ff).into(), - git_modified: rgba(0x83a598ff).into(), - git_deleted: rgba(0xfb4a35ff).into(), - git_conflict: rgba(0xf9bd2fff).into(), - git_ignored: rgba(0x998b78ff).into(), - git_renamed: rgba(0xf9bd2fff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x83a598ff).into(), - selection: rgba(0x83a5983d).into(), - }, - PlayerTheme { - cursor: rgba(0xb7bb26ff).into(), - selection: rgba(0xb7bb263d).into(), - }, - PlayerTheme { - cursor: rgba(0xa89984ff).into(), - selection: rgba(0xa899843d).into(), - }, - PlayerTheme { - cursor: rgba(0xfd801bff).into(), - selection: rgba(0xfd801b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd3869bff).into(), - selection: rgba(0xd3869b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x8ec07cff).into(), - selection: rgba(0x8ec07c3d).into(), - }, - PlayerTheme { - cursor: rgba(0xfb4a35ff).into(), - selection: rgba(0xfb4a353d).into(), - }, - PlayerTheme { - cursor: rgba(0xf9bd2fff).into(), - selection: rgba(0xf9bd2f3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_dark_hard.rs b/crates/theme2/src/themes/gruvbox_dark_hard.rs deleted file mode 100644 index 159ab28325..0000000000 --- a/crates/theme2/src/themes/gruvbox_dark_hard.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_dark_hard() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Dark Hard".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5b534dff).into(), - border_variant: rgba(0x5b534dff).into(), - border_focused: rgba(0x303a36ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x4c4642ff).into(), - surface: rgba(0x393634ff).into(), - background: rgba(0x4c4642ff).into(), - filled_element: rgba(0x4c4642ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x1e2321ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x1e2321ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfbf1c7ff).into(), - text_muted: rgba(0xc5b597ff).into(), - text_placeholder: rgba(0xfb4a35ff).into(), - text_disabled: rgba(0x998b78ff).into(), - text_accent: rgba(0x83a598ff).into(), - icon_muted: rgba(0xc5b597ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("primary".into(), rgba(0xebdbb2ff).into()), - ("label".into(), rgba(0x83a598ff).into()), - ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()), - ("variant".into(), rgba(0x83a598ff).into()), - ("type".into(), rgba(0xfabd2eff).into()), - ("string.regex".into(), rgba(0xfe7f18ff).into()), - ("function.builtin".into(), rgba(0xfb4833ff).into()), - ("title".into(), rgba(0xb8bb25ff).into()), - ("string".into(), rgba(0xb8bb25ff).into()), - ("operator".into(), rgba(0x8ec07cff).into()), - ("embedded".into(), rgba(0x8ec07cff).into()), - ("punctuation.bracket".into(), rgba(0xa89984ff).into()), - ("string.special".into(), rgba(0xd3869bff).into()), - ("attribute".into(), rgba(0x83a598ff).into()), - ("comment".into(), rgba(0xa89984ff).into()), - ("link_text".into(), rgba(0x8ec07cff).into()), - ("punctuation.special".into(), rgba(0xe5d5adff).into()), - ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()), - ("comment.doc".into(), rgba(0xc6b697ff).into()), - ("preproc".into(), rgba(0xfbf1c7ff).into()), - ("text.literal".into(), rgba(0x83a598ff).into()), - ("function".into(), rgba(0xb8bb25ff).into()), - ("predictive".into(), rgba(0x717363ff).into()), - ("emphasis.strong".into(), rgba(0x83a598ff).into()), - ("punctuation".into(), rgba(0xd5c4a1ff).into()), - ("string.special.symbol".into(), rgba(0x8ec07cff).into()), - ("property".into(), rgba(0xebdbb2ff).into()), - ("keyword".into(), rgba(0xfb4833ff).into()), - ("constructor".into(), rgba(0x83a598ff).into()), - ("tag".into(), rgba(0x8ec07cff).into()), - ("variable".into(), rgba(0x83a598ff).into()), - ("enum".into(), rgba(0xfe7f18ff).into()), - ("hint".into(), rgba(0x8c957dff).into()), - ("number".into(), rgba(0xd3869bff).into()), - ("constant".into(), rgba(0xfabd2eff).into()), - ("boolean".into(), rgba(0xd3869bff).into()), - ("link_uri".into(), rgba(0xd3869bff).into()), - ("string.escape".into(), rgba(0xc6b697ff).into()), - ("emphasis".into(), rgba(0x83a598ff).into()), - ], - }, - status_bar: rgba(0x4c4642ff).into(), - title_bar: rgba(0x4c4642ff).into(), - toolbar: rgba(0x1d2021ff).into(), - tab_bar: rgba(0x393634ff).into(), - editor: rgba(0x1d2021ff).into(), - editor_subheader: rgba(0x393634ff).into(), - editor_active_line: rgba(0x393634ff).into(), - terminal: rgba(0x1d2021ff).into(), - image_fallback_background: rgba(0x4c4642ff).into(), - git_created: rgba(0xb7bb26ff).into(), - git_modified: rgba(0x83a598ff).into(), - git_deleted: rgba(0xfb4a35ff).into(), - git_conflict: rgba(0xf9bd2fff).into(), - git_ignored: rgba(0x998b78ff).into(), - git_renamed: rgba(0xf9bd2fff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x83a598ff).into(), - selection: rgba(0x83a5983d).into(), - }, - PlayerTheme { - cursor: rgba(0xb7bb26ff).into(), - selection: rgba(0xb7bb263d).into(), - }, - PlayerTheme { - cursor: rgba(0xa89984ff).into(), - selection: rgba(0xa899843d).into(), - }, - PlayerTheme { - cursor: rgba(0xfd801bff).into(), - selection: rgba(0xfd801b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd3869bff).into(), - selection: rgba(0xd3869b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x8ec07cff).into(), - selection: rgba(0x8ec07c3d).into(), - }, - PlayerTheme { - cursor: rgba(0xfb4a35ff).into(), - selection: rgba(0xfb4a353d).into(), - }, - PlayerTheme { - cursor: rgba(0xf9bd2fff).into(), - selection: rgba(0xf9bd2f3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_dark_soft.rs b/crates/theme2/src/themes/gruvbox_dark_soft.rs deleted file mode 100644 index 6a6423389e..0000000000 --- a/crates/theme2/src/themes/gruvbox_dark_soft.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_dark_soft() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Dark Soft".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x5b534dff).into(), - border_variant: rgba(0x5b534dff).into(), - border_focused: rgba(0x303a36ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x4c4642ff).into(), - surface: rgba(0x3b3735ff).into(), - background: rgba(0x4c4642ff).into(), - filled_element: rgba(0x4c4642ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x1e2321ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x1e2321ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfbf1c7ff).into(), - text_muted: rgba(0xc5b597ff).into(), - text_placeholder: rgba(0xfb4a35ff).into(), - text_disabled: rgba(0x998b78ff).into(), - text_accent: rgba(0x83a598ff).into(), - icon_muted: rgba(0xc5b597ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("punctuation.special".into(), rgba(0xe5d5adff).into()), - ("attribute".into(), rgba(0x83a598ff).into()), - ("preproc".into(), rgba(0xfbf1c7ff).into()), - ("keyword".into(), rgba(0xfb4833ff).into()), - ("emphasis".into(), rgba(0x83a598ff).into()), - ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()), - ("punctuation.bracket".into(), rgba(0xa89984ff).into()), - ("comment".into(), rgba(0xa89984ff).into()), - ("text.literal".into(), rgba(0x83a598ff).into()), - ("predictive".into(), rgba(0x717363ff).into()), - ("link_text".into(), rgba(0x8ec07cff).into()), - ("variant".into(), rgba(0x83a598ff).into()), - ("label".into(), rgba(0x83a598ff).into()), - ("function".into(), rgba(0xb8bb25ff).into()), - ("string.regex".into(), rgba(0xfe7f18ff).into()), - ("boolean".into(), rgba(0xd3869bff).into()), - ("number".into(), rgba(0xd3869bff).into()), - ("string.escape".into(), rgba(0xc6b697ff).into()), - ("constructor".into(), rgba(0x83a598ff).into()), - ("link_uri".into(), rgba(0xd3869bff).into()), - ("string.special.symbol".into(), rgba(0x8ec07cff).into()), - ("type".into(), rgba(0xfabd2eff).into()), - ("function.builtin".into(), rgba(0xfb4833ff).into()), - ("title".into(), rgba(0xb8bb25ff).into()), - ("primary".into(), rgba(0xebdbb2ff).into()), - ("tag".into(), rgba(0x8ec07cff).into()), - ("constant".into(), rgba(0xfabd2eff).into()), - ("emphasis.strong".into(), rgba(0x83a598ff).into()), - ("string.special".into(), rgba(0xd3869bff).into()), - ("hint".into(), rgba(0x8c957dff).into()), - ("comment.doc".into(), rgba(0xc6b697ff).into()), - ("property".into(), rgba(0xebdbb2ff).into()), - ("embedded".into(), rgba(0x8ec07cff).into()), - ("operator".into(), rgba(0x8ec07cff).into()), - ("punctuation".into(), rgba(0xd5c4a1ff).into()), - ("variable".into(), rgba(0x83a598ff).into()), - ("enum".into(), rgba(0xfe7f18ff).into()), - ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()), - ("string".into(), rgba(0xb8bb25ff).into()), - ], - }, - status_bar: rgba(0x4c4642ff).into(), - title_bar: rgba(0x4c4642ff).into(), - toolbar: rgba(0x32302fff).into(), - tab_bar: rgba(0x3b3735ff).into(), - editor: rgba(0x32302fff).into(), - editor_subheader: rgba(0x3b3735ff).into(), - editor_active_line: rgba(0x3b3735ff).into(), - terminal: rgba(0x32302fff).into(), - image_fallback_background: rgba(0x4c4642ff).into(), - git_created: rgba(0xb7bb26ff).into(), - git_modified: rgba(0x83a598ff).into(), - git_deleted: rgba(0xfb4a35ff).into(), - git_conflict: rgba(0xf9bd2fff).into(), - git_ignored: rgba(0x998b78ff).into(), - git_renamed: rgba(0xf9bd2fff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x83a598ff).into(), - selection: rgba(0x83a5983d).into(), - }, - PlayerTheme { - cursor: rgba(0xb7bb26ff).into(), - selection: rgba(0xb7bb263d).into(), - }, - PlayerTheme { - cursor: rgba(0xa89984ff).into(), - selection: rgba(0xa899843d).into(), - }, - PlayerTheme { - cursor: rgba(0xfd801bff).into(), - selection: rgba(0xfd801b3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd3869bff).into(), - selection: rgba(0xd3869b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x8ec07cff).into(), - selection: rgba(0x8ec07c3d).into(), - }, - PlayerTheme { - cursor: rgba(0xfb4a35ff).into(), - selection: rgba(0xfb4a353d).into(), - }, - PlayerTheme { - cursor: rgba(0xf9bd2fff).into(), - selection: rgba(0xf9bd2f3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_light.rs b/crates/theme2/src/themes/gruvbox_light.rs deleted file mode 100644 index 7582f8bd8a..0000000000 --- a/crates/theme2/src/themes/gruvbox_light.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xc8b899ff).into(), - border_variant: rgba(0xc8b899ff).into(), - border_focused: rgba(0xadc5ccff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xd9c8a4ff).into(), - surface: rgba(0xecddb4ff).into(), - background: rgba(0xd9c8a4ff).into(), - filled_element: rgba(0xd9c8a4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xd2dee2ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xd2dee2ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x282828ff).into(), - text_muted: rgba(0x5f5650ff).into(), - text_placeholder: rgba(0x9d0308ff).into(), - text_disabled: rgba(0x897b6eff).into(), - text_accent: rgba(0x0b6678ff).into(), - icon_muted: rgba(0x5f5650ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("number".into(), rgba(0x8f3e71ff).into()), - ("link_text".into(), rgba(0x427b58ff).into()), - ("string.special".into(), rgba(0x8f3e71ff).into()), - ("string.special.symbol".into(), rgba(0x427b58ff).into()), - ("function".into(), rgba(0x79740eff).into()), - ("title".into(), rgba(0x79740eff).into()), - ("emphasis".into(), rgba(0x0b6678ff).into()), - ("punctuation".into(), rgba(0x3c3836ff).into()), - ("string.escape".into(), rgba(0x5d544eff).into()), - ("type".into(), rgba(0xb57613ff).into()), - ("string".into(), rgba(0x79740eff).into()), - ("keyword".into(), rgba(0x9d0006ff).into()), - ("tag".into(), rgba(0x427b58ff).into()), - ("primary".into(), rgba(0x282828ff).into()), - ("link_uri".into(), rgba(0x8f3e71ff).into()), - ("comment.doc".into(), rgba(0x5d544eff).into()), - ("boolean".into(), rgba(0x8f3e71ff).into()), - ("embedded".into(), rgba(0x427b58ff).into()), - ("hint".into(), rgba(0x677562ff).into()), - ("emphasis.strong".into(), rgba(0x0b6678ff).into()), - ("operator".into(), rgba(0x427b58ff).into()), - ("label".into(), rgba(0x0b6678ff).into()), - ("comment".into(), rgba(0x7c6f64ff).into()), - ("function.builtin".into(), rgba(0x9d0006ff).into()), - ("punctuation.bracket".into(), rgba(0x665c54ff).into()), - ("text.literal".into(), rgba(0x066578ff).into()), - ("string.regex".into(), rgba(0xaf3a02ff).into()), - ("property".into(), rgba(0x282828ff).into()), - ("attribute".into(), rgba(0x0b6678ff).into()), - ("punctuation.delimiter".into(), rgba(0x413d3aff).into()), - ("constructor".into(), rgba(0x0b6678ff).into()), - ("variable".into(), rgba(0x066578ff).into()), - ("constant".into(), rgba(0xb57613ff).into()), - ("preproc".into(), rgba(0x282828ff).into()), - ("punctuation.special".into(), rgba(0x413d3aff).into()), - ("punctuation.list_marker".into(), rgba(0x282828ff).into()), - ("variant".into(), rgba(0x0b6678ff).into()), - ("predictive".into(), rgba(0x7c9780ff).into()), - ("enum".into(), rgba(0xaf3a02ff).into()), - ], - }, - status_bar: rgba(0xd9c8a4ff).into(), - title_bar: rgba(0xd9c8a4ff).into(), - toolbar: rgba(0xfbf1c7ff).into(), - tab_bar: rgba(0xecddb4ff).into(), - editor: rgba(0xfbf1c7ff).into(), - editor_subheader: rgba(0xecddb4ff).into(), - editor_active_line: rgba(0xecddb4ff).into(), - terminal: rgba(0xfbf1c7ff).into(), - image_fallback_background: rgba(0xd9c8a4ff).into(), - git_created: rgba(0x797410ff).into(), - git_modified: rgba(0x0b6678ff).into(), - git_deleted: rgba(0x9d0308ff).into(), - git_conflict: rgba(0xb57615ff).into(), - git_ignored: rgba(0x897b6eff).into(), - git_renamed: rgba(0xb57615ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x0b6678ff).into(), - selection: rgba(0x0b66783d).into(), - }, - PlayerTheme { - cursor: rgba(0x797410ff).into(), - selection: rgba(0x7974103d).into(), - }, - PlayerTheme { - cursor: rgba(0x7c6f64ff).into(), - selection: rgba(0x7c6f643d).into(), - }, - PlayerTheme { - cursor: rgba(0xaf3a04ff).into(), - selection: rgba(0xaf3a043d).into(), - }, - PlayerTheme { - cursor: rgba(0x8f3f70ff).into(), - selection: rgba(0x8f3f703d).into(), - }, - PlayerTheme { - cursor: rgba(0x437b59ff).into(), - selection: rgba(0x437b593d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d0308ff).into(), - selection: rgba(0x9d03083d).into(), - }, - PlayerTheme { - cursor: rgba(0xb57615ff).into(), - selection: rgba(0xb576153d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_light_hard.rs b/crates/theme2/src/themes/gruvbox_light_hard.rs deleted file mode 100644 index e5e3fe54cf..0000000000 --- a/crates/theme2/src/themes/gruvbox_light_hard.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_light_hard() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Light Hard".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xc8b899ff).into(), - border_variant: rgba(0xc8b899ff).into(), - border_focused: rgba(0xadc5ccff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xd9c8a4ff).into(), - surface: rgba(0xecddb5ff).into(), - background: rgba(0xd9c8a4ff).into(), - filled_element: rgba(0xd9c8a4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xd2dee2ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xd2dee2ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x282828ff).into(), - text_muted: rgba(0x5f5650ff).into(), - text_placeholder: rgba(0x9d0308ff).into(), - text_disabled: rgba(0x897b6eff).into(), - text_accent: rgba(0x0b6678ff).into(), - icon_muted: rgba(0x5f5650ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("label".into(), rgba(0x0b6678ff).into()), - ("hint".into(), rgba(0x677562ff).into()), - ("boolean".into(), rgba(0x8f3e71ff).into()), - ("function.builtin".into(), rgba(0x9d0006ff).into()), - ("constant".into(), rgba(0xb57613ff).into()), - ("preproc".into(), rgba(0x282828ff).into()), - ("predictive".into(), rgba(0x7c9780ff).into()), - ("string".into(), rgba(0x79740eff).into()), - ("comment.doc".into(), rgba(0x5d544eff).into()), - ("function".into(), rgba(0x79740eff).into()), - ("title".into(), rgba(0x79740eff).into()), - ("text.literal".into(), rgba(0x066578ff).into()), - ("punctuation.bracket".into(), rgba(0x665c54ff).into()), - ("string.escape".into(), rgba(0x5d544eff).into()), - ("punctuation.delimiter".into(), rgba(0x413d3aff).into()), - ("string.special.symbol".into(), rgba(0x427b58ff).into()), - ("type".into(), rgba(0xb57613ff).into()), - ("constructor".into(), rgba(0x0b6678ff).into()), - ("property".into(), rgba(0x282828ff).into()), - ("comment".into(), rgba(0x7c6f64ff).into()), - ("enum".into(), rgba(0xaf3a02ff).into()), - ("emphasis".into(), rgba(0x0b6678ff).into()), - ("embedded".into(), rgba(0x427b58ff).into()), - ("operator".into(), rgba(0x427b58ff).into()), - ("attribute".into(), rgba(0x0b6678ff).into()), - ("emphasis.strong".into(), rgba(0x0b6678ff).into()), - ("link_text".into(), rgba(0x427b58ff).into()), - ("punctuation.special".into(), rgba(0x413d3aff).into()), - ("punctuation.list_marker".into(), rgba(0x282828ff).into()), - ("variant".into(), rgba(0x0b6678ff).into()), - ("primary".into(), rgba(0x282828ff).into()), - ("number".into(), rgba(0x8f3e71ff).into()), - ("tag".into(), rgba(0x427b58ff).into()), - ("keyword".into(), rgba(0x9d0006ff).into()), - ("link_uri".into(), rgba(0x8f3e71ff).into()), - ("string.regex".into(), rgba(0xaf3a02ff).into()), - ("variable".into(), rgba(0x066578ff).into()), - ("string.special".into(), rgba(0x8f3e71ff).into()), - ("punctuation".into(), rgba(0x3c3836ff).into()), - ], - }, - status_bar: rgba(0xd9c8a4ff).into(), - title_bar: rgba(0xd9c8a4ff).into(), - toolbar: rgba(0xf9f5d7ff).into(), - tab_bar: rgba(0xecddb5ff).into(), - editor: rgba(0xf9f5d7ff).into(), - editor_subheader: rgba(0xecddb5ff).into(), - editor_active_line: rgba(0xecddb5ff).into(), - terminal: rgba(0xf9f5d7ff).into(), - image_fallback_background: rgba(0xd9c8a4ff).into(), - git_created: rgba(0x797410ff).into(), - git_modified: rgba(0x0b6678ff).into(), - git_deleted: rgba(0x9d0308ff).into(), - git_conflict: rgba(0xb57615ff).into(), - git_ignored: rgba(0x897b6eff).into(), - git_renamed: rgba(0xb57615ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x0b6678ff).into(), - selection: rgba(0x0b66783d).into(), - }, - PlayerTheme { - cursor: rgba(0x797410ff).into(), - selection: rgba(0x7974103d).into(), - }, - PlayerTheme { - cursor: rgba(0x7c6f64ff).into(), - selection: rgba(0x7c6f643d).into(), - }, - PlayerTheme { - cursor: rgba(0xaf3a04ff).into(), - selection: rgba(0xaf3a043d).into(), - }, - PlayerTheme { - cursor: rgba(0x8f3f70ff).into(), - selection: rgba(0x8f3f703d).into(), - }, - PlayerTheme { - cursor: rgba(0x437b59ff).into(), - selection: rgba(0x437b593d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d0308ff).into(), - selection: rgba(0x9d03083d).into(), - }, - PlayerTheme { - cursor: rgba(0xb57615ff).into(), - selection: rgba(0xb576153d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/gruvbox_light_soft.rs b/crates/theme2/src/themes/gruvbox_light_soft.rs deleted file mode 100644 index 15574e2960..0000000000 --- a/crates/theme2/src/themes/gruvbox_light_soft.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn gruvbox_light_soft() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Gruvbox Light Soft".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xc8b899ff).into(), - border_variant: rgba(0xc8b899ff).into(), - border_focused: rgba(0xadc5ccff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xd9c8a4ff).into(), - surface: rgba(0xecdcb3ff).into(), - background: rgba(0xd9c8a4ff).into(), - filled_element: rgba(0xd9c8a4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xd2dee2ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xd2dee2ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x282828ff).into(), - text_muted: rgba(0x5f5650ff).into(), - text_placeholder: rgba(0x9d0308ff).into(), - text_disabled: rgba(0x897b6eff).into(), - text_accent: rgba(0x0b6678ff).into(), - icon_muted: rgba(0x5f5650ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("preproc".into(), rgba(0x282828ff).into()), - ("punctuation.list_marker".into(), rgba(0x282828ff).into()), - ("string".into(), rgba(0x79740eff).into()), - ("constant".into(), rgba(0xb57613ff).into()), - ("keyword".into(), rgba(0x9d0006ff).into()), - ("string.special.symbol".into(), rgba(0x427b58ff).into()), - ("comment.doc".into(), rgba(0x5d544eff).into()), - ("hint".into(), rgba(0x677562ff).into()), - ("number".into(), rgba(0x8f3e71ff).into()), - ("enum".into(), rgba(0xaf3a02ff).into()), - ("emphasis".into(), rgba(0x0b6678ff).into()), - ("operator".into(), rgba(0x427b58ff).into()), - ("comment".into(), rgba(0x7c6f64ff).into()), - ("embedded".into(), rgba(0x427b58ff).into()), - ("type".into(), rgba(0xb57613ff).into()), - ("title".into(), rgba(0x79740eff).into()), - ("constructor".into(), rgba(0x0b6678ff).into()), - ("punctuation.delimiter".into(), rgba(0x413d3aff).into()), - ("function".into(), rgba(0x79740eff).into()), - ("link_uri".into(), rgba(0x8f3e71ff).into()), - ("emphasis.strong".into(), rgba(0x0b6678ff).into()), - ("boolean".into(), rgba(0x8f3e71ff).into()), - ("function.builtin".into(), rgba(0x9d0006ff).into()), - ("predictive".into(), rgba(0x7c9780ff).into()), - ("string.regex".into(), rgba(0xaf3a02ff).into()), - ("tag".into(), rgba(0x427b58ff).into()), - ("text.literal".into(), rgba(0x066578ff).into()), - ("punctuation".into(), rgba(0x3c3836ff).into()), - ("punctuation.bracket".into(), rgba(0x665c54ff).into()), - ("variable".into(), rgba(0x066578ff).into()), - ("attribute".into(), rgba(0x0b6678ff).into()), - ("string.special".into(), rgba(0x8f3e71ff).into()), - ("label".into(), rgba(0x0b6678ff).into()), - ("string.escape".into(), rgba(0x5d544eff).into()), - ("link_text".into(), rgba(0x427b58ff).into()), - ("punctuation.special".into(), rgba(0x413d3aff).into()), - ("property".into(), rgba(0x282828ff).into()), - ("variant".into(), rgba(0x0b6678ff).into()), - ("primary".into(), rgba(0x282828ff).into()), - ], - }, - status_bar: rgba(0xd9c8a4ff).into(), - title_bar: rgba(0xd9c8a4ff).into(), - toolbar: rgba(0xf2e5bcff).into(), - tab_bar: rgba(0xecdcb3ff).into(), - editor: rgba(0xf2e5bcff).into(), - editor_subheader: rgba(0xecdcb3ff).into(), - editor_active_line: rgba(0xecdcb3ff).into(), - terminal: rgba(0xf2e5bcff).into(), - image_fallback_background: rgba(0xd9c8a4ff).into(), - git_created: rgba(0x797410ff).into(), - git_modified: rgba(0x0b6678ff).into(), - git_deleted: rgba(0x9d0308ff).into(), - git_conflict: rgba(0xb57615ff).into(), - git_ignored: rgba(0x897b6eff).into(), - git_renamed: rgba(0xb57615ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x0b6678ff).into(), - selection: rgba(0x0b66783d).into(), - }, - PlayerTheme { - cursor: rgba(0x797410ff).into(), - selection: rgba(0x7974103d).into(), - }, - PlayerTheme { - cursor: rgba(0x7c6f64ff).into(), - selection: rgba(0x7c6f643d).into(), - }, - PlayerTheme { - cursor: rgba(0xaf3a04ff).into(), - selection: rgba(0xaf3a043d).into(), - }, - PlayerTheme { - cursor: rgba(0x8f3f70ff).into(), - selection: rgba(0x8f3f703d).into(), - }, - PlayerTheme { - cursor: rgba(0x437b59ff).into(), - selection: rgba(0x437b593d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d0308ff).into(), - selection: rgba(0x9d03083d).into(), - }, - PlayerTheme { - cursor: rgba(0xb57615ff).into(), - selection: rgba(0xb576153d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/mod.rs b/crates/theme2/src/themes/mod.rs deleted file mode 100644 index 17cd5ac6e0..0000000000 --- a/crates/theme2/src/themes/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod andromeda; -mod atelier_cave_dark; -mod atelier_cave_light; -mod atelier_dune_dark; -mod atelier_dune_light; -mod atelier_estuary_dark; -mod atelier_estuary_light; -mod atelier_forest_dark; -mod atelier_forest_light; -mod atelier_heath_dark; -mod atelier_heath_light; -mod atelier_lakeside_dark; -mod atelier_lakeside_light; -mod atelier_plateau_dark; -mod atelier_plateau_light; -mod atelier_savanna_dark; -mod atelier_savanna_light; -mod atelier_seaside_dark; -mod atelier_seaside_light; -mod atelier_sulphurpool_dark; -mod atelier_sulphurpool_light; -mod ayu_dark; -mod ayu_light; -mod ayu_mirage; -mod gruvbox_dark; -mod gruvbox_dark_hard; -mod gruvbox_dark_soft; -mod gruvbox_light; -mod gruvbox_light_hard; -mod gruvbox_light_soft; -mod one_dark; -mod one_light; -mod rose_pine; -mod rose_pine_dawn; -mod rose_pine_moon; -mod sandcastle; -mod solarized_dark; -mod solarized_light; -mod summercamp; - -pub use andromeda::*; -pub use atelier_cave_dark::*; -pub use atelier_cave_light::*; -pub use atelier_dune_dark::*; -pub use atelier_dune_light::*; -pub use atelier_estuary_dark::*; -pub use atelier_estuary_light::*; -pub use atelier_forest_dark::*; -pub use atelier_forest_light::*; -pub use atelier_heath_dark::*; -pub use atelier_heath_light::*; -pub use atelier_lakeside_dark::*; -pub use atelier_lakeside_light::*; -pub use atelier_plateau_dark::*; -pub use atelier_plateau_light::*; -pub use atelier_savanna_dark::*; -pub use atelier_savanna_light::*; -pub use atelier_seaside_dark::*; -pub use atelier_seaside_light::*; -pub use atelier_sulphurpool_dark::*; -pub use atelier_sulphurpool_light::*; -pub use ayu_dark::*; -pub use ayu_light::*; -pub use ayu_mirage::*; -pub use gruvbox_dark::*; -pub use gruvbox_dark_hard::*; -pub use gruvbox_dark_soft::*; -pub use gruvbox_light::*; -pub use gruvbox_light_hard::*; -pub use gruvbox_light_soft::*; -pub use one_dark::*; -pub use one_light::*; -pub use rose_pine::*; -pub use rose_pine_dawn::*; -pub use rose_pine_moon::*; -pub use sandcastle::*; -pub use solarized_dark::*; -pub use solarized_light::*; -pub use summercamp::*; diff --git a/crates/theme2/src/themes/one_dark.rs b/crates/theme2/src/themes/one_dark.rs deleted file mode 100644 index c7408d1820..0000000000 --- a/crates/theme2/src/themes/one_dark.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn one_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "One Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x464b57ff).into(), - border_variant: rgba(0x464b57ff).into(), - border_focused: rgba(0x293b5bff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x3b414dff).into(), - surface: rgba(0x2f343eff).into(), - background: rgba(0x3b414dff).into(), - filled_element: rgba(0x3b414dff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x18243dff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x18243dff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xc8ccd4ff).into(), - text_muted: rgba(0x838994ff).into(), - text_placeholder: rgba(0xd07277ff).into(), - text_disabled: rgba(0x555a63ff).into(), - text_accent: rgba(0x74ade8ff).into(), - icon_muted: rgba(0x838994ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("keyword".into(), rgba(0xb477cfff).into()), - ("comment.doc".into(), rgba(0x878e98ff).into()), - ("variant".into(), rgba(0x73ade9ff).into()), - ("property".into(), rgba(0xd07277ff).into()), - ("function".into(), rgba(0x73ade9ff).into()), - ("type".into(), rgba(0x6eb4bfff).into()), - ("tag".into(), rgba(0x74ade8ff).into()), - ("string.escape".into(), rgba(0x878e98ff).into()), - ("punctuation.bracket".into(), rgba(0xb2b9c6ff).into()), - ("hint".into(), rgba(0x5a6f89ff).into()), - ("punctuation".into(), rgba(0xacb2beff).into()), - ("comment".into(), rgba(0x5d636fff).into()), - ("emphasis".into(), rgba(0x74ade8ff).into()), - ("punctuation.special".into(), rgba(0xb1574bff).into()), - ("link_uri".into(), rgba(0x6eb4bfff).into()), - ("string.regex".into(), rgba(0xbf956aff).into()), - ("constructor".into(), rgba(0x73ade9ff).into()), - ("operator".into(), rgba(0x6eb4bfff).into()), - ("constant".into(), rgba(0xdfc184ff).into()), - ("string.special".into(), rgba(0xbf956aff).into()), - ("emphasis.strong".into(), rgba(0xbf956aff).into()), - ("string.special.symbol".into(), rgba(0xbf956aff).into()), - ("primary".into(), rgba(0xacb2beff).into()), - ("preproc".into(), rgba(0xc8ccd4ff).into()), - ("string".into(), rgba(0xa1c181ff).into()), - ("punctuation.delimiter".into(), rgba(0xb2b9c6ff).into()), - ("embedded".into(), rgba(0xc8ccd4ff).into()), - ("enum".into(), rgba(0xd07277ff).into()), - ("variable.special".into(), rgba(0xbf956aff).into()), - ("text.literal".into(), rgba(0xa1c181ff).into()), - ("attribute".into(), rgba(0x74ade8ff).into()), - ("link_text".into(), rgba(0x73ade9ff).into()), - ("title".into(), rgba(0xd07277ff).into()), - ("predictive".into(), rgba(0x5a6a87ff).into()), - ("number".into(), rgba(0xbf956aff).into()), - ("label".into(), rgba(0x74ade8ff).into()), - ("variable".into(), rgba(0xc8ccd4ff).into()), - ("boolean".into(), rgba(0xbf956aff).into()), - ("punctuation.list_marker".into(), rgba(0xd07277ff).into()), - ], - }, - status_bar: rgba(0x3b414dff).into(), - title_bar: rgba(0x3b414dff).into(), - toolbar: rgba(0x282c33ff).into(), - tab_bar: rgba(0x2f343eff).into(), - editor: rgba(0x282c33ff).into(), - editor_subheader: rgba(0x2f343eff).into(), - editor_active_line: rgba(0x2f343eff).into(), - terminal: rgba(0x282c33ff).into(), - image_fallback_background: rgba(0x3b414dff).into(), - git_created: rgba(0xa1c181ff).into(), - git_modified: rgba(0x74ade8ff).into(), - git_deleted: rgba(0xd07277ff).into(), - git_conflict: rgba(0xdec184ff).into(), - git_ignored: rgba(0x555a63ff).into(), - git_renamed: rgba(0xdec184ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x74ade8ff).into(), - selection: rgba(0x74ade83d).into(), - }, - PlayerTheme { - cursor: rgba(0xa1c181ff).into(), - selection: rgba(0xa1c1813d).into(), - }, - PlayerTheme { - cursor: rgba(0xbe5046ff).into(), - selection: rgba(0xbe50463d).into(), - }, - PlayerTheme { - cursor: rgba(0xbf956aff).into(), - selection: rgba(0xbf956a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xb477cfff).into(), - selection: rgba(0xb477cf3d).into(), - }, - PlayerTheme { - cursor: rgba(0x6eb4bfff).into(), - selection: rgba(0x6eb4bf3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd07277ff).into(), - selection: rgba(0xd072773d).into(), - }, - PlayerTheme { - cursor: rgba(0xdec184ff).into(), - selection: rgba(0xdec1843d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/one_light.rs b/crates/theme2/src/themes/one_light.rs deleted file mode 100644 index ee802d57d3..0000000000 --- a/crates/theme2/src/themes/one_light.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn one_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "One Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xc9c9caff).into(), - border_variant: rgba(0xc9c9caff).into(), - border_focused: rgba(0xcbcdf6ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xdcdcddff).into(), - surface: rgba(0xebebecff).into(), - background: rgba(0xdcdcddff).into(), - filled_element: rgba(0xdcdcddff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xe2e2faff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xe2e2faff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x383a41ff).into(), - text_muted: rgba(0x7e8087ff).into(), - text_placeholder: rgba(0xd36151ff).into(), - text_disabled: rgba(0xa1a1a3ff).into(), - text_accent: rgba(0x5c78e2ff).into(), - icon_muted: rgba(0x7e8087ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.special.symbol".into(), rgba(0xad6e26ff).into()), - ("hint".into(), rgba(0x9294beff).into()), - ("link_uri".into(), rgba(0x3882b7ff).into()), - ("type".into(), rgba(0x3882b7ff).into()), - ("string.regex".into(), rgba(0xad6e26ff).into()), - ("constant".into(), rgba(0x669f59ff).into()), - ("function".into(), rgba(0x5b79e3ff).into()), - ("string.special".into(), rgba(0xad6e26ff).into()), - ("punctuation.bracket".into(), rgba(0x4d4f52ff).into()), - ("variable".into(), rgba(0x383a41ff).into()), - ("punctuation".into(), rgba(0x383a41ff).into()), - ("property".into(), rgba(0xd3604fff).into()), - ("string".into(), rgba(0x649f57ff).into()), - ("predictive".into(), rgba(0x9b9ec6ff).into()), - ("attribute".into(), rgba(0x5c78e2ff).into()), - ("number".into(), rgba(0xad6e25ff).into()), - ("constructor".into(), rgba(0x5c78e2ff).into()), - ("embedded".into(), rgba(0x383a41ff).into()), - ("title".into(), rgba(0xd3604fff).into()), - ("tag".into(), rgba(0x5c78e2ff).into()), - ("boolean".into(), rgba(0xad6e25ff).into()), - ("punctuation.list_marker".into(), rgba(0xd3604fff).into()), - ("variant".into(), rgba(0x5b79e3ff).into()), - ("emphasis".into(), rgba(0x5c78e2ff).into()), - ("link_text".into(), rgba(0x5b79e3ff).into()), - ("comment".into(), rgba(0xa2a3a7ff).into()), - ("punctuation.special".into(), rgba(0xb92b46ff).into()), - ("emphasis.strong".into(), rgba(0xad6e25ff).into()), - ("primary".into(), rgba(0x383a41ff).into()), - ("punctuation.delimiter".into(), rgba(0x4d4f52ff).into()), - ("label".into(), rgba(0x5c78e2ff).into()), - ("keyword".into(), rgba(0xa449abff).into()), - ("string.escape".into(), rgba(0x7c7e86ff).into()), - ("text.literal".into(), rgba(0x649f57ff).into()), - ("variable.special".into(), rgba(0xad6e25ff).into()), - ("comment.doc".into(), rgba(0x7c7e86ff).into()), - ("enum".into(), rgba(0xd3604fff).into()), - ("operator".into(), rgba(0x3882b7ff).into()), - ("preproc".into(), rgba(0x383a41ff).into()), - ], - }, - status_bar: rgba(0xdcdcddff).into(), - title_bar: rgba(0xdcdcddff).into(), - toolbar: rgba(0xfafafaff).into(), - tab_bar: rgba(0xebebecff).into(), - editor: rgba(0xfafafaff).into(), - editor_subheader: rgba(0xebebecff).into(), - editor_active_line: rgba(0xebebecff).into(), - terminal: rgba(0xfafafaff).into(), - image_fallback_background: rgba(0xdcdcddff).into(), - git_created: rgba(0x669f59ff).into(), - git_modified: rgba(0x5c78e2ff).into(), - git_deleted: rgba(0xd36151ff).into(), - git_conflict: rgba(0xdec184ff).into(), - git_ignored: rgba(0xa1a1a3ff).into(), - git_renamed: rgba(0xdec184ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x5c78e2ff).into(), - selection: rgba(0x5c78e23d).into(), - }, - PlayerTheme { - cursor: rgba(0x669f59ff).into(), - selection: rgba(0x669f593d).into(), - }, - PlayerTheme { - cursor: rgba(0x984ea5ff).into(), - selection: rgba(0x984ea53d).into(), - }, - PlayerTheme { - cursor: rgba(0xad6e26ff).into(), - selection: rgba(0xad6e263d).into(), - }, - PlayerTheme { - cursor: rgba(0xa349abff).into(), - selection: rgba(0xa349ab3d).into(), - }, - PlayerTheme { - cursor: rgba(0x3a82b7ff).into(), - selection: rgba(0x3a82b73d).into(), - }, - PlayerTheme { - cursor: rgba(0xd36151ff).into(), - selection: rgba(0xd361513d).into(), - }, - PlayerTheme { - cursor: rgba(0xdec184ff).into(), - selection: rgba(0xdec1843d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/rose_pine.rs b/crates/theme2/src/themes/rose_pine.rs deleted file mode 100644 index f3bd454cdc..0000000000 --- a/crates/theme2/src/themes/rose_pine.rs +++ /dev/null @@ -1,132 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn rose_pine() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Rosé Pine".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x423f55ff).into(), - border_variant: rgba(0x423f55ff).into(), - border_focused: rgba(0x435255ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x292738ff).into(), - surface: rgba(0x1c1b2aff).into(), - background: rgba(0x292738ff).into(), - filled_element: rgba(0x292738ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x2f3639ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x2f3639ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xe0def4ff).into(), - text_muted: rgba(0x74708dff).into(), - text_placeholder: rgba(0xea6e92ff).into(), - text_disabled: rgba(0x2f2b43ff).into(), - text_accent: rgba(0x9bced6ff).into(), - icon_muted: rgba(0x74708dff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("punctuation.delimiter".into(), rgba(0x9d99b6ff).into()), - ("number".into(), rgba(0x5cc1a3ff).into()), - ("punctuation.special".into(), rgba(0x9d99b6ff).into()), - ("string.escape".into(), rgba(0x76728fff).into()), - ("title".into(), rgba(0xf5c177ff).into()), - ("constant".into(), rgba(0x5cc1a3ff).into()), - ("string.regex".into(), rgba(0xc4a7e6ff).into()), - ("type.builtin".into(), rgba(0x9ccfd8ff).into()), - ("comment.doc".into(), rgba(0x76728fff).into()), - ("primary".into(), rgba(0xe0def4ff).into()), - ("string.special".into(), rgba(0xc4a7e6ff).into()), - ("punctuation".into(), rgba(0x908caaff).into()), - ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()), - ("variant".into(), rgba(0x9bced6ff).into()), - ("function.method".into(), rgba(0xebbcbaff).into()), - ("comment".into(), rgba(0x6e6a86ff).into()), - ("boolean".into(), rgba(0xebbcbaff).into()), - ("preproc".into(), rgba(0xe0def4ff).into()), - ("link_uri".into(), rgba(0xebbcbaff).into()), - ("hint".into(), rgba(0x5e768cff).into()), - ("attribute".into(), rgba(0x9bced6ff).into()), - ("text.literal".into(), rgba(0xc4a7e6ff).into()), - ("punctuation.list_marker".into(), rgba(0x9d99b6ff).into()), - ("operator".into(), rgba(0x30738fff).into()), - ("emphasis.strong".into(), rgba(0x9bced6ff).into()), - ("keyword".into(), rgba(0x30738fff).into()), - ("enum".into(), rgba(0xc4a7e6ff).into()), - ("tag".into(), rgba(0x9ccfd8ff).into()), - ("constructor".into(), rgba(0x9bced6ff).into()), - ("function".into(), rgba(0xebbcbaff).into()), - ("string".into(), rgba(0xf5c177ff).into()), - ("type".into(), rgba(0x9ccfd8ff).into()), - ("emphasis".into(), rgba(0x9bced6ff).into()), - ("link_text".into(), rgba(0x9ccfd8ff).into()), - ("property".into(), rgba(0x9bced6ff).into()), - ("predictive".into(), rgba(0x556b81ff).into()), - ("punctuation.bracket".into(), rgba(0x9d99b6ff).into()), - ("embedded".into(), rgba(0xe0def4ff).into()), - ("variable".into(), rgba(0xe0def4ff).into()), - ("label".into(), rgba(0x9bced6ff).into()), - ], - }, - status_bar: rgba(0x292738ff).into(), - title_bar: rgba(0x292738ff).into(), - toolbar: rgba(0x191724ff).into(), - tab_bar: rgba(0x1c1b2aff).into(), - editor: rgba(0x191724ff).into(), - editor_subheader: rgba(0x1c1b2aff).into(), - editor_active_line: rgba(0x1c1b2aff).into(), - terminal: rgba(0x191724ff).into(), - image_fallback_background: rgba(0x292738ff).into(), - git_created: rgba(0x5cc1a3ff).into(), - git_modified: rgba(0x9bced6ff).into(), - git_deleted: rgba(0xea6e92ff).into(), - git_conflict: rgba(0xf5c177ff).into(), - git_ignored: rgba(0x2f2b43ff).into(), - git_renamed: rgba(0xf5c177ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x9bced6ff).into(), - selection: rgba(0x9bced63d).into(), - }, - PlayerTheme { - cursor: rgba(0x5cc1a3ff).into(), - selection: rgba(0x5cc1a33d).into(), - }, - PlayerTheme { - cursor: rgba(0x9d7591ff).into(), - selection: rgba(0x9d75913d).into(), - }, - PlayerTheme { - cursor: rgba(0xc4a7e6ff).into(), - selection: rgba(0xc4a7e63d).into(), - }, - PlayerTheme { - cursor: rgba(0xc4a7e6ff).into(), - selection: rgba(0xc4a7e63d).into(), - }, - PlayerTheme { - cursor: rgba(0x31738fff).into(), - selection: rgba(0x31738f3d).into(), - }, - PlayerTheme { - cursor: rgba(0xea6e92ff).into(), - selection: rgba(0xea6e923d).into(), - }, - PlayerTheme { - cursor: rgba(0xf5c177ff).into(), - selection: rgba(0xf5c1773d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/rose_pine_dawn.rs b/crates/theme2/src/themes/rose_pine_dawn.rs deleted file mode 100644 index ba64bf9d99..0000000000 --- a/crates/theme2/src/themes/rose_pine_dawn.rs +++ /dev/null @@ -1,132 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn rose_pine_dawn() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Rosé Pine Dawn".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0xdcd6d5ff).into(), - border_variant: rgba(0xdcd6d5ff).into(), - border_focused: rgba(0xc3d7dbff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xdcd8d8ff).into(), - surface: rgba(0xfef9f2ff).into(), - background: rgba(0xdcd8d8ff).into(), - filled_element: rgba(0xdcd8d8ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdde9ebff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdde9ebff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x575279ff).into(), - text_muted: rgba(0x706c8cff).into(), - text_placeholder: rgba(0xb4647aff).into(), - text_disabled: rgba(0x938fa3ff).into(), - text_accent: rgba(0x57949fff).into(), - icon_muted: rgba(0x706c8cff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("primary".into(), rgba(0x575279ff).into()), - ("attribute".into(), rgba(0x57949fff).into()), - ("operator".into(), rgba(0x276983ff).into()), - ("boolean".into(), rgba(0xd7827dff).into()), - ("tag".into(), rgba(0x55949fff).into()), - ("enum".into(), rgba(0x9079a9ff).into()), - ("embedded".into(), rgba(0x575279ff).into()), - ("label".into(), rgba(0x57949fff).into()), - ("function.method".into(), rgba(0xd7827dff).into()), - ("punctuation.list_marker".into(), rgba(0x635e82ff).into()), - ("punctuation.delimiter".into(), rgba(0x635e82ff).into()), - ("string".into(), rgba(0xea9d34ff).into()), - ("type".into(), rgba(0x55949fff).into()), - ("string.regex".into(), rgba(0x9079a9ff).into()), - ("variable".into(), rgba(0x575279ff).into()), - ("constructor".into(), rgba(0x57949fff).into()), - ("punctuation.bracket".into(), rgba(0x635e82ff).into()), - ("emphasis".into(), rgba(0x57949fff).into()), - ("comment.doc".into(), rgba(0x6e6a8bff).into()), - ("comment".into(), rgba(0x9893a5ff).into()), - ("keyword".into(), rgba(0x276983ff).into()), - ("preproc".into(), rgba(0x575279ff).into()), - ("string.special".into(), rgba(0x9079a9ff).into()), - ("string.escape".into(), rgba(0x6e6a8bff).into()), - ("constant".into(), rgba(0x3daa8eff).into()), - ("property".into(), rgba(0x57949fff).into()), - ("punctuation.special".into(), rgba(0x635e82ff).into()), - ("text.literal".into(), rgba(0x9079a9ff).into()), - ("type.builtin".into(), rgba(0x55949fff).into()), - ("string.special.symbol".into(), rgba(0x9079a9ff).into()), - ("link_uri".into(), rgba(0xd7827dff).into()), - ("number".into(), rgba(0x3daa8eff).into()), - ("emphasis.strong".into(), rgba(0x57949fff).into()), - ("function".into(), rgba(0xd7827dff).into()), - ("title".into(), rgba(0xea9d34ff).into()), - ("punctuation".into(), rgba(0x797593ff).into()), - ("link_text".into(), rgba(0x55949fff).into()), - ("variant".into(), rgba(0x57949fff).into()), - ("predictive".into(), rgba(0xa2acbeff).into()), - ("hint".into(), rgba(0x7a92aaff).into()), - ], - }, - status_bar: rgba(0xdcd8d8ff).into(), - title_bar: rgba(0xdcd8d8ff).into(), - toolbar: rgba(0xfaf4edff).into(), - tab_bar: rgba(0xfef9f2ff).into(), - editor: rgba(0xfaf4edff).into(), - editor_subheader: rgba(0xfef9f2ff).into(), - editor_active_line: rgba(0xfef9f2ff).into(), - terminal: rgba(0xfaf4edff).into(), - image_fallback_background: rgba(0xdcd8d8ff).into(), - git_created: rgba(0x3daa8eff).into(), - git_modified: rgba(0x57949fff).into(), - git_deleted: rgba(0xb4647aff).into(), - git_conflict: rgba(0xe99d35ff).into(), - git_ignored: rgba(0x938fa3ff).into(), - git_renamed: rgba(0xe99d35ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x57949fff).into(), - selection: rgba(0x57949f3d).into(), - }, - PlayerTheme { - cursor: rgba(0x3daa8eff).into(), - selection: rgba(0x3daa8e3d).into(), - }, - PlayerTheme { - cursor: rgba(0x7c697fff).into(), - selection: rgba(0x7c697f3d).into(), - }, - PlayerTheme { - cursor: rgba(0x9079a9ff).into(), - selection: rgba(0x9079a93d).into(), - }, - PlayerTheme { - cursor: rgba(0x9079a9ff).into(), - selection: rgba(0x9079a93d).into(), - }, - PlayerTheme { - cursor: rgba(0x296983ff).into(), - selection: rgba(0x2969833d).into(), - }, - PlayerTheme { - cursor: rgba(0xb4647aff).into(), - selection: rgba(0xb4647a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xe99d35ff).into(), - selection: rgba(0xe99d353d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/rose_pine_moon.rs b/crates/theme2/src/themes/rose_pine_moon.rs deleted file mode 100644 index 167b78afb5..0000000000 --- a/crates/theme2/src/themes/rose_pine_moon.rs +++ /dev/null @@ -1,132 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn rose_pine_moon() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Rosé Pine Moon".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x504c68ff).into(), - border_variant: rgba(0x504c68ff).into(), - border_focused: rgba(0x435255ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x38354eff).into(), - surface: rgba(0x28253cff).into(), - background: rgba(0x38354eff).into(), - filled_element: rgba(0x38354eff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x2f3639ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x2f3639ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xe0def4ff).into(), - text_muted: rgba(0x85819eff).into(), - text_placeholder: rgba(0xea6e92ff).into(), - text_disabled: rgba(0x605d7aff).into(), - text_accent: rgba(0x9bced6ff).into(), - icon_muted: rgba(0x85819eff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("type.builtin".into(), rgba(0x9ccfd8ff).into()), - ("variable".into(), rgba(0xe0def4ff).into()), - ("punctuation".into(), rgba(0x908caaff).into()), - ("number".into(), rgba(0x5cc1a3ff).into()), - ("comment".into(), rgba(0x6e6a86ff).into()), - ("string.special".into(), rgba(0xc4a7e6ff).into()), - ("string.escape".into(), rgba(0x8682a0ff).into()), - ("function.method".into(), rgba(0xea9a97ff).into()), - ("predictive".into(), rgba(0x516b83ff).into()), - ("punctuation.delimiter".into(), rgba(0xaeabc6ff).into()), - ("primary".into(), rgba(0xe0def4ff).into()), - ("link_text".into(), rgba(0x9ccfd8ff).into()), - ("string.regex".into(), rgba(0xc4a7e6ff).into()), - ("constructor".into(), rgba(0x9bced6ff).into()), - ("constant".into(), rgba(0x5cc1a3ff).into()), - ("emphasis.strong".into(), rgba(0x9bced6ff).into()), - ("function".into(), rgba(0xea9a97ff).into()), - ("hint".into(), rgba(0x728aa2ff).into()), - ("preproc".into(), rgba(0xe0def4ff).into()), - ("property".into(), rgba(0x9bced6ff).into()), - ("punctuation.list_marker".into(), rgba(0xaeabc6ff).into()), - ("emphasis".into(), rgba(0x9bced6ff).into()), - ("attribute".into(), rgba(0x9bced6ff).into()), - ("title".into(), rgba(0xf5c177ff).into()), - ("keyword".into(), rgba(0x3d8fb0ff).into()), - ("string".into(), rgba(0xf5c177ff).into()), - ("text.literal".into(), rgba(0xc4a7e6ff).into()), - ("embedded".into(), rgba(0xe0def4ff).into()), - ("comment.doc".into(), rgba(0x8682a0ff).into()), - ("variant".into(), rgba(0x9bced6ff).into()), - ("label".into(), rgba(0x9bced6ff).into()), - ("punctuation.special".into(), rgba(0xaeabc6ff).into()), - ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()), - ("tag".into(), rgba(0x9ccfd8ff).into()), - ("enum".into(), rgba(0xc4a7e6ff).into()), - ("boolean".into(), rgba(0xea9a97ff).into()), - ("punctuation.bracket".into(), rgba(0xaeabc6ff).into()), - ("operator".into(), rgba(0x3d8fb0ff).into()), - ("type".into(), rgba(0x9ccfd8ff).into()), - ("link_uri".into(), rgba(0xea9a97ff).into()), - ], - }, - status_bar: rgba(0x38354eff).into(), - title_bar: rgba(0x38354eff).into(), - toolbar: rgba(0x232136ff).into(), - tab_bar: rgba(0x28253cff).into(), - editor: rgba(0x232136ff).into(), - editor_subheader: rgba(0x28253cff).into(), - editor_active_line: rgba(0x28253cff).into(), - terminal: rgba(0x232136ff).into(), - image_fallback_background: rgba(0x38354eff).into(), - git_created: rgba(0x5cc1a3ff).into(), - git_modified: rgba(0x9bced6ff).into(), - git_deleted: rgba(0xea6e92ff).into(), - git_conflict: rgba(0xf5c177ff).into(), - git_ignored: rgba(0x605d7aff).into(), - git_renamed: rgba(0xf5c177ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x9bced6ff).into(), - selection: rgba(0x9bced63d).into(), - }, - PlayerTheme { - cursor: rgba(0x5cc1a3ff).into(), - selection: rgba(0x5cc1a33d).into(), - }, - PlayerTheme { - cursor: rgba(0xa683a0ff).into(), - selection: rgba(0xa683a03d).into(), - }, - PlayerTheme { - cursor: rgba(0xc4a7e6ff).into(), - selection: rgba(0xc4a7e63d).into(), - }, - PlayerTheme { - cursor: rgba(0xc4a7e6ff).into(), - selection: rgba(0xc4a7e63d).into(), - }, - PlayerTheme { - cursor: rgba(0x3e8fb0ff).into(), - selection: rgba(0x3e8fb03d).into(), - }, - PlayerTheme { - cursor: rgba(0xea6e92ff).into(), - selection: rgba(0xea6e923d).into(), - }, - PlayerTheme { - cursor: rgba(0xf5c177ff).into(), - selection: rgba(0xf5c1773d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/sandcastle.rs b/crates/theme2/src/themes/sandcastle.rs deleted file mode 100644 index 7fa0a27fb3..0000000000 --- a/crates/theme2/src/themes/sandcastle.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn sandcastle() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Sandcastle".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x3d4350ff).into(), - border_variant: rgba(0x3d4350ff).into(), - border_focused: rgba(0x223131ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x333944ff).into(), - surface: rgba(0x2b3038ff).into(), - background: rgba(0x333944ff).into(), - filled_element: rgba(0x333944ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x171e1eff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x171e1eff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfdf4c1ff).into(), - text_muted: rgba(0xa69782ff).into(), - text_placeholder: rgba(0xb3627aff).into(), - text_disabled: rgba(0x827568ff).into(), - text_accent: rgba(0x518b8bff).into(), - icon_muted: rgba(0xa69782ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("comment".into(), rgba(0xa89984ff).into()), - ("type".into(), rgba(0x83a598ff).into()), - ("preproc".into(), rgba(0xfdf4c1ff).into()), - ("punctuation.bracket".into(), rgba(0xd5c5a1ff).into()), - ("hint".into(), rgba(0x727d68ff).into()), - ("link_uri".into(), rgba(0x83a598ff).into()), - ("text.literal".into(), rgba(0xa07d3aff).into()), - ("enum".into(), rgba(0xa07d3aff).into()), - ("string.special".into(), rgba(0xa07d3aff).into()), - ("string".into(), rgba(0xa07d3aff).into()), - ("punctuation.special".into(), rgba(0xd5c5a1ff).into()), - ("keyword".into(), rgba(0x518b8bff).into()), - ("constructor".into(), rgba(0x518b8bff).into()), - ("predictive".into(), rgba(0x5c6152ff).into()), - ("title".into(), rgba(0xfdf4c1ff).into()), - ("variable".into(), rgba(0xfdf4c1ff).into()), - ("emphasis.strong".into(), rgba(0x518b8bff).into()), - ("primary".into(), rgba(0xfdf4c1ff).into()), - ("emphasis".into(), rgba(0x518b8bff).into()), - ("punctuation".into(), rgba(0xd5c5a1ff).into()), - ("constant".into(), rgba(0x83a598ff).into()), - ("link_text".into(), rgba(0xa07d3aff).into()), - ("punctuation.delimiter".into(), rgba(0xd5c5a1ff).into()), - ("embedded".into(), rgba(0xfdf4c1ff).into()), - ("string.special.symbol".into(), rgba(0xa07d3aff).into()), - ("tag".into(), rgba(0x518b8bff).into()), - ("punctuation.list_marker".into(), rgba(0xd5c5a1ff).into()), - ("operator".into(), rgba(0xa07d3aff).into()), - ("boolean".into(), rgba(0x83a598ff).into()), - ("function".into(), rgba(0xa07d3aff).into()), - ("attribute".into(), rgba(0x518b8bff).into()), - ("number".into(), rgba(0x83a598ff).into()), - ("string.escape".into(), rgba(0xa89984ff).into()), - ("comment.doc".into(), rgba(0xa89984ff).into()), - ("label".into(), rgba(0x518b8bff).into()), - ("string.regex".into(), rgba(0xa07d3aff).into()), - ("property".into(), rgba(0x518b8bff).into()), - ("variant".into(), rgba(0x518b8bff).into()), - ], - }, - status_bar: rgba(0x333944ff).into(), - title_bar: rgba(0x333944ff).into(), - toolbar: rgba(0x282c33ff).into(), - tab_bar: rgba(0x2b3038ff).into(), - editor: rgba(0x282c33ff).into(), - editor_subheader: rgba(0x2b3038ff).into(), - editor_active_line: rgba(0x2b3038ff).into(), - terminal: rgba(0x282c33ff).into(), - image_fallback_background: rgba(0x333944ff).into(), - git_created: rgba(0x83a598ff).into(), - git_modified: rgba(0x518b8bff).into(), - git_deleted: rgba(0xb3627aff).into(), - git_conflict: rgba(0xa07d3aff).into(), - git_ignored: rgba(0x827568ff).into(), - git_renamed: rgba(0xa07d3aff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x518b8bff).into(), - selection: rgba(0x518b8b3d).into(), - }, - PlayerTheme { - cursor: rgba(0x83a598ff).into(), - selection: rgba(0x83a5983d).into(), - }, - PlayerTheme { - cursor: rgba(0xa87222ff).into(), - selection: rgba(0xa872223d).into(), - }, - PlayerTheme { - cursor: rgba(0xa07d3aff).into(), - selection: rgba(0xa07d3a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xd75f5fff).into(), - selection: rgba(0xd75f5f3d).into(), - }, - PlayerTheme { - cursor: rgba(0x83a598ff).into(), - selection: rgba(0x83a5983d).into(), - }, - PlayerTheme { - cursor: rgba(0xb3627aff).into(), - selection: rgba(0xb3627a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xa07d3aff).into(), - selection: rgba(0xa07d3a3d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/solarized_dark.rs b/crates/theme2/src/themes/solarized_dark.rs deleted file mode 100644 index 2e381a6e95..0000000000 --- a/crates/theme2/src/themes/solarized_dark.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn solarized_dark() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Solarized Dark".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x2b4e58ff).into(), - border_variant: rgba(0x2b4e58ff).into(), - border_focused: rgba(0x1b3149ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x073743ff).into(), - surface: rgba(0x04313bff).into(), - background: rgba(0x073743ff).into(), - filled_element: rgba(0x073743ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x141f2cff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x141f2cff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xfdf6e3ff).into(), - text_muted: rgba(0x93a1a1ff).into(), - text_placeholder: rgba(0xdc3330ff).into(), - text_disabled: rgba(0x6f8389ff).into(), - text_accent: rgba(0x278ad1ff).into(), - icon_muted: rgba(0x93a1a1ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("punctuation.special".into(), rgba(0xefe9d6ff).into()), - ("string".into(), rgba(0xcb4b16ff).into()), - ("variant".into(), rgba(0x278ad1ff).into()), - ("variable".into(), rgba(0xfdf6e3ff).into()), - ("string.special.symbol".into(), rgba(0xcb4b16ff).into()), - ("primary".into(), rgba(0xfdf6e3ff).into()), - ("type".into(), rgba(0x2ba198ff).into()), - ("boolean".into(), rgba(0x849903ff).into()), - ("string.special".into(), rgba(0xcb4b16ff).into()), - ("label".into(), rgba(0x278ad1ff).into()), - ("link_uri".into(), rgba(0x849903ff).into()), - ("constructor".into(), rgba(0x278ad1ff).into()), - ("hint".into(), rgba(0x4f8297ff).into()), - ("preproc".into(), rgba(0xfdf6e3ff).into()), - ("text.literal".into(), rgba(0xcb4b16ff).into()), - ("string.escape".into(), rgba(0x99a5a4ff).into()), - ("link_text".into(), rgba(0xcb4b16ff).into()), - ("comment".into(), rgba(0x99a5a4ff).into()), - ("enum".into(), rgba(0xcb4b16ff).into()), - ("constant".into(), rgba(0x849903ff).into()), - ("comment.doc".into(), rgba(0x99a5a4ff).into()), - ("emphasis".into(), rgba(0x278ad1ff).into()), - ("predictive".into(), rgba(0x3f718bff).into()), - ("attribute".into(), rgba(0x278ad1ff).into()), - ("punctuation.delimiter".into(), rgba(0xefe9d6ff).into()), - ("function".into(), rgba(0xb58902ff).into()), - ("emphasis.strong".into(), rgba(0x278ad1ff).into()), - ("tag".into(), rgba(0x278ad1ff).into()), - ("string.regex".into(), rgba(0xcb4b16ff).into()), - ("property".into(), rgba(0x278ad1ff).into()), - ("keyword".into(), rgba(0x278ad1ff).into()), - ("number".into(), rgba(0x849903ff).into()), - ("embedded".into(), rgba(0xfdf6e3ff).into()), - ("operator".into(), rgba(0xcb4b16ff).into()), - ("punctuation".into(), rgba(0xefe9d6ff).into()), - ("punctuation.bracket".into(), rgba(0xefe9d6ff).into()), - ("title".into(), rgba(0xfdf6e3ff).into()), - ("punctuation.list_marker".into(), rgba(0xefe9d6ff).into()), - ], - }, - status_bar: rgba(0x073743ff).into(), - title_bar: rgba(0x073743ff).into(), - toolbar: rgba(0x002a35ff).into(), - tab_bar: rgba(0x04313bff).into(), - editor: rgba(0x002a35ff).into(), - editor_subheader: rgba(0x04313bff).into(), - editor_active_line: rgba(0x04313bff).into(), - terminal: rgba(0x002a35ff).into(), - image_fallback_background: rgba(0x073743ff).into(), - git_created: rgba(0x849903ff).into(), - git_modified: rgba(0x278ad1ff).into(), - git_deleted: rgba(0xdc3330ff).into(), - git_conflict: rgba(0xb58902ff).into(), - git_ignored: rgba(0x6f8389ff).into(), - git_renamed: rgba(0xb58902ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x278ad1ff).into(), - selection: rgba(0x278ad13d).into(), - }, - PlayerTheme { - cursor: rgba(0x849903ff).into(), - selection: rgba(0x8499033d).into(), - }, - PlayerTheme { - cursor: rgba(0xd33781ff).into(), - selection: rgba(0xd337813d).into(), - }, - PlayerTheme { - cursor: rgba(0xcb4b16ff).into(), - selection: rgba(0xcb4b163d).into(), - }, - PlayerTheme { - cursor: rgba(0x6c71c4ff).into(), - selection: rgba(0x6c71c43d).into(), - }, - PlayerTheme { - cursor: rgba(0x2ba198ff).into(), - selection: rgba(0x2ba1983d).into(), - }, - PlayerTheme { - cursor: rgba(0xdc3330ff).into(), - selection: rgba(0xdc33303d).into(), - }, - PlayerTheme { - cursor: rgba(0xb58902ff).into(), - selection: rgba(0xb589023d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/solarized_light.rs b/crates/theme2/src/themes/solarized_light.rs deleted file mode 100644 index a959a0a9d1..0000000000 --- a/crates/theme2/src/themes/solarized_light.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn solarized_light() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Solarized Light".into(), - is_light: true, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x9faaa8ff).into(), - border_variant: rgba(0x9faaa8ff).into(), - border_focused: rgba(0xbfd3efff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0xcfd0c4ff).into(), - surface: rgba(0xf3eddaff).into(), - background: rgba(0xcfd0c4ff).into(), - filled_element: rgba(0xcfd0c4ff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0xdbe6f6ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0xdbe6f6ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0x002a35ff).into(), - text_muted: rgba(0x34555eff).into(), - text_placeholder: rgba(0xdc3330ff).into(), - text_disabled: rgba(0x6a7f86ff).into(), - text_accent: rgba(0x288bd1ff).into(), - icon_muted: rgba(0x34555eff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("string.escape".into(), rgba(0x30525bff).into()), - ("boolean".into(), rgba(0x849903ff).into()), - ("comment.doc".into(), rgba(0x30525bff).into()), - ("string.special".into(), rgba(0xcb4b17ff).into()), - ("punctuation".into(), rgba(0x04333eff).into()), - ("emphasis".into(), rgba(0x288bd1ff).into()), - ("type".into(), rgba(0x2ba198ff).into()), - ("preproc".into(), rgba(0x002a35ff).into()), - ("emphasis.strong".into(), rgba(0x288bd1ff).into()), - ("constant".into(), rgba(0x849903ff).into()), - ("title".into(), rgba(0x002a35ff).into()), - ("operator".into(), rgba(0xcb4b17ff).into()), - ("punctuation.bracket".into(), rgba(0x04333eff).into()), - ("link_uri".into(), rgba(0x849903ff).into()), - ("label".into(), rgba(0x288bd1ff).into()), - ("enum".into(), rgba(0xcb4b17ff).into()), - ("property".into(), rgba(0x288bd1ff).into()), - ("predictive".into(), rgba(0x679aafff).into()), - ("punctuation.special".into(), rgba(0x04333eff).into()), - ("text.literal".into(), rgba(0xcb4b17ff).into()), - ("string".into(), rgba(0xcb4b17ff).into()), - ("string.regex".into(), rgba(0xcb4b17ff).into()), - ("variable".into(), rgba(0x002a35ff).into()), - ("tag".into(), rgba(0x288bd1ff).into()), - ("string.special.symbol".into(), rgba(0xcb4b17ff).into()), - ("link_text".into(), rgba(0xcb4b17ff).into()), - ("punctuation.list_marker".into(), rgba(0x04333eff).into()), - ("keyword".into(), rgba(0x288bd1ff).into()), - ("constructor".into(), rgba(0x288bd1ff).into()), - ("attribute".into(), rgba(0x288bd1ff).into()), - ("variant".into(), rgba(0x288bd1ff).into()), - ("function".into(), rgba(0xb58903ff).into()), - ("primary".into(), rgba(0x002a35ff).into()), - ("hint".into(), rgba(0x5789a3ff).into()), - ("comment".into(), rgba(0x30525bff).into()), - ("number".into(), rgba(0x849903ff).into()), - ("punctuation.delimiter".into(), rgba(0x04333eff).into()), - ("embedded".into(), rgba(0x002a35ff).into()), - ], - }, - status_bar: rgba(0xcfd0c4ff).into(), - title_bar: rgba(0xcfd0c4ff).into(), - toolbar: rgba(0xfdf6e3ff).into(), - tab_bar: rgba(0xf3eddaff).into(), - editor: rgba(0xfdf6e3ff).into(), - editor_subheader: rgba(0xf3eddaff).into(), - editor_active_line: rgba(0xf3eddaff).into(), - terminal: rgba(0xfdf6e3ff).into(), - image_fallback_background: rgba(0xcfd0c4ff).into(), - git_created: rgba(0x849903ff).into(), - git_modified: rgba(0x288bd1ff).into(), - git_deleted: rgba(0xdc3330ff).into(), - git_conflict: rgba(0xb58903ff).into(), - git_ignored: rgba(0x6a7f86ff).into(), - git_renamed: rgba(0xb58903ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x288bd1ff).into(), - selection: rgba(0x288bd13d).into(), - }, - PlayerTheme { - cursor: rgba(0x849903ff).into(), - selection: rgba(0x8499033d).into(), - }, - PlayerTheme { - cursor: rgba(0xd33781ff).into(), - selection: rgba(0xd337813d).into(), - }, - PlayerTheme { - cursor: rgba(0xcb4b17ff).into(), - selection: rgba(0xcb4b173d).into(), - }, - PlayerTheme { - cursor: rgba(0x6c71c3ff).into(), - selection: rgba(0x6c71c33d).into(), - }, - PlayerTheme { - cursor: rgba(0x2ba198ff).into(), - selection: rgba(0x2ba1983d).into(), - }, - PlayerTheme { - cursor: rgba(0xdc3330ff).into(), - selection: rgba(0xdc33303d).into(), - }, - PlayerTheme { - cursor: rgba(0xb58903ff).into(), - selection: rgba(0xb589033d).into(), - }, - ], - } -} diff --git a/crates/theme2/src/themes/summercamp.rs b/crates/theme2/src/themes/summercamp.rs deleted file mode 100644 index c1e66aedd1..0000000000 --- a/crates/theme2/src/themes/summercamp.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gpui2::rgba; - -use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub fn summercamp() -> Theme { - Theme { - metadata: ThemeMetadata { - name: "Summercamp".into(), - is_light: false, - }, - transparent: rgba(0x00000000).into(), - mac_os_traffic_light_red: rgba(0xec695eff).into(), - mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(), - mac_os_traffic_light_green: rgba(0x61c553ff).into(), - border: rgba(0x302c21ff).into(), - border_variant: rgba(0x302c21ff).into(), - border_focused: rgba(0x193760ff).into(), - border_transparent: rgba(0x00000000).into(), - elevated_surface: rgba(0x2a261cff).into(), - surface: rgba(0x231f16ff).into(), - background: rgba(0x2a261cff).into(), - filled_element: rgba(0x2a261cff).into(), - filled_element_hover: rgba(0xffffff1e).into(), - filled_element_active: rgba(0xffffff28).into(), - filled_element_selected: rgba(0x0e2242ff).into(), - filled_element_disabled: rgba(0x00000000).into(), - ghost_element: rgba(0x00000000).into(), - ghost_element_hover: rgba(0xffffff14).into(), - ghost_element_active: rgba(0xffffff1e).into(), - ghost_element_selected: rgba(0x0e2242ff).into(), - ghost_element_disabled: rgba(0x00000000).into(), - text: rgba(0xf8f5deff).into(), - text_muted: rgba(0x736e55ff).into(), - text_placeholder: rgba(0xe35041ff).into(), - text_disabled: rgba(0x4c4735ff).into(), - text_accent: rgba(0x499befff).into(), - icon_muted: rgba(0x736e55ff).into(), - syntax: SyntaxTheme { - highlights: vec![ - ("predictive".into(), rgba(0x78434aff).into()), - ("title".into(), rgba(0xf8f5deff).into()), - ("primary".into(), rgba(0xf8f5deff).into()), - ("punctuation.special".into(), rgba(0xbfbb9bff).into()), - ("constant".into(), rgba(0x5dea5aff).into()), - ("string.regex".into(), rgba(0xfaa11cff).into()), - ("tag".into(), rgba(0x499befff).into()), - ("preproc".into(), rgba(0xf8f5deff).into()), - ("comment".into(), rgba(0x777159ff).into()), - ("punctuation.bracket".into(), rgba(0xbfbb9bff).into()), - ("constructor".into(), rgba(0x499befff).into()), - ("type".into(), rgba(0x5aeabbff).into()), - ("variable".into(), rgba(0xf8f5deff).into()), - ("operator".into(), rgba(0xfaa11cff).into()), - ("boolean".into(), rgba(0x5dea5aff).into()), - ("attribute".into(), rgba(0x499befff).into()), - ("link_text".into(), rgba(0xfaa11cff).into()), - ("string.escape".into(), rgba(0x777159ff).into()), - ("string.special".into(), rgba(0xfaa11cff).into()), - ("string.special.symbol".into(), rgba(0xfaa11cff).into()), - ("hint".into(), rgba(0x246e61ff).into()), - ("link_uri".into(), rgba(0x5dea5aff).into()), - ("comment.doc".into(), rgba(0x777159ff).into()), - ("emphasis".into(), rgba(0x499befff).into()), - ("punctuation".into(), rgba(0xbfbb9bff).into()), - ("text.literal".into(), rgba(0xfaa11cff).into()), - ("number".into(), rgba(0x5dea5aff).into()), - ("punctuation.delimiter".into(), rgba(0xbfbb9bff).into()), - ("label".into(), rgba(0x499befff).into()), - ("function".into(), rgba(0xf1fe28ff).into()), - ("property".into(), rgba(0x499befff).into()), - ("keyword".into(), rgba(0x499befff).into()), - ("embedded".into(), rgba(0xf8f5deff).into()), - ("string".into(), rgba(0xfaa11cff).into()), - ("punctuation.list_marker".into(), rgba(0xbfbb9bff).into()), - ("enum".into(), rgba(0xfaa11cff).into()), - ("emphasis.strong".into(), rgba(0x499befff).into()), - ("variant".into(), rgba(0x499befff).into()), - ], - }, - status_bar: rgba(0x2a261cff).into(), - title_bar: rgba(0x2a261cff).into(), - toolbar: rgba(0x1b1810ff).into(), - tab_bar: rgba(0x231f16ff).into(), - editor: rgba(0x1b1810ff).into(), - editor_subheader: rgba(0x231f16ff).into(), - editor_active_line: rgba(0x231f16ff).into(), - terminal: rgba(0x1b1810ff).into(), - image_fallback_background: rgba(0x2a261cff).into(), - git_created: rgba(0x5dea5aff).into(), - git_modified: rgba(0x499befff).into(), - git_deleted: rgba(0xe35041ff).into(), - git_conflict: rgba(0xf1fe28ff).into(), - git_ignored: rgba(0x4c4735ff).into(), - git_renamed: rgba(0xf1fe28ff).into(), - players: [ - PlayerTheme { - cursor: rgba(0x499befff).into(), - selection: rgba(0x499bef3d).into(), - }, - PlayerTheme { - cursor: rgba(0x5dea5aff).into(), - selection: rgba(0x5dea5a3d).into(), - }, - PlayerTheme { - cursor: rgba(0xf59be6ff).into(), - selection: rgba(0xf59be63d).into(), - }, - PlayerTheme { - cursor: rgba(0xfaa11cff).into(), - selection: rgba(0xfaa11c3d).into(), - }, - PlayerTheme { - cursor: rgba(0xfe8080ff).into(), - selection: rgba(0xfe80803d).into(), - }, - PlayerTheme { - cursor: rgba(0x5aeabbff).into(), - selection: rgba(0x5aeabb3d).into(), - }, - PlayerTheme { - cursor: rgba(0xe35041ff).into(), - selection: rgba(0xe350413d).into(), - }, - PlayerTheme { - cursor: rgba(0xf1fe28ff).into(), - selection: rgba(0xf1fe283d).into(), - }, - ], - } -} diff --git a/crates/theme_converter/Cargo.toml b/crates/theme_converter/Cargo.toml deleted file mode 100644 index 0ec692b7cc..0000000000 --- a/crates/theme_converter/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "theme_converter" -version = "0.1.0" -edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow.workspace = true -clap = { version = "4.4", features = ["derive", "string"] } -convert_case = "0.6.0" -gpui2 = { path = "../gpui2" } -log.workspace = true -rust-embed.workspace = true -serde.workspace = true -simplelog = "0.9" -theme2 = { path = "../theme2" } diff --git a/crates/theme_converter/src/main.rs b/crates/theme_converter/src/main.rs deleted file mode 100644 index cc0cdf9c99..0000000000 --- a/crates/theme_converter/src/main.rs +++ /dev/null @@ -1,390 +0,0 @@ -mod theme_printer; - -use std::borrow::Cow; -use std::collections::HashMap; -use std::fmt::{self, Debug}; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::str::FromStr; - -use anyhow::{anyhow, Context, Result}; -use clap::Parser; -use convert_case::{Case, Casing}; -use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, SharedString}; -use log::LevelFilter; -use rust_embed::RustEmbed; -use serde::de::Visitor; -use serde::{Deserialize, Deserializer}; -use simplelog::SimpleLogger; -use theme2::{PlayerTheme, SyntaxTheme}; - -use crate::theme_printer::ThemePrinter; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - /// The name of the theme to convert. - theme: String, -} - -fn main() -> Result<()> { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - - // let args = Args::parse(); - - let themes_path = PathBuf::from_str("crates/theme2/src/themes")?; - - let mut theme_modules = Vec::new(); - - for theme_path in Assets.list("themes/")? { - let (_, theme_name) = theme_path.split_once("themes/").unwrap(); - - if theme_name == ".gitkeep" { - continue; - } - - let (json_theme, legacy_theme) = load_theme(&theme_path)?; - - let theme = convert_theme(json_theme, legacy_theme)?; - - let theme_slug = theme - .metadata - .name - .as_ref() - .replace("é", "e") - .to_case(Case::Snake); - - let mut output_file = File::create(themes_path.join(format!("{theme_slug}.rs")))?; - - let theme_module = format!( - r#" - use gpui2::rgba; - - use crate::{{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}}; - - pub fn {theme_slug}() -> Theme {{ - {theme_definition} - }} - "#, - theme_definition = format!("{:#?}", ThemePrinter::new(theme)) - ); - - output_file.write_all(theme_module.as_bytes())?; - - theme_modules.push(theme_slug); - } - - let mut mod_rs_file = File::create(themes_path.join(format!("mod.rs")))?; - - let mod_rs_contents = format!( - r#" - {mod_statements} - - {use_statements} - "#, - mod_statements = theme_modules - .iter() - .map(|module| format!("mod {module};")) - .collect::>() - .join("\n"), - use_statements = theme_modules - .iter() - .map(|module| format!("pub use {module}::*;")) - .collect::>() - .join("\n") - ); - - mod_rs_file.write_all(mod_rs_contents.as_bytes())?; - - Ok(()) -} - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "fonts/**/*"] -#[include = "icons/**/*"] -#[include = "themes/**/*"] -#[include = "sounds/**/*"] -#[include = "*.md"] -#[exclude = "*.DS_Store"] -pub struct Assets; - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result> { - Self::get(path) - .map(|f| f.data) - .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) - } - - fn list(&self, path: &str) -> Result> { - Ok(Self::iter() - .filter(|p| p.starts_with(path)) - .map(SharedString::from) - .collect()) - } -} - -#[derive(Clone, Copy)] -pub struct PlayerThemeColors { - pub cursor: Hsla, - pub selection: Hsla, -} - -impl PlayerThemeColors { - pub fn new(theme: &LegacyTheme, ix: usize) -> Self { - if ix < theme.players.len() { - Self { - cursor: theme.players[ix].cursor, - selection: theme.players[ix].selection, - } - } else { - Self { - cursor: rgb::(0xff00ff), - selection: rgb::(0xff00ff), - } - } - } -} - -impl From for PlayerTheme { - fn from(value: PlayerThemeColors) -> Self { - Self { - cursor: value.cursor, - selection: value.selection, - } - } -} - -fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result { - let transparent = hsla(0.0, 0.0, 0.0, 0.0); - - let players: [PlayerTheme; 8] = [ - PlayerThemeColors::new(&legacy_theme, 0).into(), - PlayerThemeColors::new(&legacy_theme, 1).into(), - PlayerThemeColors::new(&legacy_theme, 2).into(), - PlayerThemeColors::new(&legacy_theme, 3).into(), - PlayerThemeColors::new(&legacy_theme, 4).into(), - PlayerThemeColors::new(&legacy_theme, 5).into(), - PlayerThemeColors::new(&legacy_theme, 6).into(), - PlayerThemeColors::new(&legacy_theme, 7).into(), - ]; - - let theme = theme2::Theme { - metadata: theme2::ThemeMetadata { - name: legacy_theme.name.clone().into(), - is_light: legacy_theme.is_light, - }, - transparent, - mac_os_traffic_light_red: rgb::(0xEC695E), - mac_os_traffic_light_yellow: rgb::(0xF4BF4F), - mac_os_traffic_light_green: rgb::(0x62C554), - border: legacy_theme.lowest.base.default.border, - border_variant: legacy_theme.lowest.variant.default.border, - border_focused: legacy_theme.lowest.accent.default.border, - border_transparent: transparent, - elevated_surface: legacy_theme.lowest.base.default.background, - surface: legacy_theme.middle.base.default.background, - background: legacy_theme.lowest.base.default.background, - filled_element: legacy_theme.lowest.base.default.background, - filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12), - filled_element_active: hsla(0.0, 0.0, 100.0, 0.16), - filled_element_selected: legacy_theme.lowest.accent.default.background, - filled_element_disabled: transparent, - ghost_element: transparent, - ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08), - ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12), - ghost_element_selected: legacy_theme.lowest.accent.default.background, - ghost_element_disabled: transparent, - text: legacy_theme.lowest.base.default.foreground, - text_muted: legacy_theme.lowest.variant.default.foreground, - /// TODO: map this to a real value - text_placeholder: legacy_theme.lowest.negative.default.foreground, - text_disabled: legacy_theme.lowest.base.disabled.foreground, - text_accent: legacy_theme.lowest.accent.default.foreground, - icon_muted: legacy_theme.lowest.variant.default.foreground, - syntax: SyntaxTheme { - highlights: json_theme - .editor - .syntax - .iter() - .map(|(token, style)| (token.clone(), style.color.clone().into())) - .collect(), - }, - status_bar: legacy_theme.lowest.base.default.background, - title_bar: legacy_theme.lowest.base.default.background, - toolbar: legacy_theme.highest.base.default.background, - tab_bar: legacy_theme.middle.base.default.background, - editor: legacy_theme.highest.base.default.background, - editor_subheader: legacy_theme.middle.base.default.background, - terminal: legacy_theme.highest.base.default.background, - editor_active_line: legacy_theme.highest.on.default.background, - image_fallback_background: legacy_theme.lowest.base.default.background, - - git_created: legacy_theme.lowest.positive.default.foreground, - git_modified: legacy_theme.lowest.accent.default.foreground, - git_deleted: legacy_theme.lowest.negative.default.foreground, - git_conflict: legacy_theme.lowest.warning.default.foreground, - git_ignored: legacy_theme.lowest.base.disabled.foreground, - git_renamed: legacy_theme.lowest.warning.default.foreground, - - players, - }; - - Ok(theme) -} - -#[derive(Deserialize)] -struct JsonTheme { - pub editor: JsonEditorTheme, - pub base_theme: serde_json::Value, -} - -#[derive(Deserialize)] -struct JsonEditorTheme { - pub syntax: HashMap, -} - -#[derive(Deserialize)] -struct JsonSyntaxStyle { - pub color: Hsla, -} - -/// Loads the [`Theme`] with the given name. -fn load_theme(theme_path: &str) -> Result<(JsonTheme, LegacyTheme)> { - let theme_contents = - Assets::get(theme_path).with_context(|| format!("theme file not found: '{theme_path}'"))?; - - let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?) - .context("failed to parse legacy theme")?; - - let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone()) - .context("failed to parse `base_theme`")?; - - Ok((json_theme, legacy_theme)) -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct LegacyTheme { - pub name: String, - pub is_light: bool, - pub lowest: Layer, - pub middle: Layer, - pub highest: Layer, - pub popover_shadow: Shadow, - pub modal_shadow: Shadow, - #[serde(deserialize_with = "deserialize_player_colors")] - pub players: Vec, - #[serde(deserialize_with = "deserialize_syntax_colors")] - pub syntax: HashMap, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct Layer { - pub base: StyleSet, - pub variant: StyleSet, - pub on: StyleSet, - pub accent: StyleSet, - pub positive: StyleSet, - pub warning: StyleSet, - pub negative: StyleSet, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct StyleSet { - #[serde(rename = "default")] - pub default: ContainerColors, - pub hovered: ContainerColors, - pub pressed: ContainerColors, - pub active: ContainerColors, - pub disabled: ContainerColors, - pub inverted: ContainerColors, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct ContainerColors { - pub background: Hsla, - pub foreground: Hsla, - pub border: Hsla, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct PlayerColors { - pub selection: Hsla, - pub cursor: Hsla, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct Shadow { - pub blur: u8, - pub color: Hsla, - pub offset: Vec, -} - -fn deserialize_player_colors<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct PlayerArrayVisitor; - - impl<'de> Visitor<'de> for PlayerArrayVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an object with integer keys") - } - - fn visit_map>( - self, - mut map: A, - ) -> Result { - let mut players = Vec::with_capacity(8); - while let Some((key, value)) = map.next_entry::()? { - if key < 8 { - players.push(value); - } else { - return Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Unsigned(key as u64), - &"a key in range 0..7", - )); - } - } - Ok(players) - } - } - - deserializer.deserialize_map(PlayerArrayVisitor) -} - -fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - #[derive(Deserialize)] - struct ColorWrapper { - color: Hsla, - } - - struct SyntaxVisitor; - - impl<'de> Visitor<'de> for SyntaxVisitor { - type Value = HashMap; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with keys and objects with a single color field as values") - } - - fn visit_map(self, mut map: M) -> Result, M::Error> - where - M: serde::de::MapAccess<'de>, - { - let mut result = HashMap::new(); - while let Some(key) = map.next_key()? { - let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla - result.insert(key, wrapper.color); - } - Ok(result) - } - } - deserializer.deserialize_map(SyntaxVisitor) -} diff --git a/crates/theme_converter/src/theme_printer.rs b/crates/theme_converter/src/theme_printer.rs deleted file mode 100644 index 3a9bdb159b..0000000000 --- a/crates/theme_converter/src/theme_printer.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::fmt::{self, Debug}; - -use gpui2::{Hsla, Rgba}; -use theme2::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}; - -pub struct ThemePrinter(Theme); - -impl ThemePrinter { - pub fn new(theme: Theme) -> Self { - Self(theme) - } -} - -struct HslaPrinter(Hsla); - -impl Debug for HslaPrinter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", IntoPrinter(&Rgba::from(self.0))) - } -} - -struct IntoPrinter<'a, D: Debug>(&'a D); - -impl<'a, D: Debug> Debug for IntoPrinter<'a, D> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}.into()", self.0) - } -} - -impl Debug for ThemePrinter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Theme") - .field("metadata", &ThemeMetadataPrinter(self.0.metadata.clone())) - .field("transparent", &HslaPrinter(self.0.transparent)) - .field( - "mac_os_traffic_light_red", - &HslaPrinter(self.0.mac_os_traffic_light_red), - ) - .field( - "mac_os_traffic_light_yellow", - &HslaPrinter(self.0.mac_os_traffic_light_yellow), - ) - .field( - "mac_os_traffic_light_green", - &HslaPrinter(self.0.mac_os_traffic_light_green), - ) - .field("border", &HslaPrinter(self.0.border)) - .field("border_variant", &HslaPrinter(self.0.border_variant)) - .field("border_focused", &HslaPrinter(self.0.border_focused)) - .field( - "border_transparent", - &HslaPrinter(self.0.border_transparent), - ) - .field("elevated_surface", &HslaPrinter(self.0.elevated_surface)) - .field("surface", &HslaPrinter(self.0.surface)) - .field("background", &HslaPrinter(self.0.background)) - .field("filled_element", &HslaPrinter(self.0.filled_element)) - .field( - "filled_element_hover", - &HslaPrinter(self.0.filled_element_hover), - ) - .field( - "filled_element_active", - &HslaPrinter(self.0.filled_element_active), - ) - .field( - "filled_element_selected", - &HslaPrinter(self.0.filled_element_selected), - ) - .field( - "filled_element_disabled", - &HslaPrinter(self.0.filled_element_disabled), - ) - .field("ghost_element", &HslaPrinter(self.0.ghost_element)) - .field( - "ghost_element_hover", - &HslaPrinter(self.0.ghost_element_hover), - ) - .field( - "ghost_element_active", - &HslaPrinter(self.0.ghost_element_active), - ) - .field( - "ghost_element_selected", - &HslaPrinter(self.0.ghost_element_selected), - ) - .field( - "ghost_element_disabled", - &HslaPrinter(self.0.ghost_element_disabled), - ) - .field("text", &HslaPrinter(self.0.text)) - .field("text_muted", &HslaPrinter(self.0.text_muted)) - .field("text_placeholder", &HslaPrinter(self.0.text_placeholder)) - .field("text_disabled", &HslaPrinter(self.0.text_disabled)) - .field("text_accent", &HslaPrinter(self.0.text_accent)) - .field("icon_muted", &HslaPrinter(self.0.icon_muted)) - .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone())) - .field("status_bar", &HslaPrinter(self.0.status_bar)) - .field("title_bar", &HslaPrinter(self.0.title_bar)) - .field("toolbar", &HslaPrinter(self.0.toolbar)) - .field("tab_bar", &HslaPrinter(self.0.tab_bar)) - .field("editor", &HslaPrinter(self.0.editor)) - .field("editor_subheader", &HslaPrinter(self.0.editor_subheader)) - .field( - "editor_active_line", - &HslaPrinter(self.0.editor_active_line), - ) - .field("terminal", &HslaPrinter(self.0.terminal)) - .field( - "image_fallback_background", - &HslaPrinter(self.0.image_fallback_background), - ) - .field("git_created", &HslaPrinter(self.0.git_created)) - .field("git_modified", &HslaPrinter(self.0.git_modified)) - .field("git_deleted", &HslaPrinter(self.0.git_deleted)) - .field("git_conflict", &HslaPrinter(self.0.git_conflict)) - .field("git_ignored", &HslaPrinter(self.0.git_ignored)) - .field("git_renamed", &HslaPrinter(self.0.git_renamed)) - .field("players", &self.0.players.map(PlayerThemePrinter)) - .finish() - } -} - -pub struct ThemeMetadataPrinter(ThemeMetadata); - -impl Debug for ThemeMetadataPrinter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ThemeMetadata") - .field("name", &IntoPrinter(&self.0.name)) - .field("is_light", &self.0.is_light) - .finish() - } -} - -pub struct SyntaxThemePrinter(SyntaxTheme); - -impl Debug for SyntaxThemePrinter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SyntaxTheme") - .field( - "highlights", - &VecPrinter( - &self - .0 - .highlights - .iter() - .map(|(token, highlight)| { - (IntoPrinter(token), HslaPrinter(highlight.color.unwrap())) - }) - .collect(), - ), - ) - .finish() - } -} - -pub struct VecPrinter<'a, T>(&'a Vec); - -impl<'a, T: Debug> Debug for VecPrinter<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "vec!{:?}", &self.0) - } -} - -pub struct PlayerThemePrinter(PlayerTheme); - -impl Debug for PlayerThemePrinter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PlayerTheme") - .field("cursor", &HslaPrinter(self.0.cursor)) - .field("selection", &HslaPrinter(self.0.selection)) - .finish() - } -} diff --git a/crates/ui2/Cargo.toml b/crates/ui2/Cargo.toml index 58013e34cd..f11fd652b6 100644 --- a/crates/ui2/Cargo.toml +++ b/crates/ui2/Cargo.toml @@ -10,6 +10,7 @@ chrono = "0.4" gpui2 = { path = "../gpui2" } itertools = { version = "0.11.0", optional = true } serde.workspace = true +settings2 = { path = "../settings2" } smallvec.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme2 = { path = "../theme2" } diff --git a/crates/ui2/src/components/breadcrumb.rs b/crates/ui2/src/components/breadcrumb.rs index 6b2dfe1cce..163dfabfb0 100644 --- a/crates/ui2/src/components/breadcrumb.rs +++ b/crates/ui2/src/components/breadcrumb.rs @@ -19,24 +19,22 @@ impl Breadcrumb { } fn render_separator(&self, cx: &WindowContext) -> Div { - let theme = theme(cx); - - div().child(" › ").text_color(theme.text_muted) + div() + .child(" › ") + .text_color(cx.theme().colors().text_muted) } fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let symbols_len = self.symbols.len(); h_stack() .id("breadcrumb") .px_1() .text_sm() - .text_color(theme.text_muted) + .text_color(cx.theme().colors().text_muted) .rounded_md() - .hover(|style| style.bg(theme.ghost_element_hover)) - .active(|style| style.bg(theme.ghost_element_active)) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .child(self.path.clone().to_str().unwrap().to_string()) .child(if !self.symbols.is_empty() { self.render_separator(cx) @@ -84,8 +82,6 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = theme(cx); - Story::container(cx) .child(Story::title_for::<_, Breadcrumb>(cx)) .child(Story::label(cx, "Default")) @@ -95,21 +91,21 @@ mod stories { Symbol(vec![ HighlightedText { text: "impl ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "BreadcrumbStory".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, ]), Symbol(vec![ HighlightedText { text: "fn ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "render".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, ]), ], diff --git a/crates/ui2/src/components/buffer.rs b/crates/ui2/src/components/buffer.rs index 33a98b6ea9..2b3db676ce 100644 --- a/crates/ui2/src/components/buffer.rs +++ b/crates/ui2/src/components/buffer.rs @@ -155,18 +155,16 @@ impl Buffer { } fn render_row(row: BufferRow, cx: &WindowContext) -> impl Component { - let theme = theme(cx); - let line_background = if row.current { - theme.editor_active_line + cx.theme().colors().editor_active_line } else { - theme.transparent + cx.theme().styles.system.transparent }; let line_number_color = if row.current { - theme.text + cx.theme().colors().text } else { - theme.syntax.get("comment").color.unwrap_or_default() + cx.theme().syntax_color("comment") }; h_stack() @@ -216,14 +214,13 @@ impl Buffer { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); let rows = self.render_rows(cx); v_stack() .flex_1() .w_full() .h_full() - .bg(theme.editor) + .bg(cx.theme().colors().editor) .children(rows) } } @@ -246,8 +243,6 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = theme(cx); - Story::container(cx) .child(Story::title_for::<_, Buffer>(cx)) .child(Story::label(cx, "Default")) @@ -257,14 +252,14 @@ mod stories { div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_example(&theme)), + .child(hello_world_rust_buffer_example(cx)), ) .child(Story::label(cx, "Hello World (Rust) with Status")) .child( div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_with_status_example(&theme)), + .child(hello_world_rust_buffer_with_status_example(cx)), ) } } diff --git a/crates/ui2/src/components/buffer_search.rs b/crates/ui2/src/components/buffer_search.rs index c5539f0a4a..5d7de1b408 100644 --- a/crates/ui2/src/components/buffer_search.rs +++ b/crates/ui2/src/components/buffer_search.rs @@ -30,9 +30,7 @@ impl Render for BufferSearch { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { - let theme = theme(cx); - - h_stack().bg(theme.toolbar).p_2().child( + h_stack().bg(cx.theme().colors().toolbar).p_2().child( h_stack().child(Input::new("Search")).child( IconButton::::new("replace", Icon::Replace) .when(self.is_replace_open, |this| this.color(IconColor::Accent)) diff --git a/crates/ui2/src/components/collab_panel.rs b/crates/ui2/src/components/collab_panel.rs index a8552c0f23..a0e3b55f63 100644 --- a/crates/ui2/src/components/collab_panel.rs +++ b/crates/ui2/src/components/collab_panel.rs @@ -15,27 +15,29 @@ impl CollabPanel { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .id(self.id.clone()) .h_full() - .bg(theme.surface) + .bg(cx.theme().colors().surface) .child( v_stack() .id("crdb") .w_full() .overflow_y_scroll() .child( - div().pb_1().border_color(theme.border).border_b().child( - List::new(static_collab_panel_current_call()) - .header( - ListHeader::new("CRDB") - .left_icon(Icon::Hash.into()) - .toggle(ToggleState::Toggled), - ) - .toggle(ToggleState::Toggled), - ), + div() + .pb_1() + .border_color(cx.theme().colors().border) + .border_b() + .child( + List::new(static_collab_panel_current_call()) + .header( + ListHeader::new("CRDB") + .left_icon(Icon::Hash.into()) + .toggle(ToggleState::Toggled), + ) + .toggle(ToggleState::Toggled), + ), ) .child( v_stack().id("channels").py_1().child( @@ -71,13 +73,13 @@ impl CollabPanel { .h_7() .px_2() .border_t() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .flex() .items_center() .child( div() .text_sm() - .text_color(theme.text_placeholder) + .text_color(cx.theme().colors().text_placeholder) .child("Find..."), ), ) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 812221036a..8345be1b35 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -44,13 +44,11 @@ impl ContextMenu { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .flex() - .bg(theme.elevated_surface) + .bg(cx.theme().colors().elevated_surface) .border() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .child( List::new( self.items diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 980a1c98aa..101c845a76 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui2::MouseButton; +use gpui2::{rems, MouseButton}; use crate::{h_stack, prelude::*}; use crate::{ClickHandler, Icon, IconColor, IconElement}; @@ -66,8 +66,6 @@ impl IconButton { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => IconColor::Disabled, _ => self.color, @@ -75,14 +73,14 @@ impl IconButton { let (bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( - theme.filled_element, - theme.filled_element_hover, - theme.filled_element_active, + cx.theme().colors().element, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, ), ButtonVariant::Ghost => ( - theme.ghost_element, - theme.ghost_element_hover, - theme.ghost_element_active, + cx.theme().colors().ghost_element, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, ), }; @@ -90,8 +88,8 @@ impl IconButton { .id(self.id.clone()) .justify_center() .rounded_md() - .py(ui_size(cx, 0.25)) - .px(ui_size(cx, 6. / 14.)) + .py(rems(0.21875)) + .px(rems(0.375)) .bg(bg_color) .hover(|style| style.bg(bg_hover_color)) .active(|style| style.bg(bg_active_color)) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 455cfe5b59..88cabbdc88 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -60,15 +60,13 @@ impl Key { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - div() .px_2() .py_0() .rounded_md() .text_sm() - .text_color(theme.text) - .bg(theme.filled_element) + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().element) .child(self.key.clone()) } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 9557e68d7f..50a86ff256 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,4 +1,4 @@ -use gpui2::{div, relative, Div}; +use gpui2::{div, px, relative, Div}; use crate::settings::user_settings; use crate::{ @@ -15,12 +15,20 @@ pub enum ListItemVariant { Inset, } +pub enum ListHeaderMeta { + // TODO: These should be IconButtons + Tools(Vec), + // TODO: This should be a button + Button(Label), + Text(Label), +} + #[derive(Component)] pub struct ListHeader { label: SharedString, left_icon: Option, + meta: Option, variant: ListItemVariant, - state: InteractionState, toggleable: Toggleable, } @@ -29,9 +37,9 @@ impl ListHeader { Self { label: label.into(), left_icon: None, + meta: None, variant: ListItemVariant::default(), - state: InteractionState::default(), - toggleable: Toggleable::Toggleable(ToggleState::Toggled), + toggleable: Toggleable::NotToggleable, } } @@ -50,8 +58,8 @@ impl ListHeader { self } - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; + pub fn meta(mut self, meta: Option) -> Self { + self.meta = meta; self } @@ -74,35 +82,36 @@ impl ListHeader { } } - fn label_color(&self) -> LabelColor { - match self.state { - InteractionState::Disabled => LabelColor::Disabled, - _ => Default::default(), - } - } - - fn icon_color(&self) -> IconColor { - match self.state { - InteractionState::Disabled => IconColor::Disabled, - _ => Default::default(), - } - } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let is_toggleable = self.toggleable != Toggleable::NotToggleable; let is_toggled = self.toggleable.is_toggled(); let disclosure_control = self.disclosure_control(); + let meta = match self.meta { + Some(ListHeaderMeta::Tools(icons)) => div().child( + h_stack() + .gap_2() + .items_center() + .children(icons.into_iter().map(|i| { + IconElement::new(i) + .color(IconColor::Muted) + .size(IconSize::Small) + })), + ), + Some(ListHeaderMeta::Button(label)) => div().child(label), + Some(ListHeaderMeta::Text(label)) => div().child(label), + None => div(), + }; + h_stack() - .flex_1() .w_full() - .bg(theme.surface) - .when(self.state == InteractionState::Focused, |this| { - this.border().border_color(theme.border_focused) - }) + .bg(cx.theme().colors().surface) + // TODO: Add focus state + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(cx.theme().colors().border_focused) + // }) .relative() .child( div() @@ -110,22 +119,28 @@ impl ListHeader { .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) .flex() .flex_1() + .items_center() + .justify_between() .w_full() .gap_1() - .items_center() .child( - div() - .flex() + h_stack() .gap_1() - .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(IconColor::Muted) - .size(IconSize::Small) - })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| { + IconElement::new(i) + .color(IconColor::Muted) + .size(IconSize::Small) + })) + .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + ) + .child(disclosure_control), ) - .child(disclosure_control), + .child(meta), ) } } @@ -363,7 +378,6 @@ impl ListEntry { fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let settings = user_settings(cx); - let theme = theme(cx); let left_content = match self.left_content.clone() { Some(LeftContent::Icon(i)) => Some( @@ -385,9 +399,10 @@ impl ListEntry { div() .relative() .group("") - .bg(theme.surface) + .bg(cx.theme().colors().surface) .when(self.state == InteractionState::Focused, |this| { - this.border().border_color(theme.border_focused) + this.border() + .border_color(cx.theme().colors().border_focused) }) .child( sized_item @@ -399,11 +414,11 @@ impl ListEntry { .h_full() .flex() .justify_center() - .group_hover("", |style| style.bg(theme.border_focused)) + .group_hover("", |style| style.bg(cx.theme().colors().border_focused)) .child( h_stack() .child(div().w_px().h_full()) - .child(div().w_px().h_full().bg(theme.border)), + .child(div().w_px().h_full().bg(cx.theme().colors().border)), ) })) .flex() @@ -472,45 +487,65 @@ impl ListDetailsEntry { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); let settings = user_settings(cx); - let (item_bg, item_bg_hover, item_bg_active) = match self.seen { - true => ( - theme.ghost_element, - theme.ghost_element_hover, - theme.ghost_element_active, - ), - false => ( - theme.filled_element, - theme.filled_element_hover, - theme.filled_element_active, - ), - }; + let (item_bg, item_bg_hover, item_bg_active) = ( + cx.theme().colors().ghost_element, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ); let label_color = match self.seen { true => LabelColor::Muted, false => LabelColor::Default, }; - v_stack() + div() .relative() .group("") .bg(item_bg) - .px_1() - .py_1_5() + .px_2() + .py_1p5() .w_full() - .line_height(relative(1.2)) - .child(Label::new(self.label.clone()).color(label_color)) - .children( - self.meta - .map(|meta| Label::new(meta).color(LabelColor::Muted)), - ) + .z_index(1) + .when(!self.seen, |this| { + this.child( + div() + .absolute() + .left(px(3.0)) + .top_3() + .rounded_full() + .border_2() + .border_color(cx.theme().colors().surface) + .w(px(9.0)) + .h(px(9.0)) + .z_index(2) + .bg(cx.theme().status().info), + ) + }) .child( - h_stack() + v_stack() + .w_full() + .line_height(relative(1.2)) .gap_1() - .justify_end() - .children(self.actions.unwrap_or_default()), + .child( + div() + .w_5() + .h_5() + .rounded_full() + .bg(cx.theme().colors().icon_accent), + ) + .child(Label::new(self.label.clone()).color(label_color)) + .children( + self.meta + .map(|meta| Label::new(meta).color(LabelColor::Muted)), + ) + .child( + h_stack() + .gap_1() + .justify_end() + .children(self.actions.unwrap_or_default()), + ), ) } } @@ -524,9 +559,7 @@ impl ListSeparator { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - - div().h_px().w_full().bg(theme.border) + div().h_px().w_full().bg(cx.theme().colors().border_variant) } } @@ -568,14 +601,15 @@ impl List { let is_toggled = Toggleable::is_toggled(&self.toggleable); let list_content = match (self.items.is_empty(), is_toggled) { - (_, false) => div(), (false, _) => div().children(self.items), - (true, _) => { + (true, false) => div(), + (true, true) => { div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) } }; v_stack() + .w_full() .py_1() .children(self.header.map(|header| header.toggleable(self.toggleable))) .child(list_content) diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 7c3efe79ba..26986474e0 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -39,22 +39,20 @@ impl Modal { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .id(self.id.clone()) .w_96() // .rounded_xl() - .bg(theme.background) + .bg(cx.theme().colors().background) .border() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .shadow_2xl() .child( h_stack() .justify_between() .p_1() .border_b() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .child(div().children(self.title.clone().map(|t| Label::new(t)))) .child(IconButton::new("close", Icon::Close)), ) @@ -65,7 +63,7 @@ impl Modal { this.child( h_stack() .border_t() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .p_1() .justify_end() .children(self.secondary_action) diff --git a/crates/ui2/src/components/multi_buffer.rs b/crates/ui2/src/components/multi_buffer.rs index 696fc77a62..ea130f20bd 100644 --- a/crates/ui2/src/components/multi_buffer.rs +++ b/crates/ui2/src/components/multi_buffer.rs @@ -12,8 +12,6 @@ impl MultiBuffer { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .w_full() .h_full() @@ -26,7 +24,7 @@ impl MultiBuffer { .items_center() .justify_between() .p_4() - .bg(theme.editor_subheader) + .bg(cx.theme().colors().editor_subheader) .child(Label::new("main.rs")) .child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)), ) @@ -50,17 +48,15 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = theme(cx); - Story::container(cx) .child(Story::title_for::<_, MultiBuffer>(cx)) .child(Story::label(cx, "Default")) .child(MultiBuffer::new(vec![ - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(cx), + hello_world_rust_buffer_example(cx), + hello_world_rust_buffer_example(cx), + hello_world_rust_buffer_example(cx), + hello_world_rust_buffer_example(cx), ])) } } diff --git a/crates/ui2/src/components/notification_toast.rs b/crates/ui2/src/components/notification_toast.rs index f7d280ed16..59078c98f4 100644 --- a/crates/ui2/src/components/notification_toast.rs +++ b/crates/ui2/src/components/notification_toast.rs @@ -1,6 +1,7 @@ use gpui2::rems; -use crate::{h_stack, prelude::*, Icon}; +use crate::prelude::*; +use crate::{h_stack, Icon}; #[derive(Component)] pub struct NotificationToast { @@ -22,8 +23,6 @@ impl NotificationToast { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - h_stack() .z_index(5) .absolute() @@ -35,7 +34,7 @@ impl NotificationToast { .px_1p5() .rounded_lg() .shadow_md() - .bg(theme.elevated_surface) + .bg(cx.theme().colors().elevated_surface) .child(div().size_full().child(self.label.clone())) } } diff --git a/crates/ui2/src/components/notifications_panel.rs b/crates/ui2/src/components/notifications_panel.rs index 6872f116e9..74f015ac06 100644 --- a/crates/ui2/src/components/notifications_panel.rs +++ b/crates/ui2/src/components/notifications_panel.rs @@ -1,5 +1,10 @@ -use crate::{prelude::*, static_new_notification_items, static_read_notification_items}; -use crate::{List, ListHeader}; +use crate::utils::naive_format_distance_from_now; +use crate::{ + h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon, + IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, + UnreadIndicator, +}; +use crate::{ClickHandler, ListHeader}; #[derive(Component)] pub struct NotificationsPanel { @@ -12,37 +17,352 @@ impl NotificationsPanel { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - div() .id(self.id.clone()) .flex() .flex_col() - .w_full() - .h_full() - .bg(theme.surface) + .size_full() + .bg(cx.theme().colors().surface) .child( - div() - .id("header") - .w_full() - .flex() - .flex_col() + ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![ + Icon::AtSign, + Icon::BellOff, + Icon::MailOpen, + ]))), + ) + .child(ListSeparator::new()) + .child( + v_stack() + .id("notifications-panel-scroll-view") + .py_1() .overflow_y_scroll() + .flex_1() .child( - List::new(static_new_notification_items()) - .header(ListHeader::new("NEW").toggle(ToggleState::Toggled)) - .toggle(ToggleState::Toggled), + div() + .mx_2() + .p_1() + // TODO: Add cursor style + // .cursor(Cursor::IBeam) + .bg(cx.theme().colors().element) + .border() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new("Search...") + .color(LabelColor::Placeholder) + .line_height_style(LineHeightStyle::UILabel), + ), + ) + .child(v_stack().px_1().children(static_new_notification_items_2())), + ) + } +} + +pub enum ButtonOrIconButton { + Button(Button), + IconButton(IconButton), +} + +impl From> for ButtonOrIconButton { + fn from(value: Button) -> Self { + Self::Button(value) + } +} + +impl From> for ButtonOrIconButton { + fn from(value: IconButton) -> Self { + Self::IconButton(value) + } +} + +pub struct NotificationAction { + button: ButtonOrIconButton, + tooltip: SharedString, + /// Shows after action is chosen + /// + /// For example, if the action is "Accept" the taken message could be: + /// + /// - `(None,"Accepted")` - "Accepted" + /// + /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted" + taken_message: (Option, SharedString), +} + +impl NotificationAction { + pub fn new( + button: impl Into>, + tooltip: impl Into, + (icon, taken_message): (Option, impl Into), + ) -> Self { + Self { + button: button.into(), + tooltip: tooltip.into(), + taken_message: (icon, taken_message.into()), + } + } +} + +pub enum ActorOrIcon { + Actor(PublicActor), + Icon(Icon), +} + +pub struct NotificationMeta { + items: Vec<(Option, SharedString, Option>)>, +} + +struct NotificationHandlers { + click: Option>, +} + +impl Default for NotificationHandlers { + fn default() -> Self { + Self { click: None } + } +} + +#[derive(Component)] +pub struct Notification { + id: ElementId, + slot: ActorOrIcon, + message: SharedString, + date_received: NaiveDateTime, + meta: Option>, + actions: Option<[NotificationAction; 2]>, + unread: bool, + new: bool, + action_taken: Option>, + handlers: NotificationHandlers, +} + +impl Notification { + fn new( + id: ElementId, + message: SharedString, + date_received: NaiveDateTime, + slot: ActorOrIcon, + click_action: Option>, + ) -> Self { + let handlers = if click_action.is_some() { + NotificationHandlers { + click: click_action, + } + } else { + NotificationHandlers::default() + }; + + Self { + id, + date_received, + message, + meta: None, + slot, + actions: None, + unread: true, + new: false, + action_taken: None, + handlers, + } + } + + /// Creates a new notification with an actor slot. + /// + /// Requires a click action. + pub fn new_actor_message( + id: impl Into, + message: impl Into, + date_received: NaiveDateTime, + actor: PublicActor, + click_action: ClickHandler, + ) -> Self { + Self::new( + id.into(), + message.into(), + date_received, + ActorOrIcon::Actor(actor), + Some(click_action), + ) + } + + /// Creates a new notification with an icon slot. + /// + /// Requires a click action. + pub fn new_icon_message( + id: impl Into, + message: impl Into, + date_received: NaiveDateTime, + icon: Icon, + click_action: ClickHandler, + ) -> Self { + Self::new( + id.into(), + message.into(), + date_received, + ActorOrIcon::Icon(icon), + Some(click_action), + ) + } + + /// Creates a new notification with an actor slot + /// and a Call To Action row. + /// + /// Cannot take a click action due to required actions. + pub fn new_actor_with_actions( + id: impl Into, + message: impl Into, + date_received: NaiveDateTime, + actor: PublicActor, + actions: [NotificationAction; 2], + ) -> Self { + Self::new( + id.into(), + message.into(), + date_received, + ActorOrIcon::Actor(actor), + None, + ) + .actions(actions) + } + + /// Creates a new notification with an icon slot + /// and a Call To Action row. + /// + /// Cannot take a click action due to required actions. + pub fn new_icon_with_actions( + id: impl Into, + message: impl Into, + date_received: NaiveDateTime, + icon: Icon, + actions: [NotificationAction; 2], + ) -> Self { + Self::new( + id.into(), + message.into(), + date_received, + ActorOrIcon::Icon(icon), + None, + ) + .actions(actions) + } + + fn on_click(mut self, handler: ClickHandler) -> Self { + self.handlers.click = Some(handler); + self + } + + pub fn actions(mut self, actions: [NotificationAction; 2]) -> Self { + self.actions = Some(actions); + self + } + + pub fn meta(mut self, meta: NotificationMeta) -> Self { + self.meta = Some(meta); + self + } + + fn render_meta_items(&self, cx: &mut ViewContext) -> impl Component { + if let Some(meta) = &self.meta { + h_stack().children( + meta.items + .iter() + .map(|(icon, text, _)| { + let mut meta_el = div(); + if let Some(icon) = icon { + meta_el = meta_el.child(IconElement::new(icon.clone())); + } + meta_el.child(Label::new(text.clone()).color(LabelColor::Muted)) + }) + .collect::>(), + ) + } else { + div() + } + } + + fn render_slot(&self, cx: &mut ViewContext) -> impl Component { + match &self.slot { + ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(), + ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(), + } + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div() + .relative() + .id(self.id.clone()) + .p_1() + .flex() + .flex_col() + .w_full() + .children( + Some( + div() + .absolute() + .left(px(3.0)) + .top_3() + .z_index(2) + .child(UnreadIndicator::new()), + ) + .filter(|_| self.unread), + ) + .child( + v_stack() + .z_index(1) + .gap_1() + .w_full() + .child( + h_stack() + .w_full() + .gap_2() + .child(self.render_slot(cx)) + .child(div().flex_1().child(Label::new(self.message.clone()))), ) .child( - List::new(static_read_notification_items()) - .header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled)) - .empty_message("No new notifications") - .toggle(ToggleState::Toggled), + h_stack() + .justify_between() + .child( + h_stack() + .gap_1() + .child( + Label::new(naive_format_distance_from_now( + self.date_received, + true, + true, + )) + .color(LabelColor::Muted), + ) + .child(self.render_meta_items(cx)), + ) + .child(match (self.actions, self.action_taken) { + // Show nothing + (None, _) => div(), + // Show the taken_message + (Some(_), Some(action_taken)) => h_stack() + .children(action_taken.taken_message.0.map(|icon| { + IconElement::new(icon).color(crate::IconColor::Muted) + })) + .child( + Label::new(action_taken.taken_message.1.clone()) + .color(LabelColor::Muted), + ), + // Show the actions + (Some(actions), None) => { + h_stack().children(actions.map(|action| match action.button { + ButtonOrIconButton::Button(button) => { + Component::render(button) + } + ButtonOrIconButton::IconButton(icon_button) => { + Component::render(icon_button) + } + })) + } + }), ), ) } } +use chrono::NaiveDateTime; +use gpui2::{px, Styled}; #[cfg(feature = "stories")] pub use stories::*; diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index e47f6a4cea..a1f3eb7e1c 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -43,22 +43,20 @@ impl Palette { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .id(self.id.clone()) .w_96() .rounded_lg() - .bg(theme.elevated_surface) + .bg(cx.theme().colors().elevated_surface) .border() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .child( v_stack() .gap_px() .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child( Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder), ))) - .child(div().h_px().w_full().bg(theme.filled_element)) + .child(div().h_px().w_full().bg(cx.theme().colors().element)) .child( v_stack() .id("items") @@ -88,8 +86,12 @@ impl Palette { .px_2() .py_0p5() .rounded_lg() - .hover(|style| style.bg(theme.ghost_element_hover)) - .active(|style| style.bg(theme.ghost_element_active)) + .hover(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .active(|style| { + style.bg(cx.theme().colors().ghost_element_active) + }) .child(item) })), ), diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 12d2207ffd..5d941eb50e 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -93,26 +93,22 @@ impl Panel { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let current_size = self.width.unwrap_or(self.initial_width); v_stack() .id(self.id.clone()) .flex_initial() - .when( - self.current_side == PanelSide::Left || self.current_side == PanelSide::Right, - |this| this.h_full().w(current_size), - ) - .when(self.current_side == PanelSide::Left, |this| this.border_r()) - .when(self.current_side == PanelSide::Right, |this| { - this.border_l() + .map(|this| match self.current_side { + PanelSide::Left | PanelSide::Right => this.h_full().w(current_size), + PanelSide::Bottom => this, }) - .when(self.current_side == PanelSide::Bottom, |this| { - this.border_b().w_full().h(current_size) + .map(|this| match self.current_side { + PanelSide::Left => this.border_r(), + PanelSide::Right => this.border_l(), + PanelSide::Bottom => this.border_b().w_full().h(current_size), }) - .bg(theme.surface) - .border_color(theme.border) + .bg(cx.theme().colors().surface) + .border_color(cx.theme().colors().border) .children(self.children) } } diff --git a/crates/ui2/src/components/panes.rs b/crates/ui2/src/components/panes.rs index 854786ebaa..bf0f27d43f 100644 --- a/crates/ui2/src/components/panes.rs +++ b/crates/ui2/src/components/panes.rs @@ -51,7 +51,7 @@ impl Pane { .id("drag-target") .drag_over::(|d| d.bg(red())) .on_drop(|_, files: View, cx| { - dbg!("dropped files!", files.read(cx)); + eprintln!("dropped files! {:?}", files.read(cx)); }) .absolute() .inset_0(), @@ -90,8 +90,6 @@ impl PaneGroup { } fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - if !self.panes.is_empty() { let el = div() .flex() @@ -115,7 +113,7 @@ impl PaneGroup { .gap_px() .w_full() .h_full() - .bg(theme.editor) + .bg(cx.theme().colors().editor) .children(self.groups.into_iter().map(|group| group.render(view, cx))); if self.split_direction == SplitDirection::Horizontal { diff --git a/crates/ui2/src/components/player_stack.rs b/crates/ui2/src/components/player_stack.rs index ced761a086..1a1231e6c4 100644 --- a/crates/ui2/src/components/player_stack.rs +++ b/crates/ui2/src/components/player_stack.rs @@ -14,9 +14,7 @@ impl PlayerStack { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); let player = self.player_with_call_status.get_player(); - self.player_with_call_status.get_call_status(); let followers = self .player_with_call_status @@ -50,7 +48,7 @@ impl PlayerStack { .pl_1() .rounded_lg() .bg(if followers.is_none() { - theme.transparent + cx.theme().styles.system.transparent } else { player.selection_color(cx) }) diff --git a/crates/ui2/src/components/project_panel.rs b/crates/ui2/src/components/project_panel.rs index 84c68119fe..76fa50d338 100644 --- a/crates/ui2/src/components/project_panel.rs +++ b/crates/ui2/src/components/project_panel.rs @@ -14,15 +14,13 @@ impl ProjectPanel { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - div() .id(self.id.clone()) .flex() .flex_col() .w_full() .h_full() - .bg(theme.surface) + .bg(cx.theme().colors().surface) .child( div() .id("project-panel-contents") diff --git a/crates/ui2/src/components/status_bar.rs b/crates/ui2/src/components/status_bar.rs index a23040193f..136472f605 100644 --- a/crates/ui2/src/components/status_bar.rs +++ b/crates/ui2/src/components/status_bar.rs @@ -86,8 +86,6 @@ impl StatusBar { view: &mut Workspace, cx: &mut ViewContext, ) -> impl Component { - let theme = theme(cx); - div() .py_0p5() .px_1() @@ -95,7 +93,7 @@ impl StatusBar { .items_center() .justify_between() .w_full() - .bg(theme.status_bar) + .bg(cx.theme().colors().status_bar) .child(self.left_tools(view, cx)) .child(self.right_tools(view, cx)) } diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index d784ec0174..e8b0ee3be5 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::{Icon, IconColor, IconElement, Label, LabelColor}; -use gpui2::{black, red, Div, ElementId, Render, View, VisualContext}; +use gpui2::{red, Div, ElementId, Render, View, VisualContext}; #[derive(Component, Clone)] pub struct Tab { @@ -87,7 +87,6 @@ impl Tab { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict; let is_deleted = self.fs_status == FileSystemStatus::Deleted; @@ -109,15 +108,15 @@ impl Tab { let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current { - true => ( - theme.ghost_element, - theme.ghost_element_hover, - theme.ghost_element_active, - ), false => ( - theme.filled_element, - theme.filled_element_hover, - theme.filled_element_active, + cx.theme().colors().tab_inactive, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ), + true => ( + cx.theme().colors().tab_active, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, ), }; @@ -128,9 +127,9 @@ impl Tab { div() .id(self.id.clone()) .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone())) - .drag_over::(|d| d.bg(black())) + .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) .on_drop(|_view, state: View, cx| { - dbg!(state.read(cx)); + eprintln!("{:?}", state.read(cx)); }) .px_2() .py_0p5() @@ -145,7 +144,7 @@ impl Tab { .px_1() .flex() .items_center() - .gap_1() + .gap_1p5() .children(has_fs_conflict.then(|| { IconElement::new(Icon::ExclamationTriangle) .size(crate::IconSize::Small) diff --git a/crates/ui2/src/components/tab_bar.rs b/crates/ui2/src/components/tab_bar.rs index da0a41a1bf..bb7fca1153 100644 --- a/crates/ui2/src/components/tab_bar.rs +++ b/crates/ui2/src/components/tab_bar.rs @@ -24,18 +24,18 @@ impl TabBar { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let (can_navigate_back, can_navigate_forward) = self.can_navigate; div() + .group("tab_bar") .id(self.id.clone()) .w_full() .flex() - .bg(theme.tab_bar) + .bg(cx.theme().colors().tab_bar) // Left Side .child( div() + .relative() .px_1() .flex() .flex_none() @@ -43,6 +43,7 @@ impl TabBar { // Nav Buttons .child( div() + .right_0() .flex() .items_center() .gap_px() @@ -69,10 +70,15 @@ impl TabBar { // Right Side .child( div() + // We only use absolute here since we don't + // have opacity or `hidden()` yet + .absolute() + .neg_top_7() .px_1() .flex() .flex_none() .gap_2() + .group_hover("tab_bar", |this| this.top_0()) // Nav Buttons .child( div() diff --git a/crates/ui2/src/components/terminal.rs b/crates/ui2/src/components/terminal.rs index a751d47dfc..051ebf7315 100644 --- a/crates/ui2/src/components/terminal.rs +++ b/crates/ui2/src/components/terminal.rs @@ -12,8 +12,6 @@ impl Terminal { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let can_navigate_back = true; let can_navigate_forward = false; @@ -26,7 +24,7 @@ impl Terminal { div() .w_full() .flex() - .bg(theme.surface) + .bg(cx.theme().colors().surface) .child( div().px_1().flex().flex_none().gap_2().child( div() @@ -73,7 +71,7 @@ impl Terminal { height: rems(36.).into(), }, ) - .child(crate::static_data::terminal_buffer(&theme)), + .child(crate::static_data::terminal_buffer(cx)), ) } } diff --git a/crates/ui2/src/components/title_bar.rs b/crates/ui2/src/components/title_bar.rs index 4b3b125dea..2fa201440a 100644 --- a/crates/ui2/src/components/title_bar.rs +++ b/crates/ui2/src/components/title_bar.rs @@ -89,7 +89,6 @@ impl Render for TitleBar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { - let theme = theme(cx); let settings = user_settings(cx); // let has_focus = cx.window_is_active(); @@ -106,7 +105,7 @@ impl Render for TitleBar { .items_center() .justify_between() .w_full() - .bg(theme.background) + .bg(cx.theme().colors().background) .py_1() .child( div() diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 814e91c498..3b81ac42b4 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -37,8 +37,6 @@ impl Toast { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let mut div = div(); if self.origin == ToastOrigin::Bottom { @@ -56,7 +54,7 @@ impl Toast { .rounded_lg() .shadow_md() .overflow_hidden() - .bg(theme.elevated_surface) + .bg(cx.theme().colors().elevated_surface) .children(self.children) } } diff --git a/crates/ui2/src/components/toolbar.rs b/crates/ui2/src/components/toolbar.rs index 4b35e2d9d2..05a5c991d6 100644 --- a/crates/ui2/src/components/toolbar.rs +++ b/crates/ui2/src/components/toolbar.rs @@ -55,10 +55,8 @@ impl Toolbar { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - div() - .bg(theme.toolbar) + .bg(cx.theme().colors().toolbar) .p_2() .flex() .justify_between() @@ -87,8 +85,6 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = theme(cx); - Story::container(cx) .child(Story::title_for::<_, Toolbar>(cx)) .child(Story::label(cx, "Default")) @@ -100,21 +96,21 @@ mod stories { Symbol(vec![ HighlightedText { text: "impl ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "ToolbarStory".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, ]), Symbol(vec![ HighlightedText { text: "fn ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "render".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, ]), ], diff --git a/crates/ui2/src/components/traffic_lights.rs b/crates/ui2/src/components/traffic_lights.rs index 8ee19d26f5..9080276cdd 100644 --- a/crates/ui2/src/components/traffic_lights.rs +++ b/crates/ui2/src/components/traffic_lights.rs @@ -22,13 +22,13 @@ impl TrafficLight { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); + let system_colors = &cx.theme().styles.system; let fill = match (self.window_has_focus, self.color) { - (true, TrafficLightColor::Red) => theme.mac_os_traffic_light_red, - (true, TrafficLightColor::Yellow) => theme.mac_os_traffic_light_yellow, - (true, TrafficLightColor::Green) => theme.mac_os_traffic_light_green, - (false, _) => theme.filled_element, + (true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red, + (true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow, + (true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green, + (false, _) => cx.theme().colors().element, }; div().w_3().h_3().rounded_full().bg(fill) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 78ab6232a8..97570a33e3 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -1,14 +1,16 @@ use std::sync::Arc; use chrono::DateTime; -use gpui2::{px, relative, rems, Div, Render, Size, View, VisualContext}; +use gpui2::{px, relative, Div, Render, Size, View, VisualContext}; +use settings2::Settings; +use theme2::ThemeSettings; -use crate::{prelude::*, NotificationsPanel}; +use crate::prelude::*; use crate::{ - static_livestream, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, - CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup, Panel, - PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, StatusBar, Terminal, - TitleBar, Toast, ToastOrigin, + static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, CollabPanel, + EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel, + PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, + Toast, ToastOrigin, }; #[derive(Clone)] @@ -150,6 +152,18 @@ impl Workspace { pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext) { self.debug.enable_user_settings = !self.debug.enable_user_settings; + let mut theme_settings = ThemeSettings::get_global(cx).clone(); + + if self.debug.enable_user_settings { + theme_settings.ui_font_size = 18.0.into(); + } else { + theme_settings.ui_font_size = 16.0.into(); + } + + ThemeSettings::override_global(theme_settings.clone(), cx); + + cx.set_rem_size(theme_settings.ui_font_size); + cx.notify(); } @@ -179,22 +193,6 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { - let theme = theme(cx); - - // HACK: This should happen inside of `debug_toggle_user_settings`, but - // we don't have `cx.global::()` in event handlers at the moment. - // Need to talk with Nathan/Antonio about this. - { - let settings = user_settings_mut(cx); - - if self.debug.enable_user_settings { - settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into()); - settings.ui_scale = SettingValue::UserDefined(1.25); - } else { - *settings = FakeSettings::default(); - } - } - let root_group = PaneGroup::new_panes( vec![Pane::new( "pane-0", @@ -216,8 +214,8 @@ impl Render for Workspace { .gap_0() .justify_start() .items_start() - .text_color(theme.text) - .bg(theme.background) + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) .child(self.title_bar.clone()) .child( div() @@ -228,7 +226,7 @@ impl Render for Workspace { .overflow_hidden() .border_t() .border_b() - .border_color(theme.border) + .border_color(cx.theme().colors().border) .children( Some( Panel::new("project-panel-outer", cx) @@ -323,7 +321,7 @@ impl Render for Workspace { v_stack() .z_index(9) .absolute() - .bottom_10() + .top_20() .left_1_4() .w_40() .gap_2() diff --git a/crates/ui2/src/elements.rs b/crates/ui2/src/elements.rs index c60902ae98..dfff2761a7 100644 --- a/crates/ui2/src/elements.rs +++ b/crates/ui2/src/elements.rs @@ -2,6 +2,7 @@ mod avatar; mod button; mod details; mod icon; +mod indicator; mod input; mod label; mod player; @@ -12,6 +13,7 @@ pub use avatar::*; pub use button::*; pub use details::*; pub use icon::*; +pub use indicator::*; pub use input::*; pub use label::*; pub use player::*; diff --git a/crates/ui2/src/elements/avatar.rs b/crates/ui2/src/elements/avatar.rs index f008eeb479..ff574a2042 100644 --- a/crates/ui2/src/elements/avatar.rs +++ b/crates/ui2/src/elements/avatar.rs @@ -22,8 +22,6 @@ impl Avatar { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let mut img = img(); if self.shape == Shape::Circle { @@ -34,7 +32,8 @@ impl Avatar { img.uri(self.src.clone()) .size_4() - .bg(theme.image_fallback_background) + // todo!(Pull the avatar fallback background from the theme.) + .bg(gpui2::red()) } } @@ -59,11 +58,26 @@ mod stories { .child(Avatar::new( "https://avatars.githubusercontent.com/u/1714999?v=4", )) + .child(Avatar::new( + "https://avatars.githubusercontent.com/u/326587?v=4", + )) + // .child(Avatar::new( + // "https://avatars.githubusercontent.com/u/326587?v=4", + // )) + // .child(Avatar::new( + // "https://avatars.githubusercontent.com/u/482957?v=4", + // )) + // .child(Avatar::new( + // "https://avatars.githubusercontent.com/u/1714999?v=4", + // )) + // .child(Avatar::new( + // "https://avatars.githubusercontent.com/u/1486634?v=4", + // )) .child(Story::label(cx, "Rounded rectangle")) - .child( - Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ) + // .child( + // Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") + // .shape(Shape::RoundedRectangle), + // ) } } } diff --git a/crates/ui2/src/elements/button.rs b/crates/ui2/src/elements/button.rs index d27a0537d8..073bcdbb45 100644 --- a/crates/ui2/src/elements/button.rs +++ b/crates/ui2/src/elements/button.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use gpui2::{div, DefiniteLength, Hsla, MouseButton, WindowContext}; +use gpui2::{div, rems, DefiniteLength, Hsla, MouseButton, WindowContext}; -use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor}; -use crate::{prelude::*, LineHeightStyle}; +use crate::prelude::*; +use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LineHeightStyle}; #[derive(Default, PartialEq, Clone, Copy)] pub enum IconPosition { @@ -21,29 +21,23 @@ pub enum ButtonVariant { impl ButtonVariant { pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla { - let theme = theme(cx); - match self { - ButtonVariant::Ghost => theme.ghost_element, - ButtonVariant::Filled => theme.filled_element, + ButtonVariant::Ghost => cx.theme().colors().ghost_element, + ButtonVariant::Filled => cx.theme().colors().element, } } pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla { - let theme = theme(cx); - match self { - ButtonVariant::Ghost => theme.ghost_element_hover, - ButtonVariant::Filled => theme.filled_element_hover, + ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover, + ButtonVariant::Filled => cx.theme().colors().element_hover, } } pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla { - let theme = theme(cx); - match self { - ButtonVariant::Ghost => theme.ghost_element_active, - ButtonVariant::Filled => theme.filled_element_active, + ButtonVariant::Ghost => cx.theme().colors().ghost_element_active, + ButtonVariant::Filled => cx.theme().colors().element_active, } } } @@ -157,7 +151,7 @@ impl Button { .relative() .id(SharedString::from(format!("{}", self.label))) .p_1() - .text_size(ui_size(cx, 1.)) + .text_size(rems(1.)) .rounded_md() .bg(self.variant.bg_color(cx)) .hover(|style| style.bg(self.variant.bg_color_hover(cx))) @@ -204,7 +198,7 @@ impl ButtonGroup { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let mut el = h_stack().text_size(ui_size(cx, 1.)); + let mut el = h_stack().text_size(rems(1.)); for button in self.buttons { el = el.child(button.render(_view, cx)); diff --git a/crates/ui2/src/elements/details.rs b/crates/ui2/src/elements/details.rs index eca7798c82..1d22c81774 100644 --- a/crates/ui2/src/elements/details.rs +++ b/crates/ui2/src/elements/details.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, v_stack, ButtonGroup}; +use crate::prelude::*; +use crate::{v_stack, ButtonGroup}; #[derive(Component)] pub struct Details { @@ -27,13 +28,11 @@ impl Details { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - v_stack() .p_1() .gap_0p5() .text_xs() - .text_color(theme.text) + .text_color(cx.theme().colors().text) .size_full() .child(self.text) .children(self.meta.map(|m| m)) diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/elements/icon.rs index 4e4ec2bce7..5885d76101 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/elements/icon.rs @@ -1,4 +1,4 @@ -use gpui2::{svg, Hsla}; +use gpui2::{rems, svg, Hsla}; use strum::EnumIter; use crate::prelude::*; @@ -26,22 +26,21 @@ pub enum IconColor { impl IconColor { pub fn color(self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); match self { - IconColor::Default => gpui2::red(), - IconColor::Muted => gpui2::red(), - IconColor::Disabled => gpui2::red(), - IconColor::Placeholder => gpui2::red(), - IconColor::Accent => gpui2::red(), - IconColor::Error => gpui2::red(), - IconColor::Warning => gpui2::red(), - IconColor::Success => gpui2::red(), - IconColor::Info => gpui2::red(), + IconColor::Default => cx.theme().colors().icon, + IconColor::Muted => cx.theme().colors().icon_muted, + IconColor::Disabled => cx.theme().colors().icon_disabled, + IconColor::Placeholder => cx.theme().colors().icon_placeholder, + IconColor::Accent => cx.theme().colors().icon_accent, + IconColor::Error => cx.theme().status().error, + IconColor::Warning => cx.theme().status().warning, + IconColor::Success => cx.theme().status().success, + IconColor::Info => cx.theme().status().info, } } } -#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)] +#[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, ArrowLeft, @@ -50,6 +49,7 @@ pub enum Icon { AudioOff, AudioOn, Bolt, + Check, ChevronDown, ChevronLeft, ChevronRight, @@ -68,7 +68,6 @@ pub enum Icon { Folder, FolderOpen, FolderX, - #[default] Hash, InlayHint, MagicWand, @@ -90,6 +89,11 @@ pub enum Icon { XCircle, Copilot, Envelope, + Bell, + BellOff, + BellRing, + MailOpen, + AtSign, } impl Icon { @@ -102,6 +106,7 @@ impl Icon { Icon::AudioOff => "icons/speaker-off.svg", Icon::AudioOn => "icons/speaker-loud.svg", Icon::Bolt => "icons/bolt.svg", + Icon::Check => "icons/check.svg", Icon::ChevronDown => "icons/chevron_down.svg", Icon::ChevronLeft => "icons/chevron_left.svg", Icon::ChevronRight => "icons/chevron_right.svg", @@ -141,6 +146,11 @@ impl Icon { Icon::XCircle => "icons/error.svg", Icon::Copilot => "icons/copilot.svg", Icon::Envelope => "icons/feedback.svg", + Icon::Bell => "icons/bell.svg", + Icon::BellOff => "icons/bell-off.svg", + Icon::BellRing => "icons/bell-ring.svg", + Icon::MailOpen => "icons/mail-open.svg", + Icon::AtSign => "icons/at-sign.svg", } } } @@ -174,8 +184,8 @@ impl IconElement { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let fill = self.color.color(cx); let svg_size = match self.size { - IconSize::Small => ui_size(cx, 12. / 14.), - IconSize::Medium => ui_size(cx, 15. / 14.), + IconSize::Small => rems(0.75), + IconSize::Medium => rems(0.9375), }; svg() diff --git a/crates/ui2/src/elements/indicator.rs b/crates/ui2/src/elements/indicator.rs new file mode 100644 index 0000000000..1f6e00e621 --- /dev/null +++ b/crates/ui2/src/elements/indicator.rs @@ -0,0 +1,23 @@ +use gpui2::px; + +use crate::prelude::*; + +#[derive(Component)] +pub struct UnreadIndicator; + +impl UnreadIndicator { + pub fn new() -> Self { + Self + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div() + .rounded_full() + .border_2() + .border_color(cx.theme().colors().surface) + .w(px(9.0)) + .h(px(9.0)) + .z_index(2) + .bg(cx.theme().status().info) + } +} diff --git a/crates/ui2/src/elements/input.rs b/crates/ui2/src/elements/input.rs index e9e92dd0a6..2884470ce2 100644 --- a/crates/ui2/src/elements/input.rs +++ b/crates/ui2/src/elements/input.rs @@ -57,18 +57,16 @@ impl Input { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - let (input_bg, input_hover_bg, input_active_bg) = match self.variant { InputVariant::Ghost => ( - theme.ghost_element, - theme.ghost_element_hover, - theme.ghost_element_active, + cx.theme().colors().ghost_element, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, ), InputVariant::Filled => ( - theme.filled_element, - theme.filled_element_hover, - theme.filled_element_active, + cx.theme().colors().element, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, ), }; @@ -90,20 +88,19 @@ impl Input { .w_full() .px_2() .border() - .border_color(theme.transparent) + .border_color(cx.theme().styles.system.transparent) .bg(input_bg) .hover(|style| style.bg(input_hover_bg)) .active(|style| style.bg(input_active_bg)) .flex() .items_center() - .child( - div() - .flex() - .items_center() - .text_sm() - .when(self.value.is_empty(), |this| this.child(placeholder_label)) - .when(!self.value.is_empty(), |this| this.child(label)), - ) + .child(div().flex().items_center().text_sm().map(|this| { + if self.value.is_empty() { + this.child(placeholder_label) + } else { + this.child(label) + } + })) } } diff --git a/crates/ui2/src/elements/label.rs b/crates/ui2/src/elements/label.rs index 4d336345fb..d1d4d6630c 100644 --- a/crates/ui2/src/elements/label.rs +++ b/crates/ui2/src/elements/label.rs @@ -1,4 +1,4 @@ -use gpui2::{relative, Hsla, WindowContext}; +use gpui2::{relative, rems, Hsla, WindowContext}; use smallvec::SmallVec; use crate::prelude::*; @@ -18,18 +18,16 @@ pub enum LabelColor { impl LabelColor { pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - match self { - Self::Default => theme.text, - Self::Muted => theme.text_muted, - Self::Created => gpui2::red(), - Self::Modified => gpui2::red(), - Self::Deleted => gpui2::red(), - Self::Disabled => theme.text_disabled, - Self::Hidden => gpui2::red(), - Self::Placeholder => theme.text_placeholder, - Self::Accent => gpui2::red(), + Self::Default => cx.theme().colors().text, + Self::Muted => cx.theme().colors().text_muted, + Self::Created => cx.theme().status().created, + Self::Modified => cx.theme().status().modified, + Self::Deleted => cx.theme().status().deleted, + Self::Disabled => cx.theme().colors().text_disabled, + Self::Hidden => cx.theme().status().hidden, + Self::Placeholder => cx.theme().colors().text_placeholder, + Self::Accent => cx.theme().colors().text_accent, } } } @@ -81,14 +79,13 @@ impl Label { this.relative().child( div() .absolute() - .top_px() - .my_auto() + .top_1_2() .w_full() .h_px() .bg(LabelColor::Hidden.hsla(cx)), ) }) - .text_size(ui_size(cx, 1.)) + .text_size(rems(1.)) .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) @@ -126,9 +123,7 @@ impl HighlightedLabel { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - - let highlight_color = theme.text_accent; + let highlight_color = cx.theme().colors().text_accent; let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); diff --git a/crates/ui2/src/elements/player.rs b/crates/ui2/src/elements/player.rs index 5bf890b8bb..c7b7ade1c1 100644 --- a/crates/ui2/src/elements/player.rs +++ b/crates/ui2/src/elements/player.rs @@ -1,6 +1,6 @@ use gpui2::{Hsla, ViewContext}; -use crate::theme; +use crate::prelude::*; #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum PlayerStatus { @@ -139,13 +139,11 @@ impl Player { } pub fn cursor_color(&self, cx: &mut ViewContext) -> Hsla { - let theme = theme(cx); - theme.players[self.index].cursor + cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor } pub fn selection_color(&self, cx: &mut ViewContext) -> Hsla { - let theme = theme(cx); - theme.players[self.index].selection + cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].selection } pub fn avatar_src(&self) -> &str { diff --git a/crates/ui2/src/elements/tool_divider.rs b/crates/ui2/src/elements/tool_divider.rs index e1ebb294a0..8a9bbad97f 100644 --- a/crates/ui2/src/elements/tool_divider.rs +++ b/crates/ui2/src/elements/tool_divider.rs @@ -9,8 +9,6 @@ impl ToolDivider { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let theme = theme(cx); - - div().w_px().h_3().bg(theme.border) + div().w_px().h_3().bg(cx.theme().colors().border) } } diff --git a/crates/ui2/src/lib.rs b/crates/ui2/src/lib.rs index c1da5e410d..5d0a57c6d9 100644 --- a/crates/ui2/src/lib.rs +++ b/crates/ui2/src/lib.rs @@ -23,6 +23,7 @@ mod elevation; pub mod prelude; pub mod settings; mod static_data; +pub mod utils; pub use components::*; pub use elements::*; diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 63405fc2cb..fbb7ccc528 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -4,19 +4,28 @@ pub use gpui2::{ }; pub use crate::elevation::*; -use crate::settings::user_settings; pub use crate::ButtonVariant; -pub use theme2::theme; +pub use theme2::ActiveTheme; -use gpui2::{rems, Hsla, Rems}; +use gpui2::Hsla; use strum::EnumIter; -pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems { - const UI_SCALE_RATIO: f32 = 0.875; +/// Represents a person with a Zed account's public profile. +/// All data in this struct should be considered public. +pub struct PublicActor { + pub username: SharedString, + pub avatar: SharedString, + pub is_contact: bool, +} - let settings = user_settings(cx); - - rems(*settings.ui_scale * UI_SCALE_RATIO * size) +impl PublicActor { + pub fn new(username: impl Into, avatar: impl Into) -> Self { + Self { + username: username.into(), + avatar: avatar.into(), + is_contact: false, + } + } } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] @@ -54,15 +63,13 @@ pub enum GitStatus { impl GitStatus { pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - match self { - Self::None => theme.transparent, - Self::Created => theme.git_created, - Self::Modified => theme.git_modified, - Self::Deleted => theme.git_deleted, - Self::Conflict => theme.git_conflict, - Self::Renamed => theme.git_renamed, + Self::None => cx.theme().styles.system.transparent, + Self::Created => cx.theme().styles.git.created, + Self::Modified => cx.theme().styles.git.modified, + Self::Deleted => cx.theme().styles.git.deleted, + Self::Conflict => cx.theme().styles.git.conflict, + Self::Renamed => cx.theme().styles.git.renamed, } } } diff --git a/crates/ui2/src/settings.rs b/crates/ui2/src/settings.rs index 48a2e8e7b4..6a9426f623 100644 --- a/crates/ui2/src/settings.rs +++ b/crates/ui2/src/settings.rs @@ -58,7 +58,6 @@ pub struct FakeSettings { pub list_disclosure_style: SettingValue, pub list_indent_depth: SettingValue, pub titlebar: TitlebarSettings, - pub ui_scale: SettingValue, } impl Default for FakeSettings { @@ -68,7 +67,6 @@ impl Default for FakeSettings { list_disclosure_style: SettingValue::Default(DisclosureControlStyle::ChevronOnHover), list_indent_depth: SettingValue::Default(rems(0.3).into()), default_panel_size: SettingValue::Default(rems(16.).into()), - ui_scale: SettingValue::Default(1.), } } } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 68f1e36b2c..68f625c75d 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -1,17 +1,20 @@ use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; -use gpui2::ViewContext; +use chrono::DateTime; +use gpui2::{AppContext, ViewContext}; use rand::Rng; -use theme2::Theme; +use theme2::ActiveTheme; use crate::{ - theme, Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, - Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus, - PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus, + Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, + HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader, + Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, + PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus, }; use crate::{HighlightedText, ListDetailsEntry}; +use crate::{ListItem, NotificationAction}; pub fn static_tabs_example() -> Vec { vec![ @@ -325,27 +328,227 @@ pub fn static_players_with_call_status() -> Vec { ] } -pub fn static_new_notification_items() -> Vec> { +pub fn static_new_notification_items_2() -> Vec> { vec![ - ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.") - .meta("4 people in stream."), - ListDetailsEntry::new("nathansobo accepted your contact request."), + Notification::new_icon_message( + "notif-1", + "You were mentioned in a note.", + DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z") + .unwrap() + .naive_local(), + Icon::AtSign, + Arc::new(|_, _| {}), + ), + Notification::new_actor_with_actions( + "notif-2", + "as-cii sent you a contact request.", + DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z") + .unwrap() + .naive_local(), + PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + [ + NotificationAction::new( + Button::new("Decline"), + "Decline Request", + (Some(Icon::XCircle), "Declined"), + ), + NotificationAction::new( + Button::new("Accept").variant(crate::ButtonVariant::Filled), + "Accept Request", + (Some(Icon::Check), "Accepted"), + ), + ], + ), + Notification::new_icon_message( + "notif-3", + "You were mentioned #design.", + DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z") + .unwrap() + .naive_local(), + Icon::MessageBubbles, + Arc::new(|_, _| {}), + ), + Notification::new_actor_with_actions( + "notif-4", + "as-cii sent you a contact request.", + DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z") + .unwrap() + .naive_local(), + PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + [ + NotificationAction::new( + Button::new("Decline"), + "Decline Request", + (Some(Icon::XCircle), "Declined"), + ), + NotificationAction::new( + Button::new("Accept").variant(crate::ButtonVariant::Filled), + "Accept Request", + (Some(Icon::Check), "Accepted"), + ), + ], + ), + Notification::new_icon_message( + "notif-5", + "You were mentioned in a note.", + DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z") + .unwrap() + .naive_local(), + Icon::AtSign, + Arc::new(|_, _| {}), + ), + Notification::new_actor_with_actions( + "notif-6", + "as-cii sent you a contact request.", + DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z") + .unwrap() + .naive_local(), + PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + [ + NotificationAction::new( + Button::new("Decline"), + "Decline Request", + (Some(Icon::XCircle), "Declined"), + ), + NotificationAction::new( + Button::new("Accept").variant(crate::ButtonVariant::Filled), + "Accept Request", + (Some(Icon::Check), "Accepted"), + ), + ], + ), + Notification::new_icon_message( + "notif-7", + "You were mentioned in a note.", + DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z") + .unwrap() + .naive_local(), + Icon::AtSign, + Arc::new(|_, _| {}), + ), + Notification::new_actor_with_actions( + "notif-8", + "as-cii sent you a contact request.", + DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z") + .unwrap() + .naive_local(), + PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + [ + NotificationAction::new( + Button::new("Decline"), + "Decline Request", + (Some(Icon::XCircle), "Declined"), + ), + NotificationAction::new( + Button::new("Accept").variant(crate::ButtonVariant::Filled), + "Accept Request", + (Some(Icon::Check), "Accepted"), + ), + ], + ), + Notification::new_icon_message( + "notif-9", + "You were mentioned in a note.", + DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z") + .unwrap() + .naive_local(), + Icon::AtSign, + Arc::new(|_, _| {}), + ), + Notification::new_actor_with_actions( + "notif-10", + "as-cii sent you a contact request.", + DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z") + .unwrap() + .naive_local(), + PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + [ + NotificationAction::new( + Button::new("Decline"), + "Decline Request", + (Some(Icon::XCircle), "Declined"), + ), + NotificationAction::new( + Button::new("Accept").variant(crate::ButtonVariant::Filled), + "Accept Request", + (Some(Icon::Check), "Accepted"), + ), + ], + ), ] - .into_iter() - .map(From::from) - .collect() } -pub fn static_read_notification_items() -> Vec> { +pub fn static_new_notification_items() -> Vec> { vec![ - ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![ - Button::new("Decline"), - Button::new("Accept").variant(crate::ButtonVariant::Filled), - ]), - ListDetailsEntry::new("maxdeviant invited you to a stream in #design.") - .seen(true) - .meta("This stream has ended."), - ListDetailsEntry::new("as-cii accepted your contact request."), + ListItem::Header(ListSubHeader::new("New")), + ListItem::Details( + ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.") + .meta("4 people in stream."), + ), + ListItem::Details(ListDetailsEntry::new( + "nathansobo accepted your contact request.", + )), + ListItem::Header(ListSubHeader::new("Earlier")), + ListItem::Details( + ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![ + Button::new("Decline"), + Button::new("Accept").variant(crate::ButtonVariant::Filled), + ]), + ), + ListItem::Details( + ListDetailsEntry::new("maxdeviant invited you to a stream in #design.") + .seen(true) + .meta("This stream has ended."), + ), + ListItem::Details(ListDetailsEntry::new( + "as-cii accepted your contact request.", + )), + ListItem::Details( + ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true), + ), + ListItem::Details(ListDetailsEntry::new( + "osiewicz accepted your contact request.", + )), + ListItem::Details(ListDetailsEntry::new( + "ConradIrwin accepted your contact request.", + )), + ListItem::Details( + ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.") + .seen(true) + .meta("This stream has ended."), + ), + ListItem::Details(ListDetailsEntry::new( + "nathansobo accepted your contact request.", + )), + ListItem::Header(ListSubHeader::new("Earlier")), + ListItem::Details( + ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![ + Button::new("Decline"), + Button::new("Accept").variant(crate::ButtonVariant::Filled), + ]), + ), + ListItem::Details( + ListDetailsEntry::new("maxdeviant invited you to a stream in #design.") + .seen(true) + .meta("This stream has ended."), + ), + ListItem::Details(ListDetailsEntry::new( + "as-cii accepted your contact request.", + )), + ListItem::Details( + ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true), + ), + ListItem::Details(ListDetailsEntry::new( + "osiewicz accepted your contact request.", + )), + ListItem::Details(ListDetailsEntry::new( + "ConradIrwin accepted your contact request.", + )), + ListItem::Details( + ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.") + .seen(true) + .meta("This stream has ended."), + ), ] .into_iter() .map(From::from) @@ -643,8 +846,6 @@ pub fn empty_buffer_example() -> Buffer { } pub fn hello_world_rust_editor_example(cx: &mut ViewContext) -> EditorPane { - let theme = theme(cx); - EditorPane::new( cx, static_tabs_example(), @@ -652,29 +853,29 @@ pub fn hello_world_rust_editor_example(cx: &mut ViewContext) -> Edit vec![Symbol(vec![ HighlightedText { text: "fn ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "main".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, ])], - hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(cx), ) } -pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer { +pub fn hello_world_rust_buffer_example(cx: &AppContext) -> Buffer { Buffer::new("hello-world-rust-buffer") .set_title("hello_world.rs".to_string()) .set_path("src/hello_world.rs".to_string()) .set_language("rust".to_string()) .set_rows(Some(BufferRows { show_line_numbers: true, - rows: hello_world_rust_buffer_rows(theme), + rows: hello_world_rust_buffer_rows(cx), })) } -pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { +pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { let show_line_number = true; vec![ @@ -686,15 +887,15 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { highlighted_texts: vec![ HighlightedText { text: "fn ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "main".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, HighlightedText { text: "() {".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, ], }), @@ -710,7 +911,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { highlighted_texts: vec![HighlightedText { text: " // Statements here are executed when the compiled binary is called." .to_string(), - color: theme.syntax.color("comment"), + color: cx.theme().syntax_color("comment"), }], }), cursors: None, @@ -733,7 +934,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: " // Print text to the console.".to_string(), - color: theme.syntax.color("comment"), + color: cx.theme().syntax_color("comment"), }], }), cursors: None, @@ -748,15 +949,15 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { highlighted_texts: vec![ HighlightedText { text: " println!(".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, HighlightedText { text: "\"Hello, world!\"".to_string(), - color: theme.syntax.color("string"), + color: cx.theme().syntax_color("string"), }, HighlightedText { text: ");".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, ], }), @@ -771,7 +972,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "}".to_string(), - color: theme.text, + color: cx.theme().colors().text, }], }), cursors: None, @@ -782,8 +983,6 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { } pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext) -> EditorPane { - let theme = theme(cx); - EditorPane::new( cx, static_tabs_example(), @@ -791,29 +990,29 @@ pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext Buffer { +pub fn hello_world_rust_buffer_with_status_example(cx: &AppContext) -> Buffer { Buffer::new("hello-world-rust-buffer-with-status") .set_title("hello_world.rs".to_string()) .set_path("src/hello_world.rs".to_string()) .set_language("rust".to_string()) .set_rows(Some(BufferRows { show_line_numbers: true, - rows: hello_world_rust_with_status_buffer_rows(theme), + rows: hello_world_rust_with_status_buffer_rows(cx), })) } -pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec { +pub fn hello_world_rust_with_status_buffer_rows(cx: &AppContext) -> Vec { let show_line_number = true; vec![ @@ -825,15 +1024,15 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec highlighted_texts: vec![ HighlightedText { text: "fn ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "main".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, HighlightedText { text: "() {".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, ], }), @@ -849,7 +1048,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec highlighted_texts: vec![HighlightedText { text: "// Statements here are executed when the compiled binary is called." .to_string(), - color: theme.syntax.color("comment"), + color: cx.theme().syntax_color("comment"), }], }), cursors: None, @@ -872,7 +1071,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: " // Print text to the console.".to_string(), - color: theme.syntax.color("comment"), + color: cx.theme().syntax_color("comment"), }], }), cursors: None, @@ -887,15 +1086,15 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec highlighted_texts: vec![ HighlightedText { text: " println!(".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, HighlightedText { text: "\"Hello, world!\"".to_string(), - color: theme.syntax.color("string"), + color: cx.theme().syntax_color("string"), }, HighlightedText { text: ");".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, ], }), @@ -910,7 +1109,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "}".to_string(), - color: theme.text, + color: cx.theme().colors().text, }], }), cursors: None, @@ -924,7 +1123,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "".to_string(), - color: theme.text, + color: cx.theme().colors().text, }], }), cursors: None, @@ -938,7 +1137,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "// Marshall and Nate were here".to_string(), - color: theme.syntax.color("comment"), + color: cx.theme().syntax_color("comment"), }], }), cursors: None, @@ -948,16 +1147,16 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec ] } -pub fn terminal_buffer(theme: &Theme) -> Buffer { +pub fn terminal_buffer(cx: &AppContext) -> Buffer { Buffer::new("terminal") .set_title("zed — fish".to_string()) .set_rows(Some(BufferRows { show_line_numbers: false, - rows: terminal_buffer_rows(theme), + rows: terminal_buffer_rows(cx), })) } -pub fn terminal_buffer_rows(theme: &Theme) -> Vec { +pub fn terminal_buffer_rows(cx: &AppContext) -> Vec { let show_line_number = false; vec![ @@ -969,31 +1168,31 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec { highlighted_texts: vec![ HighlightedText { text: "maxdeviant ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, HighlightedText { text: "in ".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, HighlightedText { text: "profaned-capital ".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, HighlightedText { text: "in ".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, HighlightedText { text: "~/p/zed ".to_string(), - color: theme.syntax.color("function"), + color: cx.theme().syntax_color("function"), }, HighlightedText { text: "on ".to_string(), - color: theme.text, + color: cx.theme().colors().text, }, HighlightedText { text: " gpui2-ui ".to_string(), - color: theme.syntax.color("keyword"), + color: cx.theme().syntax_color("keyword"), }, ], }), @@ -1008,7 +1207,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "λ ".to_string(), - color: theme.syntax.color("string"), + color: cx.theme().syntax_color("string"), }], }), cursors: None, diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index d2813bd174..dea4e342b4 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -6,8 +6,6 @@ pub struct Story {} impl Story { pub fn container(cx: &mut ViewContext) -> Div { - let theme = theme(cx); - div() .size_full() .flex() @@ -15,15 +13,13 @@ impl Story { .pt_2() .px_4() .font("Zed Mono") - .bg(theme.background) + .bg(cx.theme().colors().background) } pub fn title(cx: &mut ViewContext, title: &str) -> impl Component { - let theme = theme(cx); - div() .text_xl() - .text_color(theme.text) + .text_color(cx.theme().colors().text) .child(title.to_owned()) } @@ -32,13 +28,11 @@ impl Story { } pub fn label(cx: &mut ViewContext, label: &str) -> impl Component { - let theme = theme(cx); - div() .mt_4() .mb_2() .text_xs() - .text_color(theme.text) + .text_color(cx.theme().colors().text) .child(label.to_owned()) } } diff --git a/crates/ui2/src/utils.rs b/crates/ui2/src/utils.rs new file mode 100644 index 0000000000..573a1333ef --- /dev/null +++ b/crates/ui2/src/utils.rs @@ -0,0 +1,3 @@ +mod format_distance; + +pub use format_distance::*; diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs new file mode 100644 index 0000000000..133eabc202 --- /dev/null +++ b/crates/ui2/src/utils/format_distance.rs @@ -0,0 +1,231 @@ +use chrono::NaiveDateTime; + +/// Calculates the distance in seconds between two NaiveDateTime objects. +/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. +/// +/// ## Arguments +/// +/// * `date` - A NaiveDateTime object representing the date of interest +/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made +fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { + let duration = date.signed_duration_since(base_date); + -duration.num_seconds() +} + +/// Generates a string describing the time distance between two dates in a human-readable way. +fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String { + let suffix = if distance < 0 { " from now" } else { " ago" }; + + let d = distance.abs(); + + let minutes = d / 60; + let hours = d / 3600; + let days = d / 86400; + let months = d / 2592000; + let years = d / 31536000; + + let string = if d < 5 && include_seconds { + "less than 5 seconds".to_string() + } else if d < 10 && include_seconds { + "less than 10 seconds".to_string() + } else if d < 20 && include_seconds { + "less than 20 seconds".to_string() + } else if d < 40 && include_seconds { + "half a minute".to_string() + } else if d < 60 && include_seconds { + "less than a minute".to_string() + } else if d < 90 && include_seconds { + "1 minute".to_string() + } else if d < 30 { + "less than a minute".to_string() + } else if d < 90 { + "1 minute".to_string() + } else if d < 2700 { + format!("{} minutes", minutes) + } else if d < 5400 { + "about 1 hour".to_string() + } else if d < 86400 { + format!("about {} hours", hours) + } else if d < 172800 { + "1 day".to_string() + } else if d < 2592000 { + format!("{} days", days) + } else if d < 5184000 { + "about 1 month".to_string() + } else if d < 7776000 { + "about 2 months".to_string() + } else if d < 31540000 { + format!("{} months", months) + } else if d < 39425000 { + "about 1 year".to_string() + } else if d < 55195000 { + "over 1 year".to_string() + } else if d < 63080000 { + "almost 2 years".to_string() + } else { + let years = d / 31536000; + let remaining_months = (d % 31536000) / 2592000; + + if remaining_months < 3 { + format!("about {} years", years) + } else if remaining_months < 9 { + format!("over {} years", years) + } else { + format!("almost {} years", years + 1) + } + }; + + if add_suffix { + return format!("{}{}", string, suffix); + } else { + string + } +} + +/// Get the time difference between two dates into a relative human readable string. +/// +/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. +/// +/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now. +/// +/// # Arguments +/// +/// * `date` - The NaiveDateTime to compare. +/// * `base_date` - The NaiveDateTime to compare against. +/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed +/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future +/// +/// # Example +/// +/// ```rust +/// use chrono::DateTime; +/// use ui2::utils::naive_format_distance; +/// +/// fn time_between_moon_landings() -> String { +/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local(); +/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local(); +/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false)) +/// } +/// ``` +/// +/// Output: `"There was about 3 years between the first and last crewed moon landings."` +pub fn naive_format_distance( + date: NaiveDateTime, + base_date: NaiveDateTime, + include_seconds: bool, + add_suffix: bool, +) -> String { + let distance = distance_in_seconds(date, base_date); + + distance_string(distance, include_seconds, add_suffix) +} + +/// Get the time difference between a date and now as relative human readable string. +/// +/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. +/// +/// # Arguments +/// +/// * `datetime` - The NaiveDateTime to compare with the current time. +/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed +/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future +/// +/// # Example +/// +/// ```rust +/// use chrono::DateTime; +/// use ui2::utils::naive_format_distance_from_now; +/// +/// fn time_since_first_moon_landing() -> String { +/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local(); +/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false)) +/// } +/// ``` +/// +/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.` +pub fn naive_format_distance_from_now( + datetime: NaiveDateTime, + include_seconds: bool, + add_suffix: bool, +) -> String { + let now = chrono::offset::Local::now().naive_local(); + + naive_format_distance(datetime, now, include_seconds, add_suffix) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDateTime; + + #[test] + fn test_naive_format_distance() { + let date = + NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"); + let base_date = + NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"); + + assert_eq!( + "about 2 hours", + naive_format_distance(date, base_date, false, false) + ); + } + + #[test] + fn test_naive_format_distance_with_suffix() { + let date = + NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"); + let base_date = + NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"); + + assert_eq!( + "about 2 hours from now", + naive_format_distance(date, base_date, false, true) + ); + } + + #[test] + fn test_naive_format_distance_from_now() { + let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + .expect("Invalid NaiveDateTime for date"); + + assert_eq!( + "over 54 years ago", + naive_format_distance_from_now(date, false, true) + ); + } + + #[test] + fn test_naive_format_distance_string() { + assert_eq!(distance_string(3, false, false), "less than a minute"); + assert_eq!(distance_string(7, false, false), "less than a minute"); + assert_eq!(distance_string(13, false, false), "less than a minute"); + assert_eq!(distance_string(21, false, false), "less than a minute"); + assert_eq!(distance_string(45, false, false), "1 minute"); + assert_eq!(distance_string(61, false, false), "1 minute"); + assert_eq!(distance_string(1920, false, false), "32 minutes"); + assert_eq!(distance_string(3902, false, false), "about 1 hour"); + assert_eq!(distance_string(18002, false, false), "about 5 hours"); + assert_eq!(distance_string(86470, false, false), "1 day"); + assert_eq!(distance_string(345880, false, false), "4 days"); + assert_eq!(distance_string(2764800, false, false), "about 1 month"); + assert_eq!(distance_string(5184000, false, false), "about 2 months"); + assert_eq!(distance_string(10368000, false, false), "4 months"); + assert_eq!(distance_string(34694000, false, false), "about 1 year"); + assert_eq!(distance_string(47310000, false, false), "over 1 year"); + assert_eq!(distance_string(61503000, false, false), "almost 2 years"); + assert_eq!(distance_string(160854000, false, false), "about 5 years"); + assert_eq!(distance_string(236550000, false, false), "over 7 years"); + assert_eq!(distance_string(249166000, false, false), "almost 8 years"); + } + + #[test] + fn test_naive_format_distance_string_include_seconds() { + assert_eq!(distance_string(3, true, false), "less than 5 seconds"); + assert_eq!(distance_string(7, true, false), "less than 10 seconds"); + assert_eq!(distance_string(13, true, false), "less than 20 seconds"); + assert_eq!(distance_string(21, true, false), "half a minute"); + assert_eq!(distance_string(45, true, false), "less than a minute"); + assert_eq!(distance_string(61, true, false), "1 minute"); + } +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 6ab76b0850..cfbd7551f9 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -14,6 +14,7 @@ test-support = ["tempdir", "git2"] [dependencies] anyhow.workspace = true backtrace = "0.3" +globset.workspace = true log.workspace = true lazy_static.workspace = true futures.workspace = true diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 96d77236a9..d54e0b1cd6 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; +use globset::{Glob, GlobMatcher}; use serde::{Deserialize, Serialize}; lazy_static::lazy_static! { @@ -189,6 +190,31 @@ impl

PathLikeWithPosition

{ } } +#[derive(Clone, Debug)] +pub struct PathMatcher { + maybe_path: PathBuf, + glob: GlobMatcher, +} + +impl std::fmt::Display for PathMatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.maybe_path.to_string_lossy().fmt(f) + } +} + +impl PathMatcher { + pub fn new(maybe_glob: &str) -> Result { + Ok(PathMatcher { + glob: Glob::new(&maybe_glob)?.compile_matcher(), + maybe_path: PathBuf::from(maybe_glob), + }) + } + + pub fn is_match>(&self, other: P) -> bool { + other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml new file mode 100644 index 0000000000..f3f10d2015 --- /dev/null +++ b/crates/workspace2/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "workspace2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/workspace2.rs" +doctest = false + +[features] +test-support = [ + "call2/test-support", + "client2/test-support", + "project2/test-support", + "settings2/test-support", + "gpui/test-support", + "fs2/test-support" +] + +[dependencies] +db2 = { path = "../db2" } +call2 = { path = "../call2" } +client2 = { path = "../client2" } +collections = { path = "../collections" } +# context_menu = { path = "../context_menu" } +fs2 = { path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +install_cli2 = { path = "../install_cli2" } +language2 = { path = "../language2" } +#menu = { path = "../menu" } +node_runtime = { path = "../node_runtime" } +project2 = { path = "../project2" } +settings2 = { path = "../settings2" } +terminal2 = { path = "../terminal2" } +theme2 = { path = "../theme2" } +util = { path = "../util" } +ui = { package = "ui2", path = "../ui2" } + +async-recursion = "1.0.0" +itertools = "0.10" +bincode = "1.2.1" +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +smallvec.workspace = true +uuid.workspace = true + +[dev-dependencies] +call2 = { path = "../call2", features = ["test-support"] } +client2 = { path = "../client2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +project2 = { path = "../project2", features = ["test-support"] } +settings2 = { path = "../settings2", features = ["test-support"] } +fs2 = { path = "../fs2", features = ["test-support"] } +db2 = { path = "../db2", features = ["test-support"] } + +indoc.workspace = true +env_logger.workspace = true diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs new file mode 100644 index 0000000000..e6b6c7561d --- /dev/null +++ b/crates/workspace2/src/dock.rs @@ -0,0 +1,753 @@ +use crate::{status_bar::StatusItemView, Axis, Workspace}; +use gpui::{ + div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, + Subscription, View, ViewContext, WeakView, WindowContext, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +pub trait Panel: Render + EventEmitter { + fn persistent_name(&self) -> &'static str; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition) -> bool; + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&mut self, size: Option, cx: &mut ViewContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self) -> (String, Option>); + fn icon_label(&self, _: &WindowContext) -> Option { + None + } + fn should_change_position_on_event(_: &Self::Event) -> bool; + fn should_zoom_in_on_event(_: &Self::Event) -> bool { + false + } + fn should_zoom_out_on_event(_: &Self::Event) -> bool { + false + } + fn is_zoomed(&self, _cx: &WindowContext) -> bool { + false + } + fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + fn should_close_on_event(_: &Self::Event) -> bool { + false + } + fn has_focus(&self, cx: &WindowContext) -> bool; + fn is_focus_event(_: &Self::Event) -> bool; +} + +pub trait PanelHandle: Send + Sync { + fn id(&self) -> EntityId; + fn persistent_name(&self, cx: &WindowContext) -> &'static str; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; + fn set_position(&self, position: DockPosition, cx: &mut WindowContext); + fn is_zoomed(&self, cx: &WindowContext) -> bool; + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); + fn set_active(&self, active: bool, cx: &mut WindowContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&self, size: Option, cx: &mut WindowContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); + fn icon_label(&self, cx: &WindowContext) -> Option; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn to_any(&self) -> AnyView; +} + +impl PanelHandle for View +where + T: Panel, +{ + fn id(&self) -> EntityId { + self.entity_id() + } + + fn persistent_name(&self, cx: &WindowContext) -> &'static str { + self.read(cx).persistent_name() + } + + fn position(&self, cx: &WindowContext) -> DockPosition { + self.read(cx).position(cx) + } + + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool { + self.read(cx).position_is_valid(position) + } + + fn set_position(&self, position: DockPosition, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_position(position, cx)) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.read(cx).is_zoomed(cx) + } + + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_zoomed(zoomed, cx)) + } + + fn set_active(&self, active: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_active(active, cx)) + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.read(cx).size(cx) + } + + fn set_size(&self, size: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_size(size, cx)) + } + + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { + self.read(cx).icon_path(cx) + } + + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>) { + self.read(cx).icon_tooltip() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + self.read(cx).icon_label(cx) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus(cx) + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } +} + +impl From<&dyn PanelHandle> for AnyView { + fn from(val: &dyn PanelHandle) -> Self { + val.to_any() + } +} + +pub struct Dock { + position: DockPosition, + panel_entries: Vec, + is_open: bool, + active_panel_index: usize, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DockPosition { + Left, + Bottom, + Right, +} + +impl DockPosition { + fn to_label(&self) -> &'static str { + match self { + Self::Left => "left", + Self::Bottom => "bottom", + Self::Right => "right", + } + } + + // todo!() + // fn to_resize_handle_side(self) -> HandleSide { + // match self { + // Self::Left => HandleSide::Right, + // Self::Bottom => HandleSide::Top, + // Self::Right => HandleSide::Left, + // } + // } + + pub fn axis(&self) -> Axis { + match self { + Self::Left | Self::Right => Axis::Horizontal, + Self::Bottom => Axis::Vertical, + } + } +} + +struct PanelEntry { + panel: Arc, + // todo!() + // context_menu: View, + _subscriptions: [Subscription; 2], +} + +pub struct PanelButtons { + dock: View, + workspace: WeakView, +} + +impl Dock { + pub fn new(position: DockPosition) -> Self { + Self { + position, + panel_entries: Default::default(), + active_panel_index: 0, + is_open: false, + } + } + + pub fn position(&self) -> DockPosition { + self.position + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + // pub fn has_focus(&self, cx: &WindowContext) -> bool { + // self.visible_panel() + // .map_or(false, |panel| panel.has_focus(cx)) + // } + + // pub fn panel(&self) -> Option> { + // self.panel_entries + // .iter() + // .find_map(|entry| entry.panel.as_any().clone().downcast()) + // } + + // pub fn panel_index_for_type(&self) -> Option { + // self.panel_entries + // .iter() + // .position(|entry| entry.panel.as_any().is::()) + // } + + pub fn panel_index_for_ui_name(&self, _ui_name: &str, _cx: &AppContext) -> Option { + todo!() + // self.panel_entries.iter().position(|entry| { + // let panel = entry.panel.as_any(); + // cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name) + // }) + } + + pub fn active_panel_index(&self) -> usize { + self.active_panel_index + } + + pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(open, cx); + } + + cx.notify(); + } + } + + // todo!() + // pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext) { + // for entry in &mut self.panel_entries { + // if entry.panel.as_any() == panel { + // if zoomed != entry.panel.is_zoomed(cx) { + // entry.panel.set_zoomed(zoomed, cx); + // } + // } else if entry.panel.is_zoomed(cx) { + // entry.panel.set_zoomed(false, cx); + // } + // } + + // cx.notify(); + // } + + pub fn zoom_out(&mut self, cx: &mut ViewContext) { + for entry in &mut self.panel_entries { + if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } + } + } + + pub(crate) fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + let subscriptions = [ + cx.observe(&panel, |_, _, cx| cx.notify()), + cx.subscribe(&panel, |this, panel, event, cx| { + if T::should_activate_on_event(event) { + if let Some(ix) = this + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + this.set_open(true, cx); + this.activate_panel(ix, cx); + // todo!() + // cx.focus(&panel); + } + } else if T::should_close_on_event(event) + && this.visible_panel().map_or(false, |p| p.id() == panel.id()) + { + this.set_open(false, cx); + } + }), + ]; + + // todo!() + // let dock_view_id = cx.view_id(); + self.panel_entries.push(PanelEntry { + panel: Arc::new(panel), + // todo!() + // context_menu: cx.add_view(|cx| { + // let mut menu = ContextMenu::new(dock_view_id, cx); + // menu.set_position_mode(OverlayPositionMode::Local); + // menu + // }), + _subscriptions: subscriptions, + }); + cx.notify() + } + + pub fn remove_panel(&mut self, panel: &View, cx: &mut ViewContext) { + if let Some(panel_ix) = self + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + if panel_ix == self.active_panel_index { + self.active_panel_index = 0; + self.set_open(false, cx); + } else if panel_ix < self.active_panel_index { + self.active_panel_index -= 1; + } + self.panel_entries.remove(panel_ix); + cx.notify(); + } + } + + pub fn panels_len(&self) -> usize { + self.panel_entries.len() + } + + pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext) { + if panel_ix != self.active_panel_index { + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(false, cx); + } + + self.active_panel_index = panel_ix; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(true, cx); + } + + cx.notify(); + } + } + + pub fn visible_panel(&self) -> Option<&Arc> { + let entry = self.visible_entry()?; + Some(&entry.panel) + } + + pub fn active_panel(&self) -> Option<&Arc> { + Some(&self.panel_entries.get(self.active_panel_index)?.panel) + } + + fn visible_entry(&self) -> Option<&PanelEntry> { + if self.is_open { + self.panel_entries.get(self.active_panel_index) + } else { + None + } + } + + pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { + let entry = self.visible_entry()?; + if entry.panel.is_zoomed(cx) { + Some(entry.panel.clone()) + } else { + None + } + } + + pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option { + self.panel_entries + .iter() + .find(|entry| entry.panel.id() == panel.id()) + .map(|entry| entry.panel.size(cx)) + } + + pub fn active_panel_size(&self, cx: &WindowContext) -> Option { + if self.is_open { + self.panel_entries + .get(self.active_panel_index) + .map(|entry| entry.panel.size(cx)) + } else { + None + } + } + + pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { + if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { + entry.panel.set_size(size, cx); + cx.notify(); + } + } + + // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { + // todo!() + // if let Some(active_entry) = self.visible_entry() { + // Empty::new() + // .into_any() + // .contained() + // .with_style(self.style(cx)) + // .resizable::( + // self.position.to_resize_handle_side(), + // active_entry.panel.size(cx), + // |_, _, _| {}, + // ) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // } +} + +// todo!() +// impl View for Dock { +// fn ui_name() -> &'static str { +// "Dock" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// if let Some(active_entry) = self.visible_entry() { +// let style = self.style(cx); +// ChildView::new(active_entry.panel.as_any(), cx) +// .contained() +// .with_style(style) +// .resizable::( +// self.position.to_resize_handle_side(), +// active_entry.panel.size(cx), +// |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), +// ) +// .into_any() +// } else { +// Empty::new().into_any() +// } +// } + +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// if cx.is_self_focused() { +// if let Some(active_entry) = self.visible_entry() { +// cx.focus(active_entry.panel.as_any()); +// } else { +// cx.focus_parent(); +// } +// } +// } +// } + +impl PanelButtons { + pub fn new( + dock: View, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&dock, |_, _, cx| cx.notify()).detach(); + Self { dock, workspace } + } +} + +impl EventEmitter for PanelButtons { + type Event = (); +} + +// impl Render for PanelButtons { +// type Element = (); + +// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { +// todo!("") +// } + +// fn ui_name() -> &'static str { +// "PanelButtons" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &settings::get::(cx).theme; +// let tooltip_style = theme.tooltip.clone(); +// let theme = &theme.workspace.status_bar.panel_buttons; +// let button_style = theme.button.clone(); +// let dock = self.dock.read(cx); +// let active_ix = dock.active_panel_index; +// let is_open = dock.is_open; +// let dock_position = dock.position; +// let group_style = match dock_position { +// DockPosition::Left => theme.group_left, +// DockPosition::Bottom => theme.group_bottom, +// DockPosition::Right => theme.group_right, +// }; +// let menu_corner = match dock_position { +// DockPosition::Left => AnchorCorner::BottomLeft, +// DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight, +// }; + +// let panels = dock +// .panel_entries +// .iter() +// .map(|item| (item.panel.clone(), item.context_menu.clone())) +// .collect::>(); +// Flex::row() +// .with_children(panels.into_iter().enumerate().filter_map( +// |(panel_ix, (view, context_menu))| { +// let icon_path = view.icon_path(cx)?; +// let is_active = is_open && panel_ix == active_ix; +// let (tooltip, tooltip_action) = if is_active { +// ( +// format!("Close {} dock", dock_position.to_label()), +// Some(match dock_position { +// DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), +// DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), +// DockPosition::Right => crate::ToggleRightDock.boxed_clone(), +// }), +// ) +// } else { +// view.icon_tooltip(cx) +// }; +// Some( +// Stack::new() +// .with_child( +// MouseEventHandler::new::(panel_ix, cx, |state, cx| { +// let style = button_style.in_state(is_active); + +// let style = style.style_for(state); +// Flex::row() +// .with_child( +// Svg::new(icon_path) +// .with_color(style.icon_color) +// .constrained() +// .with_width(style.icon_size) +// .aligned(), +// ) +// .with_children(if let Some(label) = view.icon_label(cx) { +// Some( +// Label::new(label, style.label.text.clone()) +// .contained() +// .with_style(style.label.container) +// .aligned(), +// ) +// } else { +// None +// }) +// .constrained() +// .with_height(style.icon_size) +// .contained() +// .with_style(style.container) +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, { +// let tooltip_action = +// tooltip_action.as_ref().map(|action| action.boxed_clone()); +// move |_, this, cx| { +// if let Some(tooltip_action) = &tooltip_action { +// let window = cx.window(); +// let view_id = this.workspace.id(); +// let tooltip_action = tooltip_action.boxed_clone(); +// cx.spawn(|_, mut cx| async move { +// window.dispatch_action( +// view_id, +// &*tooltip_action, +// &mut cx, +// ); +// }) +// .detach(); +// } +// } +// }) +// .on_click(MouseButton::Right, { +// let view = view.clone(); +// let menu = context_menu.clone(); +// move |_, _, cx| { +// const POSITIONS: [DockPosition; 3] = [ +// DockPosition::Left, +// DockPosition::Right, +// DockPosition::Bottom, +// ]; + +// menu.update(cx, |menu, cx| { +// let items = POSITIONS +// .into_iter() +// .filter(|position| { +// *position != dock_position +// && view.position_is_valid(*position, cx) +// }) +// .map(|position| { +// let view = view.clone(); +// ContextMenuItem::handler( +// format!("Dock {}", position.to_label()), +// move |cx| view.set_position(position, cx), +// ) +// }) +// .collect(); +// menu.show(Default::default(), menu_corner, items, cx); +// }) +// } +// }) +// .with_tooltip::( +// panel_ix, +// tooltip, +// tooltip_action, +// tooltip_style.clone(), +// cx, +// ), +// ) +// .with_child(ChildView::new(&context_menu, cx)), +// ) +// }, +// )) +// .contained() +// .with_style(group_style) +// .into_any() +// } +// } + +impl Render for PanelButtons { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + // todo!() + let dock = self.dock.read(cx); + div().children( + dock.panel_entries + .iter() + .map(|panel| panel.panel.persistent_name(cx)), + ) + } +} + +impl StatusItemView for PanelButtons { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn crate::ItemHandle>, + _cx: &mut ViewContext, + ) { + // todo!(This is empty in the old `workspace::dock`) + } +} + +#[cfg(any(test, feature = "test-support"))] +pub mod test { + use super::*; + use gpui::{div, Div, ViewContext, WindowContext}; + + #[derive(Debug)] + pub enum TestPanelEvent { + PositionChanged, + Activated, + Closed, + ZoomIn, + ZoomOut, + Focus, + } + + pub struct TestPanel { + pub position: DockPosition, + pub zoomed: bool, + pub active: bool, + pub has_focus: bool, + pub size: f32, + } + + impl EventEmitter for TestPanel { + type Event = TestPanelEvent; + } + + impl TestPanel { + pub fn new(position: DockPosition) -> Self { + Self { + position, + zoomed: false, + active: false, + has_focus: false, + size: 300., + } + } + } + + impl Render for TestPanel { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + } + } + + impl Panel for TestPanel { + fn persistent_name(&self) -> &'static str { + "TestPanel" + } + + fn position(&self, _: &gpui::WindowContext) -> super::DockPosition { + self.position + } + + fn position_is_valid(&self, _: super::DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + self.position = position; + cx.emit(TestPanelEvent::PositionChanged); + } + + fn size(&self, _: &WindowContext) -> f32 { + self.size + } + + fn set_size(&mut self, size: Option, _: &mut ViewContext) { + self.size = size.unwrap_or(300.); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/test_panel.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Test Panel".into(), None) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::PositionChanged) + } + + fn should_zoom_in_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomOut) + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { + self.zoomed = zoomed; + } + + fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { + self.active = active; + } + + fn should_activate_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Activated) + } + + fn should_close_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Closed) + } + + fn has_focus(&self, _cx: &WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Focus) + } + } +} diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 90d08d6c4a..15b387cbed 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -1,88 +1,80 @@ -// use crate::{ -// pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders, -// ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, -// }; -// use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; +use crate::{ + pane::{self, Pane}, + persistence::model::ItemId, + searchable::SearchableItemHandle, + workspace_settings::{AutosaveSetting, WorkspaceSettings}, + DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation, + ViewId, Workspace, WorkspaceId, +}; use anyhow::Result; use client2::{ - proto::{self, PeerId, ViewId}, + proto::{self, PeerId}, Client, }; +use gpui::{ + AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, HighlightStyle, Model, Pixels, + Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext, +}; +use parking_lot::Mutex; +use project2::{Project, ProjectEntryId, ProjectPath}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use settings2::Settings; -use theme2::Theme; -// use client2::{ -// proto::{self, PeerId}, -// Client, -// }; -// use gpui2::geometry::vector::Vector2F; -// use gpui2::AnyWindowHandle; -// use gpui2::{ -// fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, Handle, Task, View, -// ViewContext, View, WeakViewHandle, WindowContext, -// }; -// use project2::{Project, ProjectEntryId, ProjectPath}; -// use schemars::JsonSchema; -// use serde_derive::{Deserialize, Serialize}; -// use settings2::Setting; -// use smallvec::SmallVec; -// use std::{ -// any::{Any, TypeId}, -// borrow::Cow, -// cell::RefCell, -// fmt, -// ops::Range, -// path::PathBuf, -// rc::Rc, -// sync::{ -// atomic::{AtomicBool, Ordering}, -// Arc, -// }, -// time::Duration, -// }; -// use theme2::Theme; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use theme2::ThemeVariant; -// #[derive(Deserialize)] -// pub struct ItemSettings { -// pub git_status: bool, -// pub close_position: ClosePosition, -// } +#[derive(Deserialize)] +pub struct ItemSettings { + pub git_status: bool, + pub close_position: ClosePosition, +} -// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -// #[serde(rename_all = "lowercase")] -// pub enum ClosePosition { -// Left, -// #[default] -// Right, -// } +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ClosePosition { + Left, + #[default] + Right, +} -// impl ClosePosition { -// pub fn right(&self) -> bool { -// match self { -// ClosePosition::Left => false, -// ClosePosition::Right => true, -// } -// } -// } +impl ClosePosition { + pub fn right(&self) -> bool { + match self { + ClosePosition::Left => false, + ClosePosition::Right => true, + } + } +} -// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -// pub struct ItemSettingsContent { -// git_status: Option, -// close_position: Option, -// } +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ItemSettingsContent { + git_status: Option, + close_position: Option, +} -// impl Setting for ItemSettings { -// const KEY: Option<&'static str> = Some("tabs"); +impl Settings for ItemSettings { + const KEY: Option<&'static str> = Some("tabs"); -// type FileContent = ItemSettingsContent; + type FileContent = ItemSettingsContent; -// fn load( -// default_value: &Self::FileContent, -// user_values: &[&Self::FileContent], -// _: &gpui2::AppContext, -// ) -> anyhow::Result { -// Self::load_via_json_merge(default_value, user_values) -// } -// } + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} #[derive(Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { @@ -98,12 +90,12 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: EventEmitter + Sized { - // fn deactivated(&mut self, _: &mut ViewContext) {} - // fn workspace_deactivated(&mut self, _: &mut ViewContext) {} - // fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { - // false - // } +pub trait Item: Render + EventEmitter + Send { + fn deactivated(&mut self, _: &mut ViewContext) {} + fn workspace_deactivated(&mut self, _: &mut ViewContext) {} + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } fn tab_tooltip_text(&self, _: &AppContext) -> Option { None } @@ -117,138 +109,110 @@ pub trait Item: EventEmitter + Sized { fn is_singleton(&self, _cx: &AppContext) -> bool { false } - // fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} - fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext) -> Option + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + _: &mut ViewContext, + ) -> Option> where Self: Sized, { None } - // fn is_dirty(&self, _: &AppContext) -> bool { - // false - // } - // fn has_conflict(&self, _: &AppContext) -> bool { - // false - // } - // fn can_save(&self, _cx: &AppContext) -> bool { - // false - // } - // fn save( - // &mut self, - // _project: Handle, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("save() must be implemented if can_save() returns true") - // } - // fn save_as( - // &mut self, - // _project: Handle, - // _abs_path: PathBuf, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("save_as() must be implemented if can_save() returns true") - // } - // fn reload( - // &mut self, - // _project: Handle, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("reload() must be implemented if can_save() returns true") - // } + fn is_dirty(&self, _: &AppContext) -> bool { + false + } + fn has_conflict(&self, _: &AppContext) -> bool { + false + } + fn can_save(&self, _cx: &AppContext) -> bool { + false + } + fn save(&mut self, _project: Model, _cx: &mut ViewContext) -> Task> { + unimplemented!("save() must be implemented if can_save() returns true") + } + fn save_as( + &mut self, + _project: Model, + _abs_path: PathBuf, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save_as() must be implemented if can_save() returns true") + } + fn reload( + &mut self, + _project: Model, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("reload() must be implemented if can_save() returns true") + } fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { SmallVec::new() } - // fn should_close_item_on_event(_: &Self::Event) -> bool { - // false - // } - // fn should_update_tab_on_event(_: &Self::Event) -> bool { - // false - // } + fn should_close_item_on_event(_: &Self::Event) -> bool { + false + } + fn should_update_tab_on_event(_: &Self::Event) -> bool { + false + } - // fn act_as_type<'a>( - // &'a self, - // type_id: TypeId, - // self_handle: &'a View, - // _: &'a AppContext, - // ) -> Option<&AnyViewHandle> { - // if TypeId::of::() == type_id { - // Some(self_handle) - // } else { - // None - // } - // } + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if TypeId::of::() == type_id { + Some(self_handle.clone().into()) + } else { + None + } + } - // fn as_searchable(&self, _: &View) -> Option> { - // None - // } + fn as_searchable(&self, _: &View) -> Option> { + None + } - // fn breadcrumb_location(&self) -> ToolbarItemLocation { - // ToolbarItemLocation::Hidden - // } + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::Hidden + } - // fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option> { - // None - // } + fn breadcrumbs(&self, _theme: &ThemeVariant, _cx: &AppContext) -> Option> { + None + } - // fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} + fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} - // fn serialized_item_kind() -> Option<&'static str> { - // None - // } + fn serialized_item_kind() -> Option<&'static str> { + None + } - // fn deserialize( - // _project: Handle, - // _workspace: WeakViewHandle, - // _workspace_id: WorkspaceId, - // _item_id: ItemId, - // _cx: &mut ViewContext, - // ) -> Task>> { - // unimplemented!( - // "deserialize() must be implemented if serialized_item_kind() returns Some(_)" - // ) - // } - // fn show_toolbar(&self) -> bool { - // true - // } - // fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { - // None - // } + fn deserialize( + _project: Model, + _workspace: WeakView, + _workspace_id: WorkspaceId, + _item_id: ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + unimplemented!( + "deserialize() must be implemented if serialized_item_kind() returns Some(_)" + ) + } + fn show_toolbar(&self) -> bool { + true + } + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { + None + } } -use std::{ - any::Any, - cell::RefCell, - ops::Range, - path::PathBuf, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::Duration, -}; - -use gpui2::{ - AnyElement, AnyWindowHandle, AppContext, EventEmitter, Handle, HighlightStyle, Pixels, Point, - SharedString, Task, View, ViewContext, VisualContext, WindowContext, -}; -use project2::{Project, ProjectEntryId, ProjectPath}; -use smallvec::SmallVec; - -use crate::{ - pane::{self, Pane}, - searchable::SearchableItemHandle, - workspace_settings::{AutosaveSetting, WorkspaceSettings}, - DelayedDebouncedEditAction, FollowableItemBuilders, ToolbarItemLocation, Workspace, - WorkspaceId, -}; - pub trait ItemHandle: 'static + Send { fn subscribe_to_item_events( &self, cx: &mut WindowContext, handler: Box, - ) -> gpui2::Subscription; + ) -> gpui::Subscription; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement; @@ -273,59 +237,56 @@ pub trait ItemHandle: 'static + Send { fn deactivated(&self, cx: &mut WindowContext); fn workspace_deactivated(&self, cx: &mut WindowContext); fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool; - fn id(&self) -> usize; - fn window(&self) -> AnyWindowHandle; - // fn as_any(&self) -> &AnyView; todo!() + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; fn is_dirty(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; - fn save(&self, project: Handle, cx: &mut WindowContext) -> Task>; + fn save(&self, project: Model, cx: &mut WindowContext) -> Task>; fn save_as( &self, - project: Handle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task>; - fn reload(&self, project: Handle, cx: &mut WindowContext) -> Task>; - // fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>; todo!() + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( - &self, + &mut self, cx: &mut AppContext, - callback: Box, - ) -> gpui2::Subscription; + callback: Box, + ) -> gpui::Subscription; fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; - fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; + fn breadcrumbs(&self, theme: &ThemeVariant, cx: &AppContext) -> Option>; fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; } -pub trait WeakItemHandle { - fn id(&self) -> usize; - fn window(&self) -> AnyWindowHandle; - fn upgrade(&self, cx: &AppContext) -> Option>; +pub trait WeakItemHandle: Send + Sync { + fn id(&self) -> EntityId; + fn upgrade(&self) -> Option>; } -// todo!() -// impl dyn ItemHandle { -// pub fn downcast(&self) -> Option> { -// self.as_any().clone().downcast() -// } +impl dyn ItemHandle { + pub fn downcast(&self) -> Option> { + self.to_any().downcast().ok() + } -// pub fn act_as(&self, cx: &AppContext) -> Option> { -// self.act_as_type(TypeId::of::(), cx) -// .and_then(|t| t.clone().downcast()) -// } -// } + pub fn act_as(&self, cx: &AppContext) -> Option> { + self.act_as_type(TypeId::of::(), cx) + .and_then(|t| t.downcast().ok()) + } +} impl ItemHandle for View { fn subscribe_to_item_events( &self, cx: &mut WindowContext, handler: Box, - ) -> gpui2::Subscription { + ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { for item_event in T::to_item_events(event) { handler(item_event, cx) @@ -399,10 +360,8 @@ impl ItemHandle for View { workspace_id: WorkspaceId, cx: &mut WindowContext, ) -> Option> { - self.update(cx, |item, cx| { - cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx)) - }) - .map(|handle| Box::new(handle) as Box) + self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx)) + .map(|handle| Box::new(handle) as Box) } fn added_to_pane( @@ -439,109 +398,107 @@ impl ItemHandle for View { .is_none() { let mut pending_autosave = DelayedDebouncedEditAction::new(); - let pending_update = Rc::new(RefCell::new(None)); - let pending_update_scheduled = Rc::new(AtomicBool::new(false)); + let pending_update = Arc::new(Mutex::new(None)); + let pending_update_scheduled = Arc::new(AtomicBool::new(false)); - let mut event_subscription = - Some(cx.subscribe(self, move |workspace, item, event, cx| { - let pane = if let Some(pane) = workspace - .panes_by_item - .get(&item.id()) - .and_then(|pane| pane.upgrade(cx)) + let event_subscription = Some(cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = workspace + .panes_by_item + .get(&item.id()) + .and_then(|pane| pane.upgrade()) + { + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + + if let Some(item) = item.to_followable_item_handle(cx) { + let _is_project_item = item.is_project_item(cx); + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); + } + + if item.add_event_to_update_proto(event, &mut *pending_update.lock(), cx) + && !pending_update_scheduled.load(Ordering::SeqCst) { - pane - } else { - log::error!("unexpected item event after pane was dropped"); - return; - }; + pending_update_scheduled.store(true, Ordering::SeqCst); + todo!("replace with on_next_frame?"); + // cx.after_window_update({ + // let pending_update = pending_update.clone(); + // let pending_update_scheduled = pending_update_scheduled.clone(); + // move |this, cx| { + // pending_update_scheduled.store(false, Ordering::SeqCst); + // this.update_followers( + // is_project_item, + // proto::update_followers::Variant::UpdateView( + // proto::UpdateView { + // id: item + // .remote_id(&this.app_state.client, cx) + // .map(|id| id.to_proto()), + // variant: pending_update.borrow_mut().take(), + // leader_id, + // }, + // ), + // cx, + // ); + // } + // }); + } + } - if let Some(item) = item.to_followable_item_handle(cx) { - let is_project_item = item.is_project_item(cx); - let leader_id = workspace.leader_for_pane(&pane); - - if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { - workspace.unfollow(&pane, cx); + for item_event in T::to_item_events(event).into_iter() { + match item_event { + ItemEvent::CloseItem => { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) + }) + .detach_and_log_err(cx); + return; } - if item.add_event_to_update_proto( - event, - &mut *pending_update.borrow_mut(), - cx, - ) && !pending_update_scheduled.load(Ordering::SeqCst) - { - pending_update_scheduled.store(true, Ordering::SeqCst); - cx.after_window_update({ - let pending_update = pending_update.clone(); - let pending_update_scheduled = pending_update_scheduled.clone(); - move |this, cx| { - pending_update_scheduled.store(false, Ordering::SeqCst); - this.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateView( - proto::UpdateView { - id: item - .remote_id(&this.app_state.client, cx) - .map(|id| id.to_proto()), - variant: pending_update.borrow_mut().take(), - leader_id, - }, - ), - cx, - ); - } + ItemEvent::UpdateTab => { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::ChangeItemTitle); + cx.notify(); }); } - } - for item_event in T::to_item_events(event).into_iter() { - match item_event { - ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) - }) - .detach_and_log_err(cx); - return; - } - - ItemEvent::UpdateTab => { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::ChangeItemTitle); - cx.notify(); + ItemEvent::Edit => { + let autosave = WorkspaceSettings::get_global(cx).autosave; + if let AutosaveSetting::AfterDelay { milliseconds } = autosave { + let delay = Duration::from_millis(milliseconds); + let item = item.clone(); + pending_autosave.fire_new(delay, cx, move |workspace, cx| { + Pane::autosave_item(&item, workspace.project().clone(), cx) }); } - - ItemEvent::Edit => { - let autosave = WorkspaceSettings::get_global(cx).autosave; - if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds); - let item = item.clone(); - pending_autosave.fire_new(delay, cx, move |workspace, cx| { - Pane::autosave_item(&item, workspace.project().clone(), cx) - }); - } - } - - _ => {} } + + _ => {} } - })); - - cx.observe_focus(self, move |workspace, item, focused, cx| { - if !focused - && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange - { - Pane::autosave_item(&item, workspace.project.clone(), cx) - .detach_and_log_err(cx); } - }) - .detach(); + })); - let item_id = self.id(); - cx.observe_release(self, move |workspace, _, _| { - workspace.panes_by_item.remove(&item_id); - event_subscription.take(); - }) - .detach(); + todo!("observe focus"); + // cx.observe_focus(self, move |workspace, item, focused, cx| { + // if !focused + // && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange + // { + // Pane::autosave_item(&item, workspace.project.clone(), cx) + // .detach_and_log_err(cx); + // } + // }) + // .detach(); + + // let item_id = self.id(); + // cx.observe_release(self, move |workspace, _, _| { + // workspace.panes_by_item.remove(&item_id); + // event_subscription.take(); + // }) + // .detach(); } cx.defer(|workspace, cx| { @@ -561,20 +518,14 @@ impl ItemHandle for View { self.update(cx, |this, cx| this.navigate(data, cx)) } - fn id(&self) -> usize { - self.id() + fn id(&self) -> EntityId { + self.entity_id() } - fn window(&self) -> AnyWindowHandle { - todo!() - // AnyViewHandle::window(self) + fn to_any(&self) -> AnyView { + self.clone().into() } - // todo!() - // fn as_any(&self) -> &AnyViewHandle { - // self - // } - fn is_dirty(&self, cx: &AppContext) -> bool { self.read(cx).is_dirty(cx) } @@ -587,43 +538,42 @@ impl ItemHandle for View { self.read(cx).can_save(cx) } - fn save(&self, project: Handle, cx: &mut WindowContext) -> Task> { + fn save(&self, project: Model, cx: &mut WindowContext) -> Task> { self.update(cx, |item, cx| item.save(project, cx)) } fn save_as( &self, - project: Handle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task> { self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } - fn reload(&self, project: Handle, cx: &mut WindowContext) -> Task> { + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task> { self.update(cx, |item, cx| item.reload(project, cx)) } - // todo!() - // fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> { - // self.read(cx).act_as_type(type_id, self, cx) - // } + fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option { + self.read(cx).act_as_type(type_id, self, cx) + } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { if cx.has_global::() { let builders = cx.global::(); - let item = self.as_any(); - Some(builders.get(&item.view_type())?.1(item)) + let item = self.to_any(); + Some(builders.get(&item.entity_type())?.1(&item)) } else { None } } fn on_release( - &self, + &mut self, cx: &mut AppContext, - callback: Box, - ) -> gpui2::Subscription { + callback: Box, + ) -> gpui::Subscription { cx.observe_release(self, move |_, cx| callback(cx)) } @@ -635,7 +585,7 @@ impl ItemHandle for View { self.read(cx).breadcrumb_location() } - fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { + fn breadcrumbs(&self, theme: &ThemeVariant, cx: &AppContext) -> Option> { self.read(cx).breadcrumbs(theme, cx) } @@ -652,17 +602,17 @@ impl ItemHandle for View { } } -// impl From> for AnyViewHandle { -// fn from(val: Box) -> Self { -// val.as_any().clone() -// } -// } +impl From> for AnyView { + fn from(val: Box) -> Self { + val.to_any() + } +} -// impl From<&Box> for AnyViewHandle { -// fn from(val: &Box) -> Self { -// val.as_any().clone() -// } -// } +impl From<&Box> for AnyView { + fn from(val: &Box) -> Self { + val.to_any() + } +} impl Clone for Box { fn clone(&self) -> Box { @@ -670,26 +620,22 @@ impl Clone for Box { } } -// impl WeakItemHandle for WeakViewHandle { -// fn id(&self) -> usize { -// self.id() -// } +impl WeakItemHandle for WeakView { + fn id(&self) -> EntityId { + self.entity_id() + } -// fn window(&self) -> AnyWindowHandle { -// self.window() -// } - -// fn upgrade(&self, cx: &AppContext) -> Option> { -// self.upgrade(cx).map(|v| Box::new(v) as Box) -// } -// } + fn upgrade(&self) -> Option> { + self.upgrade().map(|v| Box::new(v) as Box) + } +} pub trait ProjectItem: Item { type Item: project2::Item; fn for_project_item( - project: Handle, - item: Handle, + project: Model, + item: Model, cx: &mut ViewContext, ) -> Self where @@ -714,7 +660,7 @@ pub trait FollowableItem: Item { ) -> bool; fn apply_update_proto( &mut self, - project: &Handle, + project: &Model, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Task>; @@ -736,7 +682,7 @@ pub trait FollowableItemHandle: ItemHandle { ) -> bool; fn apply_update_proto( &self, - project: &Handle, + project: &Model, message: proto::update_view::Variant, cx: &mut WindowContext, ) -> Task>; @@ -744,65 +690,65 @@ pub trait FollowableItemHandle: ItemHandle { fn is_project_item(&self, cx: &AppContext) -> bool; } -// impl FollowableItemHandle for View { -// fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { -// self.read(cx).remote_id().or_else(|| { -// client.peer_id().map(|creator| ViewId { -// creator, -// id: self.id() as u64, -// }) -// }) -// } +impl FollowableItemHandle for View { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { + self.read(cx).remote_id().or_else(|| { + client.peer_id().map(|creator| ViewId { + creator, + id: self.id().as_u64(), + }) + }) + } -// fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { -// self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) -// } + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) + } -// fn to_state_proto(&self, cx: &AppContext) -> Option { -// self.read(cx).to_state_proto(cx) -// } + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) + } -// fn add_event_to_update_proto( -// &self, -// event: &dyn Any, -// update: &mut Option, -// cx: &AppContext, -// ) -> bool { -// if let Some(event) = event.downcast_ref() { -// self.read(cx).add_event_to_update_proto(event, update, cx) -// } else { -// false -// } -// } + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } + } -// fn apply_update_proto( -// &self, -// project: &Handle, -// message: proto::update_view::Variant, -// cx: &mut WindowContext, -// ) -> Task> { -// self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) -// } + fn apply_update_proto( + &self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut WindowContext, + ) -> Task> { + self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) + } -// fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { -// if let Some(event) = event.downcast_ref() { -// T::should_unfollow_on_event(event, cx) -// } else { -// false -// } -// } + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { + if let Some(event) = event.downcast_ref() { + T::should_unfollow_on_event(event, cx) + } else { + false + } + } -// fn is_project_item(&self, cx: &AppContext) -> bool { -// self.read(cx).is_project_item(cx) -// } -// } + fn is_project_item(&self, cx: &AppContext) -> bool { + self.read(cx).is_project_item(cx) + } +} // #[cfg(any(test, feature = "test-support"))] // pub mod test { // use super::{Item, ItemEvent}; // use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; -// use gpui2::{ -// elements::Empty, AnyElement, AppContext, Element, Entity, Handle, Task, View, +// use gpui::{ +// elements::Empty, AnyElement, AppContext, Element, Entity, Model, Task, View, // ViewContext, View, WeakViewHandle, // }; // use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId}; @@ -824,7 +770,7 @@ pub trait FollowableItemHandle: ItemHandle { // pub is_dirty: bool, // pub is_singleton: bool, // pub has_conflict: bool, -// pub project_items: Vec>, +// pub project_items: Vec>, // pub nav_history: Option, // pub tab_descriptions: Option>, // pub tab_detail: Cell>, @@ -869,7 +815,7 @@ pub trait FollowableItemHandle: ItemHandle { // } // impl TestProjectItem { -// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Handle { +// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { // let entry_id = Some(ProjectEntryId::from_proto(id)); // let project_path = Some(ProjectPath { // worktree_id: WorktreeId::from_usize(0), @@ -881,7 +827,7 @@ pub trait FollowableItemHandle: ItemHandle { // }) // } -// pub fn new_untitled(cx: &mut AppContext) -> Handle { +// pub fn new_untitled(cx: &mut AppContext) -> Model { // cx.add_model(|_| Self { // project_path: None, // entry_id: None, @@ -934,7 +880,7 @@ pub trait FollowableItemHandle: ItemHandle { // self // } -// pub fn with_project_items(mut self, items: &[Handle]) -> Self { +// pub fn with_project_items(mut self, items: &[Model]) -> Self { // self.project_items.clear(); // self.project_items.extend(items.iter().cloned()); // self @@ -1045,7 +991,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn save( // &mut self, -// _: Handle, +// _: Model, // _: &mut ViewContext, // ) -> Task> { // self.save_count += 1; @@ -1055,7 +1001,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn save_as( // &mut self, -// _: Handle, +// _: Model, // _: std::path::PathBuf, // _: &mut ViewContext, // ) -> Task> { @@ -1066,7 +1012,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn reload( // &mut self, -// _: Handle, +// _: Model, // _: &mut ViewContext, // ) -> Task> { // self.reload_count += 1; @@ -1083,7 +1029,7 @@ pub trait FollowableItemHandle: ItemHandle { // } // fn deserialize( -// _project: Handle, +// _project: Model, // _workspace: WeakViewHandle, // workspace_id: WorkspaceId, // _item_id: ItemId, diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs new file mode 100644 index 0000000000..5dd5b2c7ae --- /dev/null +++ b/crates/workspace2/src/notifications.rs @@ -0,0 +1,404 @@ +use crate::{Toast, Workspace}; +use collections::HashMap; +use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext}; +use std::{any::TypeId, ops::DerefMut}; + +pub fn init(cx: &mut AppContext) { + cx.set_global(NotificationTracker::new()); + // todo!() + // simple_message_notification::init(cx); +} + +pub trait Notification: EventEmitter + Render { + fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool; +} + +pub trait NotificationHandle: Send { + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; +} + +impl NotificationHandle for View { + fn id(&self) -> EntityId { + self.entity_id() + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } +} + +impl From<&dyn NotificationHandle> for AnyView { + fn from(val: &dyn NotificationHandle) -> Self { + val.to_any() + } +} + +pub(crate) struct NotificationTracker { + notifications_sent: HashMap>, +} + +impl std::ops::Deref for NotificationTracker { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.notifications_sent + } +} + +impl DerefMut for NotificationTracker { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.notifications_sent + } +} + +impl NotificationTracker { + fn new() -> Self { + Self { + notifications_sent: Default::default(), + } + } +} + +impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx.global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + + pub fn show_notification_once( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> View, + ) { + if !self.has_shown_notification_once::(id, cx) { + let tracker = cx.global_mut::(); + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); + self.show_notification::(id, cx, build_notification) + } + } + + pub fn show_notification( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> View, + ) { + let type_id = TypeId::of::(); + if self + .notifications + .iter() + .all(|(existing_type_id, existing_id, _)| { + (*existing_type_id, *existing_id) != (type_id, id) + }) + { + let notification = build_notification(cx); + cx.subscribe(¬ification, move |this, handle, event, cx| { + if handle.read(cx).should_dismiss_notification_on_event(event) { + this.dismiss_notification_internal(type_id, id, cx); + } + }) + .detach(); + self.notifications + .push((type_id, id, Box::new(notification))); + cx.notify(); + } + } + + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + let type_id = TypeId::of::(); + + self.dismiss_notification_internal(type_id, id, cx) + } + + pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { + todo!() + // self.dismiss_notification::(toast.id, cx); + // self.show_notification(toast.id, cx, |cx| { + // cx.add_view(|_cx| match toast.on_click.as_ref() { + // Some((click_msg, on_click)) => { + // let on_click = on_click.clone(); + // simple_message_notification::MessageNotification::new(toast.msg.clone()) + // .with_click_message(click_msg.clone()) + // .on_click(move |cx| on_click(cx)) + // } + // None => simple_message_notification::MessageNotification::new(toast.msg.clone()), + // }) + // }) + } + + pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext) { + todo!() + // self.dismiss_notification::(id, cx); + } + + fn dismiss_notification_internal( + &mut self, + type_id: TypeId, + id: usize, + cx: &mut ViewContext, + ) { + self.notifications + .retain(|(existing_type_id, existing_id, _)| { + if (*existing_type_id, *existing_id) == (type_id, id) { + cx.notify(); + false + } else { + true + } + }); + } +} + +pub mod simple_message_notification { + use super::Notification; + use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; + use serde::Deserialize; + use std::{borrow::Cow, sync::Arc}; + + // todo!() + // actions!(message_notifications, [CancelMessageNotification]); + + #[derive(Clone, Default, Deserialize, PartialEq)] + pub struct OsOpen(pub Cow<'static, str>); + + impl OsOpen { + pub fn new>>(url: I) -> Self { + OsOpen(url.into()) + } + } + + // todo!() + // impl_actions!(message_notifications, [OsOpen]); + // + // todo!() + // pub fn init(cx: &mut AppContext) { + // cx.add_action(MessageNotification::dismiss); + // cx.add_action( + // |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { + // cx.platform().open_url(open_action.0.as_ref()); + // }, + // ) + // } + + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + + pub struct MessageNotification { + message: NotificationMessage, + on_click: Option) + Send + Sync>>, + click_message: Option>, + } + + pub enum MessageNotificationEvent { + Dismiss, + } + + impl EventEmitter for MessageNotification { + type Event = MessageNotificationEvent; + } + + impl MessageNotification { + pub fn new(message: S) -> MessageNotification + where + S: Into>, + { + Self { + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + pub fn new_element( + message: fn(TextStyle, &AppContext) -> AnyElement, + ) -> MessageNotification { + Self { + message: NotificationMessage::Element(message), + on_click: None, + click_message: None, + } + } + + pub fn with_click_message(mut self, message: S) -> Self + where + S: Into>, + { + self.click_message = Some(message.into()); + self + } + + pub fn on_click(mut self, on_click: F) -> Self + where + F: 'static + Send + Sync + Fn(&mut ViewContext), + { + self.on_click = Some(Arc::new(on_click)); + self + } + + // todo!() + // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { + // cx.emit(MessageNotificationEvent::Dismiss); + // } + } + + impl Render for MessageNotification { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } + } + // todo!() + // impl View for MessageNotification { + // fn ui_name() -> &'static str { + // "MessageNotification" + // } + + // fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + // let theme = theme2::current(cx).clone(); + // let theme = &theme.simple_message_notification; + + // enum MessageNotificationTag {} + + // let click_message = self.click_message.clone(); + // let message = match &self.message { + // NotificationMessage::Text(text) => { + // Text::new(text.to_owned(), theme.message.text.clone()).into_any() + // } + // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + // }; + // let on_click = self.on_click.clone(); + // let has_click_action = on_click.is_some(); + + // Flex::column() + // .with_child( + // Flex::row() + // .with_child( + // message + // .contained() + // .with_style(theme.message.container) + // .aligned() + // .top() + // .left() + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.dismiss_button.style_for(state); + // Svg::new("icons/x.svg") + // .with_color(style.color) + // .constrained() + // .with_width(style.icon_width) + // .aligned() + // .contained() + // .with_style(style.container) + // .constrained() + // .with_width(style.button_width) + // .with_height(style.button_width) + // }) + // .with_padding(Padding::uniform(5.)) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.dismiss(&Default::default(), cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .aligned() + // .constrained() + // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + // .aligned() + // .top() + // .flex_float(), + // ), + // ) + // .with_children({ + // click_message + // .map(|click_message| { + // MouseEventHandler::new::( + // 0, + // cx, + // |state, _| { + // let style = theme.action_message.style_for(state); + + // Flex::row() + // .with_child( + // Text::new(click_message, style.text.clone()) + // .contained() + // .with_style(style.container), + // ) + // .contained() + // }, + // ) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(on_click) = on_click.as_ref() { + // on_click(cx); + // this.dismiss(&Default::default(), cx); + // } + // }) + // // Since we're not using a proper overlay, we have to capture these extra events + // .on_down(MouseButton::Left, |_, _, _| {}) + // .on_up(MouseButton::Left, |_, _, _| {}) + // .with_cursor_style(if has_click_action { + // CursorStyle::PointingHand + // } else { + // CursorStyle::Arrow + // }) + // }) + // .into_iter() + // }) + // .into_any() + // } + // } + + impl Notification for MessageNotification { + fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool { + match event { + MessageNotificationEvent::Dismiss => true, + } + } + } +} + +pub trait NotifyResultExt { + type Ok; + + fn notify_err( + self, + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Option; +} + +impl NotifyResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + log::error!("TODO {err:?}"); + // todo!() + // workspace.show_notification(0, cx, |cx| { + // cx.add_view(|_cx| { + // simple_message_notification::MessageNotification::new(format!( + // "Error: {err:?}", + // )) + // }) + // }); + None + } + } + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index e0eb1b7ec2..16dbfda361 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,48 +1,33 @@ // mod dragged_item_receiver; -// use super::{ItemHandle, SplitDirection}; -// pub use crate::toolbar::Toolbar; -// use crate::{ -// item::{ItemSettings, WeakItemHandle}, -// notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom, -// Workspace, WorkspaceSettings, -// }; -// use anyhow::Result; -// use collections::{HashMap, HashSet, VecDeque}; -// // use context_menu::{ContextMenu, ContextMenuItem}; - -// use dragged_item_receiver::dragged_item_receiver; -// use fs2::repository::GitFileStatus; -// use futures::StreamExt; -// use gpui2::{ -// actions, -// elements::*, -// geometry::{ -// rect::RectF, -// vector::{vec2f, Vector2F}, -// }, -// impl_actions, -// keymap_matcher::KeymapContext, -// platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel}, -// Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext, -// ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle, -// WindowContext, -// }; -// use project2::{Project, ProjectEntryId, ProjectPath}; +use crate::{ + item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, + toolbar::Toolbar, + workspace_settings::{AutosaveSetting, WorkspaceSettings}, + SplitDirection, Workspace, +}; +use anyhow::Result; +use collections::{HashMap, HashSet, VecDeque}; +use gpui::{ + AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, Model, PromptLevel, + Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, +}; +use parking_lot::Mutex; +use project2::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; -// use std::{ -// any::Any, -// cell::RefCell, -// cmp, mem, -// path::{Path, PathBuf}, -// rc::Rc, -// sync::{ -// atomic::{AtomicUsize, Ordering}, -// Arc, -// }, -// }; -// use theme2::{Theme, ThemeSettings}; -// use util::truncate_and_remove_front; +use settings2::Settings; +use std::{ + any::Any, + cmp, fmt, mem, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use ui::v_stack; +use ui::{prelude::*, Icon, IconButton, IconColor, IconElement}; +use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -69,19 +54,19 @@ pub enum SaveIntent { // #[derive(Clone, PartialEq)] // pub struct CloseItemById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq)] // pub struct CloseItemsToTheLeftById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq)] // pub struct CloseItemsToTheRightById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq, Debug, Deserialize, Default)] @@ -96,6 +81,7 @@ pub enum SaveIntent { // pub save_intent: Option, // } +// todo!() // actions!( // pane, // [ @@ -118,40 +104,40 @@ pub enum SaveIntent { // impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); -// const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; +const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; -// pub fn init(cx: &mut AppContext) { -// cx.add_action(Pane::toggle_zoom); -// cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { -// pane.activate_item(action.0, true, true, cx); -// }); -// cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { -// pane.activate_item(pane.items.len() - 1, true, true, cx); -// }); -// cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { -// pane.activate_prev_item(true, cx); -// }); -// cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { -// pane.activate_next_item(true, cx); -// }); -// cx.add_async_action(Pane::close_active_item); -// cx.add_async_action(Pane::close_inactive_items); -// cx.add_async_action(Pane::close_clean_items); -// cx.add_async_action(Pane::close_items_to_the_left); -// cx.add_async_action(Pane::close_items_to_the_right); -// cx.add_async_action(Pane::close_all_items); -// cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)); -// cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); -// cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); -// cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)); -// } +pub fn init(cx: &mut AppContext) { + // todo!() + // cx.add_action(Pane::toggle_zoom); + // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { + // pane.activate_item(action.0, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + // pane.activate_item(pane.items.len() - 1, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { + // pane.activate_prev_item(true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { + // pane.activate_next_item(true, cx); + // }); + // cx.add_async_action(Pane::close_active_item); + // cx.add_async_action(Pane::close_inactive_items); + // cx.add_async_action(Pane::close_clean_items); + // cx.add_async_action(Pane::close_items_to_the_left); + // cx.add_async_action(Pane::close_items_to_the_right); + // cx.add_async_action(Pane::close_all_items); + // cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)); +} -#[derive(Debug)] pub enum Event { AddItem { item: Box }, ActivateItem { local: bool }, Remove, - RemoveItem { item_id: usize }, + RemoveItem { item_id: EntityId }, Split(SplitDirection), ChangeItemTitle, Focus, @@ -159,36 +145,45 @@ pub enum Event { ZoomOut, } -use crate::{ - item::{ItemHandle, WeakItemHandle}, - SplitDirection, -}; -use collections::{HashMap, VecDeque}; -use gpui2::{Handle, ViewContext, WeakView}; -use project2::{Project, ProjectEntryId, ProjectPath}; -use std::{ - any::Any, - cell::RefCell, - cmp, mem, - path::PathBuf, - rc::Rc, - sync::{atomic::AtomicUsize, Arc}, -}; +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Event::AddItem { item } => f.debug_struct("AddItem").field("item", &item.id()).finish(), + Event::ActivateItem { local } => f + .debug_struct("ActivateItem") + .field("local", local) + .finish(), + Event::Remove => f.write_str("Remove"), + Event::RemoveItem { item_id } => f + .debug_struct("RemoveItem") + .field("item_id", item_id) + .finish(), + Event::Split(direction) => f + .debug_struct("Split") + .field("direction", direction) + .finish(), + Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), + Event::Focus => f.write_str("Focus"), + Event::ZoomIn => f.write_str("ZoomIn"), + Event::ZoomOut => f.write_str("ZoomOut"), + } + } +} pub struct Pane { items: Vec>, - // activation_history: Vec, - // zoomed: bool, - // active_item_index: usize, + activation_history: Vec, + zoomed: bool, + active_item_index: usize, // last_focused_view_by_item: HashMap, - // autoscroll: bool, + autoscroll: bool, nav_history: NavHistory, - // toolbar: ViewHandle, + toolbar: View, // tab_bar_context_menu: TabBarContextMenu, // tab_context_menu: ViewHandle, - // workspace: WeakViewHandle, - project: Handle, - // has_focus: bool, + workspace: WeakView, + project: Model, + has_focus: bool, // can_drop: Rc, &WindowContext) -> bool>, // can_split: bool, // render_tab_bar_buttons: Rc) -> AnyElement>, @@ -196,18 +191,18 @@ pub struct Pane { pub struct ItemNavHistory { history: NavHistory, - item: Rc, + item: Arc, } #[derive(Clone)] -pub struct NavHistory(Rc>); +pub struct NavHistory(Arc>); struct NavHistoryState { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, closed_stack: VecDeque, - paths_by_item: HashMap)>, + paths_by_item: HashMap)>, pane: WeakView, next_timestamp: Arc, } @@ -229,14 +224,14 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item: Rc, - pub data: Option>, + pub item: Arc, + pub data: Option>, pub timestamp: usize, } // pub struct DraggedItem { // pub handle: Box, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // pub enum ReorderBehavior { @@ -315,118 +310,123 @@ pub struct NavigationEntry { // .into_any_named("nav button") // } +impl EventEmitter for Pane { + type Event = Event; +} + impl Pane { - // pub fn new( - // workspace: WeakViewHandle, - // project: ModelHandle, - // next_timestamp: Arc, - // cx: &mut ViewContext, - // ) -> Self { - // let pane_view_id = cx.view_id(); - // let handle = cx.weak_handle(); - // let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)); - // context_menu.update(cx, |menu, _| { - // menu.set_position_mode(OverlayPositionMode::Local) - // }); + pub fn new( + workspace: WeakView, + project: Model, + next_timestamp: Arc, + cx: &mut ViewContext, + ) -> Self { + // todo!("context menu") + // let pane_view_id = cx.view_id(); + // let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)); + // context_menu.update(cx, |menu, _| { + // menu.set_position_mode(OverlayPositionMode::Local) + // }); - // Self { - // items: Vec::new(), - // activation_history: Vec::new(), - // zoomed: false, - // active_item_index: 0, - // last_focused_view_by_item: Default::default(), - // autoscroll: false, - // nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState { - // mode: NavigationMode::Normal, - // backward_stack: Default::default(), - // forward_stack: Default::default(), - // closed_stack: Default::default(), - // paths_by_item: Default::default(), - // pane: handle.clone(), - // next_timestamp, - // }))), - // toolbar: cx.add_view(|_| Toolbar::new()), - // tab_bar_context_menu: TabBarContextMenu { - // kind: TabBarContextMenuKind::New, - // handle: context_menu, - // }, - // tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)), - // workspace, - // project, - // has_focus: false, - // can_drop: Rc::new(|_, _| true), - // can_split: true, - // render_tab_bar_buttons: Rc::new(move |pane, cx| { - // Flex::row() - // // New menu - // .with_child(Self::render_tab_bar_button( - // 0, - // "icons/plus.svg", - // false, - // Some(("New...".into(), None)), - // cx, - // |pane, cx| pane.deploy_new_menu(cx), - // |pane, cx| { - // pane.tab_bar_context_menu - // .handle - // .update(cx, |menu, _| menu.delay_cancel()) - // }, - // pane.tab_bar_context_menu - // .handle_if_kind(TabBarContextMenuKind::New), - // )) - // .with_child(Self::render_tab_bar_button( - // 1, - // "icons/split.svg", - // false, - // Some(("Split Pane".into(), None)), - // cx, - // |pane, cx| pane.deploy_split_menu(cx), - // |pane, cx| { - // pane.tab_bar_context_menu - // .handle - // .update(cx, |menu, _| menu.delay_cancel()) - // }, - // pane.tab_bar_context_menu - // .handle_if_kind(TabBarContextMenuKind::Split), - // )) - // .with_child({ - // let icon_path; - // let tooltip_label; - // if pane.is_zoomed() { - // icon_path = "icons/minimize.svg"; - // tooltip_label = "Zoom In"; - // } else { - // icon_path = "icons/maximize.svg"; - // tooltip_label = "Zoom In"; - // } + let handle = cx.view().downgrade(); + Self { + items: Vec::new(), + activation_history: Vec::new(), + zoomed: false, + active_item_index: 0, + // last_focused_view_by_item: Default::default(), + autoscroll: false, + nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState { + mode: NavigationMode::Normal, + backward_stack: Default::default(), + forward_stack: Default::default(), + closed_stack: Default::default(), + paths_by_item: Default::default(), + pane: handle.clone(), + next_timestamp, + }))), + toolbar: cx.build_view(|_| Toolbar::new()), + // tab_bar_context_menu: TabBarContextMenu { + // kind: TabBarContextMenuKind::New, + // handle: context_menu, + // }, + // tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)), + workspace, + project, + has_focus: false, + // can_drop: Rc::new(|_, _| true), + // can_split: true, + // render_tab_bar_buttons: Rc::new(move |pane, cx| { + // Flex::row() + // // New menu + // .with_child(Self::render_tab_bar_button( + // 0, + // "icons/plus.svg", + // false, + // Some(("New...".into(), None)), + // cx, + // |pane, cx| pane.deploy_new_menu(cx), + // |pane, cx| { + // pane.tab_bar_context_menu + // .handle + // .update(cx, |menu, _| menu.delay_cancel()) + // }, + // pane.tab_bar_context_menu + // .handle_if_kind(TabBarContextMenuKind::New), + // )) + // .with_child(Self::render_tab_bar_button( + // 1, + // "icons/split.svg", + // false, + // Some(("Split Pane".into(), None)), + // cx, + // |pane, cx| pane.deploy_split_menu(cx), + // |pane, cx| { + // pane.tab_bar_context_menu + // .handle + // .update(cx, |menu, _| menu.delay_cancel()) + // }, + // pane.tab_bar_context_menu + // .handle_if_kind(TabBarContextMenuKind::Split), + // )) + // .with_child({ + // let icon_path; + // let tooltip_label; + // if pane.is_zoomed() { + // icon_path = "icons/minimize.svg"; + // tooltip_label = "Zoom In"; + // } else { + // icon_path = "icons/maximize.svg"; + // tooltip_label = "Zoom In"; + // } - // Pane::render_tab_bar_button( - // 2, - // icon_path, - // pane.is_zoomed(), - // Some((tooltip_label, Some(Box::new(ToggleZoom)))), - // cx, - // move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - // move |_, _| {}, - // None, - // ) - // }) - // .into_any() - // }), - // } - // } + // Pane::render_tab_bar_button( + // 2, + // icon_path, + // pane.is_zoomed(), + // Some((tooltip_label, Some(Box::new(ToggleZoom)))), + // cx, + // move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + // move |_, _| {}, + // None, + // ) + // }) + // .into_any() + // }), + } + } - // pub(crate) fn workspace(&self) -> &WeakViewHandle { - // &self.workspace - // } + pub(crate) fn workspace(&self) -> &WeakView { + &self.workspace + } - // pub fn has_focus(&self) -> bool { - // self.has_focus - // } + pub fn has_focus(&self) -> bool { + self.has_focus + } - // pub fn active_item_index(&self) -> usize { - // self.active_item_index - // } + pub fn active_item_index(&self) -> usize { + self.active_item_index + } // pub fn on_can_drop(&mut self, can_drop: F) // where @@ -455,40 +455,40 @@ impl Pane { // cx.notify(); // } - // pub fn nav_history_for_item(&self, item: &ViewHandle) -> ItemNavHistory { - // ItemNavHistory { - // history: self.nav_history.clone(), - // item: Rc::new(item.downgrade()), - // } - // } + pub fn nav_history_for_item(&self, item: &View) -> ItemNavHistory { + ItemNavHistory { + history: self.nav_history.clone(), + item: Arc::new(item.downgrade()), + } + } - // pub fn nav_history(&self) -> &NavHistory { - // &self.nav_history - // } + pub fn nav_history(&self) -> &NavHistory { + &self.nav_history + } - // pub fn nav_history_mut(&mut self) -> &mut NavHistory { - // &mut self.nav_history - // } + pub fn nav_history_mut(&mut self) -> &mut NavHistory { + &mut self.nav_history + } - // pub fn disable_history(&mut self) { - // self.nav_history.disable(); - // } + pub fn disable_history(&mut self) { + self.nav_history.disable(); + } - // pub fn enable_history(&mut self) { - // self.nav_history.enable(); - // } + pub fn enable_history(&mut self) { + self.nav_history.enable(); + } - // pub fn can_navigate_backward(&self) -> bool { - // !self.nav_history.0.borrow().backward_stack.is_empty() - // } + pub fn can_navigate_backward(&self) -> bool { + !self.nav_history.0.lock().backward_stack.is_empty() + } - // pub fn can_navigate_forward(&self) -> bool { - // !self.nav_history.0.borrow().forward_stack.is_empty() - // } + pub fn can_navigate_forward(&self) -> bool { + !self.nav_history.0.lock().forward_stack.is_empty() + } - // fn history_updated(&mut self, cx: &mut ViewContext) { - // self.toolbar.update(cx, |_, cx| cx.notify()); - // } + fn history_updated(&mut self, cx: &mut ViewContext) { + self.toolbar.update(cx, |_, cx| cx.notify()); + } pub(crate) fn open_item( &mut self, @@ -532,7 +532,7 @@ impl Pane { let abs_path = project.absolute_path(&project_path, cx); self.nav_history .0 - .borrow_mut() + .lock() .paths_by_item .insert(item.id(), (project_path, abs_path)); } @@ -615,13 +615,13 @@ impl Pane { cx.emit(Event::AddItem { item }); } - // pub fn items_len(&self) -> usize { - // self.items.len() - // } + pub fn items_len(&self) -> usize { + self.items.len() + } - // pub fn items(&self) -> impl Iterator> + DoubleEndedIterator { - // self.items.iter() - // } + pub fn items(&self) -> impl Iterator> + DoubleEndedIterator { + self.items.iter() + } // pub fn items_of_type(&self) -> impl '_ + Iterator> { // self.items @@ -629,9 +629,9 @@ impl Pane { // .filter_map(|item| item.as_any().clone().downcast()) // } - // pub fn active_item(&self) -> Option> { - // self.items.get(self.active_item_index).cloned() - // } + pub fn active_item(&self) -> Option> { + self.items.get(self.active_item_index).cloned() + } // pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { // self.items @@ -653,9 +653,9 @@ impl Pane { // }) // } - // pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - // self.items.iter().position(|i| i.id() == item.id()) - // } + pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { + self.items.iter().position(|i| i.id() == item.id()) + } // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { // // Potentially warn the user of the new keybinding @@ -751,448 +751,445 @@ impl Pane { // )) // } - // pub fn close_item_by_id( - // &mut self, - // item_id_to_close: usize, - // save_intent: SaveIntent, - // cx: &mut ViewContext, - // ) -> Task> { - // self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) + pub fn close_item_by_id( + &mut self, + item_id_to_close: EntityId, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) + } + + // pub fn close_inactive_items( + // &mut self, + // _: &CloseInactiveItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; // } - // pub fn close_inactive_items( - // &mut self, - // _: &CloseInactiveItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_id != active_item_id + // })) + // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_id != active_item_id - // })) + // pub fn close_clean_items( + // &mut self, + // _: &CloseCleanItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // let item_ids: Vec<_> = self + // .items() + // .filter(|item| !item.is_dirty(cx)) + // .map(|item| item.id()) + // .collect(); + // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // })) + // } + + // pub fn close_items_to_the_left( + // &mut self, + // _: &CloseItemsToTheLeft, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; + // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items_to_the_left_by_id(active_item_id, cx)) + // } + + // pub fn close_items_to_the_left_by_id( + // &mut self, + // item_id: usize, + // cx: &mut ViewContext, + // ) -> Task> { + // let item_ids: Vec<_> = self + // .items() + // .take_while(|item| item.id() != item_id) + // .map(|item| item.id()) + // .collect(); + // self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // }) + // } + + // pub fn close_items_to_the_right( + // &mut self, + // _: &CloseItemsToTheRight, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; + // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items_to_the_right_by_id(active_item_id, cx)) + // } + + // pub fn close_items_to_the_right_by_id( + // &mut self, + // item_id: usize, + // cx: &mut ViewContext, + // ) -> Task> { + // let item_ids: Vec<_> = self + // .items() + // .rev() + // .take_while(|item| item.id() != item_id) + // .map(|item| item.id()) + // .collect(); + // self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // }) + // } + + // pub fn close_all_items( + // &mut self, + // action: &CloseAllItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; // } - // pub fn close_clean_items( - // &mut self, - // _: &CloseCleanItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // let item_ids: Vec<_> = self - // .items() - // .filter(|item| !item.is_dirty(cx)) - // .map(|item| item.id()) - // .collect(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // })) - // } + // Some( + // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { + // true + // }), + // ) + // } - // pub fn close_items_to_the_left( - // &mut self, - // _: &CloseItemsToTheLeft, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_left_by_id(active_item_id, cx)) - // } + pub(super) fn file_names_for_prompt( + items: &mut dyn Iterator>, + all_dirty_items: usize, + cx: &AppContext, + ) -> String { + /// Quantity of item paths displayed in prompt prior to cutoff.. + const FILE_NAMES_CUTOFF_POINT: usize = 10; + let mut file_names: Vec<_> = items + .filter_map(|item| { + item.project_path(cx).and_then(|project_path| { + project_path + .path + .file_name() + .and_then(|name| name.to_str().map(ToOwned::to_owned)) + }) + }) + .take(FILE_NAMES_CUTOFF_POINT) + .collect(); + let should_display_followup_text = + all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; + if should_display_followup_text { + let not_shown_files = all_dirty_items - file_names.len(); + if not_shown_files == 1 { + file_names.push(".. 1 file not shown".into()); + } else { + file_names.push(format!(".. {} files not shown", not_shown_files).into()); + } + } + let file_names = file_names.join("\n"); + format!( + "Do you want to save changes to the following {} files?\n{file_names}", + all_dirty_items + ) + } - // pub fn close_items_to_the_left_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + pub fn close_items( + &mut self, + cx: &mut ViewContext, + mut save_intent: SaveIntent, + should_close: impl 'static + Fn(EntityId) -> bool, + ) -> Task> { + // Find the items to close. + let mut items_to_close = Vec::new(); + let mut dirty_items = Vec::new(); + for item in &self.items { + if should_close(item.id()) { + items_to_close.push(item.boxed_clone()); + if item.is_dirty(cx) { + dirty_items.push(item.boxed_clone()); + } + } + } - // pub fn close_items_to_the_right( - // &mut self, - // _: &CloseItemsToTheRight, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_right_by_id(active_item_id, cx)) - // } + // If a buffer is open both in a singleton editor and in a multibuffer, make sure + // to focus the singleton buffer when prompting to save that buffer, as opposed + // to focusing the multibuffer, because this gives the user a more clear idea + // of what content they would be saving. + items_to_close.sort_by_key(|item| !item.is_singleton(cx)); - // pub fn close_items_to_the_right_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .rev() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + let workspace = self.workspace.clone(); + cx.spawn(|pane, mut cx| async move { + if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let answer = pane.update(&mut cx, |_, cx| { + let prompt = + Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.await { + Ok(0) => save_intent = SaveIntent::SaveAll, + Ok(1) => save_intent = SaveIntent::Skip, + _ => {} + } + } + let mut saved_project_items_ids = HashSet::default(); + for item in items_to_close.clone() { + // Find the item's current index and its set of project item models. Avoid + // storing these in advance, in case they have changed since this task + // was started. + let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| { + (pane.index_for_item(&*item), item.project_item_model_ids(cx)) + })?; + let item_ix = if let Some(ix) = item_ix { + ix + } else { + continue; + }; - // pub fn close_all_items( - // &mut self, - // action: &CloseAllItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + // Check if this view has any project items that are not open anywhere else + // in the workspace, AND that the user has not already been prompted to save. + // If there are any such project entries, prompt the user to save this item. + let project = workspace.update(&mut cx, |workspace, cx| { + for item in workspace.items(cx) { + if !items_to_close + .iter() + .any(|item_to_close| item_to_close.id() == item.id()) + { + let other_project_item_ids = item.project_item_model_ids(cx); + project_item_ids.retain(|id| !other_project_item_ids.contains(id)); + } + } + workspace.project().clone() + })?; + let should_save = project_item_ids + .iter() + .any(|id| saved_project_items_ids.insert(*id)); - // Some( - // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { - // true - // }), - // ) - // } + if should_save + && !Self::save_item( + project.clone(), + &pane, + item_ix, + &*item, + save_intent, + &mut cx, + ) + .await? + { + break; + } - // pub(super) fn file_names_for_prompt( - // items: &mut dyn Iterator>, - // all_dirty_items: usize, - // cx: &AppContext, - // ) -> String { - // /// Quantity of item paths displayed in prompt prior to cutoff.. - // const FILE_NAMES_CUTOFF_POINT: usize = 10; - // let mut file_names: Vec<_> = items - // .filter_map(|item| { - // item.project_path(cx).and_then(|project_path| { - // project_path - // .path - // .file_name() - // .and_then(|name| name.to_str().map(ToOwned::to_owned)) - // }) - // }) - // .take(FILE_NAMES_CUTOFF_POINT) - // .collect(); - // let should_display_followup_text = - // all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; - // if should_display_followup_text { - // let not_shown_files = all_dirty_items - file_names.len(); - // if not_shown_files == 1 { - // file_names.push(".. 1 file not shown".into()); - // } else { - // file_names.push(format!(".. {} files not shown", not_shown_files).into()); - // } - // } - // let file_names = file_names.join("\n"); - // format!( - // "Do you want to save changes to the following {} files?\n{file_names}", - // all_dirty_items - // ) - // } + // Remove the item from the pane. + pane.update(&mut cx, |pane, cx| { + if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { + pane.remove_item(item_ix, false, cx); + } + })?; + } - // pub fn close_items( - // &mut self, - // cx: &mut ViewContext, - // mut save_intent: SaveIntent, - // should_close: impl 'static + Fn(usize) -> bool, - // ) -> Task> { - // // Find the items to close. - // let mut items_to_close = Vec::new(); - // let mut dirty_items = Vec::new(); - // for item in &self.items { - // if should_close(item.id()) { - // items_to_close.push(item.boxed_clone()); - // if item.is_dirty(cx) { - // dirty_items.push(item.boxed_clone()); - // } - // } - // } + pane.update(&mut cx, |_, cx| cx.notify())?; + Ok(()) + }) + } - // // If a buffer is open both in a singleton editor and in a multibuffer, make sure - // // to focus the singleton buffer when prompting to save that buffer, as opposed - // // to focusing the multibuffer, because this gives the user a more clear idea - // // of what content they would be saving. - // items_to_close.sort_by_key(|item| !item.is_singleton(cx)); + pub fn remove_item( + &mut self, + item_index: usize, + activate_pane: bool, + cx: &mut ViewContext, + ) { + self.activation_history + .retain(|&history_entry| history_entry != self.items[item_index].id()); - // let workspace = self.workspace.clone(); - // cx.spawn(|pane, mut cx| async move { - // if save_intent == SaveIntent::Close && dirty_items.len() > 1 { - // let mut answer = pane.update(&mut cx, |_, cx| { - // let prompt = - // Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx); - // cx.prompt( - // PromptLevel::Warning, - // &prompt, - // &["Save all", "Discard all", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => save_intent = SaveIntent::SaveAll, - // Some(1) => save_intent = SaveIntent::Skip, - // _ => {} - // } - // } - // let mut saved_project_items_ids = HashSet::default(); - // for item in items_to_close.clone() { - // // Find the item's current index and its set of project item models. Avoid - // // storing these in advance, in case they have changed since this task - // // was started. - // let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| { - // (pane.index_for_item(&*item), item.project_item_model_ids(cx)) - // })?; - // let item_ix = if let Some(ix) = item_ix { - // ix - // } else { - // continue; - // }; + if item_index == self.active_item_index { + let index_to_activate = self + .activation_history + .pop() + .and_then(|last_activated_item| { + self.items.iter().enumerate().find_map(|(index, item)| { + (item.id() == last_activated_item).then_some(index) + }) + }) + // We didn't have a valid activation history entry, so fallback + // to activating the item to the left + .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); - // // Check if this view has any project items that are not open anywhere else - // // in the workspace, AND that the user has not already been prompted to save. - // // If there are any such project entries, prompt the user to save this item. - // let project = workspace.read_with(&cx, |workspace, cx| { - // for item in workspace.items(cx) { - // if !items_to_close - // .iter() - // .any(|item_to_close| item_to_close.id() == item.id()) - // { - // let other_project_item_ids = item.project_item_model_ids(cx); - // project_item_ids.retain(|id| !other_project_item_ids.contains(id)); - // } - // } - // workspace.project().clone() - // })?; - // let should_save = project_item_ids - // .iter() - // .any(|id| saved_project_items_ids.insert(*id)); + let should_activate = activate_pane || self.has_focus; + self.activate_item(index_to_activate, should_activate, should_activate, cx); + } - // if should_save - // && !Self::save_item( - // project.clone(), - // &pane, - // item_ix, - // &*item, - // save_intent, - // &mut cx, - // ) - // .await? - // { - // break; - // } + let item = self.items.remove(item_index); - // // Remove the item from the pane. - // pane.update(&mut cx, |pane, cx| { - // if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { - // pane.remove_item(item_ix, false, cx); - // } - // })?; - // } + cx.emit(Event::RemoveItem { item_id: item.id() }); + if self.items.is_empty() { + item.deactivated(cx); + self.update_toolbar(cx); + cx.emit(Event::Remove); + } - // pane.update(&mut cx, |_, cx| cx.notify())?; - // Ok(()) - // }) - // } + if item_index < self.active_item_index { + self.active_item_index -= 1; + } - // pub fn remove_item( - // &mut self, - // item_index: usize, - // activate_pane: bool, - // cx: &mut ViewContext, - // ) { - // self.activation_history - // .retain(|&history_entry| history_entry != self.items[item_index].id()); + self.nav_history.set_mode(NavigationMode::ClosingItem); + item.deactivated(cx); + self.nav_history.set_mode(NavigationMode::Normal); - // if item_index == self.active_item_index { - // let index_to_activate = self - // .activation_history - // .pop() - // .and_then(|last_activated_item| { - // self.items.iter().enumerate().find_map(|(index, item)| { - // (item.id() == last_activated_item).then_some(index) - // }) - // }) - // // We didn't have a valid activation history entry, so fallback - // // to activating the item to the left - // .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); + if let Some(path) = item.project_path(cx) { + let abs_path = self + .nav_history + .0 + .lock() + .paths_by_item + .get(&item.id()) + .and_then(|(_, abs_path)| abs_path.clone()); - // let should_activate = activate_pane || self.has_focus; - // self.activate_item(index_to_activate, should_activate, should_activate, cx); - // } + self.nav_history + .0 + .lock() + .paths_by_item + .insert(item.id(), (path, abs_path)); + } else { + self.nav_history.0.lock().paths_by_item.remove(&item.id()); + } - // let item = self.items.remove(item_index); + if self.items.is_empty() && self.zoomed { + cx.emit(Event::ZoomOut); + } - // cx.emit(Event::RemoveItem { item_id: item.id() }); - // if self.items.is_empty() { - // item.deactivated(cx); - // self.update_toolbar(cx); - // cx.emit(Event::Remove); - // } + cx.notify(); + } - // if item_index < self.active_item_index { - // self.active_item_index -= 1; - // } + pub async fn save_item( + project: Model, + pane: &WeakView, + item_ix: usize, + item: &dyn ItemHandle, + save_intent: SaveIntent, + cx: &mut AsyncWindowContext, + ) -> Result { + const CONFLICT_MESSAGE: &str = + "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - // self.nav_history.set_mode(NavigationMode::ClosingItem); - // item.deactivated(cx); - // self.nav_history.set_mode(NavigationMode::Normal); + if save_intent == SaveIntent::Skip { + return Ok(true); + } - // if let Some(path) = item.project_path(cx) { - // let abs_path = self - // .nav_history - // .0 - // .borrow() - // .paths_by_item - // .get(&item.id()) - // .and_then(|(_, abs_path)| abs_path.clone()); + let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|_, cx| { + ( + item.has_conflict(cx), + item.is_dirty(cx), + item.can_save(cx), + item.is_singleton(cx), + ) + })?; - // self.nav_history - // .0 - // .borrow_mut() - // .paths_by_item - // .insert(item.id(), (path, abs_path)); - // } else { - // self.nav_history - // .0 - // .borrow_mut() - // .paths_by_item - // .remove(&item.id()); - // } + // when saving a single buffer, we ignore whether or not it's dirty. + if save_intent == SaveIntent::Save { + is_dirty = true; + } - // if self.items.is_empty() && self.zoomed { - // cx.emit(Event::ZoomOut); - // } + if save_intent == SaveIntent::SaveAs { + is_dirty = true; + has_conflict = false; + can_save = false; + } - // cx.notify(); - // } + if save_intent == SaveIntent::Overwrite { + has_conflict = false; + } - // pub async fn save_item( - // project: ModelHandle, - // pane: &WeakViewHandle, - // item_ix: usize, - // item: &dyn ItemHandle, - // save_intent: SaveIntent, - // cx: &mut AsyncAppContext, - // ) -> Result { - // const CONFLICT_MESSAGE: &str = - // "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + if has_conflict && can_save { + let answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Discard", "Cancel"], + ) + })?; + match answer.await { + Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, + Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, + _ => return Ok(false), + } + } else if is_dirty && (can_save || can_save_as) { + if save_intent == SaveIntent::Close { + let will_autosave = cx.update(|_, cx| { + matches!( + WorkspaceSettings::get_global(cx).autosave, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange + ) && Self::can_autosave_item(&*item, cx) + })?; + if !will_autosave { + let answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + let prompt = dirty_message_for(item.project_path(cx)); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save", "Don't Save", "Cancel"], + ) + })?; + match answer.await { + Ok(0) => {} + Ok(1) => return Ok(true), // Don't save this file + _ => return Ok(false), // Cancel + } + } + } - // if save_intent == SaveIntent::Skip { - // return Ok(true); - // } + if can_save { + pane.update(cx, |_, cx| item.save(project, cx))?.await?; + } else if can_save_as { + let start_abs_path = project + .update(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next()?; + Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) + })? + .unwrap_or_else(|| Path::new("").into()); - // let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| { - // ( - // item.has_conflict(cx), - // item.is_dirty(cx), - // item.can_save(cx), - // item.is_singleton(cx), - // ) - // }); + let abs_path = cx.update(|_, cx| cx.prompt_for_new_path(&start_abs_path))?; + if let Some(abs_path) = abs_path.await.ok().flatten() { + pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? + .await?; + } else { + return Ok(false); + } + } + } + Ok(true) + } - // // when saving a single buffer, we ignore whether or not it's dirty. - // if save_intent == SaveIntent::Save { - // is_dirty = true; - // } + fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool { + let is_deleted = item.project_entry_ids(cx).is_empty(); + item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted + } - // if save_intent == SaveIntent::SaveAs { - // is_dirty = true; - // has_conflict = false; - // can_save = false; - // } + pub fn autosave_item( + item: &dyn ItemHandle, + project: Model, + cx: &mut WindowContext, + ) -> Task> { + if Self::can_autosave_item(item, cx) { + item.save(project, cx) + } else { + Task::ready(Ok(())) + } + } - // if save_intent == SaveIntent::Overwrite { - // has_conflict = false; - // } - - // if has_conflict && can_save { - // let mut answer = pane.update(cx, |pane, cx| { - // pane.activate_item(item_ix, true, true, cx); - // cx.prompt( - // PromptLevel::Warning, - // CONFLICT_MESSAGE, - // &["Overwrite", "Discard", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, - // Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, - // _ => return Ok(false), - // } - // } else if is_dirty && (can_save || can_save_as) { - // if save_intent == SaveIntent::Close { - // let will_autosave = cx.read(|cx| { - // matches!( - // settings::get::(cx).autosave, - // AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - // ) && Self::can_autosave_item(&*item, cx) - // }); - // if !will_autosave { - // let mut answer = pane.update(cx, |pane, cx| { - // pane.activate_item(item_ix, true, true, cx); - // let prompt = dirty_message_for(item.project_path(cx)); - // cx.prompt( - // PromptLevel::Warning, - // &prompt, - // &["Save", "Don't Save", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => {} - // Some(1) => return Ok(true), // Don't save his file - // _ => return Ok(false), // Cancel - // } - // } - // } - - // if can_save { - // pane.update(cx, |_, cx| item.save(project, cx))?.await?; - // } else if can_save_as { - // let start_abs_path = project - // .read_with(cx, |project, cx| { - // let worktree = project.visible_worktrees(cx).next()?; - // Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - // }) - // .unwrap_or_else(|| Path::new("").into()); - - // let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); - // if let Some(abs_path) = abs_path.next().await.flatten() { - // pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? - // .await?; - // } else { - // return Ok(false); - // } - // } - // } - // Ok(true) - // } - - // fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool { - // let is_deleted = item.project_entry_ids(cx).is_empty(); - // item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted - // } - - // pub fn autosave_item( - // item: &dyn ItemHandle, - // project: ModelHandle, - // cx: &mut WindowContext, - // ) -> Task> { - // if Self::can_autosave_item(item, cx) { - // item.save(project, cx) - // } else { - // Task::ready(Ok(())) - // } - // } - - // pub fn focus_active_item(&mut self, cx: &mut ViewContext) { - // if let Some(active_item) = self.active_item() { - // cx.focus(active_item.as_any()); - // } - // } + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { + todo!(); + // if let Some(active_item) = self.active_item() { + // cx.focus(active_item.as_any()); + // } + } // pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext) { // cx.emit(Event::Split(direction)); @@ -1317,38 +1314,178 @@ impl Pane { // }); // } - // pub fn toolbar(&self) -> &ViewHandle { - // &self.toolbar - // } + pub fn toolbar(&self) -> &View { + &self.toolbar + } - // pub fn handle_deleted_project_item( - // &mut self, - // entry_id: ProjectEntryId, - // cx: &mut ViewContext, - // ) -> Option<()> { - // let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { - // if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { - // Some((i, item.id())) - // } else { - // None - // } - // })?; + pub fn handle_deleted_project_item( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) -> Option<()> { + let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some((i, item.id())) + } else { + None + } + })?; - // self.remove_item(item_index_to_delete, false, cx); - // self.nav_history.remove_item(item_id); + self.remove_item(item_index_to_delete, false, cx); + self.nav_history.remove_item(item_id); - // Some(()) - // } + Some(()) + } - // fn update_toolbar(&mut self, cx: &mut ViewContext) { - // let active_item = self - // .items - // .get(self.active_item_index) - // .map(|item| item.as_ref()); - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.set_active_item(active_item, cx); - // }); - // } + fn update_toolbar(&mut self, cx: &mut ViewContext) { + let active_item = self + .items + .get(self.active_item_index) + .map(|item| item.as_ref()); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(active_item, cx); + }); + } + + fn render_tab( + &self, + ix: usize, + item: &Box, + detail: usize, + cx: &mut ViewContext<'_, Pane>, + ) -> impl Component { + let label = item.tab_content(Some(detail), cx); + let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + + let (tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { + false => ( + cx.theme().colors().tab_inactive, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ), + true => ( + cx.theme().colors().tab_active, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, + ), + }; + + let close_right = ItemSettings::get_global(cx).close_position.right(); + + div() + .id(item.id()) + // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) + // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) + // .on_drop(|_view, state: View, cx| { + // eprintln!("{:?}", state.read(cx)); + // }) + .px_2() + .py_0p5() + .flex() + .items_center() + .justify_center() + .bg(tab_bg) + .hover(|h| h.bg(tab_hover_bg)) + .active(|a| a.bg(tab_active_bg)) + .child( + div() + .px_1() + .flex() + .items_center() + .gap_1p5() + .children(if item.has_conflict(cx) { + Some( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(IconColor::Warning), + ) + } else if item.is_dirty(cx) { + Some( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(IconColor::Info), + ) + } else { + None + }) + .children(if !close_right { + Some(close_icon()) + } else { + None + }) + .child(label) + .children(if close_right { + Some(close_icon()) + } else { + None + }), + ) + } + + fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl Component { + div() + .group("tab_bar") + .id("tab_bar") + .w_full() + .flex() + .bg(cx.theme().colors().tab_bar) + // Left Side + .child( + div() + .relative() + .px_1() + .flex() + .flex_none() + .gap_2() + // Nav Buttons + .child( + div() + .right_0() + .flex() + .items_center() + .gap_px() + .child(IconButton::new("navigate_backward", Icon::ArrowLeft).state( + InteractionState::Enabled.if_enabled(self.can_navigate_backward()), + )) + .child(IconButton::new("navigate_forward", Icon::ArrowRight).state( + InteractionState::Enabled.if_enabled(self.can_navigate_forward()), + )), + ), + ) + .child( + div().w_0().flex_1().h_full().child( + div().id("tabs").flex().overflow_x_scroll().children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + ), + ), + ) + // Right Side + .child( + div() + // We only use absolute here since we don't + // have opacity or `hidden()` yet + .absolute() + .neg_top_7() + .px_1() + .flex() + .flex_none() + .gap_2() + .group_hover("tab_bar", |this| this.top_0()) + // Nav Buttons + .child( + div() + .flex() + .items_center() + .gap_px() + .child(IconButton::new("plus", Icon::Plus)) + .child(IconButton::new("split", Icon::Split)), + ), + ) + } // fn render_tabs(&mut self, cx: &mut ViewContext) -> impl Element { // let theme = theme::current(cx).clone(); @@ -1505,46 +1642,46 @@ impl Pane { // row // } - // fn tab_details(&self, cx: &AppContext) -> Vec { - // let mut tab_details = (0..self.items.len()).map(|_| 0).collect::>(); + fn tab_details(&self, cx: &AppContext) -> Vec { + let mut tab_details = self.items.iter().map(|_| 0).collect::>(); - // let mut tab_descriptions = HashMap::default(); - // let mut done = false; - // while !done { - // done = true; + let mut tab_descriptions = HashMap::default(); + let mut done = false; + while !done { + done = true; - // // Store item indices by their tab description. - // for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() { - // if let Some(description) = item.tab_description(*detail, cx) { - // if *detail == 0 - // || Some(&description) != item.tab_description(detail - 1, cx).as_ref() - // { - // tab_descriptions - // .entry(description) - // .or_insert(Vec::new()) - // .push(ix); - // } - // } - // } + // Store item indices by their tab description. + for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() { + if let Some(description) = item.tab_description(*detail, cx) { + if *detail == 0 + || Some(&description) != item.tab_description(detail - 1, cx).as_ref() + { + tab_descriptions + .entry(description) + .or_insert(Vec::new()) + .push(ix); + } + } + } - // // If two or more items have the same tab description, increase their level - // // of detail and try again. - // for (_, item_ixs) in tab_descriptions.drain() { - // if item_ixs.len() > 1 { - // done = false; - // for ix in item_ixs { - // tab_details[ix] += 1; - // } - // } - // } - // } + // If two or more items have the same tab description, increase eir level + // of detail and try again. + for (_, item_ixs) in tab_descriptions.drain() { + if item_ixs.len() > 1 { + done = false; + for ix in item_ixs { + tab_details[ix] += 1; + } + } + } + } - // tab_details - // } + tab_details + } // fn render_tab( // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // detail: Option, // hovered: bool, @@ -1557,7 +1694,7 @@ impl Pane { // fn render_dragged_tab( // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // detail: Option, // hovered: bool, @@ -1571,7 +1708,7 @@ impl Pane { // fn render_tab_with_title( // title: AnyElement, // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // hovered: bool, // tab_style: &theme::Tab, @@ -1728,407 +1865,415 @@ impl Pane { // .into_any() // } - // pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - // self.zoomed = zoomed; - // cx.notify(); - // } - - // pub fn is_zoomed(&self) -> bool { - // self.zoomed - // } - // } - - // impl Entity for Pane { - // type Event = Event; - // } - - // impl View for Pane { - // fn ui_name() -> &'static str { - // "Pane" - // } - - // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - // enum MouseNavigationHandler {} - - // MouseEventHandler::new::(0, cx, |_, cx| { - // let active_item_index = self.active_item_index; - - // if let Some(active_item) = self.active_item() { - // Flex::column() - // .with_child({ - // let theme = theme::current(cx).clone(); - - // let mut stack = Stack::new(); - - // enum TabBarEventHandler {} - // stack.add_child( - // MouseEventHandler::new::(0, cx, |_, _| { - // Empty::new() - // .contained() - // .with_style(theme.workspace.tab_bar.container) - // }) - // .on_down( - // MouseButton::Left, - // move |_, this, cx| { - // this.activate_item(active_item_index, true, true, cx); - // }, - // ), - // ); - // let tooltip_style = theme.tooltip.clone(); - // let tab_bar_theme = theme.workspace.tab_bar.clone(); - - // let nav_button_height = tab_bar_theme.height; - // let button_style = tab_bar_theme.nav_button; - // let border_for_nav_buttons = tab_bar_theme - // .tab_style(false, false) - // .container - // .border - // .clone(); - - // let mut tab_row = Flex::row() - // .with_child(nav_button( - // "icons/arrow_left.svg", - // button_style.clone(), - // nav_button_height, - // tooltip_style.clone(), - // self.can_navigate_backward(), - // { - // move |pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace - // .go_back(pane, cx) - // .detach_and_log_err(cx) - // }) - // }) - // } - // } - // }, - // super::GoBack, - // "Go Back", - // cx, - // )) - // .with_child( - // nav_button( - // "icons/arrow_right.svg", - // button_style.clone(), - // nav_button_height, - // tooltip_style, - // self.can_navigate_forward(), - // { - // move |pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace - // .go_forward(pane, cx) - // .detach_and_log_err(cx) - // }) - // }) - // } - // } - // }, - // super::GoForward, - // "Go Forward", - // cx, - // ) - // .contained() - // .with_border(border_for_nav_buttons), - // ) - // .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); - - // if self.has_focus { - // let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); - // tab_row.add_child( - // (render_tab_bar_buttons)(self, cx) - // .contained() - // .with_style(theme.workspace.tab_bar.pane_button_container) - // .flex(1., false) - // .into_any(), - // ) - // } - - // stack.add_child(tab_row); - // stack - // .constrained() - // .with_height(theme.workspace.tab_bar.height) - // .flex(1., false) - // .into_any_named("tab bar") - // }) - // .with_child({ - // enum PaneContentTabDropTarget {} - // dragged_item_receiver::( - // self, - // 0, - // self.active_item_index + 1, - // !self.can_split, - // if self.can_split { Some(100.) } else { None }, - // cx, - // { - // let toolbar = self.toolbar.clone(); - // let toolbar_hidden = toolbar.read(cx).hidden(); - // move |_, cx| { - // Flex::column() - // .with_children( - // (!toolbar_hidden) - // .then(|| ChildView::new(&toolbar, cx).expanded()), - // ) - // .with_child( - // ChildView::new(active_item.as_any(), cx).flex(1., true), - // ) - // } - // }, - // ) - // .flex(1., true) - // }) - // .with_child(ChildView::new(&self.tab_context_menu, cx)) - // .into_any() - // } else { - // enum EmptyPane {} - // let theme = theme::current(cx).clone(); - - // dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { - // self.render_blank_pane(&theme, cx) - // }) - // .on_down(MouseButton::Left, |_, _, cx| { - // cx.focus_parent(); - // }) - // .into_any() - // } - // }) - // .on_down( - // MouseButton::Navigate(NavigationDirection::Back), - // move |_, pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace.go_back(pane, cx).detach_and_log_err(cx) - // }) - // }) - // } - // }, - // ) - // .on_down(MouseButton::Navigate(NavigationDirection::Forward), { - // move |_, pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace.go_forward(pane, cx).detach_and_log_err(cx) - // }) - // }) - // } - // } - // }) - // .into_any_named("pane") - // } - - // fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { - // if !self.has_focus { - // self.has_focus = true; - // cx.emit(Event::Focus); - // cx.notify(); - // } - - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.focus_changed(true, cx); - // }); - - // if let Some(active_item) = self.active_item() { - // if cx.is_self_focused() { - // // Pane was focused directly. We need to either focus a view inside the active item, - // // or focus the active item itself - // if let Some(weak_last_focused_view) = - // self.last_focused_view_by_item.get(&active_item.id()) - // { - // if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) { - // cx.focus(&last_focused_view); - // return; - // } else { - // self.last_focused_view_by_item.remove(&active_item.id()); - // } - // } - - // cx.focus(active_item.as_any()); - // } else if focused != self.tab_bar_context_menu.handle { - // self.last_focused_view_by_item - // .insert(active_item.id(), focused.downgrade()); - // } - // } - // } - - // fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - // self.has_focus = false; - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.focus_changed(false, cx); - // }); - // cx.notify(); - // } - - // fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { - // Self::reset_to_default_keymap_context(keymap); - // } - // } - - // impl ItemNavHistory { - // pub fn push(&mut self, data: Option, cx: &mut WindowContext) { - // self.history.push(data, self.item.clone(), cx); - // } - - // pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { - // self.history.pop(NavigationMode::GoingBack, cx) - // } - - // pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { - // self.history.pop(NavigationMode::GoingForward, cx) - // } - // } - - // impl NavHistory { - // pub fn for_each_entry( - // &self, - // cx: &AppContext, - // mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), - // ) { - // let borrowed_history = self.0.borrow(); - // borrowed_history - // .forward_stack - // .iter() - // .chain(borrowed_history.backward_stack.iter()) - // .chain(borrowed_history.closed_stack.iter()) - // .for_each(|entry| { - // if let Some(project_and_abs_path) = - // borrowed_history.paths_by_item.get(&entry.item.id()) - // { - // f(entry, project_and_abs_path.clone()); - // } else if let Some(item) = entry.item.upgrade(cx) { - // if let Some(path) = item.project_path(cx) { - // f(entry, (path, None)); - // } - // } - // }) - // } - - // pub fn set_mode(&mut self, mode: NavigationMode) { - // self.0.borrow_mut().mode = mode; - // } - - pub fn mode(&self) -> NavigationMode { - self.0.borrow().mode + pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); } - // pub fn disable(&mut self) { - // self.0.borrow_mut().mode = NavigationMode::Disabled; - // } - - // pub fn enable(&mut self) { - // self.0.borrow_mut().mode = NavigationMode::Normal; - // } - - // pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { - // let mut state = self.0.borrow_mut(); - // let entry = match mode { - // NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { - // return None - // } - // NavigationMode::GoingBack => &mut state.backward_stack, - // NavigationMode::GoingForward => &mut state.forward_stack, - // NavigationMode::ReopeningClosedItem => &mut state.closed_stack, - // } - // .pop_back(); - // if entry.is_some() { - // state.did_update(cx); - // } - // entry - // } - - // pub fn push( - // &mut self, - // data: Option, - // item: Rc, - // cx: &mut WindowContext, - // ) { - // let state = &mut *self.0.borrow_mut(); - // match state.mode { - // NavigationMode::Disabled => {} - // NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { - // if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.backward_stack.pop_front(); - // } - // state.backward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // state.forward_stack.clear(); - // } - // NavigationMode::GoingBack => { - // if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.forward_stack.pop_front(); - // } - // state.forward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // NavigationMode::GoingForward => { - // if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.backward_stack.pop_front(); - // } - // state.backward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // NavigationMode::ClosingItem => { - // if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.closed_stack.pop_front(); - // } - // state.closed_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // } - // state.did_update(cx); - // } - - // pub fn remove_item(&mut self, item_id: usize) { - // let mut state = self.0.borrow_mut(); - // state.paths_by_item.remove(&item_id); - // state - // .backward_stack - // .retain(|entry| entry.item.id() != item_id); - // state - // .forward_stack - // .retain(|entry| entry.item.id() != item_id); - // state - // .closed_stack - // .retain(|entry| entry.item.id() != item_id); - // } - - // pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option)> { - // self.0.borrow().paths_by_item.get(&item_id).cloned() - // } + pub fn is_zoomed(&self) -> bool { + self.zoomed + } } -// impl NavHistoryState { -// pub fn did_update(&self, cx: &mut WindowContext) { -// if let Some(pane) = self.pane.upgrade(cx) { -// cx.defer(move |cx| { -// pane.update(cx, |pane, cx| pane.history_updated(cx)); -// }); -// } -// } +// impl Entity for Pane { +// type Event = Event; // } +impl Render for Pane { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + v_stack() + .child(self.render_tab_bar(cx)) + .child(div() /* toolbar */) + .child(if let Some(item) = self.active_item() { + item.to_any().render() + } else { + // todo!() + div().child("Empty Pane").render() + }) + + // enum MouseNavigationHandler {} + + // MouseEventHandler::new::(0, cx, |_, cx| { + // let active_item_index = self.active_item_index; + + // if let Some(active_item) = self.active_item() { + // Flex::column() + // .with_child({ + // let theme = theme::current(cx).clone(); + + // let mut stack = Stack::new(); + + // enum TabBarEventHandler {} + // stack.add_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // Empty::new() + // .contained() + // .with_style(theme.workspace.tab_bar.container) + // }) + // .on_down( + // MouseButton::Left, + // move |_, this, cx| { + // this.activate_item(active_item_index, true, true, cx); + // }, + // ), + // ); + // let tooltip_style = theme.tooltip.clone(); + // let tab_bar_theme = theme.workspace.tab_bar.clone(); + + // let nav_button_height = tab_bar_theme.height; + // let button_style = tab_bar_theme.nav_button; + // let border_for_nav_buttons = tab_bar_theme + // .tab_style(false, false) + // .container + // .border + // .clone(); + + // let mut tab_row = Flex::row() + // .with_child(nav_button( + // "icons/arrow_left.svg", + // button_style.clone(), + // nav_button_height, + // tooltip_style.clone(), + // self.can_navigate_backward(), + // { + // move |pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace + // .go_back(pane, cx) + // .detach_and_log_err(cx) + // }) + // }) + // } + // } + // }, + // super::GoBack, + // "Go Back", + // cx, + // )) + // .with_child( + // nav_button( + // "icons/arrow_right.svg", + // button_style.clone(), + // nav_button_height, + // tooltip_style, + // self.can_navigate_forward(), + // { + // move |pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace + // .go_forward(pane, cx) + // .detach_and_log_err(cx) + // }) + // }) + // } + // } + // }, + // super::GoForward, + // "Go Forward", + // cx, + // ) + // .contained() + // .with_border(border_for_nav_buttons), + // ) + // .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); + + // if self.has_focus { + // let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); + // tab_row.add_child( + // (render_tab_bar_buttons)(self, cx) + // .contained() + // .with_style(theme.workspace.tab_bar.pane_button_container) + // .flex(1., false) + // .into_any(), + // ) + // } + + // stack.add_child(tab_row); + // stack + // .constrained() + // .with_height(theme.workspace.tab_bar.height) + // .flex(1., false) + // .into_any_named("tab bar") + // }) + // .with_child({ + // enum PaneContentTabDropTarget {} + // dragged_item_receiver::( + // self, + // 0, + // self.active_item_index + 1, + // !self.can_split, + // if self.can_split { Some(100.) } else { None }, + // cx, + // { + // let toolbar = self.toolbar.clone(); + // let toolbar_hidden = toolbar.read(cx).hidden(); + // move |_, cx| { + // Flex::column() + // .with_children( + // (!toolbar_hidden) + // .then(|| ChildView::new(&toolbar, cx).expanded()), + // ) + // .with_child( + // ChildView::new(active_item.as_any(), cx).flex(1., true), + // ) + // } + // }, + // ) + // .flex(1., true) + // }) + // .with_child(ChildView::new(&self.tab_context_menu, cx)) + // .into_any() + // } else { + // enum EmptyPane {} + // let theme = theme::current(cx).clone(); + + // dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { + // self.render_blank_pane(&theme, cx) + // }) + // .on_down(MouseButton::Left, |_, _, cx| { + // cx.focus_parent(); + // }) + // .into_any() + // } + // }) + // .on_down( + // MouseButton::Navigate(NavigationDirection::Back), + // move |_, pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace.go_back(pane, cx).detach_and_log_err(cx) + // }) + // }) + // } + // }, + // ) + // .on_down(MouseButton::Navigate(NavigationDirection::Forward), { + // move |_, pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace.go_forward(pane, cx).detach_and_log_err(cx) + // }) + // }) + // } + // } + // }) + // .into_any_named("pane") + } + + // fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + // if !self.has_focus { + // self.has_focus = true; + // cx.emit(Event::Focus); + // cx.notify(); + // } + + // self.toolbar.update(cx, |toolbar, cx| { + // toolbar.focus_changed(true, cx); + // }); + + // if let Some(active_item) = self.active_item() { + // if cx.is_self_focused() { + // // Pane was focused directly. We need to either focus a view inside the active item, + // // or focus the active item itself + // if let Some(weak_last_focused_view) = + // self.last_focused_view_by_item.get(&active_item.id()) + // { + // if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) { + // cx.focus(&last_focused_view); + // return; + // } else { + // self.last_focused_view_by_item.remove(&active_item.id()); + // } + // } + + // cx.focus(active_item.as_any()); + // } else if focused != self.tab_bar_context_menu.handle { + // self.last_focused_view_by_item + // .insert(active_item.id(), focused.downgrade()); + // } + // } + // } + + // fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + // self.has_focus = false; + // self.toolbar.update(cx, |toolbar, cx| { + // toolbar.focus_changed(false, cx); + // }); + // cx.notify(); + // } + + // fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + // Self::reset_to_default_keymap_context(keymap); + // } +} + +impl ItemNavHistory { + pub fn push(&mut self, data: Option, cx: &mut WindowContext) { + self.history.push(data, self.item.clone(), cx); + } + + pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingBack, cx) + } + + pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingForward, cx) + } +} + +impl NavHistory { + pub fn for_each_entry( + &self, + cx: &AppContext, + mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), + ) { + let borrowed_history = self.0.lock(); + borrowed_history + .forward_stack + .iter() + .chain(borrowed_history.backward_stack.iter()) + .chain(borrowed_history.closed_stack.iter()) + .for_each(|entry| { + if let Some(project_and_abs_path) = + borrowed_history.paths_by_item.get(&entry.item.id()) + { + f(entry, project_and_abs_path.clone()); + } else if let Some(item) = entry.item.upgrade() { + if let Some(path) = item.project_path(cx) { + f(entry, (path, None)); + } + } + }) + } + + pub fn set_mode(&mut self, mode: NavigationMode) { + self.0.lock().mode = mode; + } + + pub fn mode(&self) -> NavigationMode { + self.0.lock().mode + } + + pub fn disable(&mut self) { + self.0.lock().mode = NavigationMode::Disabled; + } + + pub fn enable(&mut self) { + self.0.lock().mode = NavigationMode::Normal; + } + + pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { + let mut state = self.0.lock(); + let entry = match mode { + NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { + return None + } + NavigationMode::GoingBack => &mut state.backward_stack, + NavigationMode::GoingForward => &mut state.forward_stack, + NavigationMode::ReopeningClosedItem => &mut state.closed_stack, + } + .pop_back(); + if entry.is_some() { + state.did_update(cx); + } + entry + } + + pub fn push( + &mut self, + data: Option, + item: Arc, + cx: &mut WindowContext, + ) { + let state = &mut *self.0.lock(); + match state.mode { + NavigationMode::Disabled => {} + NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + state.forward_stack.clear(); + } + NavigationMode::GoingBack => { + if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.forward_stack.pop_front(); + } + state.forward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::GoingForward => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::ClosingItem => { + if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.closed_stack.pop_front(); + } + state.closed_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + } + state.did_update(cx); + } + + pub fn remove_item(&mut self, item_id: EntityId) { + let mut state = self.0.lock(); + state.paths_by_item.remove(&item_id); + state + .backward_stack + .retain(|entry| entry.item.id() != item_id); + state + .forward_stack + .retain(|entry| entry.item.id() != item_id); + state + .closed_stack + .retain(|entry| entry.item.id() != item_id); + } + + pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option)> { + self.0.lock().paths_by_item.get(&item_id).cloned() + } +} + +impl NavHistoryState { + pub fn did_update(&self, cx: &mut WindowContext) { + if let Some(pane) = self.pane.upgrade() { + cx.defer(move |cx| { + pane.update(cx, |pane, cx| pane.history_updated(cx)); + }); + } + } +} + // pub struct PaneBackdrop { // child_view: usize, // child: AnyElement, @@ -2221,14 +2366,14 @@ impl Pane { // } // } -// fn dirty_message_for(buffer_path: Option) -> String { -// let path = buffer_path -// .as_ref() -// .and_then(|p| p.path.to_str()) -// .unwrap_or(&"This buffer"); -// let path = truncate_and_remove_front(path, 80); -// format!("{path} contains unsaved edits. Do you want to save it?") -// } +fn dirty_message_for(buffer_path: Option) -> String { + let path = buffer_path + .as_ref() + .and_then(|p| p.path.to_str()) + .unwrap_or(&"This buffer"); + let path = truncate_and_remove_front(path, 80); + format!("{path} contains unsaved edits. Do you want to save it?") +} // todo!("uncomment tests") // #[cfg(test)] @@ -2752,3 +2897,16 @@ impl Pane { // }) // } // } + +#[derive(Clone, Debug)] +struct DraggedTab { + title: String, +} + +impl Render for DraggedTab { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().w_8().h_4().bg(gpui::red()) + } +} diff --git a/crates/workspace2/src/pane/dragged_item_receiver.rs b/crates/workspace2/src/pane/dragged_item_receiver.rs new file mode 100644 index 0000000000..d8e967dd75 --- /dev/null +++ b/crates/workspace2/src/pane/dragged_item_receiver.rs @@ -0,0 +1,239 @@ +use super::DraggedItem; +use crate::{Pane, SplitDirection, Workspace}; +use gpui::{ + color::Color, + elements::{Canvas, MouseEventHandler, ParentElement, Stack}, + geometry::{rect::RectF, vector::Vector2F}, + platform::MouseButton, + scene::MouseUp, + AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle, +}; +use project2::ProjectEntryId; + +pub fn dragged_item_receiver( + pane: &Pane, + region_id: usize, + drop_index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut ViewContext, + render_child: F, +) -> MouseEventHandler +where + Tag: 'static, + D: Element, + F: FnOnce(&mut MouseState, &mut ViewContext) -> D, +{ + let drag_and_drop = cx.global::>(); + let drag_position = if (pane.can_drop)(drag_and_drop, cx) { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + }) + } else { + None + }; + + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { + // Observing hovered will cause a render when the mouse enters regardless + // of if mouse position was accessed before + let drag_position = if state.dragging() { + drag_position + } else { + None + }; + Stack::new() + .with_child(render_child(state, cx)) + .with_children(drag_position.map(|drag_position| { + Canvas::new(move |bounds, _, _, cx| { + if bounds.contains_point(drag_position) { + let overlay_region = split_margin + .and_then(|split_margin| { + drop_split_direction(drag_position, bounds, split_margin) + .map(|dir| (dir, split_margin)) + }) + .map(|(dir, margin)| dir.along_edge(bounds, margin)) + .unwrap_or(bounds); + + cx.scene().push_stacking_context(None, None); + let background = overlay_color(cx); + cx.scene().push_quad(Quad { + bounds: overlay_region, + background: Some(background), + border: Default::default(), + corner_radii: Default::default(), + }); + cx.scene().pop_stacking_context(); + } + }) + })) + }); + + if drag_position.is_some() { + handler = handler + .on_up(MouseButton::Left, { + move |event, pane, cx| { + let workspace = pane.workspace.clone(); + let pane = cx.weak_handle(); + handle_dropped_item( + event, + workspace, + &pane, + drop_index, + allow_same_pane, + split_margin, + cx, + ); + cx.notify(); + } + }) + .on_move(|_, _, cx| { + let drag_and_drop = cx.global::>(); + + if drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + || drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + { + cx.notify(); + } else { + cx.propagate_event(); + } + }) + } + + handler +} + +pub fn handle_dropped_item( + event: MouseUp, + workspace: WeakViewHandle, + pane: &WeakViewHandle, + index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut EventContext, +) { + enum Action { + Move(WeakViewHandle, usize), + Open(ProjectEntryId), + } + let drag_and_drop = cx.global::>(); + let action = if let Some((_, dragged_item)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) + } else if let Some((_, project_entry)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Open(*project_entry) + } else { + cx.propagate_event(); + return; + }; + + if let Some(split_direction) = + split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin)) + { + let pane_to_split = pane.clone(); + match action { + Action::Move(from, item_id_to_move) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.split_pane_with_item( + pane_to_split, + split_direction, + from, + item_id_to_move, + cx, + ); + }) + } + }); + } + Action::Open(project_entry) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(task) = workspace.split_pane_with_project_entry( + pane_to_split, + split_direction, + project_entry, + cx, + ) { + task.detach_and_log_err(cx); + } + }) + } + }); + } + }; + } else { + match action { + Action::Move(from, item_id) => { + if pane != &from || allow_same_pane { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(((workspace, from), to)) = workspace + .upgrade(cx) + .zip(from.upgrade(cx)) + .zip(pane.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + workspace.move_item(from, to, item_id, index, cx); + }) + } + }); + } else { + cx.propagate_event(); + } + } + Action::Open(project_entry) => { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(path) = + workspace.project.read(cx).path_for_entry(project_entry, cx) + { + workspace + .open_path(path, Some(pane), true, cx) + .detach_and_log_err(cx); + } + }); + } + }); + } + } + } +} + +fn drop_split_direction( + position: Vector2F, + region: RectF, + split_margin: f32, +) -> Option { + let mut min_direction = None; + let mut min_distance = split_margin; + for direction in SplitDirection::all() { + let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs(); + + if edge_distance < min_distance { + min_direction = Some(direction); + min_distance = edge_distance; + } + } + + min_direction +} + +fn overlay_color(cx: &AppContext) -> Color { + theme2::current(cx).workspace.drop_target_overlay_color +} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index f226f7fc43..441aef21f5 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,23 +1,55 @@ use crate::{AppState, FollowerState, Pane, Workspace}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use call2::ActiveCall; use collections::HashMap; -use gpui2::{size, AnyElement, AnyView, Bounds, Handle, Pixels, Point, View, ViewContext}; +use db2::sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + statement::Statement, +}; +use gpui::{point, size, AnyElement, AnyWeakView, Bounds, Model, Pixels, Point, View, ViewContext}; +use parking_lot::Mutex; use project2::Project; use serde::Deserialize; -use std::{cell::RefCell, rc::Rc, sync::Arc}; -use theme2::Theme; +use std::sync::Arc; +use ui::prelude::*; const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; const VERTICAL_MIN_SIZE: f32 = 100.; +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Axis { Vertical, Horizontal, } -#[derive(Clone, Debug, PartialEq)] +impl StaticColumnCount for Axis {} +impl Bind for Axis { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + Axis::Horizontal => "Horizontal", + Axis::Vertical => "Vertical", + } + .bind(statement, start_index) + } +} + +impl Column for Axis { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(axis_text, next_index)| { + Ok(( + match axis_text.as_str() { + "Horizontal" => Axis::Horizontal, + "Vertical" => Axis::Vertical, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + +#[derive(Clone, PartialEq)] pub struct PaneGroup { pub(crate) root: Member, } @@ -91,19 +123,17 @@ impl PaneGroup { pub(crate) fn render( &self, - project: &Handle, - theme: &Theme, + project: &Model, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { + ) -> impl Component { self.root.render( project, 0, - theme, follower_states, active_call, active_pane, @@ -120,7 +150,7 @@ impl PaneGroup { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub(crate) enum Member { Axis(PaneAxis), Pane(View), @@ -153,17 +183,51 @@ impl Member { pub fn render( &self, - project: &Handle, + project: &Model, basis: usize, - theme: &Theme, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { - todo!() + ) -> impl Component { + match self { + Member::Pane(pane) => { + // todo!() + // let pane_element = if Some(pane.into()) == zoomed { + // None + // } else { + // Some(pane) + // }; + + div().child(pane.clone()).render() + + // Stack::new() + // .with_child(pane_element.contained().with_border(leader_border)) + // .with_children(leader_status_box) + // .into_any() + + // let el = div() + // .flex() + // .flex_1() + // .gap_px() + // .w_full() + // .h_full() + // .bg(cx.theme().colors().editor) + // .children(); + } + Member::Axis(axis) => axis.render( + project, + basis + 1, + follower_states, + active_call, + active_pane, + zoomed, + app_state, + cx, + ), + } // enum FollowIntoExternalProject {} @@ -305,18 +369,24 @@ impl Member { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone)] pub(crate) struct PaneAxis { pub axis: Axis, pub members: Vec, - pub flexes: Rc>>, - pub bounding_boxes: Rc>>>>, + pub flexes: Arc>>, + pub bounding_boxes: Arc>>>>, +} + +impl PartialEq for PaneAxis { + fn eq(&self, other: &Self) -> bool { + todo!() + } } impl PaneAxis { pub fn new(axis: Axis, members: Vec) -> Self { - let flexes = Rc::new(RefCell::new(vec![1.; members.len()])); - let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); + let flexes = Arc::new(Mutex::new(vec![1.; members.len()])); + let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); Self { axis, members, @@ -329,8 +399,8 @@ impl PaneAxis { let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]); debug_assert!(members.len() == flexes.len()); - let flexes = Rc::new(RefCell::new(flexes)); - let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); + let flexes = Arc::new(Mutex::new(flexes)); + let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); Self { axis, members, @@ -360,7 +430,7 @@ impl PaneAxis { } self.members.insert(idx, Member::Pane(new_pane.clone())); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; } else { *member = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); @@ -400,12 +470,12 @@ impl PaneAxis { if found_pane { if let Some(idx) = remove_member { self.members.remove(idx); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; } if self.members.len() == 1 { let result = self.members.pop(); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; Ok(result) } else { Ok(None) @@ -431,13 +501,13 @@ impl PaneAxis { } fn bounding_box_for_pane(&self, pane: &View) -> Option> { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); for (idx, member) in self.members.iter().enumerate() { match member { Member::Pane(found) => { if pane == found { - return self.bounding_boxes.borrow()[idx]; + return self.bounding_boxes.lock()[idx]; } } Member::Axis(axis) => { @@ -451,9 +521,9 @@ impl PaneAxis { } fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&View> { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - let bounding_boxes = self.bounding_boxes.borrow(); + let bounding_boxes = self.bounding_boxes.lock(); for (idx, member) in self.members.iter().enumerate() { if let Some(coordinates) = bounding_boxes[idx] { @@ -470,17 +540,16 @@ impl PaneAxis { fn render( &self, - project: &Handle, + project: &Model, basis: usize, - theme: &Theme, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { - debug_assert!(self.members.len() == self.flexes.borrow().len()); + debug_assert!(self.members.len() == self.flexes.lock().len()); todo!() // let mut pane_axis = PaneAxisElement::new( @@ -546,32 +615,32 @@ impl SplitDirection { [Self::Up, Self::Down, Self::Left, Self::Right] } - pub fn edge(&self, rect: Bounds) -> f32 { + pub fn edge(&self, rect: Bounds) -> Pixels { match self { - Self::Up => rect.min_y(), - Self::Down => rect.max_y(), - Self::Left => rect.min_x(), - Self::Right => rect.max_x(), + Self::Up => rect.origin.y, + Self::Down => rect.lower_left().y, + Self::Left => rect.lower_left().x, + Self::Right => rect.lower_right().x, } } pub fn along_edge(&self, bounds: Bounds, length: Pixels) -> Bounds { match self { Self::Up => Bounds { - origin: bounds.origin(), - size: size(bounds.width(), length), + origin: bounds.origin, + size: size(bounds.size.width, length), }, Self::Down => Bounds { - origin: size(bounds.min_x(), bounds.max_y() - length), - size: size(bounds.width(), length), + origin: point(bounds.lower_left().x, bounds.lower_left().y - length), + size: size(bounds.size.width, length), }, Self::Left => Bounds { - origin: bounds.origin(), - size: size(length, bounds.height()), + origin: bounds.origin, + size: size(length, bounds.size.height), }, Self::Right => Bounds { - origin: size(bounds.max_x() - length, bounds.min_y()), - size: size(length, bounds.height()), + origin: point(bounds.lower_right().x - length, bounds.lower_left().y), + size: size(length, bounds.size.height), }, } } diff --git a/crates/workspace2/src/persistence.rs b/crates/workspace2/src/persistence.rs new file mode 100644 index 0000000000..9790495087 --- /dev/null +++ b/crates/workspace2/src/persistence.rs @@ -0,0 +1,973 @@ +#![allow(dead_code)] + +pub mod model; + +use std::path::Path; + +use anyhow::{anyhow, bail, Context, Result}; +use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use gpui::WindowBounds; + +use util::{unzip_option, ResultExt}; +use uuid::Uuid; + +use crate::{Axis, WorkspaceId}; + +use model::{ + GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + WorkspaceLocation, +}; + +use self::model::DockStructure; + +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // workspace_location: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Optiopn, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // ) + pub static ref DB: WorkspaceDb<()> = + &[sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ) + ]; +} + +impl WorkspaceDb { + /// Returns a serialized workspace for the given worktree_roots. If the passed array + /// is empty, the most recent workspace is returned instead. If no workspace for the + /// passed roots is stored, returns none. + pub fn workspace_for_roots>( + &self, + worktree_roots: &[P], + ) -> Option { + let workspace_location: WorkspaceLocation = worktree_roots.into(); + + // Note that we re-assign the workspace_id here in case it's empty + // and we've grabbed the most recent workspace + let (workspace_id, workspace_location, bounds, display, docks): ( + WorkspaceId, + WorkspaceLocation, + Option, + Option, + DockStructure, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + workspace_location, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom + FROM workspaces + WHERE workspace_location = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: workspace_location.clone(), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + bounds, + display, + docks, + }) + } + + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces + /// that used this workspace previously + pub async fn save_workspace(&self, workspace: SerializedWorkspace) { + self.write(move |conn| { + conn.with_savepoint("update_worktrees", || { + // Clear out panes and pane_groups + conn.exec_bound(sql!( + DELETE FROM pane_groups WHERE workspace_id = ?1; + DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) + .expect("Clearing old panes"); + + conn.exec_bound(sql!( + DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? + ))?((&workspace.location, workspace.id.clone())) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + workspace_location, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + workspace_location = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + timestamp = CURRENT_TIMESTAMP + ))?((workspace.id, &workspace.location, workspace.docks)) + .context("Updating workspace")?; + + // Save center pane group + Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) + .context("save pane group in save workspace")?; + + Ok(()) + }) + .log_err(); + }) + .await; + } + + query! { + pub async fn next_id() -> Result { + INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id + } + } + + query! { + fn recent_workspaces() -> Result> { + SELECT workspace_id, workspace_location + FROM workspaces + WHERE workspace_location IS NOT NULL + ORDER BY timestamp DESC + } + } + + query! { + async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> { + DELETE FROM workspaces + WHERE workspace_id IS ? + } + } + + // Returns the recent locations which are still valid on disk and deletes ones which no longer + // exist. + pub async fn recent_workspaces_on_disk(&self) -> Result> { + let mut result = Vec::new(); + let mut delete_tasks = Vec::new(); + for (id, location) in self.recent_workspaces()? { + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) + { + result.push((id, location)); + } else { + delete_tasks.push(self.delete_stale_workspace(id)); + } + } + + futures::future::join_all(delete_tasks).await; + Ok(result) + } + + pub async fn last_workspace(&self) -> Result> { + Ok(self + .recent_workspaces_on_disk() + .await? + .into_iter() + .next() + .map(|(_, location)| location)) + } + + fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { + Ok(self + .get_pane_group(workspace_id, None)? + .into_iter() + .next() + .unwrap_or_else(|| { + SerializedPaneGroup::Pane(SerializedPane { + active: true, + children: vec![], + }) + })) + } + + fn get_pane_group( + &self, + workspace_id: WorkspaceId, + group_id: Option, + ) -> Result> { + type GroupKey = (Option, WorkspaceId); + type GroupOrPane = ( + Option, + Option, + Option, + Option, + Option, + ); + self.select_bound::(sql!( + SELECT group_id, axis, pane_id, active, flexes + FROM (SELECT + group_id, + axis, + NULL as pane_id, + NULL as active, + position, + parent_group_id, + workspace_id, + flexes + FROM pane_groups + UNION + SELECT + NULL, + NULL, + center_panes.pane_id, + panes.active as active, + position, + parent_group_id, + panes.workspace_id as workspace_id, + NULL + FROM center_panes + JOIN panes ON center_panes.pane_id = panes.pane_id) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position + ))?((group_id, workspace_id))? + .into_iter() + .map(|(group_id, axis, pane_id, active, flexes)| { + if let Some((group_id, axis)) = group_id.zip(axis) { + let flexes = flexes + .map(|flexes| serde_json::from_str::>(&flexes)) + .transpose()?; + + Ok(SerializedPaneGroup::Group { + axis, + children: self.get_pane_group(workspace_id, Some(group_id))?, + flexes, + }) + } else if let Some((pane_id, active)) = pane_id.zip(active) { + Ok(SerializedPaneGroup::Pane(SerializedPane::new( + self.get_items(pane_id)?, + active, + ))) + } else { + bail!("Pane Group Child was neither a pane group or a pane"); + } + }) + // Filter out panes and pane groups which don't have any children or items + .filter(|pane_group| match pane_group { + Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(), + Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), + _ => true, + }) + .collect::>() + } + + fn save_pane_group( + conn: &Connection, + workspace_id: WorkspaceId, + pane_group: &SerializedPaneGroup, + parent: Option<(GroupId, usize)>, + ) -> Result<()> { + match pane_group { + SerializedPaneGroup::Group { + axis, + children, + flexes, + } => { + let (parent_id, position) = unzip_option(parent); + + let flex_string = flexes + .as_ref() + .map(|flexes| serde_json::json!(flexes).to_string()); + + let group_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO pane_groups( + workspace_id, + parent_group_id, + position, + axis, + flexes + ) + VALUES (?, ?, ?, ?, ?) + RETURNING group_id + ))?(( + workspace_id, + parent_id, + position, + *axis, + flex_string, + ))? + .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?; + + for (position, group) in children.iter().enumerate() { + Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? + } + + Ok(()) + } + SerializedPaneGroup::Pane(pane) => { + Self::save_pane(conn, workspace_id, &pane, parent)?; + Ok(()) + } + } + } + + fn save_pane( + conn: &Connection, + workspace_id: WorkspaceId, + pane: &SerializedPane, + parent: Option<(GroupId, usize)>, + ) -> Result { + let pane_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO panes(workspace_id, active) + VALUES (?, ?) + RETURNING pane_id + ))?((workspace_id, pane.active))? + .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?; + + let (parent_id, order) = unzip_option(parent); + conn.exec_bound(sql!( + INSERT INTO center_panes(pane_id, parent_group_id, position) + VALUES (?, ?, ?) + ))?((pane_id, parent_id, order))?; + + Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; + + Ok(pane_id) + } + + fn get_items(&self, pane_id: PaneId) -> Result> { + Ok(self.select_bound(sql!( + SELECT kind, item_id, active FROM items + WHERE pane_id = ? + ORDER BY position + ))?(pane_id)?) + } + + fn save_items( + conn: &Connection, + workspace_id: WorkspaceId, + pane_id: PaneId, + items: &[SerializedItem], + ) -> Result<()> { + let mut insert = conn.exec_bound(sql!( + INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?) + )).context("Preparing insertion")?; + for (position, item) in items.iter().enumerate() { + insert((workspace_id, pane_id, position, item))?; + } + + Ok(()) + } + + query! { + pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> { + UPDATE workspaces + SET timestamp = CURRENT_TIMESTAMP + WHERE workspace_id = ? + } + } + + query! { + pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> { + UPDATE workspaces + SET window_state = ?2, + window_x = ?3, + window_y = ?4, + window_width = ?5, + window_height = ?6, + display = ?7 + WHERE workspace_id = ?1 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use db2::open_test_db; + use gpui; + + #[gpui::test] + async fn test_next_id_stability() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_next_id_stability").await); + + db.write(|conn| { + conn.migrate( + "test_table", + &[sql!( + CREATE TABLE test_table( + text TEXT, + workspace_id INTEGER, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + )], + ) + .unwrap(); + }) + .await; + + let id = db.next_id().await.unwrap(); + // Assert the empty row got inserted + assert_eq!( + Some(id), + db.select_row_bound::(sql!( + SELECT workspace_id FROM workspaces WHERE workspace_id = ? + )) + .unwrap()(id) + .unwrap() + ); + + db.write(move |conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-1", id)) + .unwrap() + }) + .await; + + let test_text_1 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(1) + .unwrap() + .unwrap(); + assert_eq!(test_text_1, "test-text-1"); + } + + #[gpui::test] + async fn test_workspace_id_stability() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await); + + db.write(|conn| { + conn.migrate( + "test_table", + &[sql!( + CREATE TABLE test_table( + text TEXT, + workspace_id INTEGER, + FOREIGN KEY(workspace_id) + REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT;)], + ) + }) + .await + .unwrap(); + + let mut workspace_1 = SerializedWorkspace { + id: 1, + location: (["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + let workspace_2 = SerializedWorkspace { + id: 2, + location: (["/tmp"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_1.clone()).await; + + db.write(|conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-1", 1)) + .unwrap(); + }) + .await; + + db.save_workspace(workspace_2.clone()).await; + + db.write(|conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-2", 2)) + .unwrap(); + }) + .await; + + workspace_1.location = (["/tmp", "/tmp3"]).into(); + db.save_workspace(workspace_1.clone()).await; + db.save_workspace(workspace_1).await; + db.save_workspace(workspace_2).await; + + let test_text_2 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(2) + .unwrap() + .unwrap(); + assert_eq!(test_text_2, "test-text-2"); + + let test_text_1 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(1) + .unwrap() + .unwrap(); + assert_eq!(test_text_1, "test-text-1"); + } + + fn group(axis: Axis, children: Vec) -> SerializedPaneGroup { + SerializedPaneGroup::Group { + axis, + flexes: None, + children, + } + } + + #[gpui::test] + async fn test_full_workspace_serialization() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await); + + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_group = group( + Axis::Horizontal, + vec![ + group( + Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, false), + SerializedItem::new("Terminal", 6, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 7, true), + SerializedItem::new("Terminal", 8, false), + ], + false, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 9, false), + SerializedItem::new("Terminal", 10, true), + ], + false, + )), + ], + ); + + let workspace = SerializedWorkspace { + id: 5, + location: (["/tmp", "/tmp2"]).into(), + center_group, + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace.clone()).await; + let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]); + + assert_eq!(workspace, round_trip_workspace.unwrap()); + + // Test guaranteed duplicate IDs + db.save_workspace(workspace.clone()).await; + db.save_workspace(workspace.clone()).await; + + let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]); + assert_eq!(workspace, round_trip_workspace.unwrap()); + } + + #[gpui::test] + async fn test_workspace_assignment() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_basic_functionality").await); + + let workspace_1 = SerializedWorkspace { + id: 1, + location: (["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + let mut workspace_2 = SerializedWorkspace { + id: 2, + location: (["/tmp"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_1.clone()).await; + db.save_workspace(workspace_2.clone()).await; + + // Test that paths are treated as a set + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_1 + ); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), + workspace_1 + ); + + // Make sure that other keys work + assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); + assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); + + // Test 'mutate' case of updating a pre-existing id + workspace_2.location = (["/tmp", "/tmp2"]).into(); + + db.save_workspace(workspace_2.clone()).await; + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_2 + ); + + // Test other mechanism for mutating + let mut workspace_3 = SerializedWorkspace { + id: 3, + location: (&["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_3.clone()).await; + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_3 + ); + + // Make sure that updating paths differently also works + workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into(); + db.save_workspace(workspace_3.clone()).await; + assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) + .unwrap(), + workspace_3 + ); + } + + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + + fn default_workspace>( + workspace_id: &[P], + center_group: &SerializedPaneGroup, + ) -> SerializedWorkspace { + SerializedWorkspace { + id: 4, + location: workspace_id.into(), + center_group: center_group.clone(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + } + } + + #[gpui::test] + async fn test_simple_split() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("simple_split").await); + + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_pane = group( + Axis::Horizontal, + vec![ + group( + Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, false), + SerializedItem::new("Terminal", 3, true), + ], + true, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, true), + SerializedItem::new("Terminal", 6, false), + ], + false, + )), + ], + ); + + let workspace = default_workspace(&["/tmp"], ¢er_pane); + + db.save_workspace(workspace.clone()).await; + + let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); + } + + #[gpui::test] + async fn test_cleanup_panes() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_cleanup_panes").await); + + let center_pane = group( + Axis::Horizontal, + vec![ + group( + Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, false), + SerializedItem::new("Terminal", 3, true), + ], + true, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, false), + SerializedItem::new("Terminal", 6, true), + ], + false, + )), + ], + ); + + let id = &["/tmp"]; + + let mut workspace = default_workspace(id, ¢er_pane); + + db.save_workspace(workspace.clone()).await; + + workspace.center_group = group( + Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, true), + SerializedItem::new("Terminal", 3, false), + ], + true, + )), + ], + ); + + db.save_workspace(workspace.clone()).await; + + let new_workspace = db.workspace_for_roots(id).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); + } +} diff --git a/crates/workspace2/src/persistence/model.rs b/crates/workspace2/src/persistence/model.rs index 2e28dabffb..2b8ec94bd4 100644 --- a/crates/workspace2/src/persistence/model.rs +++ b/crates/workspace2/src/persistence/model.rs @@ -7,7 +7,7 @@ use db2::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui2::{AsyncAppContext, Handle, Task, View, WeakView, WindowBounds}; +use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds}; use project2::Project; use std::{ path::{Path, PathBuf}, @@ -151,10 +151,10 @@ impl SerializedPaneGroup { #[async_recursion(?Send)] pub(crate) async fn deserialize( self, - project: &Handle, + project: &Model, workspace_id: WorkspaceId, - workspace: &WeakView, - cx: &mut AsyncAppContext, + workspace: WeakView, + cx: &mut AsyncWindowContext, ) -> Option<(Member, Option>, Vec>>)> { match self { SerializedPaneGroup::Group { @@ -167,7 +167,7 @@ impl SerializedPaneGroup { let mut items = Vec::new(); for child in children { if let Some((new_member, active_pane, new_items)) = child - .deserialize(project, workspace_id, workspace, cx) + .deserialize(project, workspace_id, workspace.clone(), cx) .await { members.push(new_member); @@ -196,14 +196,11 @@ impl SerializedPaneGroup { .log_err()?; let active = serialized_pane.active; let new_items = serialized_pane - .deserialize_to(project, &pane, workspace_id, workspace, cx) + .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx) .await .log_err()?; - if pane - .read_with(cx, |pane, _| pane.items_len() != 0) - .log_err()? - { + if pane.update(cx, |pane, _| pane.items_len() != 0).log_err()? { let pane = pane.upgrade()?; Some((Member::Pane(pane.clone()), active.then(|| pane), new_items)) } else { @@ -231,11 +228,11 @@ impl SerializedPane { pub async fn deserialize_to( &self, - project: &Handle, + project: &Model, pane: &WeakView, workspace_id: WorkspaceId, - workspace: &WeakView, - cx: &mut AsyncAppContext, + workspace: WeakView, + cx: &mut AsyncWindowContext, ) -> Result>>> { let mut items = Vec::new(); let mut active_item_index = None; diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs new file mode 100644 index 0000000000..2b870c2944 --- /dev/null +++ b/crates/workspace2/src/searchable.rs @@ -0,0 +1,285 @@ +use std::{any::Any, sync::Arc}; + +use gpui::{AnyView, AppContext, Subscription, Task, View, ViewContext, WindowContext}; +use project2::search::SearchQuery; + +use crate::{ + item::{Item, WeakItemHandle}, + ItemHandle, +}; + +#[derive(Debug)] +pub enum SearchEvent { + MatchesInvalidated, + ActiveMatchChanged, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Direction { + Prev, + Next, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct SearchOptions { + pub case: bool, + pub word: bool, + pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, +} + +pub trait SearchableItem: Item { + type Match: Any + Sync + Send + Clone; + + fn supported_options() -> SearchOptions { + SearchOptions { + case: true, + word: true, + regex: true, + replacement: true, + } + } + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option; + fn clear_matches(&mut self, cx: &mut ViewContext); + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; + fn activate_match( + &mut self, + index: usize, + matches: Vec, + cx: &mut ViewContext, + ); + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); + fn match_index_for_direction( + &mut self, + matches: &Vec, + current_index: usize, + direction: Direction, + count: usize, + _: &mut ViewContext, + ) -> usize { + match direction { + Direction::Prev => { + let count = count % matches.len(); + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } + } + Direction::Next => (current_index + count) % matches.len(), + } + } + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task>; + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option; +} + +pub trait SearchableItemHandle: ItemHandle { + fn downgrade(&self) -> Box; + fn boxed_clone(&self) -> Box; + fn supported_options(&self) -> SearchOptions; + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription; + fn clear_matches(&self, cx: &mut WindowContext); + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn query_suggestion(&self, cx: &mut WindowContext) -> String; + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ); + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize; + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>>; + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option; +} + +// todo!("here is where we need to use AnyWeakView"); +impl SearchableItemHandle for View { + fn downgrade(&self) -> Box { + // Box::new(self.downgrade()) + todo!() + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn supported_options(&self) -> SearchOptions { + T::supported_options() + } + + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription { + cx.subscribe(self, move |handle, event, cx| { + let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx)); + if let Some(search_event) = search_event { + handler(search_event, cx) + } + }) + } + + fn clear_matches(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.clear_matches(cx)); + } + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.update_matches(matches, cx)); + } + fn query_suggestion(&self, cx: &mut WindowContext) -> String { + self.update(cx, |this, cx| this.query_suggestion(cx)) + } + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.activate_match(index, matches, cx)); + } + + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.select_matches(matches, cx)); + } + + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| { + this.match_index_for_direction(&matches, current_index, direction, count, cx) + }) + } + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>> { + let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); + cx.spawn(|cx| async { + let matches = matches.await; + matches + .into_iter() + .map::, _>(|range| Box::new(range)) + .collect() + }) + } + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.active_match_index(matches, cx)) + } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } +} + +fn downcast_matches(matches: &Vec>) -> Vec { + matches + .iter() + .map(|range| range.downcast_ref::().cloned()) + .collect::>>() + .expect( + "SearchableItemHandle function called with vec of matches of a different type than expected", + ) +} + +impl From> for AnyView { + fn from(this: Box) -> Self { + this.to_any().clone() + } +} + +impl From<&Box> for AnyView { + fn from(this: &Box) -> Self { + this.to_any().clone() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Box {} + +pub trait WeakSearchableItemHandle: WeakItemHandle { + fn upgrade(&self, cx: &AppContext) -> Option>; + + // fn into_any(self) -> AnyWeakView; +} + +// todo!() +// impl WeakSearchableItemHandle for WeakView { +// fn upgrade(&self, cx: &AppContext) -> Option> { +// Some(Box::new(self.upgrade(cx)?)) +// } + +// // fn into_any(self) -> AnyView { +// // self.into_any() +// // } +// } + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Box {} + +impl std::hash::Hash for Box { + fn hash(&self, state: &mut H) { + self.id().hash(state) + } +} diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs new file mode 100644 index 0000000000..b99c5f3ab9 --- /dev/null +++ b/crates/workspace2/src/shared_screen.rs @@ -0,0 +1,151 @@ +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; +use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + platform::MouseButton, + AppContext, Entity, Task, View, ViewContext, +}; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + sync::{Arc, Weak}, +}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + } + } +} + +impl Entity for SharedScreen { + type Event = Event; +} + +impl View for SharedScreen { + fn ui_name() -> &'static str { + "SharedScreen" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum Focus {} + + let frame = self.frame.clone(); + MouseEventHandler::new::(0, cx, |_, cx| { + Canvas::new(move |bounds, _, _, cx| { + if let Some(frame) = frame.clone() { + let size = constrain_size_preserving_aspect_ratio( + bounds.size(), + vec2f(frame.width() as f32, frame.height() as f32), + ); + let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; + cx.scene().push_surface(gpui::platform::mac::Surface { + bounds: RectF::new(origin, size), + image_buffer: frame.image(), + }); + } + }) + .contained() + .with_style(theme::current(cx).shared_screen) + }) + .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) + .into_any() + } +} + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + Svg::new("icons/desktop.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(style.type_icon_width) + .aligned() + .contained() + .with_margin_right(style.spacing), + ) + .with_child( + Label::new( + format!("{}'s screen", self.user.github_login), + style.label.clone(), + ) + .aligned(), + ) + .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option { + let track = self.track.upgrade()?; + Some(Self::new(&track, self.peer_id, self.user.clone(), cx)) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + match event { + Event::Close => smallvec::smallvec!(ItemEvent::CloseItem), + } + } +} diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs new file mode 100644 index 0000000000..ca4ebcdb13 --- /dev/null +++ b/crates/workspace2/src/status_bar.rs @@ -0,0 +1,301 @@ +use std::any::TypeId; + +use crate::{ItemHandle, Pane}; +use gpui::{ + div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext, + WindowContext, +}; +use theme2::ActiveTheme; +use util::ResultExt; + +pub trait StatusItemView: Render { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ); +} + +trait StatusItemViewHandle: Send { + fn to_any(&self) -> AnyView; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ); + fn item_type(&self) -> TypeId; +} + +pub struct StatusBar { + left_items: Vec>, + right_items: Vec>, + active_pane: View, + _observe_active_pane: Subscription, +} + +impl Render for StatusBar { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div() + .py_0p5() + .px_1() + .flex() + .items_center() + .justify_between() + .w_full() + .bg(cx.theme().colors().status_bar) + .child(self.render_left_tools(cx)) + .child(self.render_right_tools(cx)) + } +} + +impl StatusBar { + fn render_left_tools(&self, cx: &mut ViewContext) -> impl Component { + div() + .flex() + .items_center() + .gap_1() + .children(self.left_items.iter().map(|item| item.to_any())) + } + + fn render_right_tools(&self, cx: &mut ViewContext) -> impl Component { + div() + .flex() + .items_center() + .gap_2() + .children(self.right_items.iter().map(|item| item.to_any())) + } +} + +// todo!() +// impl View for StatusBar { +// fn ui_name() -> &'static str { +// "StatusBar" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &theme::current(cx).workspace.status_bar; + +// StatusBarElement { +// left: Flex::row() +// .with_children(self.left_items.iter().map(|i| { +// ChildView::new(i.as_any(), cx) +// .aligned() +// .contained() +// .with_margin_right(theme.item_spacing) +// })) +// .into_any(), +// right: Flex::row() +// .with_children(self.right_items.iter().rev().map(|i| { +// ChildView::new(i.as_any(), cx) +// .aligned() +// .contained() +// .with_margin_left(theme.item_spacing) +// })) +// .into_any(), +// } +// .contained() +// .with_style(theme.container) +// .constrained() +// .with_height(theme.height) +// .into_any() +// } +// } + +impl StatusBar { + pub fn new(active_pane: &View, cx: &mut ViewContext) -> Self { + let mut this = Self { + left_items: Default::default(), + right_items: Default::default(), + active_pane: active_pane.clone(), + _observe_active_pane: cx + .observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)), + }; + this.update_active_pane_item(cx); + this + } + + pub fn add_left_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.left_items.push(Box::new(item)); + cx.notify(); + } + + pub fn item_of_type(&self) -> Option> { + self.left_items + .iter() + .chain(self.right_items.iter()) + .find_map(|item| item.to_any().clone().downcast().log_err()) + } + + pub fn position_of_item(&self) -> Option + where + T: StatusItemView, + { + for (index, item) in self.left_items.iter().enumerate() { + if item.item_type() == TypeId::of::() { + return Some(index); + } + } + for (index, item) in self.right_items.iter().enumerate() { + if item.item_type() == TypeId::of::() { + return Some(index + self.left_items.len()); + } + } + return None; + } + + pub fn insert_item_after( + &mut self, + position: usize, + item: View, + cx: &mut ViewContext, + ) where + T: 'static + StatusItemView, + { + if position < self.left_items.len() { + self.left_items.insert(position + 1, Box::new(item)) + } else { + self.right_items + .insert(position + 1 - self.left_items.len(), Box::new(item)) + } + cx.notify() + } + + pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext) { + if position < self.left_items.len() { + self.left_items.remove(position); + } else { + self.right_items.remove(position - self.left_items.len()); + } + cx.notify(); + } + + pub fn add_right_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.right_items.push(Box::new(item)); + cx.notify(); + } + + pub fn set_active_pane(&mut self, active_pane: &View, cx: &mut ViewContext) { + self.active_pane = active_pane.clone(); + self._observe_active_pane = + cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)); + self.update_active_pane_item(cx); + } + + fn update_active_pane_item(&mut self, cx: &mut ViewContext) { + let active_pane_item = self.active_pane.read(cx).active_item(); + for item in self.left_items.iter().chain(&self.right_items) { + item.set_active_pane_item(active_pane_item.as_deref(), cx); + } + } +} + +impl StatusItemViewHandle for View { + fn to_any(&self) -> AnyView { + self.clone().into() + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) { + self.update(cx, |this, cx| { + this.set_active_pane_item(active_pane_item, cx) + }); + } + + fn item_type(&self) -> TypeId { + TypeId::of::() + } +} + +impl From<&dyn StatusItemViewHandle> for AnyView { + fn from(val: &dyn StatusItemViewHandle) -> Self { + val.to_any().clone() + } +} + +// todo!() +// struct StatusBarElement { +// left: AnyElement, +// right: AnyElement, +// } + +// todo!() +// impl Element for StatusBarElement { +// type LayoutState = (); +// type PaintState = (); + +// fn layout( +// &mut self, +// mut constraint: SizeConstraint, +// view: &mut StatusBar, +// cx: &mut ViewContext, +// ) -> (Vector2F, Self::LayoutState) { +// let max_width = constraint.max.x(); +// constraint.min = vec2f(0., constraint.min.y()); + +// let right_size = self.right.layout(constraint, view, cx); +// let constraint = SizeConstraint::new( +// vec2f(0., constraint.min.y()), +// vec2f(max_width - right_size.x(), constraint.max.y()), +// ); + +// self.left.layout(constraint, view, cx); + +// (vec2f(max_width, right_size.y()), ()) +// } + +// fn paint( +// &mut self, +// bounds: RectF, +// visible_bounds: RectF, +// _: &mut Self::LayoutState, +// view: &mut StatusBar, +// cx: &mut ViewContext, +// ) -> Self::PaintState { +// let origin_y = bounds.upper_right().y(); +// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + +// let left_origin = vec2f(bounds.lower_left().x(), origin_y); +// self.left.paint(left_origin, visible_bounds, view, cx); + +// let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y); +// self.right.paint(right_origin, visible_bounds, view, cx); +// } + +// fn rect_for_text_range( +// &self, +// _: Range, +// _: RectF, +// _: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &StatusBar, +// _: &ViewContext, +// ) -> Option { +// None +// } + +// fn debug( +// &self, +// bounds: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &StatusBar, +// _: &ViewContext, +// ) -> serde_json::Value { +// json!({ +// "type": "StatusBarElement", +// "bounds": bounds.to_json() +// }) +// } +// } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs new file mode 100644 index 0000000000..80503ad7bb --- /dev/null +++ b/crates/workspace2/src/toolbar.rs @@ -0,0 +1,298 @@ +use crate::ItemHandle; +use gpui::{ + AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, +}; + +pub trait ToolbarItemView: Render + EventEmitter { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation; + + fn location_for_event( + &self, + _event: &Self::Event, + current_location: ToolbarItemLocation, + _cx: &AppContext, + ) -> ToolbarItemLocation { + current_location + } + + fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext) {} + + /// Number of times toolbar's height will be repeated to get the effective height. + /// Useful when multiple rows one under each other are needed. + /// The rows have the same width and act as a whole when reacting to resizes and similar events. + fn row_count(&self, _cx: &WindowContext) -> usize { + 1 + } +} + +trait ToolbarItemViewHandle: Send { + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) -> ToolbarItemLocation; + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext); + fn row_count(&self, cx: &WindowContext) -> usize; +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ToolbarItemLocation { + Hidden, + PrimaryLeft { flex: Option<(f32, bool)> }, + PrimaryRight { flex: Option<(f32, bool)> }, + Secondary, +} + +pub struct Toolbar { + active_item: Option>, + hidden: bool, + can_navigate: bool, + items: Vec<(Box, ToolbarItemLocation)>, +} + +// todo!() +// impl View for Toolbar { +// fn ui_name() -> &'static str { +// "Toolbar" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &theme::current(cx).workspace.toolbar; + +// let mut primary_left_items = Vec::new(); +// let mut primary_right_items = Vec::new(); +// let mut secondary_item = None; +// let spacing = theme.item_spacing; +// let mut primary_items_row_count = 1; + +// for (item, position) in &self.items { +// match *position { +// ToolbarItemLocation::Hidden => {} + +// ToolbarItemLocation::PrimaryLeft { flex } => { +// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); +// let left_item = ChildView::new(item.as_any(), cx).aligned(); +// if let Some((flex, expanded)) = flex { +// primary_left_items.push(left_item.flex(flex, expanded).into_any()); +// } else { +// primary_left_items.push(left_item.into_any()); +// } +// } + +// ToolbarItemLocation::PrimaryRight { flex } => { +// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); +// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); +// if let Some((flex, expanded)) = flex { +// primary_right_items.push(right_item.flex(flex, expanded).into_any()); +// } else { +// primary_right_items.push(right_item.into_any()); +// } +// } + +// ToolbarItemLocation::Secondary => { +// secondary_item = Some( +// ChildView::new(item.as_any(), cx) +// .constrained() +// .with_height(theme.height * item.row_count(cx) as f32) +// .into_any(), +// ); +// } +// } +// } + +// let container_style = theme.container; +// let height = theme.height * primary_items_row_count as f32; + +// let mut primary_items = Flex::row().with_spacing(spacing); +// primary_items.extend(primary_left_items); +// primary_items.extend(primary_right_items); + +// let mut toolbar = Flex::column(); +// if !primary_items.is_empty() { +// toolbar.add_child(primary_items.constrained().with_height(height)); +// } +// if let Some(secondary_item) = secondary_item { +// toolbar.add_child(secondary_item); +// } + +// if toolbar.is_empty() { +// toolbar.into_any_named("toolbar") +// } else { +// toolbar +// .contained() +// .with_style(container_style) +// .into_any_named("toolbar") +// } +// } +// } + +// <<<<<<< HEAD +// ======= +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// spacing: f32, +// on_click: F, +// tooltip_action: A, +// action_name: &'static str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = if enabled { +// style.style_for(state) +// } else { +// style.disabled_style() +// }; +// Svg::new(svg_path) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .contained() +// .with_style(style.container) +// .constrained() +// .with_width(style.button_width) +// .with_height(nav_button_height) +// .aligned() +// .top() +// }) +// .with_cursor_style(if enabled { +// CursorStyle::PointingHand +// } else { +// CursorStyle::default() +// }) +// .on_click(MouseButton::Left, move |_, toolbar, cx| { +// on_click(toolbar, cx) +// }) +// .with_tooltip::( +// 0, +// action_name, +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .with_margin_right(spacing) +// .into_any_named("nav button") +// } + +// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e +impl Toolbar { + pub fn new() -> Self { + Self { + active_item: None, + items: Default::default(), + hidden: false, + can_navigate: true, + } + } + + pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { + self.can_navigate = can_navigate; + cx.notify(); + } + + pub fn add_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + ToolbarItemView, + { + let location = item.set_active_pane_item(self.active_item.as_deref(), cx); + cx.subscribe(&item, |this, item, event, cx| { + if let Some((_, current_location)) = + this.items.iter_mut().find(|(i, _)| i.id() == item.id()) + { + let new_location = item + .read(cx) + .location_for_event(event, *current_location, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + }) + .detach(); + self.items.push((Box::new(item), location)); + cx.notify(); + } + + pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + self.active_item = item.map(|item| item.boxed_clone()); + self.hidden = self + .active_item + .as_ref() + .map(|item| !item.show_toolbar(cx)) + .unwrap_or(false); + + for (toolbar_item, current_location) in self.items.iter_mut() { + let new_location = toolbar_item.set_active_pane_item(item, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + } + + pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext) { + for (toolbar_item, _) in self.items.iter_mut() { + toolbar_item.focus_changed(focused, cx); + } + } + + pub fn item_of_type(&self) -> Option> { + self.items + .iter() + .find_map(|(item, _)| item.to_any().downcast().ok()) + } + + pub fn hidden(&self) -> bool { + self.hidden + } +} + +impl ToolbarItemViewHandle for View { + fn id(&self) -> EntityId { + self.entity_id() + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) -> ToolbarItemLocation { + self.update(cx, |this, cx| { + this.set_active_pane_item(active_pane_item, cx) + }) + } + + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| { + this.pane_focus_update(pane_focused, cx); + cx.notify(); + }); + } + + fn row_count(&self, cx: &WindowContext) -> usize { + self.read(cx).row_count(cx) + } +} + +// todo!() +// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle { +// fn from(val: &dyn ToolbarItemViewHandle) -> Self { +// val.as_any().clone() +// } +// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 3731094b26..bb9cb7e527 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1,86 +1,84 @@ -// pub mod dock; +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + +pub mod dock; pub mod item; -// pub mod notifications; +pub mod notifications; pub mod pane; pub mod pane_group; mod persistence; pub mod searchable; +// todo!() // pub mod shared_screen; -// mod status_bar; +mod status_bar; mod toolbar; mod workspace_settings; -use anyhow::{anyhow, Result}; -// use call2::ActiveCall; -// use client2::{ -// proto::{self, PeerId}, -// Client, Status, TypedEnvelope, UserStore, -// }; -// use collections::{hash_map, HashMap, HashSet}; -// use futures::{ -// channel::{mpsc, oneshot}, -// future::try_join_all, -// FutureExt, StreamExt, -// }; -// use gpui2::{ -// actions, -// elements::*, -// geometry::{ -// rect::RectF, -// vector::{vec2f, Vector2F}, -// }, -// impl_actions, -// platform::{ -// CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, -// WindowBounds, WindowOptions, -// }, -// AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, -// Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, -// View, WeakViewHandle, WindowContext, WindowHandle, -// }; -// use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; -// use itertools::Itertools; -// use language2::{LanguageRegistry, Rope}; -// use node_runtime::NodeRuntime;// // - -use futures::channel::oneshot; -// use crate::{ -// notifications::{simple_message_notification::MessageNotification, NotificationTracker}, -// persistence::model::{ -// DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, -// }, -// }; -// use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; -// use lazy_static::lazy_static; -// use notifications::{NotificationHandle, NotifyResultExt}; +use crate::persistence::model::{ + DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedWorkspace, +}; +use anyhow::{anyhow, Context as _, Result}; +use call2::ActiveCall; +use client2::{ + proto::{self, PeerId}, + Client, TypedEnvelope, UserStore, +}; +use collections::{hash_map, HashMap, HashSet}; +use dock::{Dock, DockPosition, PanelButtons}; +use futures::{ + channel::{mpsc, oneshot}, + future::try_join_all, + Future, FutureExt, StreamExt, +}; +use gpui::{ + div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, + AsyncWindowContext, Bounds, Component, Div, EntityId, EventEmitter, GlobalPixels, Model, + ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, Styled, Subscription, + Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, +}; +use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; +use itertools::Itertools; +use language2::LanguageRegistry; +use lazy_static::lazy_static; +use node_runtime::NodeRuntime; +use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; pub use pane::*; pub use pane_group::*; -// use persistence::{model::SerializedItem, DB}; -// pub use persistence::{ -// model::{ItemId, WorkspaceLocation}, -// WorkspaceDb, DB as WORKSPACE_DB, -// }; -// use postage::prelude::Stream; -// use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; -// use serde::Deserialize; -// use shared_screen::SharedScreen; -// use status_bar::StatusBar; -// pub use status_bar::StatusItemView; -// use theme::{Theme, ThemeSettings}; +use persistence::{ + model::{ItemId, WorkspaceLocation}, + DB, +}; +use postage::stream::Stream; +use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; +use serde::Deserialize; +use settings2::Settings; +use status_bar::StatusBar; +use std::{ + any::TypeId, + borrow::Cow, + env, + path::{Path, PathBuf}, + sync::{atomic::AtomicUsize, Arc}, + time::Duration, +}; +use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -// use util::ResultExt; -// pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; +use util::ResultExt; +use uuid::Uuid; +use workspace_settings::{AutosaveSetting, WorkspaceSettings}; -// lazy_static! { -// static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") -// .ok() -// .as_deref() -// .and_then(parse_pixel_position_env_var); -// static ref ZED_WINDOW_POSITION: Option = env::var("ZED_WINDOW_POSITION") -// .ok() -// .as_deref() -// .and_then(parse_pixel_position_env_var); -// } +lazy_static! { + static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") + .ok() + .as_deref() + .and_then(parse_pixel_size_env_var); + static ref ZED_WINDOW_POSITION: Option> = env::var("ZED_WINDOW_POSITION") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); +} // pub trait Modal: View { // fn has_focus(&self) -> bool; @@ -170,50 +168,50 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; // pub save_intent: Option, // } -// #[derive(Deserialize)] -// pub struct Toast { -// id: usize, -// msg: Cow<'static, str>, -// #[serde(skip)] -// on_click: Option<(Cow<'static, str>, Arc)>, -// } +#[derive(Deserialize)] +pub struct Toast { + id: usize, + msg: Cow<'static, str>, + #[serde(skip)] + on_click: Option<(Cow<'static, str>, Arc)>, +} -// impl Toast { -// pub fn new>>(id: usize, msg: I) -> Self { -// Toast { -// id, -// msg: msg.into(), -// on_click: None, -// } -// } +impl Toast { + pub fn new>>(id: usize, msg: I) -> Self { + Toast { + id, + msg: msg.into(), + on_click: None, + } + } -// pub fn on_click(mut self, message: M, on_click: F) -> Self -// where -// M: Into>, -// F: Fn(&mut WindowContext) + 'static, -// { -// self.on_click = Some((message.into(), Arc::new(on_click))); -// self -// } -// } + pub fn on_click(mut self, message: M, on_click: F) -> Self + where + M: Into>, + F: Fn(&mut WindowContext) + 'static, + { + self.on_click = Some((message.into(), Arc::new(on_click))); + self + } +} -// impl PartialEq for Toast { -// fn eq(&self, other: &Self) -> bool { -// self.id == other.id -// && self.msg == other.msg -// && self.on_click.is_some() == other.on_click.is_some() -// } -// } +impl PartialEq for Toast { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.msg == other.msg + && self.on_click.is_some() == other.on_click.is_some() + } +} -// impl Clone for Toast { -// fn clone(&self) -> Self { -// Toast { -// id: self.id, -// msg: self.msg.to_owned(), -// on_click: self.on_click.clone(), -// } -// } -// } +impl Clone for Toast { + fn clone(&self) -> Self { + Toast { + id: self.id, + msg: self.msg.to_owned(), + on_click: self.on_click.clone(), + } + } +} // #[derive(Clone, Deserialize, PartialEq)] // pub struct OpenTerminal { @@ -237,143 +235,142 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub type WorkspaceId = i64; -// pub fn init_settings(cx: &mut AppContext) { -// settings::register::(cx); -// settings::register::(cx); -// } +pub fn init_settings(cx: &mut AppContext) { + WorkspaceSettings::register(cx); + ItemSettings::register(cx); +} -// pub fn init(app_state: Arc, cx: &mut AppContext) { -// init_settings(cx); -// pane::init(cx); -// notifications::init(cx); +pub fn init(app_state: Arc, cx: &mut AppContext) { + init_settings(cx); + pane::init(cx); + notifications::init(cx); -// cx.add_global_action({ -// let app_state = Arc::downgrade(&app_state); -// move |_: &Open, cx: &mut AppContext| { -// let mut paths = cx.prompt_for_paths(PathPromptOptions { -// files: true, -// directories: true, -// multiple: true, -// }); + // cx.add_global_action({ + // let app_state = Arc::downgrade(&app_state); + // move |_: &Open, cx: &mut AppContext| { + // let mut paths = cx.prompt_for_paths(PathPromptOptions { + // files: true, + // directories: true, + // multiple: true, + // }); -// if let Some(app_state) = app_state.upgrade() { -// cx.spawn(move |mut cx| async move { -// if let Some(paths) = paths.recv().await.flatten() { -// cx.update(|cx| { -// open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) -// }); -// } -// }) -// .detach(); -// } -// } -// }); -// cx.add_async_action(Workspace::open); + // if let Some(app_state) = app_state.upgrade() { + // cx.spawn(move |mut cx| async move { + // if let Some(paths) = paths.recv().await.flatten() { + // cx.update(|cx| { + // open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) + // }); + // } + // }) + // .detach(); + // } + // } + // }); + // cx.add_async_action(Workspace::open); -// cx.add_async_action(Workspace::follow_next_collaborator); -// cx.add_async_action(Workspace::close); -// cx.add_async_action(Workspace::close_inactive_items_and_panes); -// cx.add_async_action(Workspace::close_all_items_and_panes); -// cx.add_global_action(Workspace::close_global); -// cx.add_global_action(restart); -// cx.add_async_action(Workspace::save_all); -// cx.add_action(Workspace::add_folder_to_project); -// cx.add_action( -// |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { -// let pane = workspace.active_pane().clone(); -// workspace.unfollow(&pane, cx); -// }, -// ); -// cx.add_action( -// |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { -// workspace -// .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) -// .detach_and_log_err(cx); -// }, -// ); -// cx.add_action( -// |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { -// workspace -// .save_active_item(SaveIntent::SaveAs, cx) -// .detach_and_log_err(cx); -// }, -// ); -// cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { -// workspace.activate_previous_pane(cx) -// }); -// cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { -// workspace.activate_next_pane(cx) -// }); + // cx.add_async_action(Workspace::follow_next_collaborator); + // cx.add_async_action(Workspace::close); + // cx.add_async_action(Workspace::close_inactive_items_and_panes); + // cx.add_async_action(Workspace::close_all_items_and_panes); + // cx.add_global_action(Workspace::close_global); + // cx.add_global_action(restart); + // cx.add_async_action(Workspace::save_all); + // cx.add_action(Workspace::add_folder_to_project); + // cx.add_action( + // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + // let pane = workspace.active_pane().clone(); + // workspace.unfollow(&pane, cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + // workspace + // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { + // workspace + // .save_active_item(SaveIntent::SaveAs, cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + // workspace.activate_previous_pane(cx) + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + // workspace.activate_next_pane(cx) + // }); -// cx.add_action( -// |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { -// workspace.activate_pane_in_direction(action.0, cx) -// }, -// ); + // cx.add_action( + // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { + // workspace.activate_pane_in_direction(action.0, cx) + // }, + // ); -// cx.add_action( -// |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { -// workspace.swap_pane_in_direction(action.0, cx) -// }, -// ); + // cx.add_action( + // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { + // workspace.swap_pane_in_direction(action.0, cx) + // }, + // ); -// cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { -// workspace.toggle_dock(DockPosition::Left, cx); -// }); -// cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { -// workspace.toggle_dock(DockPosition::Right, cx); -// }); -// cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { -// workspace.toggle_dock(DockPosition::Bottom, cx); -// }); -// cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { -// workspace.close_all_docks(cx); -// }); -// cx.add_action(Workspace::activate_pane_at_index); -// cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { -// workspace.reopen_closed_item(cx).detach(); -// }); -// cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { -// workspace -// .go_back(workspace.active_pane().downgrade(), cx) -// .detach(); -// }); -// cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { -// workspace -// .go_forward(workspace.active_pane().downgrade(), cx) -// .detach(); -// }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { + // workspace.toggle_dock(DockPosition::Left, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + // workspace.toggle_dock(DockPosition::Bottom, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + // workspace.close_all_docks(cx); + // }); + // cx.add_action(Workspace::activate_pane_at_index); + // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { + // workspace.reopen_closed_item(cx).detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { + // workspace + // .go_back(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { + // workspace + // .go_forward(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); -// cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { -// cx.spawn(|workspace, mut cx| async move { -// let err = install_cli::install_cli(&cx) -// .await -// .context("Failed to create CLI symlink"); + // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { + // cx.spawn(|workspace, mut cx| async move { + // let err = install_cli::install_cli(&cx) + // .await + // .context("Failed to create CLI symlink"); -// workspace.update(&mut cx, |workspace, cx| { -// if matches!(err, Err(_)) { -// err.notify_err(workspace, cx); -// } else { -// workspace.show_notification(1, cx, |cx| { -// cx.add_view(|_| { -// MessageNotification::new("Successfully installed the `zed` binary") -// }) -// }); -// } -// }) -// }) -// .detach(); -// }); -// } + // workspace.update(&mut cx, |workspace, cx| { + // if matches!(err, Err(_)) { + // err.notify_err(workspace, cx); + // } else { + // workspace.show_notification(1, cx, |cx| { + // cx.build_view(|_| { + // MessageNotification::new("Successfully installed the `zed` binary") + // }) + // }); + // } + // }) + // }) + // .detach(); + // }); +} type ProjectItemBuilders = - HashMap, AnyHandle, &mut ViewContext) -> Box>; + HashMap, AnyModel, &mut ViewContext) -> Box>; pub fn register_project_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { - builders.insert(TypeId::of::(), |project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx))) - }); + let builders = cx.default_global::(); + builders.insert(TypeId::of::(), |project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.build_view(|cx| I::for_project_item(project, item, cx))) }); } @@ -392,26 +389,25 @@ type FollowableItemBuilders = HashMap< ), >; pub fn register_followable_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { - builders.insert( - TypeId::of::(), - ( - |pane, workspace, id, state, cx| { - I::from_state_proto(pane, workspace, id, state, cx).map(|task| { - cx.foreground() - .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) - }) - }, - |this| Box::new(this.clone().downcast::().unwrap()), - ), - ); - }); + let builders = cx.default_global::(); + builders.insert( + TypeId::of::(), + ( + |pane, workspace, id, state, cx| { + I::from_state_proto(pane, workspace, id, state, cx).map(|task| { + cx.foreground_executor() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + |this| Box::new(this.clone().downcast::().unwrap()), + ), + ); } type ItemDeserializers = HashMap< Arc, fn( - Handle, + Model, WeakView, WorkspaceId, ItemId, @@ -419,13 +415,13 @@ type ItemDeserializers = HashMap< ) -> Task>>, >; pub fn register_deserializable_item(cx: &mut AppContext) { - cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| { + cx.update_global(|deserializers: &mut ItemDeserializers, _cx| { if let Some(serialized_item_kind) = I::serialized_item_kind() { deserializers.insert( Arc::from(serialized_item_kind), |project, workspace, workspace_id, item_id, cx| { let task = I::deserialize(project, workspace, workspace_id, item_id, cx); - cx.foreground() + cx.foreground_executor() .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) }, ); @@ -436,18 +432,22 @@ pub fn register_deserializable_item(cx: &mut AppContext) { pub struct AppState { pub languages: Arc, pub client: Arc, - pub user_store: Handle, - pub workspace_store: Handle, + pub user_store: Model, + pub workspace_store: Model, pub fs: Arc, pub build_window_options: - fn(Option, Option, &MainThread) -> WindowOptions, - pub initialize_workspace: - fn(WeakHandle, bool, Arc, AsyncAppContext) -> Task>, + fn(Option, Option, &mut AppContext) -> WindowOptions, + pub initialize_workspace: fn( + WeakView, + bool, + Arc, + AsyncWindowContext, + ) -> Task>, pub node_runtime: Arc, } pub struct WorkspaceStore { - workspaces: HashSet>, + workspaces: HashSet>, followers: Vec, client: Arc, _subscriptions: Vec, @@ -459,40 +459,41 @@ struct Follower { peer_id: PeerId, } -// impl AppState { -// #[cfg(any(test, feature = "test-support"))] -// pub fn test(cx: &mut AppContext) -> Arc { -// use node_runtime::FakeNodeRuntime; -// use settings::SettingsStore; +impl AppState { + #[cfg(any(test, feature = "test-support"))] + pub fn test(cx: &mut AppContext) -> Arc { + use gpui::Context; + use node_runtime::FakeNodeRuntime; + use settings2::SettingsStore; -// if !cx.has_global::() { -// cx.set_global(SettingsStore::test(cx)); -// } + if !cx.has_global::() { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + } -// let fs = fs::FakeFs::new(cx.background().clone()); -// let languages = Arc::new(LanguageRegistry::test()); -// let http_client = util::http::FakeHttpClient::with_404_response(); -// let client = Client::new(http_client.clone(), cx); -// let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); -// let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let fs = fs2::FakeFs::new(cx.background_executor().clone()); + let languages = Arc::new(LanguageRegistry::test()); + let http_client = util::http::FakeHttpClient::with_404_response(); + let client = Client::new(http_client.clone(), cx); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); -// theme::init((), cx); -// client::init(&client, cx); -// crate::init_settings(cx); + theme2::init(cx); + client2::init(&client, cx); + crate::init_settings(cx); -// Arc::new(Self { -// client, -// fs, -// languages, -// user_store, -// // channel_store, -// workspace_store, -// node_runtime: FakeNodeRuntime::new(), -// initialize_workspace: |_, _, _, _| Task::ready(Ok(())), -// build_window_options: |_, _, _| Default::default(), -// }) -// } -// } + Arc::new(Self { + client, + fs, + languages, + user_store, + workspace_store, + node_runtime: FakeNodeRuntime::new(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + build_window_options: |_, _, _| Default::default(), + }) + } +} struct DelayedDebouncedEditAction { task: Option>, @@ -509,7 +510,7 @@ impl DelayedDebouncedEditAction { fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) where - F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, + F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, { if let Some(channel) = self.cancel_channel.take() { _ = channel.send(()); @@ -519,8 +520,8 @@ impl DelayedDebouncedEditAction { self.cancel_channel = Some(sender); let previous_task = self.task.take(); - self.task = Some(cx.spawn(|workspace, mut cx| async move { - let mut timer = cx.background().timer(delay).fuse(); + self.task = Some(cx.spawn(move |workspace, mut cx| async move { + let mut timer = cx.background_executor().timer(delay).fuse(); if let Some(previous_task) = previous_task { previous_task.await; } @@ -540,41 +541,41 @@ impl DelayedDebouncedEditAction { } } -// pub enum Event { -// PaneAdded(View), -// ContactRequestedJoin(u64), -// } +pub enum Event { + PaneAdded(View), + ContactRequestedJoin(u64), +} pub struct Workspace { - weak_self: WeakHandle, + weak_self: WeakView, // modal: Option, - // zoomed: Option, - // zoomed_position: Option, - // center: PaneGroup, - // left_dock: View, - // bottom_dock: View, - // right_dock: View, + zoomed: Option, + zoomed_position: Option, + center: PaneGroup, + left_dock: View, + bottom_dock: View, + right_dock: View, panes: Vec>, - // panes_by_item: HashMap>, - // active_pane: View, + panes_by_item: HashMap>, + active_pane: View, last_active_center_pane: Option>, - // last_active_view_id: Option, - // status_bar: View, + last_active_view_id: Option, + status_bar: View, // titlebar_item: Option, - // notifications: Vec<(TypeId, usize, Box)>, - project: Handle, - // follower_states: HashMap, FollowerState>, - // last_leaders_by_pane: HashMap, PeerId>, - // window_edited: bool, - // active_call: Option<(ModelHandle, Vec)>, - // leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, - // database_id: WorkspaceId, + notifications: Vec<(TypeId, usize, Box)>, + project: Model, + follower_states: HashMap, FollowerState>, + last_leaders_by_pane: HashMap, PeerId>, + window_edited: bool, + active_call: Option<(Model, Vec)>, + leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, + database_id: WorkspaceId, app_state: Arc, - // subscriptions: Vec, - // _apply_leader_updates: Task>, - // _observe_current_user: Task>, - // _schedule_serialize: Option>, - // pane_history_timestamp: Arc, + subscriptions: Vec, + _apply_leader_updates: Task>, + _observe_current_user: Task>, + _schedule_serialize: Option>, + pane_history_timestamp: Arc, } // struct ActiveModal { @@ -582,11 +583,11 @@ pub struct Workspace { // previously_focused_view_id: Option, // } -// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -// pub struct ViewId { -// pub creator: PeerId, -// pub id: u64, -// } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ViewId { + pub creator: PeerId, + pub id: u64, +} #[derive(Default)] struct FollowerState { @@ -595,353 +596,362 @@ struct FollowerState { items_by_leader_view_id: HashMap>, } -// enum WorkspaceBounds {} +enum WorkspaceBounds {} impl Workspace { - // pub fn new( - // workspace_id: WorkspaceId, - // project: ModelHandle, - // app_state: Arc, - // cx: &mut ViewContext, - // ) -> Self { - // cx.observe(&project, |_, _, cx| cx.notify()).detach(); - // cx.subscribe(&project, move |this, _, event, cx| { - // match event { - // project::Event::RemoteIdChanged(_) => { - // this.update_window_title(cx); - // } + pub fn new( + workspace_id: WorkspaceId, + project: Model, + app_state: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&project, move |this, _, event, cx| { + match event { + project2::Event::RemoteIdChanged(_) => { + this.update_window_title(cx); + } - // project::Event::CollaboratorLeft(peer_id) => { - // this.collaborator_left(*peer_id, cx); - // } + project2::Event::CollaboratorLeft(peer_id) => { + this.collaborator_left(*peer_id, cx); + } - // project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { - // this.update_window_title(cx); - // this.serialize_workspace(cx); - // } + project2::Event::WorktreeRemoved(_) | project2::Event::WorktreeAdded => { + this.update_window_title(cx); + this.serialize_workspace(cx); + } - // project::Event::DisconnectedFromHost => { - // this.update_window_edited(cx); - // cx.blur(); - // } + project2::Event::DisconnectedFromHost => { + this.update_window_edited(cx); + cx.blur(); + } - // project::Event::Closed => { - // cx.remove_window(); - // } + project2::Event::Closed => { + cx.remove_window(); + } - // project::Event::DeletedEntry(entry_id) => { - // for pane in this.panes.iter() { - // pane.update(cx, |pane, cx| { - // pane.handle_deleted_project_item(*entry_id, cx) - // }); - // } - // } + project2::Event::DeletedEntry(entry_id) => { + for pane in this.panes.iter() { + pane.update(cx, |pane, cx| { + pane.handle_deleted_project_item(*entry_id, cx) + }); + } + } - // project::Event::Notification(message) => this.show_notification(0, cx, |cx| { - // cx.add_view(|_| MessageNotification::new(message.clone())) - // }), + project2::Event::Notification(message) => this.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new(message.clone())) + }), - // _ => {} - // } - // cx.notify() - // }) - // .detach(); + _ => {} + } + cx.notify() + }) + .detach(); - // let weak_handle = cx.weak_handle(); - // let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); + let weak_handle = cx.view().downgrade(); + let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - // let center_pane = cx.add_view(|cx| { - // Pane::new( - // weak_handle.clone(), - // project.clone(), - // pane_history_timestamp.clone(), - // cx, - // ) - // }); - // cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); - // cx.focus(¢er_pane); - // cx.emit(Event::PaneAdded(center_pane.clone())); + let center_pane = cx.build_view(|cx| { + Pane::new( + weak_handle.clone(), + project.clone(), + pane_history_timestamp.clone(), + cx, + ) + }); + cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); + // todo!() + // cx.focus(¢er_pane); + cx.emit(Event::PaneAdded(center_pane.clone())); - // app_state.workspace_store.update(cx, |store, _| { - // store.workspaces.insert(weak_handle.clone()); - // }); + let window_handle = cx.window_handle().downcast::().unwrap(); + app_state.workspace_store.update(cx, |store, _| { + store.workspaces.insert(window_handle); + }); - // let mut current_user = app_state.user_store.read(cx).watch_current_user(); - // let mut connection_status = app_state.client.status(); - // let _observe_current_user = cx.spawn(|this, mut cx| async move { - // current_user.recv().await; - // connection_status.recv().await; - // let mut stream = - // Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); + let mut current_user = app_state.user_store.read(cx).watch_current_user(); + let mut connection_status = app_state.client.status(); + let _observe_current_user = cx.spawn(|this, mut cx| async move { + current_user.next().await; + connection_status.next().await; + let mut stream = + Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); - // while stream.recv().await.is_some() { - // this.update(&mut cx, |_, cx| cx.notify())?; - // } - // anyhow::Ok(()) - // }); + while stream.recv().await.is_some() { + this.update(&mut cx, |_, cx| cx.notify())?; + } + anyhow::Ok(()) + }); - // // All leader updates are enqueued and then processed in a single task, so - // // that each asynchronous operation can be run in order. - // let (leader_updates_tx, mut leader_updates_rx) = - // mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); - // let _apply_leader_updates = cx.spawn(|this, mut cx| async move { - // while let Some((leader_id, update)) = leader_updates_rx.next().await { - // Self::process_leader_update(&this, leader_id, update, &mut cx) - // .await - // .log_err(); - // } + // All leader updates are enqueued and then processed in a single task, so + // that each asynchronous operation can be run in order. + let (leader_updates_tx, mut leader_updates_rx) = + mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); + let _apply_leader_updates = cx.spawn(|this, mut cx| async move { + while let Some((leader_id, update)) = leader_updates_rx.next().await { + Self::process_leader_update(&this, leader_id, update, &mut cx) + .await + .log_err(); + } - // Ok(()) - // }); + Ok(()) + }); - // cx.emit_global(WorkspaceCreated(weak_handle.clone())); + // todo!("replace with a different mechanism") + // cx.emit_global(WorkspaceCreated(weak_handle.clone())); - // let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left)); - // let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom)); - // let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right)); - // let left_dock_buttons = - // cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx)); - // let bottom_dock_buttons = - // cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx)); - // let right_dock_buttons = - // cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx)); - // let status_bar = cx.add_view(|cx| { - // let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); - // status_bar.add_left_item(left_dock_buttons, cx); - // status_bar.add_right_item(right_dock_buttons, cx); - // status_bar.add_right_item(bottom_dock_buttons, cx); - // status_bar - // }); + let left_dock = cx.build_view(|_| Dock::new(DockPosition::Left)); + let bottom_dock = cx.build_view(|_| Dock::new(DockPosition::Bottom)); + let right_dock = cx.build_view(|_| Dock::new(DockPosition::Right)); + let left_dock_buttons = + cx.build_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx)); + let bottom_dock_buttons = + cx.build_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx)); + let right_dock_buttons = + cx.build_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx)); + let status_bar = cx.build_view(|cx| { + let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); + status_bar.add_left_item(left_dock_buttons, cx); + status_bar.add_right_item(right_dock_buttons, cx); + status_bar.add_right_item(bottom_dock_buttons, cx); + status_bar + }); - // cx.update_default_global::, _, _>(|drag_and_drop, _| { - // drag_and_drop.register_container(weak_handle.clone()); - // }); + // todo!() + // cx.update_default_global::, _, _>(|drag_and_drop, _| { + // drag_and_drop.register_container(weak_handle.clone()); + // }); - // let mut active_call = None; - // if cx.has_global::>() { - // let call = cx.global::>().clone(); - // let mut subscriptions = Vec::new(); - // subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); - // active_call = Some((call, subscriptions)); - // } + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); + active_call = Some((call, subscriptions)); + } - // let subscriptions = vec![ - // cx.observe_fullscreen(|_, _, cx| cx.notify()), - // cx.observe_window_activation(Self::on_window_activation_changed), - // cx.observe_window_bounds(move |_, mut bounds, display, cx| { - // // Transform fixed bounds to be stored in terms of the containing display - // if let WindowBounds::Fixed(mut window_bounds) = bounds { - // if let Some(screen) = cx.platform().screen_by_id(display) { - // let screen_bounds = screen.bounds(); - // window_bounds - // .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x()); - // window_bounds - // .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y()); - // bounds = WindowBounds::Fixed(window_bounds); - // } - // } + let subscriptions = vec![ + cx.observe_window_activation(Self::on_window_activation_changed), + cx.observe_window_bounds(move |_, cx| { + if let Some(display) = cx.display() { + // Transform fixed bounds to be stored in terms of the containing display + let mut bounds = cx.window_bounds(); + if let WindowBounds::Fixed(window_bounds) = &mut bounds { + let display_bounds = display.bounds(); + window_bounds.origin.x -= display_bounds.origin.x; + window_bounds.origin.y -= display_bounds.origin.y; + } - // cx.background() - // .spawn(DB.set_window_bounds(workspace_id, bounds, display)) - // .detach_and_log_err(cx); - // }), - // cx.observe(&left_dock, |this, _, cx| { - // this.serialize_workspace(cx); - // cx.notify(); - // }), - // cx.observe(&bottom_dock, |this, _, cx| { - // this.serialize_workspace(cx); - // cx.notify(); - // }), - // cx.observe(&right_dock, |this, _, cx| { - // this.serialize_workspace(cx); - // cx.notify(); - // }), - // ]; + if let Some(display_uuid) = display.uuid().log_err() { + cx.background_executor() + .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid)) + .detach_and_log_err(cx); + } + } + cx.notify(); + }), + cx.observe(&left_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), + cx.observe(&bottom_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), + cx.observe(&right_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), + ]; - // cx.defer(|this, cx| this.update_window_title(cx)); - // Workspace { - // weak_self: weak_handle.clone(), - // modal: None, - // zoomed: None, - // zoomed_position: None, - // center: PaneGroup::new(center_pane.clone()), - // panes: vec![center_pane.clone()], - // panes_by_item: Default::default(), - // active_pane: center_pane.clone(), - // last_active_center_pane: Some(center_pane.downgrade()), - // last_active_view_id: None, - // status_bar, - // titlebar_item: None, - // notifications: Default::default(), - // left_dock, - // bottom_dock, - // right_dock, - // project: project.clone(), - // follower_states: Default::default(), - // last_leaders_by_pane: Default::default(), - // window_edited: false, - // active_call, - // database_id: workspace_id, - // app_state, - // _observe_current_user, - // _apply_leader_updates, - // _schedule_serialize: None, - // leader_updates_tx, - // subscriptions, - // pane_history_timestamp, - // } - // } + cx.defer(|this, cx| this.update_window_title(cx)); + Workspace { + weak_self: weak_handle.clone(), + // modal: None, + zoomed: None, + zoomed_position: None, + center: PaneGroup::new(center_pane.clone()), + panes: vec![center_pane.clone()], + panes_by_item: Default::default(), + active_pane: center_pane.clone(), + last_active_center_pane: Some(center_pane.downgrade()), + last_active_view_id: None, + status_bar, + // titlebar_item: None, + notifications: Default::default(), + left_dock, + bottom_dock, + right_dock, + project: project.clone(), + follower_states: Default::default(), + last_leaders_by_pane: Default::default(), + window_edited: false, + active_call, + database_id: workspace_id, + app_state, + _observe_current_user, + _apply_leader_updates, + _schedule_serialize: None, + leader_updates_tx, + subscriptions, + pane_history_timestamp, + } + } - // fn new_local( - // abs_paths: Vec, - // app_state: Arc, - // requesting_window: Option>, - // cx: &mut AppContext, - // ) -> Task<( - // WeakViewHandle, - // Vec, anyhow::Error>>>, - // )> { - // let project_handle = Project::local( - // app_state.client.clone(), - // app_state.node_runtime.clone(), - // app_state.user_store.clone(), - // app_state.languages.clone(), - // app_state.fs.clone(), - // cx, - // ); + fn new_local( + abs_paths: Vec, + app_state: Arc, + _requesting_window: Option>, + cx: &mut AppContext, + ) -> Task< + anyhow::Result<( + WindowHandle, + Vec, anyhow::Error>>>, + )>, + > { + let project_handle = Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ); - // cx.spawn(|mut cx| async move { - // let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + cx.spawn(|mut cx| async move { + let serialized_workspace: Option = None; //persistence::DB.workspace_for_roots(&abs_paths.as_slice()); - // let paths_to_open = Arc::new(abs_paths); + let paths_to_open = Arc::new(abs_paths); - // // Get project paths for all of the abs_paths - // let mut worktree_roots: HashSet> = Default::default(); - // let mut project_paths: Vec<(PathBuf, Option)> = - // Vec::with_capacity(paths_to_open.len()); - // for path in paths_to_open.iter().cloned() { - // if let Some((worktree, project_entry)) = cx - // .update(|cx| { - // Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) - // }) - // .await - // .log_err() - // { - // worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path())); - // project_paths.push((path, Some(project_entry))); - // } else { - // project_paths.push((path, None)); - // } - // } + // Get project paths for all of the abs_paths + let mut worktree_roots: HashSet> = Default::default(); + let mut project_paths: Vec<(PathBuf, Option)> = + Vec::with_capacity(paths_to_open.len()); + for path in paths_to_open.iter().cloned() { + if let Some((worktree, project_entry)) = cx + .update(|cx| { + Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) + })? + .await + .log_err() + { + worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok()); + project_paths.push((path, Some(project_entry))); + } else { + project_paths.push((path, None)); + } + } - // let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { - // serialized_workspace.id - // } else { - // DB.next_id().await.unwrap_or(0) - // }; + let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { + serialized_workspace.id + } else { + DB.next_id().await.unwrap_or(0) + }; - // let window = if let Some(window) = requesting_window { - // window.replace_root(&mut cx, |cx| { - // Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) - // }); - // window - // } else { - // { - // let window_bounds_override = window_bounds_env_override(&cx); - // let (bounds, display) = if let Some(bounds) = window_bounds_override { - // (Some(bounds), None) - // } else { - // serialized_workspace - // .as_ref() - // .and_then(|serialized_workspace| { - // let display = serialized_workspace.display?; - // let mut bounds = serialized_workspace.bounds?; + // todo!() + let window = /*if let Some(window) = requesting_window { + cx.update_window(window.into(), |old_workspace, cx| { + cx.replace_root_view(|cx| { + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) + }); + }); + window + } else */ { + let window_bounds_override = window_bounds_env_override(&cx); + let (bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(bounds), None) + } else { + serialized_workspace + .as_ref() + .and_then(|serialized_workspace| { + let serialized_display = serialized_workspace.display?; + let mut bounds = serialized_workspace.bounds?; - // // Stored bounds are relative to the containing display. - // // So convert back to global coordinates if that screen still exists - // if let WindowBounds::Fixed(mut window_bounds) = bounds { - // if let Some(screen) = cx.platform().screen_by_id(display) { - // let screen_bounds = screen.bounds(); - // window_bounds.set_origin_x( - // window_bounds.origin_x() + screen_bounds.origin_x(), - // ); - // window_bounds.set_origin_y( - // window_bounds.origin_y() + screen_bounds.origin_y(), - // ); - // bounds = WindowBounds::Fixed(window_bounds); - // } else { - // // Screen no longer exists. Return none here. - // return None; - // } - // } + // Stored bounds are relative to the containing display. + // So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + let screen = + cx.update(|cx| + cx.displays() + .into_iter() + .find(|display| display.uuid().ok() == Some(serialized_display)) + ).ok()??; + let screen_bounds = screen.bounds(); + window_bounds.origin.x += screen_bounds.origin.x; + window_bounds.origin.y += screen_bounds.origin.y; + bounds = WindowBounds::Fixed(window_bounds); + } - // Some((bounds, display)) - // }) - // .unzip() - // }; + Some((bounds, serialized_display)) + }) + .unzip() + }; - // // Use the serialized workspace to construct the new window - // cx.add_window( - // (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - // |cx| { - // Workspace::new( - // workspace_id, - // project_handle.clone(), - // app_state.clone(), - // cx, - // ) - // }, - // ) - // } - // }; + // Use the serialized workspace to construct the new window + let options = + cx.update(|cx| (app_state.build_window_options)(bounds, display, cx))?; - // // We haven't yielded the main thread since obtaining the window handle, - // // so the window exists. - // let workspace = window.root(&cx).unwrap(); + cx.open_window(options, { + let app_state = app_state.clone(); + let workspace_id = workspace_id.clone(); + let project_handle = project_handle.clone(); + move |cx| { + cx.build_view(|cx| { + Workspace::new(workspace_id, project_handle, app_state, cx) + }) + } + })? + }; - // (app_state.initialize_workspace)( - // workspace.downgrade(), - // serialized_workspace.is_some(), - // app_state.clone(), - // cx.clone(), - // ) - // .await - // .log_err(); + // todo!() Ask how to do this + let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?; + let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?; - // window.update(&mut cx, |cx| cx.activate_window()); + (app_state.initialize_workspace)( + weak_view, + serialized_workspace.is_some(), + app_state.clone(), + async_cx, + ) + .await + .log_err(); - // let workspace = workspace.downgrade(); - // notify_if_database_failed(&workspace, &mut cx); - // let opened_items = open_items( - // serialized_workspace, - // &workspace, - // project_paths, - // app_state, - // cx, - // ) - // .await - // .unwrap_or_default(); + window + .update(&mut cx, |_, cx| cx.activate_window()) + .log_err(); - // (workspace, opened_items) - // }) - // } + notify_if_database_failed(window, &mut cx); + let opened_items = window + .update(&mut cx, |_workspace, cx| { + open_items( + serialized_workspace, + project_paths, + app_state, + cx, + ) + })? + .await + .unwrap_or_default(); - // pub fn weak_handle(&self) -> WeakViewHandle { - // self.weak_self.clone() - // } + Ok((window, opened_items)) + }) + } - // pub fn left_dock(&self) -> &View { - // &self.left_dock - // } + pub fn weak_handle(&self) -> WeakView { + self.weak_self.clone() + } - // pub fn bottom_dock(&self) -> &View { - // &self.bottom_dock - // } + pub fn left_dock(&self) -> &View { + &self.left_dock + } - // pub fn right_dock(&self) -> &View { - // &self.right_dock - // } + pub fn bottom_dock(&self) -> &View { + &self.bottom_dock + } + + pub fn right_dock(&self) -> &View { + &self.right_dock + } // pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) // where @@ -1034,199 +1044,201 @@ impl Workspace { // dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); // } - // pub fn status_bar(&self) -> &View { - // &self.status_bar - // } + pub fn status_bar(&self) -> &View { + &self.status_bar + } - // pub fn app_state(&self) -> &Arc { - // &self.app_state - // } + pub fn app_state(&self) -> &Arc { + &self.app_state + } - // pub fn user_store(&self) -> &ModelHandle { - // &self.app_state.user_store - // } + pub fn user_store(&self) -> &Model { + &self.app_state.user_store + } - pub fn project(&self) -> &Handle { + pub fn project(&self) -> &Model { &self.project } - // pub fn recent_navigation_history( - // &self, - // limit: Option, - // cx: &AppContext, - // ) -> Vec<(ProjectPath, Option)> { - // let mut abs_paths_opened: HashMap> = HashMap::default(); - // let mut history: HashMap, usize)> = HashMap::default(); - // for pane in &self.panes { - // let pane = pane.read(cx); - // pane.nav_history() - // .for_each_entry(cx, |entry, (project_path, fs_path)| { - // if let Some(fs_path) = &fs_path { - // abs_paths_opened - // .entry(fs_path.clone()) - // .or_default() - // .insert(project_path.clone()); - // } - // let timestamp = entry.timestamp; - // match history.entry(project_path) { - // hash_map::Entry::Occupied(mut entry) => { - // let (_, old_timestamp) = entry.get(); - // if ×tamp > old_timestamp { - // entry.insert((fs_path, timestamp)); - // } - // } - // hash_map::Entry::Vacant(entry) => { - // entry.insert((fs_path, timestamp)); - // } - // } - // }); - // } + pub fn recent_navigation_history( + &self, + limit: Option, + cx: &AppContext, + ) -> Vec<(ProjectPath, Option)> { + let mut abs_paths_opened: HashMap> = HashMap::default(); + let mut history: HashMap, usize)> = HashMap::default(); + for pane in &self.panes { + let pane = pane.read(cx); + pane.nav_history() + .for_each_entry(cx, |entry, (project_path, fs_path)| { + if let Some(fs_path) = &fs_path { + abs_paths_opened + .entry(fs_path.clone()) + .or_default() + .insert(project_path.clone()); + } + let timestamp = entry.timestamp; + match history.entry(project_path) { + hash_map::Entry::Occupied(mut entry) => { + let (_, old_timestamp) = entry.get(); + if ×tamp > old_timestamp { + entry.insert((fs_path, timestamp)); + } + } + hash_map::Entry::Vacant(entry) => { + entry.insert((fs_path, timestamp)); + } + } + }); + } - // history - // .into_iter() - // .sorted_by_key(|(_, (_, timestamp))| *timestamp) - // .map(|(project_path, (fs_path, _))| (project_path, fs_path)) - // .rev() - // .filter(|(history_path, abs_path)| { - // let latest_project_path_opened = abs_path - // .as_ref() - // .and_then(|abs_path| abs_paths_opened.get(abs_path)) - // .and_then(|project_paths| { - // project_paths - // .iter() - // .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) - // }); + history + .into_iter() + .sorted_by_key(|(_, (_, timestamp))| *timestamp) + .map(|(project_path, (fs_path, _))| (project_path, fs_path)) + .rev() + .filter(|(history_path, abs_path)| { + let latest_project_path_opened = abs_path + .as_ref() + .and_then(|abs_path| abs_paths_opened.get(abs_path)) + .and_then(|project_paths| { + project_paths + .iter() + .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) + }); - // match latest_project_path_opened { - // Some(latest_project_path_opened) => latest_project_path_opened == history_path, - // None => true, - // } - // }) - // .take(limit.unwrap_or(usize::MAX)) - // .collect() - // } + match latest_project_path_opened { + Some(latest_project_path_opened) => latest_project_path_opened == history_path, + None => true, + } + }) + .take(limit.unwrap_or(usize::MAX)) + .collect() + } - // fn navigate_history( - // &mut self, - // pane: WeakViewHandle, - // mode: NavigationMode, - // cx: &mut ViewContext, - // ) -> Task> { - // let to_load = if let Some(pane) = pane.upgrade(cx) { - // cx.focus(&pane); + fn navigate_history( + &mut self, + pane: WeakView, + mode: NavigationMode, + cx: &mut ViewContext, + ) -> Task> { + let to_load = if let Some(pane) = pane.upgrade() { + // todo!("focus") + // cx.focus(&pane); - // pane.update(cx, |pane, cx| { - // loop { - // // Retrieve the weak item handle from the history. - // let entry = pane.nav_history_mut().pop(mode, cx)?; + pane.update(cx, |pane, cx| { + loop { + // Retrieve the weak item handle from the history. + let entry = pane.nav_history_mut().pop(mode, cx)?; - // // If the item is still present in this pane, then activate it. - // if let Some(index) = entry - // .item - // .upgrade(cx) - // .and_then(|v| pane.index_for_item(v.as_ref())) - // { - // let prev_active_item_index = pane.active_item_index(); - // pane.nav_history_mut().set_mode(mode); - // pane.activate_item(index, true, true, cx); - // pane.nav_history_mut().set_mode(NavigationMode::Normal); + // If the item is still present in this pane, then activate it. + if let Some(index) = entry + .item + .upgrade() + .and_then(|v| pane.index_for_item(v.as_ref())) + { + let prev_active_item_index = pane.active_item_index(); + pane.nav_history_mut().set_mode(mode); + pane.activate_item(index, true, true, cx); + pane.nav_history_mut().set_mode(NavigationMode::Normal); - // let mut navigated = prev_active_item_index != pane.active_item_index(); - // if let Some(data) = entry.data { - // navigated |= pane.active_item()?.navigate(data, cx); - // } + let mut navigated = prev_active_item_index != pane.active_item_index(); + if let Some(data) = entry.data { + navigated |= pane.active_item()?.navigate(data, cx); + } - // if navigated { - // break None; - // } - // } - // // If the item is no longer present in this pane, then retrieve its - // // project path in order to reopen it. - // else { - // break pane - // .nav_history() - // .path_for_item(entry.item.id()) - // .map(|(project_path, _)| (project_path, entry)); - // } - // } - // }) - // } else { - // None - // }; + if navigated { + break None; + } + } + // If the item is no longer present in this pane, then retrieve its + // project path in order to reopen it. + else { + break pane + .nav_history() + .path_for_item(entry.item.id()) + .map(|(project_path, _)| (project_path, entry)); + } + } + }) + } else { + None + }; - // if let Some((project_path, entry)) = to_load { - // // If the item was no longer present, then load it again from its previous path. - // let task = self.load_path(project_path, cx); - // cx.spawn(|workspace, mut cx| async move { - // let task = task.await; - // let mut navigated = false; - // if let Some((project_entry_id, build_item)) = task.log_err() { - // let prev_active_item_id = pane.update(&mut cx, |pane, _| { - // pane.nav_history_mut().set_mode(mode); - // pane.active_item().map(|p| p.id()) - // })?; + if let Some((project_path, entry)) = to_load { + // If the item was no longer present, then load it again from its previous path. + let task = self.load_path(project_path, cx); + cx.spawn(|workspace, mut cx| async move { + let task = task.await; + let mut navigated = false; + if let Some((project_entry_id, build_item)) = task.log_err() { + let prev_active_item_id = pane.update(&mut cx, |pane, _| { + pane.nav_history_mut().set_mode(mode); + pane.active_item().map(|p| p.id()) + })?; - // pane.update(&mut cx, |pane, cx| { - // let item = pane.open_item(project_entry_id, true, cx, build_item); - // navigated |= Some(item.id()) != prev_active_item_id; - // pane.nav_history_mut().set_mode(NavigationMode::Normal); - // if let Some(data) = entry.data { - // navigated |= item.navigate(data, cx); - // } - // })?; - // } + pane.update(&mut cx, |pane, cx| { + let item = pane.open_item(project_entry_id, true, cx, build_item); + navigated |= Some(item.id()) != prev_active_item_id; + pane.nav_history_mut().set_mode(NavigationMode::Normal); + if let Some(data) = entry.data { + navigated |= item.navigate(data, cx); + } + })?; + } - // if !navigated { - // workspace - // .update(&mut cx, |workspace, cx| { - // Self::navigate_history(workspace, pane, mode, cx) - // })? - // .await?; - // } + if !navigated { + workspace + .update(&mut cx, |workspace, cx| { + Self::navigate_history(workspace, pane, mode, cx) + })? + .await?; + } - // Ok(()) - // }) - // } else { - // Task::ready(Ok(())) - // } - // } + Ok(()) + }) + } else { + Task::ready(Ok(())) + } + } - // pub fn go_back( - // &mut self, - // pane: WeakViewHandle, - // cx: &mut ViewContext, - // ) -> Task> { - // self.navigate_history(pane, NavigationMode::GoingBack, cx) - // } + pub fn go_back( + &mut self, + pane: WeakView, + cx: &mut ViewContext, + ) -> Task> { + self.navigate_history(pane, NavigationMode::GoingBack, cx) + } - // pub fn go_forward( - // &mut self, - // pane: WeakViewHandle, - // cx: &mut ViewContext, - // ) -> Task> { - // self.navigate_history(pane, NavigationMode::GoingForward, cx) - // } + pub fn go_forward( + &mut self, + pane: WeakView, + cx: &mut ViewContext, + ) -> Task> { + self.navigate_history(pane, NavigationMode::GoingForward, cx) + } - // pub fn reopen_closed_item(&mut self, cx: &mut ViewContext) -> Task> { - // self.navigate_history( - // self.active_pane().downgrade(), - // NavigationMode::ReopeningClosedItem, - // cx, - // ) - // } + pub fn reopen_closed_item(&mut self, cx: &mut ViewContext) -> Task> { + self.navigate_history( + self.active_pane().downgrade(), + NavigationMode::ReopeningClosedItem, + cx, + ) + } - // pub fn client(&self) -> &Client { - // &self.app_state.client - // } + pub fn client(&self) -> &Client { + &self.app_state.client + } - // pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext) { - // self.titlebar_item = Some(item); - // cx.notify(); - // } + // todo!() + // pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext) { + // self.titlebar_item = Some(item); + // cx.notify(); + // } - // pub fn titlebar_item(&self) -> Option { - // self.titlebar_item.clone() - // } + // pub fn titlebar_item(&self) -> Option { + // self.titlebar_item.clone() + // } // /// Call the given callback with a workspace whose project is local. // /// @@ -1252,32 +1264,29 @@ impl Workspace { // } // } - // pub fn worktrees<'a>( - // &self, - // cx: &'a AppContext, - // ) -> impl 'a + Iterator> { - // self.project.read(cx).worktrees(cx) - // } + pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator> { + self.project.read(cx).worktrees() + } - // pub fn visible_worktrees<'a>( - // &self, - // cx: &'a AppContext, - // ) -> impl 'a + Iterator> { - // self.project.read(cx).visible_worktrees(cx) - // } + pub fn visible_worktrees<'a>( + &self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.project.read(cx).visible_worktrees(cx) + } - // pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { - // let futures = self - // .worktrees(cx) - // .filter_map(|worktree| worktree.read(cx).as_local()) - // .map(|worktree| worktree.scan_complete()) - // .collect::>(); - // async move { - // for future in futures { - // future.await; - // } - // } - // } + pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { + let futures = self + .worktrees(cx) + .filter_map(|worktree| worktree.read(cx).as_local()) + .map(|worktree| worktree.scan_complete()) + .collect::>(); + async move { + for future in futures { + future.await; + } + } + } // pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { // cx.spawn(|mut cx| async move { @@ -1498,13 +1507,13 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { - log::info!("open paths {:?}", abs_paths); + log::info!("open paths {abs_paths:?}"); let fs = self.app_state.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. abs_paths.sort_unstable(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let mut tasks = Vec::with_capacity(abs_paths.len()); for abs_path in &abs_paths { let project_path = match this @@ -1523,45 +1532,41 @@ impl Workspace { }; let this = this.clone(); - let task = cx.spawn(|mut cx| { - let fs = fs.clone(); - let abs_path = abs_path.clone(); - async move { - let (worktree, project_path) = project_path?; - if fs.is_file(&abs_path).await { - Some( - this.update(&mut cx, |this, cx| { - this.open_path(project_path, None, true, cx) - }) - .log_err()? - .await, - ) - } else { - this.update(&mut cx, |workspace, cx| { - let worktree = worktree.read(cx); - let worktree_abs_path = worktree.abs_path(); - let entry_id = if abs_path == worktree_abs_path.as_ref() { - worktree.root_entry() - } else { - abs_path - .strip_prefix(worktree_abs_path.as_ref()) - .ok() - .and_then(|relative_path| { - worktree.entry_for_path(relative_path) - }) - } - .map(|entry| entry.id); - if let Some(entry_id) = entry_id { - workspace.project.update(cx, |_, cx| { - cx.emit(project2::Event::ActiveEntryChanged(Some( - entry_id, - ))); - }) - } + let abs_path = abs_path.clone(); + let fs = fs.clone(); + let task = cx.spawn(move |mut cx| async move { + let (worktree, project_path) = project_path?; + if fs.is_file(&abs_path).await { + Some( + this.update(&mut cx, |this, cx| { + this.open_path(project_path, None, true, cx) }) - .log_err()?; - None - } + .log_err()? + .await, + ) + } else { + this.update(&mut cx, |workspace, cx| { + let worktree = worktree.read(cx); + let worktree_abs_path = worktree.abs_path(); + let entry_id = if abs_path == worktree_abs_path.as_ref() { + worktree.root_entry() + } else { + abs_path + .strip_prefix(worktree_abs_path.as_ref()) + .ok() + .and_then(|relative_path| { + worktree.entry_for_path(relative_path) + }) + } + .map(|entry| entry.id); + if let Some(entry_id) = entry_id { + workspace.project.update(cx, |_, cx| { + cx.emit(project2::Event::ActiveEntryChanged(Some(entry_id))); + }) + } + }) + .log_err()?; + None } }); tasks.push(task); @@ -1592,15 +1597,15 @@ impl Workspace { // } fn project_path_for_path( - project: Handle, + project: Model, abs_path: &Path, visible: bool, cx: &mut AppContext, - ) -> Task, ProjectPath)>> { + ) -> Task, ProjectPath)>> { let entry = project.update(cx, |project, cx| { project.find_or_create_local_worktree(abs_path, visible, cx) }); - cx.spawn(|cx| async move { + cx.spawn(|mut cx| async move { let (worktree, path) = entry.await?; let worktree_id = worktree.update(&mut cx, |t, _| t.id())?; Ok(( @@ -1617,7 +1622,7 @@ impl Workspace { // pub fn toggle_modal( // &mut self, // cx: &mut ViewContext, - // add_view: F, + // build_view: F, // ) -> Option> // where // V: 'static + Modal, @@ -1633,7 +1638,7 @@ impl Workspace { // cx.focus_self(); // Some(already_open_modal) // } else { - // let modal = add_view(self, cx); + // let modal = build_view(self, cx); // cx.subscribe(&modal, |this, _, event, cx| { // if V::dismiss_on_event(event) { // this.dismiss_modal(cx); @@ -1670,12 +1675,12 @@ impl Workspace { // } // } - // pub fn items<'a>( - // &'a self, - // cx: &'a AppContext, - // ) -> impl 'a + Iterator> { - // self.panes.iter().flat_map(|pane| pane.read(cx).items()) - // } + pub fn items<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| pane.read(cx).items()) + } // pub fn item_of_type(&self, cx: &AppContext) -> Option> { // self.items_of_type(cx).max_by_key(|item| item.id()) @@ -1690,35 +1695,35 @@ impl Workspace { // .flat_map(|pane| pane.read(cx).items_of_type()) // } - // pub fn active_item(&self, cx: &AppContext) -> Option> { - // self.active_pane().read(cx).active_item() - // } + pub fn active_item(&self, cx: &AppContext) -> Option> { + self.active_pane().read(cx).active_item() + } - // fn active_project_path(&self, cx: &ViewContext) -> Option { - // self.active_item(cx).and_then(|item| item.project_path(cx)) - // } + fn active_project_path(&self, cx: &ViewContext) -> Option { + self.active_item(cx).and_then(|item| item.project_path(cx)) + } - // pub fn save_active_item( - // &mut self, - // save_intent: SaveIntent, - // cx: &mut ViewContext, - // ) -> Task> { - // let project = self.project.clone(); - // let pane = self.active_pane(); - // let item_ix = pane.read(cx).active_item_index(); - // let item = pane.read(cx).active_item(); - // let pane = pane.downgrade(); + pub fn save_active_item( + &mut self, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + let project = self.project.clone(); + let pane = self.active_pane(); + let item_ix = pane.read(cx).active_item_index(); + let item = pane.read(cx).active_item(); + let pane = pane.downgrade(); - // cx.spawn(|_, mut cx| async move { - // if let Some(item) = item { - // Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx) - // .await - // .map(|_| ()) - // } else { - // Ok(()) - // } - // }) - // } + cx.spawn(|_, mut cx| async move { + if let Some(item) = item { + Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx) + .await + .map(|_| ()) + } else { + Ok(()) + } + }) + } // pub fn close_inactive_items_and_panes( // &mut self, @@ -1820,19 +1825,20 @@ impl Workspace { // self.serialize_workspace(cx); // } - // pub fn close_all_docks(&mut self, cx: &mut ViewContext) { - // let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; + pub fn close_all_docks(&mut self, cx: &mut ViewContext) { + let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; - // for dock in docks { - // dock.update(cx, |dock, cx| { - // dock.set_open(false, cx); - // }); - // } + for dock in docks { + dock.update(cx, |dock, cx| { + dock.set_open(false, cx); + }); + } - // cx.focus_self(); - // cx.notify(); - // self.serialize_workspace(cx); - // } + // todo!("focus") + // cx.focus_self(); + cx.notify(); + self.serialize_workspace(cx); + } // /// Transfer focus to the panel of the given type. // pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { @@ -1899,19 +1905,19 @@ impl Workspace { // None // } - // fn zoom_out(&mut self, cx: &mut ViewContext) { - // for pane in &self.panes { - // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - // } + fn zoom_out(&mut self, cx: &mut ViewContext) { + for pane in &self.panes { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } - // self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - // self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - // self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - // self.zoomed = None; - // self.zoomed_position = None; + self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + self.zoomed = None; + self.zoomed_position = None; - // cx.notify(); - // } + cx.notify(); + } // #[cfg(any(test, feature = "test-support"))] // pub fn zoomed_view(&self, cx: &AppContext) -> Option { @@ -1957,21 +1963,22 @@ impl Workspace { // cx.notify(); // } - // fn add_pane(&mut self, cx: &mut ViewContext) -> View { - // let pane = cx.add_view(|cx| { - // Pane::new( - // self.weak_handle(), - // self.project.clone(), - // self.pane_history_timestamp.clone(), - // cx, - // ) - // }); - // cx.subscribe(&pane, Self::handle_pane_event).detach(); - // self.panes.push(pane.clone()); - // cx.focus(&pane); - // cx.emit(Event::PaneAdded(pane.clone())); - // pane - // } + fn add_pane(&mut self, cx: &mut ViewContext) -> View { + let pane = cx.build_view(|cx| { + Pane::new( + self.weak_handle(), + self.project.clone(), + self.pane_history_timestamp.clone(), + cx, + ) + }); + cx.subscribe(&pane, Self::handle_pane_event).detach(); + self.panes.push(pane.clone()); + // todo!() + // cx.focus(&pane); + cx.emit(Event::PaneAdded(pane.clone())); + pane + } // pub fn add_item_to_center( // &mut self, @@ -2069,7 +2076,7 @@ impl Workspace { }); let task = self.load_path(path.into(), cx); - cx.spawn(|_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let (project_entry_id, build_item) = task.await?; pane.update(&mut cx, |pane, cx| { pane.open_item(project_entry_id, focus_item, cx, build_item) @@ -2116,19 +2123,19 @@ impl Workspace { ) -> Task< Result<( ProjectEntryId, - impl 'static + FnOnce(&mut ViewContext) -> Box, + impl 'static + Send + FnOnce(&mut ViewContext) -> Box, )>, > { let project = self.project().clone(); let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); cx.spawn(|_, mut cx| async move { let (project_entry_id, project_item) = project_item.await?; - let build_item = cx.update(|cx| { + let build_item = cx.update(|_, cx| { cx.default_global::() - .get(&project_item.model_type()) + .get(&project_item.entity_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() - })?; + })??; let build_item = move |cx: &mut ViewContext| build_item(project, project_item, cx); Ok((project_entry_id, build_item)) @@ -2154,7 +2161,7 @@ impl Workspace { // return item; // } - // let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); // self.add_item(Box::new(item.clone()), cx); // item // } @@ -2178,7 +2185,7 @@ impl Workspace { // return item; // } - // let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); // self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); // item // } @@ -2303,64 +2310,65 @@ impl Workspace { // cx.notify(); // } - // fn handle_pane_event( - // &mut self, - // pane: View, - // event: &pane::Event, - // cx: &mut ViewContext, - // ) { - // match event { - // pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), - // pane::Event::Split(direction) => { - // self.split_and_clone(pane, *direction, cx); - // } - // pane::Event::Remove => self.remove_pane(pane, cx), - // pane::Event::ActivateItem { local } => { - // if *local { - // self.unfollow(&pane, cx); - // } - // if &pane == self.active_pane() { - // self.active_item_path_changed(cx); - // } - // } - // pane::Event::ChangeItemTitle => { - // if pane == self.active_pane { - // self.active_item_path_changed(cx); - // } - // self.update_window_edited(cx); - // } - // pane::Event::RemoveItem { item_id } => { - // self.update_window_edited(cx); - // if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { - // if entry.get().id() == pane.id() { - // entry.remove(); - // } - // } - // } - // pane::Event::Focus => { - // self.handle_pane_focused(pane.clone(), cx); - // } - // pane::Event::ZoomIn => { - // if pane == self.active_pane { - // pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - // if pane.read(cx).has_focus() { - // self.zoomed = Some(pane.downgrade().into_any()); - // self.zoomed_position = None; - // } - // cx.notify(); - // } - // } - // pane::Event::ZoomOut => { - // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - // if self.zoomed_position.is_none() { - // self.zoomed = None; - // } - // cx.notify(); - // } - // } + fn handle_pane_event( + &mut self, + _pane: View, + _event: &pane::Event, + _cx: &mut ViewContext, + ) { + todo!() + // match event { + // pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), + // pane::Event::Split(direction) => { + // self.split_and_clone(pane, *direction, cx); + // } + // pane::Event::Remove => self.remove_pane(pane, cx), + // pane::Event::ActivateItem { local } => { + // if *local { + // self.unfollow(&pane, cx); + // } + // if &pane == self.active_pane() { + // self.active_item_path_changed(cx); + // } + // } + // pane::Event::ChangeItemTitle => { + // if pane == self.active_pane { + // self.active_item_path_changed(cx); + // } + // self.update_window_edited(cx); + // } + // pane::Event::RemoveItem { item_id } => { + // self.update_window_edited(cx); + // if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { + // if entry.get().id() == pane.id() { + // entry.remove(); + // } + // } + // } + // pane::Event::Focus => { + // self.handle_pane_focused(pane.clone(), cx); + // } + // pane::Event::ZoomIn => { + // if pane == self.active_pane { + // pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); + // if pane.read(cx).has_focus() { + // self.zoomed = Some(pane.downgrade().into_any()); + // self.zoomed_position = None; + // } + // cx.notify(); + // } + // } + // pane::Event::ZoomOut => { + // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + // if self.zoomed_position.is_none() { + // self.zoomed = None; + // } + // cx.notify(); + // } + // } - // self.serialize_workspace(cx); - // } + // self.serialize_workspace(cx); + } // pub fn split_pane( // &mut self, @@ -2397,9 +2405,9 @@ impl Workspace { // pub fn split_pane_with_item( // &mut self, - // pane_to_split: WeakViewHandle, + // pane_to_split: WeakView, // split_direction: SplitDirection, - // from: WeakViewHandle, + // from: WeakView, // item_id_to_move: usize, // cx: &mut ViewContext, // ) { @@ -2420,7 +2428,7 @@ impl Workspace { // pub fn split_pane_with_project_entry( // &mut self, - // pane_to_split: WeakViewHandle, + // pane_to_split: WeakView, // split_direction: SplitDirection, // project_entry: ProjectEntryId, // cx: &mut ViewContext, @@ -2489,27 +2497,27 @@ impl Workspace { // } // } - // pub fn panes(&self) -> &[View] { - // &self.panes - // } + pub fn panes(&self) -> &[View] { + &self.panes + } - // pub fn active_pane(&self) -> &View { - // &self.active_pane - // } + pub fn active_pane(&self) -> &View { + &self.active_pane + } - // fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - // self.follower_states.retain(|_, state| { - // if state.leader_id == peer_id { - // for item in state.items_by_leader_view_id.values() { - // item.set_leader_peer_id(None, cx); - // } - // false - // } else { - // true - // } - // }); - // cx.notify(); - // } + fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + self.follower_states.retain(|_, state| { + if state.leader_id == peer_id { + for item in state.items_by_leader_view_id.values() { + item.set_leader_peer_id(None, cx); + } + false + } else { + true + } + }); + cx.notify(); + } // fn start_following( // &mut self, @@ -2650,37 +2658,33 @@ impl Workspace { // self.start_following(leader_id, cx) // } - // pub fn unfollow( - // &mut self, - // pane: &View, - // cx: &mut ViewContext, - // ) -> Option { - // let state = self.follower_states.remove(pane)?; - // let leader_id = state.leader_id; - // for (_, item) in state.items_by_leader_view_id { - // item.set_leader_peer_id(None, cx); - // } + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { + let state = self.follower_states.remove(pane)?; + let leader_id = state.leader_id; + for (_, item) in state.items_by_leader_view_id { + item.set_leader_peer_id(None, cx); + } - // if self - // .follower_states - // .values() - // .all(|state| state.leader_id != state.leader_id) - // { - // let project_id = self.project.read(cx).remote_id(); - // let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - // self.app_state - // .client - // .send(proto::Unfollow { - // room_id, - // project_id, - // leader_id: Some(leader_id), - // }) - // .log_err(); - // } + if self + .follower_states + .values() + .all(|state| state.leader_id != state.leader_id) + { + let project_id = self.project.read(cx).remote_id(); + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + self.app_state + .client + .send(proto::Unfollow { + room_id, + project_id, + leader_id: Some(leader_id), + }) + .log_err(); + } - // cx.notify(); - // Some(leader_id) - // } + cx.notify(); + Some(leader_id) + } // pub fn is_being_followed(&self, peer_id: PeerId) -> bool { // self.follower_states @@ -2688,100 +2692,85 @@ impl Workspace { // .any(|state| state.leader_id == peer_id) // } - // fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // // TODO: There should be a better system in place for this - // // (https://github.com/zed-industries/zed/issues/1290) - // let is_fullscreen = cx.window_is_fullscreen(); - // let container_theme = if is_fullscreen { - // let mut container_theme = theme.titlebar.container; - // container_theme.padding.left = container_theme.padding.right; - // container_theme - // } else { - // theme.titlebar.container - // }; + fn render_titlebar(&self, cx: &mut ViewContext) -> impl Component { + div() + .bg(cx.theme().colors().title_bar) + .when( + !matches!(cx.window_bounds(), WindowBounds::Fullscreen), + |s| s.pl_20(), + ) + .id("titlebar") + .on_click(|_, event, cx| { + if event.up.click_count == 2 { + cx.zoom_window(); + } + }) + .child("Collab title bar Item") // self.titlebar_item + } - // enum TitleBar {} - // MouseEventHandler::new::(0, cx, |_, cx| { - // Stack::new() - // .with_children( - // self.titlebar_item - // .as_ref() - // .map(|item| ChildView::new(item, cx)), - // ) - // .contained() - // .with_style(container_theme) - // }) - // .on_click(MouseButton::Left, |event, _, cx| { - // if event.click_count == 2 { - // cx.zoom_window(); - // } - // }) - // .constrained() - // .with_height(theme.titlebar.height) - // .into_any_named("titlebar") - // } + // fn active_item_path_changed(&mut self, cx: &mut ViewContext) { + // let active_entry = self.active_project_path(cx); + // self.project + // .update(cx, |project, cx| project.set_active_path(active_entry, cx)); + // self.update_window_title(cx); + // } - // fn active_item_path_changed(&mut self, cx: &mut ViewContext) { - // let active_entry = self.active_project_path(cx); - // self.project - // .update(cx, |project, cx| project.set_active_path(active_entry, cx)); - // self.update_window_title(cx); - // } + fn update_window_title(&mut self, cx: &mut ViewContext) { + let project = self.project().read(cx); + let mut title = String::new(); - // fn update_window_title(&mut self, cx: &mut ViewContext) { - // let project = self.project().read(cx); - // let mut title = String::new(); + if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { + let filename = path + .path + .file_name() + .map(|s| s.to_string_lossy()) + .or_else(|| { + Some(Cow::Borrowed( + project + .worktree_for_id(path.worktree_id, cx)? + .read(cx) + .root_name(), + )) + }); - // if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { - // let filename = path - // .path - // .file_name() - // .map(|s| s.to_string_lossy()) - // .or_else(|| { - // Some(Cow::Borrowed( - // project - // .worktree_for_id(path.worktree_id, cx)? - // .read(cx) - // .root_name(), - // )) - // }); + if let Some(filename) = filename { + title.push_str(filename.as_ref()); + title.push_str(" — "); + } + } - // if let Some(filename) = filename { - // title.push_str(filename.as_ref()); - // title.push_str(" — "); - // } - // } + for (i, name) in project.worktree_root_names(cx).enumerate() { + if i > 0 { + title.push_str(", "); + } + title.push_str(name); + } - // for (i, name) in project.worktree_root_names(cx).enumerate() { - // if i > 0 { - // title.push_str(", "); - // } - // title.push_str(name); - // } + if title.is_empty() { + title = "empty project".to_string(); + } - // if title.is_empty() { - // title = "empty project".to_string(); - // } + if project.is_remote() { + title.push_str(" ↙"); + } else if project.is_shared() { + title.push_str(" ↗"); + } - // if project.is_remote() { - // title.push_str(" ↙"); - // } else if project.is_shared() { - // title.push_str(" ↗"); - // } + // todo!() + // cx.set_window_title(&title); + } - // cx.set_window_title(&title); - // } - - // fn update_window_edited(&mut self, cx: &mut ViewContext) { - // let is_edited = !self.project.read(cx).is_read_only() - // && self - // .items(cx) - // .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); - // if is_edited != self.window_edited { - // self.window_edited = is_edited; - // cx.set_window_edited(self.window_edited) - // } - // } + fn update_window_edited(&mut self, cx: &mut ViewContext) { + let is_edited = !self.project.read(cx).is_read_only() + && self + .items(cx) + .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); + if is_edited != self.window_edited { + self.window_edited = is_edited; + todo!() + // cx.set_window_edited(self.window_edited) + } + } // fn render_disconnected_overlay( // &self, @@ -2838,299 +2827,302 @@ impl Workspace { // // RPC handlers - // fn handle_follow( - // &mut self, - // follower_project_id: Option, - // cx: &mut ViewContext, - // ) -> proto::FollowResponse { - // let client = &self.app_state.client; - // let project_id = self.project.read(cx).remote_id(); + fn handle_follow( + &mut self, + _follower_project_id: Option, + _cx: &mut ViewContext, + ) -> proto::FollowResponse { + todo!() - // let active_view_id = self.active_item(cx).and_then(|i| { - // Some( - // i.to_followable_item_handle(cx)? - // .remote_id(client, cx)? - // .to_proto(), - // ) - // }); + // let client = &self.app_state.client; + // let project_id = self.project.read(cx).remote_id(); - // cx.notify(); + // let active_view_id = self.active_item(cx).and_then(|i| { + // Some( + // i.to_followable_item_handle(cx)? + // .remote_id(client, cx)? + // .to_proto(), + // ) + // }); - // self.last_active_view_id = active_view_id.clone(); - // proto::FollowResponse { - // active_view_id, - // views: self - // .panes() - // .iter() - // .flat_map(|pane| { - // let leader_id = self.leader_for_pane(pane); - // pane.read(cx).items().filter_map({ - // let cx = &cx; - // move |item| { - // let item = item.to_followable_item_handle(cx)?; - // if (project_id.is_none() || project_id != follower_project_id) - // && item.is_project_item(cx) - // { - // return None; - // } - // let id = item.remote_id(client, cx)?.to_proto(); - // let variant = item.to_state_proto(cx)?; - // Some(proto::View { - // id: Some(id), - // leader_id, - // variant: Some(variant), - // }) - // } - // }) - // }) - // .collect(), - // } - // } + // cx.notify(); - // fn handle_update_followers( - // &mut self, - // leader_id: PeerId, - // message: proto::UpdateFollowers, - // _cx: &mut ViewContext, - // ) { - // self.leader_updates_tx - // .unbounded_send((leader_id, message)) - // .ok(); - // } + // self.last_active_view_id = active_view_id.clone(); + // proto::FollowResponse { + // active_view_id, + // views: self + // .panes() + // .iter() + // .flat_map(|pane| { + // let leader_id = self.leader_for_pane(pane); + // pane.read(cx).items().filter_map({ + // let cx = &cx; + // move |item| { + // let item = item.to_followable_item_handle(cx)?; + // if (project_id.is_none() || project_id != follower_project_id) + // && item.is_project_item(cx) + // { + // return None; + // } + // let id = item.remote_id(client, cx)?.to_proto(); + // let variant = item.to_state_proto(cx)?; + // Some(proto::View { + // id: Some(id), + // leader_id, + // variant: Some(variant), + // }) + // } + // }) + // }) + // .collect(), + // } + } - // async fn process_leader_update( - // this: &WeakViewHandle, - // leader_id: PeerId, - // update: proto::UpdateFollowers, - // cx: &mut AsyncAppContext, - // ) -> Result<()> { - // match update.variant.ok_or_else(|| anyhow!("invalid update"))? { - // proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - // this.update(cx, |this, _| { - // for (_, state) in &mut this.follower_states { - // if state.leader_id == leader_id { - // state.active_view_id = - // if let Some(active_view_id) = update_active_view.id.clone() { - // Some(ViewId::from_proto(active_view_id)?) - // } else { - // None - // }; - // } - // } - // anyhow::Ok(()) - // })??; - // } - // proto::update_followers::Variant::UpdateView(update_view) => { - // let variant = update_view - // .variant - // .ok_or_else(|| anyhow!("missing update view variant"))?; - // let id = update_view - // .id - // .ok_or_else(|| anyhow!("missing update view id"))?; - // let mut tasks = Vec::new(); - // this.update(cx, |this, cx| { - // let project = this.project.clone(); - // for (_, state) in &mut this.follower_states { - // if state.leader_id == leader_id { - // let view_id = ViewId::from_proto(id.clone())?; - // if let Some(item) = state.items_by_leader_view_id.get(&view_id) { - // tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); - // } - // } - // } - // anyhow::Ok(()) - // })??; - // try_join_all(tasks).await.log_err(); - // } - // proto::update_followers::Variant::CreateView(view) => { - // let panes = this.read_with(cx, |this, _| { - // this.follower_states - // .iter() - // .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) - // .cloned() - // .collect() - // })?; - // Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; - // } - // } - // this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?; - // Ok(()) - // } + fn handle_update_followers( + &mut self, + leader_id: PeerId, + message: proto::UpdateFollowers, + _cx: &mut ViewContext, + ) { + self.leader_updates_tx + .unbounded_send((leader_id, message)) + .ok(); + } - // async fn add_views_from_leader( - // this: WeakViewHandle, - // leader_id: PeerId, - // panes: Vec>, - // views: Vec, - // cx: &mut AsyncAppContext, - // ) -> Result<()> { - // let this = this - // .upgrade(cx) - // .ok_or_else(|| anyhow!("workspace dropped"))?; + async fn process_leader_update( + this: &WeakView, + leader_id: PeerId, + update: proto::UpdateFollowers, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + match update.variant.ok_or_else(|| anyhow!("invalid update"))? { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + this.update(cx, |this, _| { + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { + state.active_view_id = + if let Some(active_view_id) = update_active_view.id.clone() { + Some(ViewId::from_proto(active_view_id)?) + } else { + None + }; + } + } + anyhow::Ok(()) + })??; + } + proto::update_followers::Variant::UpdateView(update_view) => { + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + let id = update_view + .id + .ok_or_else(|| anyhow!("missing update view id"))?; + let mut tasks = Vec::new(); + this.update(cx, |this, cx| { + let project = this.project.clone(); + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { + let view_id = ViewId::from_proto(id.clone())?; + if let Some(item) = state.items_by_leader_view_id.get(&view_id) { + tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); + } + } + } + anyhow::Ok(()) + })??; + try_join_all(tasks).await.log_err(); + } + proto::update_followers::Variant::CreateView(view) => { + let panes = this.update(cx, |this, _| { + this.follower_states + .iter() + .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) + .cloned() + .collect() + })?; + Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; + } + } + this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?; + Ok(()) + } - // let item_builders = cx.update(|cx| { - // cx.default_global::() - // .values() - // .map(|b| b.0) - // .collect::>() - // }); + async fn add_views_from_leader( + this: WeakView, + leader_id: PeerId, + panes: Vec>, + views: Vec, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let this = this.upgrade().context("workspace dropped")?; - // let mut item_tasks_by_pane = HashMap::default(); - // for pane in panes { - // let mut item_tasks = Vec::new(); - // let mut leader_view_ids = Vec::new(); - // for view in &views { - // let Some(id) = &view.id else { continue }; - // let id = ViewId::from_proto(id.clone())?; - // let mut variant = view.variant.clone(); - // if variant.is_none() { - // Err(anyhow!("missing view variant"))?; - // } - // for build_item in &item_builders { - // let task = cx - // .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)); - // if let Some(task) = task { - // item_tasks.push(task); - // leader_view_ids.push(id); - // break; - // } else { - // assert!(variant.is_some()); - // } - // } - // } + let item_builders = cx.update(|_, cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + })?; - // item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); - // } + let mut item_tasks_by_pane = HashMap::default(); + for pane in panes { + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in &views { + let Some(id) = &view.id else { continue }; + let id = ViewId::from_proto(id.clone())?; + let mut variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing view variant"))?; + } + for build_item in &item_builders { + let task = cx.update(|_, cx| { + build_item(pane.clone(), this.clone(), id, &mut variant, cx) + })?; + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(id); + break; + } else { + assert!(variant.is_some()); + } + } + } - // for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { - // let items = futures::future::try_join_all(item_tasks).await?; - // this.update(cx, |this, cx| { - // let state = this.follower_states.get_mut(&pane)?; - // for (id, item) in leader_view_ids.into_iter().zip(items) { - // item.set_leader_peer_id(Some(leader_id), cx); - // state.items_by_leader_view_id.insert(id, item); - // } + item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); + } - // Some(()) - // }); - // } - // Ok(()) - // } + for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this.follower_states.get_mut(&pane)?; + for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_leader_peer_id(Some(leader_id), cx); + state.items_by_leader_view_id.insert(id, item); + } - // fn update_active_view_for_followers(&mut self, cx: &AppContext) { - // let mut is_project_item = true; - // let mut update = proto::UpdateActiveView::default(); - // if self.active_pane.read(cx).has_focus() { - // let item = self - // .active_item(cx) - // .and_then(|item| item.to_followable_item_handle(cx)); - // if let Some(item) = item { - // is_project_item = item.is_project_item(cx); - // update = proto::UpdateActiveView { - // id: item - // .remote_id(&self.app_state.client, cx) - // .map(|id| id.to_proto()), - // leader_id: self.leader_for_pane(&self.active_pane), - // }; - // } - // } + Some(()) + })?; + } + Ok(()) + } - // if update.id != self.last_active_view_id { - // self.last_active_view_id = update.id.clone(); - // self.update_followers( - // is_project_item, - // proto::update_followers::Variant::UpdateActiveView(update), - // cx, - // ); - // } - // } + fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { + let mut is_project_item = true; + let mut update = proto::UpdateActiveView::default(); + if self.active_pane.read(cx).has_focus() { + let item = self + .active_item(cx) + .and_then(|item| item.to_followable_item_handle(cx)); + if let Some(item) = item { + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { + id: item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()), + leader_id: self.leader_for_pane(&self.active_pane), + }; + } + } - // fn update_followers( - // &self, - // project_only: bool, - // update: proto::update_followers::Variant, - // cx: &AppContext, - // ) -> Option<()> { - // let project_id = if project_only { - // self.project.read(cx).remote_id() - // } else { - // None - // }; - // self.app_state().workspace_store.read_with(cx, |store, cx| { - // store.update_followers(project_id, update, cx) - // }) - // } + if update.id != self.last_active_view_id { + self.last_active_view_id = update.id.clone(); + self.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateActiveView(update), + cx, + ); + } + } - // pub fn leader_for_pane(&self, pane: &View) -> Option { - // self.follower_states.get(pane).map(|state| state.leader_id) - // } + fn update_followers( + &self, + project_only: bool, + update: proto::update_followers::Variant, + cx: &mut WindowContext, + ) -> Option<()> { + let project_id = if project_only { + self.project.read(cx).remote_id() + } else { + None + }; + self.app_state().workspace_store.update(cx, |store, cx| { + store.update_followers(project_id, update, cx) + }) + } - // fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - // cx.notify(); + pub fn leader_for_pane(&self, pane: &View) -> Option { + self.follower_states.get(pane).map(|state| state.leader_id) + } - // let call = self.active_call()?; - // let room = call.read(cx).room()?.read(cx); - // let participant = room.remote_participant_for_peer_id(leader_id)?; - // let mut items_to_activate = Vec::new(); + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + cx.notify(); - // let leader_in_this_app; - // let leader_in_this_project; - // match participant.location { - // call::ParticipantLocation::SharedProject { project_id } => { - // leader_in_this_app = true; - // leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); - // } - // call::ParticipantLocation::UnsharedProject => { - // leader_in_this_app = true; - // leader_in_this_project = false; - // } - // call::ParticipantLocation::External => { - // leader_in_this_app = false; - // leader_in_this_project = false; - // } - // }; + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(leader_id)?; + let mut items_to_activate = Vec::new(); - // for (pane, state) in &self.follower_states { - // if state.leader_id != leader_id { - // continue; - // } - // if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { - // if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { - // if leader_in_this_project || !item.is_project_item(cx) { - // items_to_activate.push((pane.clone(), item.boxed_clone())); - // } - // } else { - // log::warn!( - // "unknown view id {:?} for leader {:?}", - // active_view_id, - // leader_id - // ); - // } - // continue; - // } - // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - // items_to_activate.push((pane.clone(), Box::new(shared_screen))); - // } - // } + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + call2::ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); + } + call2::ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + call2::ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; - // for (pane, item) in items_to_activate { - // let pane_was_focused = pane.read(cx).has_focus(); - // if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { - // pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); - // } else { - // pane.update(cx, |pane, cx| { - // pane.add_item(item.boxed_clone(), false, false, None, cx) - // }); - // } + for (pane, state) in &self.follower_states { + if state.leader_id != leader_id { + continue; + } + if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { + if leader_in_this_project || !item.is_project_item(cx) { + items_to_activate.push((pane.clone(), item.boxed_clone())); + } + } else { + log::warn!( + "unknown view id {:?} for leader {:?}", + active_view_id, + leader_id + ); + } + continue; + } + // todo!() + // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { + // items_to_activate.push((pane.clone(), Box::new(shared_screen))); + // } + } - // if pane_was_focused { - // pane.update(cx, |pane, cx| pane.focus_active_item(cx)); - // } - // } + for (pane, item) in items_to_activate { + let pane_was_focused = pane.read(cx).has_focus(); + if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { + pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); + } else { + pane.update(cx, |pane, cx| { + pane.add_item(item.boxed_clone(), false, false, None, cx) + }); + } - // None - // } + if pane_was_focused { + pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + } + } + None + } + + // todo!() // fn shared_screen_for_peer( // &self, // peer_id: PeerId, @@ -3149,95 +3141,98 @@ impl Workspace { // } // } - // Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) // } - // pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { - // if active { - // self.update_active_view_for_followers(cx); - // cx.background() - // .spawn(persistence::DB.update_timestamp(self.database_id())) - // .detach(); - // } else { - // for pane in &self.panes { - // pane.update(cx, |pane, cx| { - // if let Some(item) = pane.active_item() { - // item.workspace_deactivated(cx); - // } - // if matches!( - // settings::get::(cx).autosave, - // AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange - // ) { - // for item in pane.items() { - // Pane::autosave_item(item.as_ref(), self.project.clone(), cx) - // .detach_and_log_err(cx); - // } - // } - // }); - // } - // } - // } + pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { + if cx.is_window_active() { + self.update_active_view_for_followers(cx); + cx.background_executor() + .spawn(persistence::DB.update_timestamp(self.database_id())) + .detach(); + } else { + for pane in &self.panes { + pane.update(cx, |pane, cx| { + if let Some(item) = pane.active_item() { + item.workspace_deactivated(cx); + } + if matches!( + WorkspaceSettings::get_global(cx).autosave, + AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange + ) { + for item in pane.items() { + Pane::autosave_item(item.as_ref(), self.project.clone(), cx) + .detach_and_log_err(cx); + } + } + }); + } + } + } - // fn active_call(&self) -> Option<&ModelHandle> { - // self.active_call.as_ref().map(|(call, _)| call) - // } + fn active_call(&self) -> Option<&Model> { + self.active_call.as_ref().map(|(call, _)| call) + } - // fn on_active_call_event( - // &mut self, - // _: ModelHandle, - // event: &call::room::Event, - // cx: &mut ViewContext, - // ) { - // match event { - // call::room::Event::ParticipantLocationChanged { participant_id } - // | call::room::Event::RemoteVideoTracksChanged { participant_id } => { - // self.leader_updated(*participant_id, cx); - // } - // _ => {} - // } - // } + fn on_active_call_event( + &mut self, + _: Model, + event: &call2::room::Event, + cx: &mut ViewContext, + ) { + match event { + call2::room::Event::ParticipantLocationChanged { participant_id } + | call2::room::Event::RemoteVideoTracksChanged { participant_id } => { + self.leader_updated(*participant_id, cx); + } + _ => {} + } + } - // pub fn database_id(&self) -> WorkspaceId { - // self.database_id - // } + pub fn database_id(&self) -> WorkspaceId { + self.database_id + } - // fn location(&self, cx: &AppContext) -> Option { - // let project = self.project().read(cx); + fn location(&self, cx: &AppContext) -> Option { + let project = self.project().read(cx); - // if project.is_local() { - // Some( - // project - // .visible_worktrees(cx) - // .map(|worktree| worktree.read(cx).abs_path()) - // .collect::>() - // .into(), - // ) - // } else { - // None - // } - // } + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>() + .into(), + ) + } else { + None + } + } - // fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { - // match member { - // Member::Axis(PaneAxis { members, .. }) => { - // for child in members.iter() { - // self.remove_panes(child.clone(), cx) - // } - // } - // Member::Pane(pane) => { - // self.force_remove_pane(&pane, cx); - // } - // } - // } + fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { + match member { + Member::Axis(PaneAxis { members, .. }) => { + for child in members.iter() { + self.remove_panes(child.clone(), cx) + } + } + Member::Pane(pane) => { + self.force_remove_pane(&pane, cx); + } + } + } - // fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { - // self.panes.retain(|p| p != pane); - // cx.focus(self.panes.last().unwrap()); - // if self.last_active_center_pane == Some(pane.downgrade()) { - // self.last_active_center_pane = None; - // } - // cx.notify(); - // } + fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { + self.panes.retain(|p| p != pane); + if true { + todo!() + // cx.focus(self.panes.last().unwrap()); + } + if self.last_active_center_pane == Some(pane.downgrade()) { + self.last_active_center_pane = None; + } + cx.notify(); + } // fn schedule_serialize(&mut self, cx: &mut ViewContext) { // self._schedule_serialize = Some(cx.spawn(|this, cx| async move { @@ -3247,264 +3242,264 @@ impl Workspace { // })); // } - // fn serialize_workspace(&self, cx: &ViewContext) { - // fn serialize_pane_handle( - // pane_handle: &View, - // cx: &AppContext, - // ) -> SerializedPane { - // let (items, active) = { - // let pane = pane_handle.read(cx); - // let active_item_id = pane.active_item().map(|item| item.id()); - // ( - // pane.items() - // .filter_map(|item_handle| { - // Some(SerializedItem { - // kind: Arc::from(item_handle.serialized_item_kind()?), - // item_id: item_handle.id(), - // active: Some(item_handle.id()) == active_item_id, - // }) - // }) - // .collect::>(), - // pane.has_focus(), - // ) - // }; + fn serialize_workspace(&self, cx: &mut ViewContext) { + fn serialize_pane_handle(pane_handle: &View, cx: &AppContext) -> SerializedPane { + let (items, active) = { + let pane = pane_handle.read(cx); + let active_item_id = pane.active_item().map(|item| item.id()); + ( + pane.items() + .filter_map(|item_handle| { + Some(SerializedItem { + kind: Arc::from(item_handle.serialized_item_kind()?), + item_id: item_handle.id().as_u64() as usize, + active: Some(item_handle.id()) == active_item_id, + }) + }) + .collect::>(), + pane.has_focus(), + ) + }; - // SerializedPane::new(items, active) - // } + SerializedPane::new(items, active) + } - // fn build_serialized_pane_group( - // pane_group: &Member, - // cx: &AppContext, - // ) -> SerializedPaneGroup { - // match pane_group { - // Member::Axis(PaneAxis { - // axis, - // members, - // flexes, - // bounding_boxes: _, - // }) => SerializedPaneGroup::Group { - // axis: *axis, - // children: members - // .iter() - // .map(|member| build_serialized_pane_group(member, cx)) - // .collect::>(), - // flexes: Some(flexes.borrow().clone()), - // }, - // Member::Pane(pane_handle) => { - // SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) - // } - // } - // } + fn build_serialized_pane_group( + pane_group: &Member, + cx: &AppContext, + ) -> SerializedPaneGroup { + match pane_group { + Member::Axis(PaneAxis { + axis, + members, + flexes, + bounding_boxes: _, + }) => SerializedPaneGroup::Group { + axis: *axis, + children: members + .iter() + .map(|member| build_serialized_pane_group(member, cx)) + .collect::>(), + flexes: Some(flexes.lock().clone()), + }, + Member::Pane(pane_handle) => { + SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) + } + } + } - // fn build_serialized_docks(this: &Workspace, cx: &ViewContext) -> DockStructure { - // let left_dock = this.left_dock.read(cx); - // let left_visible = left_dock.is_open(); - // let left_active_panel = left_dock.visible_panel().and_then(|panel| { - // Some( - // cx.view_ui_name(panel.as_any().window(), panel.id())? - // .to_string(), - // ) - // }); - // let left_dock_zoom = left_dock - // .visible_panel() - // .map(|panel| panel.is_zoomed(cx)) - // .unwrap_or(false); + fn build_serialized_docks( + this: &Workspace, + cx: &mut ViewContext, + ) -> DockStructure { + let left_dock = this.left_dock.read(cx); + let left_visible = left_dock.is_open(); + let left_active_panel = left_dock + .visible_panel() + .and_then(|panel| Some(panel.persistent_name(cx).to_string())); + let left_dock_zoom = left_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); - // let right_dock = this.right_dock.read(cx); - // let right_visible = right_dock.is_open(); - // let right_active_panel = right_dock.visible_panel().and_then(|panel| { - // Some( - // cx.view_ui_name(panel.as_any().window(), panel.id())? - // .to_string(), - // ) - // }); - // let right_dock_zoom = right_dock - // .visible_panel() - // .map(|panel| panel.is_zoomed(cx)) - // .unwrap_or(false); + let right_dock = this.right_dock.read(cx); + let right_visible = right_dock.is_open(); + let right_active_panel = right_dock + .visible_panel() + .and_then(|panel| Some(panel.persistent_name(cx).to_string())); + let right_dock_zoom = right_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); - // let bottom_dock = this.bottom_dock.read(cx); - // let bottom_visible = bottom_dock.is_open(); - // let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| { - // Some( - // cx.view_ui_name(panel.as_any().window(), panel.id())? - // .to_string(), - // ) - // }); - // let bottom_dock_zoom = bottom_dock - // .visible_panel() - // .map(|panel| panel.is_zoomed(cx)) - // .unwrap_or(false); + let bottom_dock = this.bottom_dock.read(cx); + let bottom_visible = bottom_dock.is_open(); + let bottom_active_panel = bottom_dock + .visible_panel() + .and_then(|panel| Some(panel.persistent_name(cx).to_string())); + let bottom_dock_zoom = bottom_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); - // DockStructure { - // left: DockData { - // visible: left_visible, - // active_panel: left_active_panel, - // zoom: left_dock_zoom, - // }, - // right: DockData { - // visible: right_visible, - // active_panel: right_active_panel, - // zoom: right_dock_zoom, - // }, - // bottom: DockData { - // visible: bottom_visible, - // active_panel: bottom_active_panel, - // zoom: bottom_dock_zoom, - // }, - // } - // } + DockStructure { + left: DockData { + visible: left_visible, + active_panel: left_active_panel, + zoom: left_dock_zoom, + }, + right: DockData { + visible: right_visible, + active_panel: right_active_panel, + zoom: right_dock_zoom, + }, + bottom: DockData { + visible: bottom_visible, + active_panel: bottom_active_panel, + zoom: bottom_dock_zoom, + }, + } + } - // if let Some(location) = self.location(cx) { - // // Load bearing special case: - // // - with_local_workspace() relies on this to not have other stuff open - // // when you open your log - // if !location.paths().is_empty() { - // let center_group = build_serialized_pane_group(&self.center.root, cx); - // let docks = build_serialized_docks(self, cx); + if let Some(location) = self.location(cx) { + // Load bearing special case: + // - with_local_workspace() relies on this to not have other stuff open + // when you open your log + if !location.paths().is_empty() { + let center_group = build_serialized_pane_group(&self.center.root, cx); + let docks = build_serialized_docks(self, cx); - // let serialized_workspace = SerializedWorkspace { - // id: self.database_id, - // location, - // center_group, - // bounds: Default::default(), - // display: Default::default(), - // docks, - // }; + let serialized_workspace = SerializedWorkspace { + id: self.database_id, + location, + center_group, + bounds: Default::default(), + display: Default::default(), + docks, + }; - // cx.background() - // .spawn(persistence::DB.save_workspace(serialized_workspace)) - // .detach(); - // } - // } - // } + cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace)) + .detach(); + } + } + } - // pub(crate) fn load_workspace( - // workspace: WeakViewHandle, - // serialized_workspace: SerializedWorkspace, - // paths_to_open: Vec>, - // cx: &mut AppContext, - // ) -> Task>>>> { - // cx.spawn(|mut cx| async move { - // let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { - // ( - // workspace.project().clone(), - // workspace.last_active_center_pane.clone(), - // ) - // })?; + pub(crate) fn load_workspace( + serialized_workspace: SerializedWorkspace, + paths_to_open: Vec>, + cx: &mut ViewContext, + ) -> Task>>>> { + cx.spawn(|workspace, mut cx| async move { + let (project, old_center_pane) = workspace.update(&mut cx, |workspace, _| { + ( + workspace.project().clone(), + workspace.last_active_center_pane.clone(), + ) + })?; - // let mut center_group = None; - // let mut center_items = None; - // // Traverse the splits tree and add to things - // if let Some((group, active_pane, items)) = serialized_workspace - // .center_group - // .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) - // .await - // { - // center_items = Some(items); - // center_group = Some((group, active_pane)) - // } + let mut center_group = None; + let mut center_items = None; - // let mut items_by_project_path = cx.read(|cx| { - // center_items - // .unwrap_or_default() - // .into_iter() - // .filter_map(|item| { - // let item = item?; - // let project_path = item.project_path(cx)?; - // Some((project_path, item)) - // }) - // .collect::>() - // }); + // Traverse the splits tree and add to things + if let Some((group, active_pane, items)) = serialized_workspace + .center_group + .deserialize( + &project, + serialized_workspace.id, + workspace.clone(), + &mut cx, + ) + .await + { + center_items = Some(items); + center_group = Some((group, active_pane)) + } - // let opened_items = paths_to_open - // .into_iter() - // .map(|path_to_open| { - // path_to_open - // .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) - // }) - // .collect::>(); + let mut items_by_project_path = cx.update(|_, cx| { + center_items + .unwrap_or_default() + .into_iter() + .filter_map(|item| { + let item = item?; + let project_path = item.project_path(cx)?; + Some((project_path, item)) + }) + .collect::>() + })?; - // // Remove old panes from workspace panes list - // workspace.update(&mut cx, |workspace, cx| { - // if let Some((center_group, active_pane)) = center_group { - // workspace.remove_panes(workspace.center.root.clone(), cx); + let opened_items = paths_to_open + .into_iter() + .map(|path_to_open| { + path_to_open + .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) + }) + .collect::>(); - // // Swap workspace center group - // workspace.center = PaneGroup::with_root(center_group); + // Remove old panes from workspace panes list + workspace.update(&mut cx, |workspace, cx| { + if let Some((center_group, active_pane)) = center_group { + workspace.remove_panes(workspace.center.root.clone(), cx); - // // Change the focus to the workspace first so that we retrigger focus in on the pane. - // cx.focus_self(); + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); - // if let Some(active_pane) = active_pane { - // cx.focus(&active_pane); - // } else { - // cx.focus(workspace.panes.last().unwrap()); - // } - // } else { - // let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - // if let Some(old_center_handle) = old_center_handle { - // cx.focus(&old_center_handle) - // } else { - // cx.focus_self() - // } - // } + // Change the focus to the workspace first so that we retrigger focus in on the pane. + // todo!() + // cx.focus_self(); + // if let Some(active_pane) = active_pane { + // cx.focus(&active_pane); + // } else { + // cx.focus(workspace.panes.last().unwrap()); + // } + } else { + // todo!() + // let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade()); + // if let Some(old_center_handle) = old_center_handle { + // cx.focus(&old_center_handle) + // } else { + // cx.focus_self() + // } + } - // let docks = serialized_workspace.docks; - // workspace.left_dock.update(cx, |dock, cx| { - // dock.set_open(docks.left.visible, cx); - // if let Some(active_panel) = docks.left.active_panel { - // if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - // dock.activate_panel(ix, cx); - // } - // } - // dock.active_panel() - // .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); - // if docks.left.visible && docks.left.zoom { - // cx.focus_self() - // } - // }); - // // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something - // workspace.right_dock.update(cx, |dock, cx| { - // dock.set_open(docks.right.visible, cx); - // if let Some(active_panel) = docks.right.active_panel { - // if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - // dock.activate_panel(ix, cx); - // } - // } - // dock.active_panel() - // .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); + let docks = serialized_workspace.docks; + workspace.left_dock.update(cx, |dock, cx| { + dock.set_open(docks.left.visible, cx); + if let Some(active_panel) = docks.left.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); + if docks.left.visible && docks.left.zoom { + // todo!() + // cx.focus_self() + } + }); + // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something + workspace.right_dock.update(cx, |dock, cx| { + dock.set_open(docks.right.visible, cx); + if let Some(active_panel) = docks.right.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); - // if docks.right.visible && docks.right.zoom { - // cx.focus_self() - // } - // }); - // workspace.bottom_dock.update(cx, |dock, cx| { - // dock.set_open(docks.bottom.visible, cx); - // if let Some(active_panel) = docks.bottom.active_panel { - // if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - // dock.activate_panel(ix, cx); - // } - // } + if docks.right.visible && docks.right.zoom { + // todo!() + // cx.focus_self() + } + }); + workspace.bottom_dock.update(cx, |dock, cx| { + dock.set_open(docks.bottom.visible, cx); + if let Some(active_panel) = docks.bottom.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } - // dock.active_panel() - // .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); - // if docks.bottom.visible && docks.bottom.zoom { - // cx.focus_self() - // } - // }); + if docks.bottom.visible && docks.bottom.zoom { + // todo!() + // cx.focus_self() + } + }); - // cx.notify(); - // })?; + cx.notify(); + })?; - // // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - // workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?; - // Ok(opened_items) - // }) - // } + Ok(opened_items) + }) + } + // todo!() // #[cfg(any(test, feature = "test-support"))] // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { // use node_runtime::FakeNodeRuntime; @@ -3556,209 +3551,372 @@ impl Workspace { // ) // } // } - - // fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { - // ZED_WINDOW_POSITION - // .zip(*ZED_WINDOW_SIZE) - // .map(|(position, size)| { - // WindowBounds::Fixed(RectF::new( - // cx.platform().screens()[0].bounds().origin() + position, - // size, - // )) - // }) - // } - - // async fn open_items( - // serialized_workspace: Option, - // workspace: &WeakViewHandle, - // mut project_paths_to_open: Vec<(PathBuf, Option)>, - // app_state: Arc, - // mut cx: AsyncAppContext, - // ) -> Result>>>> { - // let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - - // if let Some(serialized_workspace) = serialized_workspace { - // let workspace = workspace.clone(); - // let restored_items = cx - // .update(|cx| { - // Workspace::load_workspace( - // workspace, - // serialized_workspace, - // project_paths_to_open - // .iter() - // .map(|(_, project_path)| project_path) - // .cloned() - // .collect(), - // cx, - // ) - // }) - // .await?; - - // let restored_project_paths = cx.read(|cx| { - // restored_items - // .iter() - // .filter_map(|item| item.as_ref()?.project_path(cx)) - // .collect::>() - // }); - - // for restored_item in restored_items { - // opened_items.push(restored_item.map(Ok)); - // } - - // project_paths_to_open - // .iter_mut() - // .for_each(|(_, project_path)| { - // if let Some(project_path_to_open) = project_path { - // if restored_project_paths.contains(project_path_to_open) { - // *project_path = None; - // } - // } - // }); - // } else { - // for _ in 0..project_paths_to_open.len() { - // opened_items.push(None); - // } - // } - // assert!(opened_items.len() == project_paths_to_open.len()); - - // let tasks = - // project_paths_to_open - // .into_iter() - // .enumerate() - // .map(|(i, (abs_path, project_path))| { - // let workspace = workspace.clone(); - // cx.spawn(|mut cx| { - // let fs = app_state.fs.clone(); - // async move { - // let file_project_path = project_path?; - // if fs.is_file(&abs_path).await { - // Some(( - // i, - // workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_path(file_project_path, None, true, cx) - // }) - // .log_err()? - // .await, - // )) - // } else { - // None - // } - // } - // }) - // }); - - // for maybe_opened_path in futures::future::join_all(tasks.into_iter()) - // .await - // .into_iter() - // { - // if let Some((i, path_open_result)) = maybe_opened_path { - // opened_items[i] = Some(path_open_result); - // } - // } - - // Ok(opened_items) - // } - - // fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { - // const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; - // const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; - // const MESSAGE_ID: usize = 2; - - // if workspace - // .read_with(cx, |workspace, cx| { - // workspace.has_shown_notification_once::(MESSAGE_ID, cx) - // }) - // .unwrap_or(false) - // { - // return; - // } - - // if db::kvp::KEY_VALUE_STORE - // .read_kvp(NEW_DOCK_HINT_KEY) - // .ok() - // .flatten() - // .is_some() - // { - // if !workspace - // .read_with(cx, |workspace, cx| { - // workspace.has_shown_notification_once::(MESSAGE_ID, cx) - // }) - // .unwrap_or(false) - // { - // cx.update(|cx| { - // cx.update_global::(|tracker, _| { - // let entry = tracker - // .entry(TypeId::of::()) - // .or_default(); - // if !entry.contains(&MESSAGE_ID) { - // entry.push(MESSAGE_ID); - // } - // }); - // }); - // } - - // return; - // } - - // cx.spawn(|_| async move { - // db::kvp::KEY_VALUE_STORE - // .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) - // .await - // .ok(); - // }) - // .detach(); - - // workspace - // .update(cx, |workspace, cx| { - // workspace.show_notification_once(2, cx, |cx| { - // cx.add_view(|_| { - // MessageNotification::new_element(|text, _| { - // Text::new( - // "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", - // text, - // ) - // .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { - // let code_span_background_color = settings::get::(cx) - // .theme - // .editor - // .document_highlight_read_background; - - // cx.scene().push_quad(gpui::Quad { - // bounds, - // background: Some(code_span_background_color), - // border: Default::default(), - // corner_radii: (2.0).into(), - // }) - // }) - // .into_any() - // }) - // .with_click_message("Read more about the new panel system") - // .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) - // }) - // }) - // }) - // .ok(); } -// fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { -// const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; +fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { + let display_origin = cx + .update(|cx| Some(cx.displays().first()?.bounds().origin)) + .ok()??; + ZED_WINDOW_POSITION + .zip(*ZED_WINDOW_SIZE) + .map(|(position, size)| { + WindowBounds::Fixed(Bounds { + origin: display_origin + position, + size, + }) + }) +} + +fn open_items( + serialized_workspace: Option, + mut project_paths_to_open: Vec<(PathBuf, Option)>, + app_state: Arc, + cx: &mut ViewContext, +) -> impl 'static + Future>>>>> { + let restored_items = serialized_workspace.map(|serialized_workspace| { + Workspace::load_workspace( + serialized_workspace, + project_paths_to_open + .iter() + .map(|(_, project_path)| project_path) + .cloned() + .collect(), + cx, + ) + }); + + cx.spawn(|workspace, mut cx| async move { + let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); + + if let Some(restored_items) = restored_items { + let restored_items = restored_items.await?; + + let restored_project_paths = restored_items + .iter() + .filter_map(|item| { + cx.update(|_, cx| item.as_ref()?.project_path(cx)) + .ok() + .flatten() + }) + .collect::>(); + + for restored_item in restored_items { + opened_items.push(restored_item.map(Ok)); + } + + project_paths_to_open + .iter_mut() + .for_each(|(_, project_path)| { + if let Some(project_path_to_open) = project_path { + if restored_project_paths.contains(project_path_to_open) { + *project_path = None; + } + } + }); + } else { + for _ in 0..project_paths_to_open.len() { + opened_items.push(None); + } + } + assert!(opened_items.len() == project_paths_to_open.len()); + + let tasks = + project_paths_to_open + .into_iter() + .enumerate() + .map(|(i, (abs_path, project_path))| { + let workspace = workspace.clone(); + cx.spawn(|mut cx| { + let fs = app_state.fs.clone(); + async move { + let file_project_path = project_path?; + if fs.is_file(&abs_path).await { + Some(( + i, + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path(file_project_path, None, true, cx) + }) + .log_err()? + .await, + )) + } else { + None + } + } + }) + }); + + let tasks = tasks.collect::>(); + + let tasks = futures::future::join_all(tasks.into_iter()); + for maybe_opened_path in tasks.await.into_iter() { + if let Some((i, path_open_result)) = maybe_opened_path { + opened_items[i] = Some(path_open_result); + } + } + + Ok(opened_items) + }) +} + +// todo!() +// fn notify_of_new_dock(workspace: &WeakView, cx: &mut AsyncAppContext) { +// const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; +// const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; +// const MESSAGE_ID: usize = 2; + +// if workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// return; +// } + +// if db::kvp::KEY_VALUE_STORE +// .read_kvp(NEW_DOCK_HINT_KEY) +// .ok() +// .flatten() +// .is_some() +// { +// if !workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// cx.update(|cx| { +// cx.update_global::(|tracker, _| { +// let entry = tracker +// .entry(TypeId::of::()) +// .or_default(); +// if !entry.contains(&MESSAGE_ID) { +// entry.push(MESSAGE_ID); +// } +// }); +// }); +// } + +// return; +// } + +// cx.spawn(|_| async move { +// db::kvp::KEY_VALUE_STORE +// .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) +// .await +// .ok(); +// }) +// .detach(); // workspace // .update(cx, |workspace, cx| { -// if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { -// workspace.show_notification_once(0, cx, |cx| { -// cx.add_view(|_| { -// MessageNotification::new("Failed to load the database file.") -// .with_click_message("Click to let us know about this error") -// .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL)) -// }) -// }); -// } -// }) -// .log_err(); -// } +// workspace.show_notification_once(2, cx, |cx| { +// cx.build_view(|_| { +// MessageNotification::new_element(|text, _| { +// Text::new( +// "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", +// text, +// ) +// .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { +// let code_span_background_color = settings::get::(cx) +// .theme +// .editor +// .document_highlight_read_background; +// cx.scene().push_quad(gpui::Quad { +// bounds, +// background: Some(code_span_background_color), +// border: Default::default(), +// corner_radii: (2.0).into(), +// }) +// }) +// .into_any() +// }) +// .with_click_message("Read more about the new panel system") +// .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) +// }) +// }) +// }) +// .ok(); + +fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncAppContext) { + const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; + + workspace + .update(cx, |workspace, cx| { + if (*db2::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { + workspace.show_notification_once(0, cx, |cx| { + cx.build_view(|_| { + MessageNotification::new("Failed to load the database file.") + .with_click_message("Click to let us know about this error") + .on_click(|cx| cx.open_url(REPORT_ISSUE_URL)) + }) + }); + } + }) + .log_err(); +} + +impl EventEmitter for Workspace { + type Event = Event; +} + +impl Render for Workspace { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div() + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + div() + .flex_1() + .w_full() + .flex() + .flex_row() + .overflow_hidden() + .border_t() + .border_b() + .border_color(cx.theme().colors().border) + // .children( + // Some( + // Panel::new("project-panel-outer", cx) + // .side(PanelSide::Left) + // .child(ProjectPanel::new("project-panel-inner")), + // ) + // .filter(|_| self.is_project_panel_open()), + // ) + // .children( + // Some( + // Panel::new("collab-panel-outer", cx) + // .child(CollabPanel::new("collab-panel-inner")) + // .side(PanelSide::Left), + // ) + // .filter(|_| self.is_collab_panel_open()), + // ) + // .child(NotificationToast::new( + // "maxbrunsfeld has requested to add you as a contact.".into(), + // )) + .child( + div().flex().flex_col().flex_1().h_full().child( + div().flex().flex_1().child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )), + ), // .children( + // Some( + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), + // ) + // .filter(|_| self.is_terminal_open()), + // ), + ), // .children( + // Some( + // Panel::new("chat-panel-outer", cx) + // .side(PanelSide::Right) + // .child(ChatPanel::new("chat-panel-inner").messages(vec![ + // ChatMessage::new( + // "osiewicz".to_string(), + // "is this thing on?".to_string(), + // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ChatMessage::new( + // "maxdeviant".to_string(), + // "Reading you loud and clear!".to_string(), + // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ])), + // ) + // .filter(|_| self.is_chat_panel_open()), + // ) + // .children( + // Some( + // Panel::new("notifications-panel-outer", cx) + // .side(PanelSide::Right) + // .child(NotificationsPanel::new("notifications-panel-inner")), + // ) + // .filter(|_| self.is_notifications_panel_open()), + // ) + // .children( + // Some( + // Panel::new("assistant-panel-outer", cx) + // .child(AssistantPanel::new("assistant-panel-inner")), + // ) + // .filter(|_| self.is_assistant_panel_open()), + // ), + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) + } +} + +// todo!() // impl Entity for Workspace { // type Event = Event; @@ -3909,168 +4067,161 @@ impl Workspace { // } // } -// impl WorkspaceStore { -// pub fn new(client: Arc, cx: &mut ModelContext) -> Self { -// Self { -// workspaces: Default::default(), -// followers: Default::default(), -// _subscriptions: vec![ -// client.add_request_handler(cx.handle(), Self::handle_follow), -// client.add_message_handler(cx.handle(), Self::handle_unfollow), -// client.add_message_handler(cx.handle(), Self::handle_update_followers), -// ], -// client, -// } -// } +impl WorkspaceStore { + pub fn new(client: Arc, _cx: &mut ModelContext) -> Self { + Self { + workspaces: Default::default(), + followers: Default::default(), + _subscriptions: vec![], + // client.add_request_handler(cx.weak_model(), Self::handle_follow), + // client.add_message_handler(cx.weak_model(), Self::handle_unfollow), + // client.add_message_handler(cx.weak_model(), Self::handle_update_followers), + // ], + client, + } + } -// pub fn update_followers( -// &self, -// project_id: Option, -// update: proto::update_followers::Variant, -// cx: &AppContext, -// ) -> Option<()> { -// if !cx.has_global::>() { -// return None; -// } + pub fn update_followers( + &self, + project_id: Option, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + if !cx.has_global::>() { + return None; + } -// let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); -// let follower_ids: Vec<_> = self -// .followers -// .iter() -// .filter_map(|follower| { -// if follower.project_id == project_id || project_id.is_none() { -// Some(follower.peer_id.into()) -// } else { -// None -// } -// }) -// .collect(); -// if follower_ids.is_empty() { -// return None; -// } -// self.client -// .send(proto::UpdateFollowers { -// room_id, -// project_id, -// follower_ids, -// variant: Some(update), -// }) -// .log_err() -// } + let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); + let follower_ids: Vec<_> = self + .followers + .iter() + .filter_map(|follower| { + if follower.project_id == project_id || project_id.is_none() { + Some(follower.peer_id.into()) + } else { + None + } + }) + .collect(); + if follower_ids.is_empty() { + return None; + } + self.client + .send(proto::UpdateFollowers { + room_id, + project_id, + follower_ids, + variant: Some(update), + }) + .log_err() + } -// async fn handle_follow( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result { -// this.update(&mut cx, |this, cx| { -// let follower = Follower { -// project_id: envelope.payload.project_id, -// peer_id: envelope.original_sender_id()?, -// }; -// let active_project = ActiveCall::global(cx) -// .read(cx) -// .location() -// .map(|project| project.id()); + pub async fn handle_follow( + this: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + let active_project = ActiveCall::global(cx).read(cx).location().cloned(); -// let mut response = proto::FollowResponse::default(); -// for workspace in &this.workspaces { -// let Some(workspace) = workspace.upgrade(cx) else { -// continue; -// }; + let mut response = proto::FollowResponse::default(); + for workspace in &this.workspaces { + workspace + .update(cx, |workspace, cx| { + let handler_response = workspace.handle_follow(follower.project_id, cx); + if response.views.is_empty() { + response.views = handler_response.views; + } else { + response.views.extend_from_slice(&handler_response.views); + } -// workspace.update(cx.as_mut(), |workspace, cx| { -// let handler_response = workspace.handle_follow(follower.project_id, cx); -// if response.views.is_empty() { -// response.views = handler_response.views; -// } else { -// response.views.extend_from_slice(&handler_response.views); -// } + if let Some(active_view_id) = handler_response.active_view_id.clone() { + if response.active_view_id.is_none() + || Some(workspace.project.downgrade()) == active_project + { + response.active_view_id = Some(active_view_id); + } + } + }) + .ok(); + } -// if let Some(active_view_id) = handler_response.active_view_id.clone() { -// if response.active_view_id.is_none() -// || Some(workspace.project.id()) == active_project -// { -// response.active_view_id = Some(active_view_id); -// } -// } -// }); -// } + if let Err(ix) = this.followers.binary_search(&follower) { + this.followers.insert(ix, follower); + } -// if let Err(ix) = this.followers.binary_search(&follower) { -// this.followers.insert(ix, follower); -// } + Ok(response) + })? + } -// Ok(response) -// }) -// } + async fn handle_unfollow( + model: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + model.update(&mut cx, |this, _| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + if let Ok(ix) = this.followers.binary_search(&follower) { + this.followers.remove(ix); + } + Ok(()) + })? + } -// async fn handle_unfollow( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result<()> { -// this.update(&mut cx, |this, _| { -// let follower = Follower { -// project_id: envelope.payload.project_id, -// peer_id: envelope.original_sender_id()?, -// }; -// if let Ok(ix) = this.followers.binary_search(&follower) { -// this.followers.remove(ix); -// } -// Ok(()) -// }) -// } + async fn handle_update_followers( + this: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncWindowContext, + ) -> Result<()> { + let leader_id = envelope.original_sender_id()?; + let update = envelope.payload; -// async fn handle_update_followers( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result<()> { -// let leader_id = envelope.original_sender_id()?; -// let update = envelope.payload; -// this.update(&mut cx, |this, cx| { -// for workspace in &this.workspaces { -// let Some(workspace) = workspace.upgrade(cx) else { -// continue; -// }; -// workspace.update(cx.as_mut(), |workspace, cx| { -// let project_id = workspace.project.read(cx).remote_id(); -// if update.project_id != project_id && update.project_id.is_some() { -// return; -// } -// workspace.handle_update_followers(leader_id, update.clone(), cx); -// }); -// } -// Ok(()) -// }) -// } -// } + this.update(&mut cx, |this, cx| { + for workspace in &this.workspaces { + workspace.update(cx, |workspace, cx| { + let project_id = workspace.project.read(cx).remote_id(); + if update.project_id != project_id && update.project_id.is_some() { + return; + } + workspace.handle_update_followers(leader_id, update.clone(), cx); + })?; + } + Ok(()) + })? + } +} -// impl Entity for WorkspaceStore { -// type Event = (); -// } +impl EventEmitter for WorkspaceStore { + type Event = (); +} -// impl ViewId { -// pub(crate) fn from_proto(message: proto::ViewId) -> Result { -// Ok(Self { -// creator: message -// .creator -// .ok_or_else(|| anyhow!("creator is missing"))?, -// id: message.id, -// }) -// } +impl ViewId { + pub(crate) fn from_proto(message: proto::ViewId) -> Result { + Ok(Self { + creator: message + .creator + .ok_or_else(|| anyhow!("creator is missing"))?, + id: message.id, + }) + } -// pub(crate) fn to_proto(&self) -> proto::ViewId { -// proto::ViewId { -// creator: Some(self.creator), -// id: self.id, -// } -// } -// } + pub(crate) fn to_proto(&self) -> proto::ViewId { + proto::ViewId { + creator: Some(self.creator), + id: self.id, + } + } +} // pub trait WorkspaceHandle { // fn file_project_paths(&self, cx: &AppContext) -> Vec; @@ -4099,45 +4250,41 @@ impl Workspace { // } // } -// pub struct WorkspaceCreated(pub WeakViewHandle); +// pub struct WorkspaceCreated(pub WeakView); -pub async fn activate_workspace_for_project( - cx: &mut AsyncAppContext, +pub fn activate_workspace_for_project( + cx: &mut AppContext, predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static, ) -> Option> { - cx.run_on_main(move |cx| { - for window in cx.windows() { - let Some(workspace) = window.downcast::() else { - continue; - }; + for window in cx.windows() { + let Some(workspace) = window.downcast::() else { + continue; + }; - let predicate = cx - .update_window_root(&workspace, |workspace, cx| { - let project = workspace.project.read(cx); - if predicate(project, cx) { - cx.activate_window(); - true - } else { - false - } - }) - .log_err() - .unwrap_or(false); + let predicate = workspace + .update(cx, |workspace, cx| { + let project = workspace.project.read(cx); + if predicate(project, cx) { + cx.activate_window(); + true + } else { + false + } + }) + .log_err() + .unwrap_or(false); - if predicate { - return Some(workspace); - } + if predicate { + return Some(workspace); } + } - None - }) - .ok()? - .await + None } -// pub async fn last_opened_workspace_paths() -> Option { -// DB.last_workspace().await.log_err().flatten() -// } +pub async fn last_opened_workspace_paths() -> Option { + DB.last_workspace().await.log_err().flatten() +} // async fn join_channel_internal( // channel_id: u64, @@ -4321,27 +4468,6 @@ pub async fn activate_workspace_for_project( // None // } -use client2::{ - proto::{self, PeerId, ViewId}, - Client, UserStore, -}; -use collections::{HashMap, HashSet}; -use gpui2::{ - AnyHandle, AnyView, AppContext, AsyncAppContext, DisplayId, Handle, MainThread, Task, View, - ViewContext, WeakHandle, WeakView, WindowBounds, WindowHandle, WindowOptions, -}; -use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; -use language2::LanguageRegistry; -use node_runtime::NodeRuntime; -use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; -use std::{ - any::TypeId, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use util::ResultExt; - #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], @@ -4356,50 +4482,48 @@ pub fn open_paths( > { let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); - cx.spawn(|mut cx| async move { - // Open paths in existing workspace if possible - let existing = activate_workspace_for_project(&mut cx, |project, cx| { - project.contains_paths(&abs_paths, cx) - }) - .await; - + // Open paths in existing workspace if possible + let existing = activate_workspace_for_project(cx, { + let abs_paths = abs_paths.clone(); + move |project, cx| project.contains_paths(&abs_paths, cx) + }); + cx.spawn(move |mut cx| async move { if let Some(existing) = existing { - Ok(( - existing.clone(), - cx.update_window_root(&existing, |workspace, cx| { - workspace.open_paths(abs_paths, true, cx) - })? - .await, - )) - } else { + // // Ok(( + // existing.clone(), + // cx.update_window_root(&existing, |workspace, cx| { + // workspace.open_paths(abs_paths, true, cx) + // })? + // .await, + // )) todo!() - // Ok(cx - // .update(|cx| { - // Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) - // }) - // .await) + } else { + cx.update(move |cx| { + Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) + })? + .await } }) } -// pub fn open_new( -// app_state: &Arc, -// cx: &mut AppContext, -// init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static, -// ) -> Task<()> { -// let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); -// cx.spawn(|mut cx| async move { -// let (workspace, opened_paths) = task.await; - -// workspace -// .update(&mut cx, |workspace, cx| { -// if opened_paths.is_empty() { -// init(workspace, cx) -// } -// }) -// .log_err(); -// }) -// } +pub fn open_new( + app_state: &Arc, + cx: &mut AppContext, + init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static + Send, +) -> Task<()> { + let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); + cx.spawn(|mut cx| async move { + if let Some((workspace, opened_paths)) = task.await.log_err() { + workspace + .update(&mut cx, |workspace, cx| { + if opened_paths.is_empty() { + init(workspace, cx) + } + }) + .log_err(); + } + }) +} // pub fn create_and_open_local_file( // path: &'static Path, @@ -4569,12 +4693,19 @@ pub fn open_paths( // .detach_and_log_err(cx); // } -// fn parse_pixel_position_env_var(value: &str) -> Option { -// let mut parts = value.split(','); -// let width: usize = parts.next()?.parse().ok()?; -// let height: usize = parts.next()?.parse().ok()?; -// Some(vec2f(width as f32, height as f32)) -// } +fn parse_pixel_position_env_var(value: &str) -> Option> { + let mut parts = value.split(','); + let x: usize = parts.next()?.parse().ok()?; + let y: usize = parts.next()?.parse().ok()?; + Some(point((x as f64).into(), (y as f64).into())) +} + +fn parse_pixel_size_env_var(value: &str) -> Option> { + let mut parts = value.split(','); + let width: usize = parts.next()?.parse().ok()?; + let height: usize = parts.next()?.parse().ok()?; + Some(size((width as f64).into(), (height as f64).into())) +} // #[cfg(test)] // mod tests { @@ -4600,7 +4731,7 @@ pub fn open_paths( // let workspace = window.root(cx); // // Adding an item with no ambiguity renders the tab without detail. -// let item1 = window.add_view(cx, |_| { +// let item1 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); // item @@ -4612,7 +4743,7 @@ pub fn open_paths( // // Adding an item that creates ambiguity increases the level of detail on // // both tabs. -// let item2 = window.add_view(cx, |_| { +// let item2 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); // item @@ -4626,7 +4757,7 @@ pub fn open_paths( // // Adding an item that creates ambiguity increases the level of detail only // // on the ambiguous tabs. In this case, the ambiguity can't be resolved so // // we stop at the highest detail available. -// let item3 = window.add_view(cx, |_| { +// let item3 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); // item @@ -4668,10 +4799,10 @@ pub fn open_paths( // project.worktrees(cx).next().unwrap().read(cx).id() // }); -// let item1 = window.add_view(cx, |cx| { +// let item1 = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) // }); -// let item2 = window.add_view(cx, |cx| { +// let item2 = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) // }); @@ -4744,15 +4875,15 @@ pub fn open_paths( // let workspace = window.root(cx); // // When there are no dirty items, there's nothing to do. -// let item1 = window.add_view(cx, |_| TestItem::new()); +// let item1 = window.build_view(cx, |_| TestItem::new()); // workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); // let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); // assert!(task.await.unwrap()); // // When there are dirty untitled items, prompt to save each one. If the user // // cancels any prompt, then abort. -// let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true)); -// let item3 = window.add_view(cx, |cx| { +// let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true)); +// let item3 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) @@ -4781,24 +4912,24 @@ pub fn open_paths( // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); // let workspace = window.root(cx); -// let item1 = window.add_view(cx, |cx| { +// let item1 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) // }); -// let item2 = window.add_view(cx, |cx| { +// let item2 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_conflict(true) // .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) // }); -// let item3 = window.add_view(cx, |cx| { +// let item3 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_conflict(true) // .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) // }); -// let item4 = window.add_view(cx, |cx| { +// let item4 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new_untitled(cx)]) @@ -4892,7 +5023,7 @@ pub fn open_paths( // // workspace items with multiple project entries. // let single_entry_items = (0..=4) // .map(|project_entry_id| { -// window.add_view(cx, |cx| { +// window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new( @@ -4903,7 +5034,7 @@ pub fn open_paths( // }) // }) // .collect::>(); -// let item_2_3 = window.add_view(cx, |cx| { +// let item_2_3 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_singleton(false) @@ -4912,7 +5043,7 @@ pub fn open_paths( // single_entry_items[3].read(cx).project_items[0].clone(), // ]) // }); -// let item_3_4 = window.add_view(cx, |cx| { +// let item_3_4 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_singleton(false) @@ -4999,7 +5130,7 @@ pub fn open_paths( // let workspace = window.root(cx); // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let item = window.add_view(cx, |cx| { +// let item = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) // }); // let item_id = item.id(); @@ -5119,7 +5250,7 @@ pub fn open_paths( // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); // let workspace = window.root(cx); -// let item = window.add_view(cx, |cx| { +// let item = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) // }); // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); @@ -5174,7 +5305,7 @@ pub fn open_paths( // let workspace = window.root(cx); // let panel = workspace.update(cx, |workspace, cx| { -// let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right)); +// let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right)); // workspace.add_panel(panel.clone(), cx); // workspace @@ -5186,7 +5317,7 @@ pub fn open_paths( // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // pane.update(cx, |pane, cx| { -// let item = cx.add_view(|_| TestItem::new()); +// let item = cx.build_view(|_| TestItem::new()); // pane.add_item(Box::new(item), true, true, None, cx); // }); @@ -5323,12 +5454,12 @@ pub fn open_paths( // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { // // Add panel_1 on the left, panel_2 on the right. -// let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left)); +// let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left)); // workspace.add_panel(panel_1.clone(), cx); // workspace // .left_dock() // .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); -// let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right)); +// let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right)); // workspace.add_panel(panel_2.clone(), cx); // workspace // .right_dock() @@ -5476,7 +5607,7 @@ pub fn open_paths( // // If focus is transferred to another view that's not a panel or another pane, we still show // // the panel as zoomed. -// let focus_receiver = window.add_view(cx, |_| EmptyView); +// let focus_receiver = window.build_view(cx, |_| EmptyView); // focus_receiver.update(cx, |_, cx| cx.focus_self()); // workspace.read_with(cx, |workspace, _| { // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); diff --git a/crates/workspace2/src/workspace_settings.rs b/crates/workspace2/src/workspace_settings.rs new file mode 100644 index 0000000000..c4d1bb41cd --- /dev/null +++ b/crates/workspace2/src/workspace_settings.rs @@ -0,0 +1,56 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings2::Settings; + +#[derive(Deserialize)] +pub struct WorkspaceSettings { + pub active_pane_magnification: f32, + pub confirm_quit: bool, + pub show_call_status_icon: bool, + pub autosave: AutosaveSetting, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct WorkspaceSettingsContent { + pub active_pane_magnification: Option, + pub confirm_quit: Option, + pub show_call_status_icon: Option, + pub autosave: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AutosaveSetting { + Off, + AfterDelay { milliseconds: u64 }, + OnFocusChange, + OnWindowChange, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, + pub gutter_debounce: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + #[default] + TrackedFiles, + Hide, +} + +impl Settings for WorkspaceSettings { + const KEY: Option<&'static str> = None; + + type FileContent = WorkspaceSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 250a1814aa..c9012a3a14 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.111.0" +version = "0.112.0" publish = false [lib] diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index b75e2d881f..71963ff30b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -15,12 +15,12 @@ name = "Zed" path = "src/main.rs" [dependencies] -ai2 = { path = "../ai2"} +ai = { package = "ai2", path = "../ai2"} # audio = { path = "../audio" } # activity_indicator = { path = "../activity_indicator" } # auto_update = { path = "../auto_update" } # breadcrumbs = { path = "../breadcrumbs" } -call2 = { path = "../call2" } +call = { package = "call2", path = "../call2" } # channel = { path = "../channel" } cli = { path = "../cli" } # collab_ui = { path = "../collab_ui" } @@ -28,49 +28,49 @@ collections = { path = "../collections" } # command_palette = { path = "../command_palette" } # component_test = { path = "../component_test" } # context_menu = { path = "../context_menu" } -client2 = { path = "../client2" } +client = { package = "client2", path = "../client2" } # clock = { path = "../clock" } -copilot2 = { path = "../copilot2" } +copilot = { package = "copilot2", path = "../copilot2" } # copilot_button = { path = "../copilot_button" } # diagnostics = { path = "../diagnostics" } -db2 = { path = "../db2" } +db = { package = "db2", path = "../db2" } # editor = { path = "../editor" } # feedback = { path = "../feedback" } # file_finder = { path = "../file_finder" } # search = { path = "../search" } -fs2 = { path = "../fs2" } +fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } # go_to_line = { path = "../go_to_line" } -gpui2 = { path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2" } install_cli = { path = "../install_cli" } -journal2 = { path = "../journal2" } -language2 = { path = "../language2" } +journal = { package = "journal2", path = "../journal2" } +language = { package = "language2", path = "../language2" } # language_selector = { path = "../language_selector" } -lsp2 = { path = "../lsp2" } +lsp = { package = "lsp2", path = "../lsp2" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } # assistant = { path = "../assistant" } # outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } -project2 = { path = "../project2" } +project = { package = "project2", path = "../project2" } # project_panel = { path = "../project_panel" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } -rpc2 = { path = "../rpc2" } -settings2 = { path = "../settings2" } -feature_flags2 = { path = "../feature_flags2" } +rpc = { package = "rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } shellexpand = "2.1.0" -text = { path = "../text" } +text = { package = "text2", path = "../text2" } # terminal_view = { path = "../terminal_view" } -theme2 = { path = "../theme2" } +theme = { package = "theme2", path = "../theme2" } # theme_selector = { path = "../theme_selector" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } -# workspace = { path = "../workspace" } +workspace2 = { path = "../workspace2" } # welcome = { path = "../welcome" } # zed-actions = {path = "../zed-actions"} anyhow.workspace = true @@ -142,17 +142,17 @@ urlencoding = "2.1.2" uuid.workspace = true [dev-dependencies] -call2 = { path = "../call2", features = ["test-support"] } +call = { package = "call2", path = "../call2", features = ["test-support"] } # client = { path = "../client", features = ["test-support"] } # editor = { path = "../editor", features = ["test-support"] } # gpui = { path = "../gpui", features = ["test-support"] } -gpui2 = { path = "../gpui2", features = ["test-support"] } -language2 = { path = "../language2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } # lsp = { path = "../lsp", features = ["test-support"] } -project2 = { path = "../project2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } # rpc = { path = "../rpc", features = ["test-support"] } # settings = { path = "../settings", features = ["test-support"] } -# text = { path = "../text", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } # util = { path = "../util", features = ["test-support"] } # workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true diff --git a/crates/zed2/src/assets.rs b/crates/zed2/src/assets.rs index c4010edc9f..873138c244 100644 --- a/crates/zed2/src/assets.rs +++ b/crates/zed2/src/assets.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; -use gpui2::{AssetSource, Result, SharedString}; + +use gpui::{AssetSource, Result, SharedString}; use rust_embed::RustEmbed; #[derive(RustEmbed)] diff --git a/crates/zed2/src/languages.rs b/crates/zed2/src/languages.rs index 4f7a97cb97..555f12dd0f 100644 --- a/crates/zed2/src/languages.rs +++ b/crates/zed2/src/languages.rs @@ -1,9 +1,9 @@ use anyhow::Context; -use gpui2::AppContext; -pub use language2::*; +use gpui::AppContext; +pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; -use settings2::Settings; +use settings::Settings; use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; diff --git a/crates/zed2/src/languages/c.rs b/crates/zed2/src/languages/c.rs index c836fdc740..280d9dd921 100644 --- a/crates/zed2/src/languages/c.rs +++ b/crates/zed2/src/languages/c.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; -pub use language2::*; -use lsp2::LanguageServerBinary; +pub use language::*; +use lsp::LanguageServerBinary; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; use util::{ @@ -108,7 +108,7 @@ impl super::LspAdapter for CLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { let label = completion @@ -118,7 +118,7 @@ impl super::LspAdapter for CLspAdapter { .trim(); match completion.kind { - Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => { + Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => { let detail = completion.detail.as_ref().unwrap(); let text = format!("{} {}", detail, label); let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); @@ -129,7 +129,7 @@ impl super::LspAdapter for CLspAdapter { runs, }); } - Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE) + Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) if completion.detail.is_some() => { let detail = completion.detail.as_ref().unwrap(); @@ -141,7 +141,7 @@ impl super::LspAdapter for CLspAdapter { runs, }); } - Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD) + Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) if completion.detail.is_some() => { let detail = completion.detail.as_ref().unwrap(); @@ -155,13 +155,13 @@ impl super::LspAdapter for CLspAdapter { } Some(kind) => { let highlight_name = match kind { - lsp2::CompletionItemKind::STRUCT - | lsp2::CompletionItemKind::INTERFACE - | lsp2::CompletionItemKind::CLASS - | lsp2::CompletionItemKind::ENUM => Some("type"), - lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"), - lsp2::CompletionItemKind::KEYWORD => Some("keyword"), - lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => { + lsp::CompletionItemKind::STRUCT + | lsp::CompletionItemKind::INTERFACE + | lsp::CompletionItemKind::CLASS + | lsp::CompletionItemKind::ENUM => Some("type"), + lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp::CompletionItemKind::KEYWORD => Some("keyword"), + lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => { Some("constant") } _ => None, @@ -186,47 +186,47 @@ impl super::LspAdapter for CLspAdapter { async fn label_for_symbol( &self, name: &str, - kind: lsp2::SymbolKind, + kind: lsp::SymbolKind, language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { - lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { let text = format!("void {} () {{}}", name); let filter_range = 0..name.len(); let display_range = 5..5 + name.len(); (text, filter_range, display_range) } - lsp2::SymbolKind::STRUCT => { + lsp::SymbolKind::STRUCT => { let text = format!("struct {} {{}}", name); let filter_range = 7..7 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::ENUM => { + lsp::SymbolKind::ENUM => { let text = format!("enum {} {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::INTERFACE | lsp2::SymbolKind::CLASS => { + lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => { let text = format!("class {} {{}}", name); let filter_range = 6..6 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::CONSTANT => { + lsp::SymbolKind::CONSTANT => { let text = format!("const int {} = 0;", name); let filter_range = 10..10 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::MODULE => { + lsp::SymbolKind::MODULE => { let text = format!("namespace {} {{}}", name); let filter_range = 10..10 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::TYPE_PARAMETER => { + lsp::SymbolKind::TYPE_PARAMETER => { let text = format!("typename {} {{}};", name); let filter_range = 9..9 + name.len(); let display_range = 0..filter_range.end; @@ -273,18 +273,18 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option(|store, cx| { store.update_user_settings::(cx, |s| { s.defaults.tab_size = NonZeroU32::new(2); diff --git a/crates/zed2/src/languages/css.rs b/crates/zed2/src/languages/css.rs index fb6fcabe8e..fdbc179209 100644 --- a/crates/zed2/src/languages/css.rs +++ b/crates/zed2/src/languages/css.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; diff --git a/crates/zed2/src/languages/elixir.rs b/crates/zed2/src/languages/elixir.rs index 09c7305fb0..bd38377c99 100644 --- a/crates/zed2/src/languages/elixir.rs +++ b/crates/zed2/src/languages/elixir.rs @@ -1,12 +1,12 @@ use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use futures::StreamExt; -use gpui2::{AsyncAppContext, Task}; -pub use language2::*; -use lsp2::{CompletionItemKind, LanguageServerBinary, SymbolKind}; +use gpui::{AsyncAppContext, Task}; +pub use language::*; +use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use smol::fs::{self, File}; use std::{ any::Any, @@ -54,7 +54,7 @@ impl Settings for ElixirSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &mut gpui2::AppContext, + _: &mut gpui::AppContext, ) -> Result where Self: Sized, @@ -200,7 +200,7 @@ impl LspAdapter for ElixirLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { match completion.kind.zip(completion.detail.as_ref()) { @@ -404,7 +404,7 @@ impl LspAdapter for NextLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { label_for_completion_elixir(completion, language) @@ -506,7 +506,7 @@ impl LspAdapter for LocalLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { label_for_completion_elixir(completion, language) @@ -523,7 +523,7 @@ impl LspAdapter for LocalLspAdapter { } fn label_for_completion_elixir( - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { return Some(CodeLabel { diff --git a/crates/zed2/src/languages/go.rs b/crates/zed2/src/languages/go.rs index 21001015b9..0daf1527c3 100644 --- a/crates/zed2/src/languages/go.rs +++ b/crates/zed2/src/languages/go.rs @@ -1,10 +1,10 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use gpui2::{AsyncAppContext, Task}; -pub use language2::*; +use gpui::{AsyncAppContext, Task}; +pub use language::*; use lazy_static::lazy_static; -use lsp2::LanguageServerBinary; +use lsp::LanguageServerBinary; use regex::Regex; use smol::{fs, process}; use std::{ @@ -170,7 +170,7 @@ impl super::LspAdapter for GoLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { let label = &completion.label; @@ -181,7 +181,7 @@ impl super::LspAdapter for GoLspAdapter { let name_offset = label.rfind('.').unwrap_or(0); match completion.kind.zip(completion.detail.as_ref()) { - Some((lsp2::CompletionItemKind::MODULE, detail)) => { + Some((lsp::CompletionItemKind::MODULE, detail)) => { let text = format!("{label} {detail}"); let source = Rope::from(format!("import {text}").as_str()); let runs = language.highlight_text(&source, 7..7 + text.len()); @@ -192,7 +192,7 @@ impl super::LspAdapter for GoLspAdapter { }); } Some(( - lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE, + lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE, detail, )) => { let text = format!("{label} {detail}"); @@ -208,7 +208,7 @@ impl super::LspAdapter for GoLspAdapter { filter_range: 0..label.len(), }); } - Some((lsp2::CompletionItemKind::STRUCT, _)) => { + Some((lsp::CompletionItemKind::STRUCT, _)) => { let text = format!("{label} struct {{}}"); let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); let runs = adjust_runs( @@ -221,7 +221,7 @@ impl super::LspAdapter for GoLspAdapter { filter_range: 0..label.len(), }); } - Some((lsp2::CompletionItemKind::INTERFACE, _)) => { + Some((lsp::CompletionItemKind::INTERFACE, _)) => { let text = format!("{label} interface {{}}"); let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); let runs = adjust_runs( @@ -234,7 +234,7 @@ impl super::LspAdapter for GoLspAdapter { filter_range: 0..label.len(), }); } - Some((lsp2::CompletionItemKind::FIELD, detail)) => { + Some((lsp::CompletionItemKind::FIELD, detail)) => { let text = format!("{label} {detail}"); let source = Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str()); @@ -248,10 +248,7 @@ impl super::LspAdapter for GoLspAdapter { filter_range: 0..label.len(), }); } - Some(( - lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD, - detail, - )) => { + Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => { if let Some(signature) = detail.strip_prefix("func") { let text = format!("{label}{signature}"); let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str()); @@ -274,47 +271,47 @@ impl super::LspAdapter for GoLspAdapter { async fn label_for_symbol( &self, name: &str, - kind: lsp2::SymbolKind, + kind: lsp::SymbolKind, language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { - lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { let text = format!("func {} () {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::STRUCT => { + lsp::SymbolKind::STRUCT => { let text = format!("type {} struct {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..text.len(); (text, filter_range, display_range) } - lsp2::SymbolKind::INTERFACE => { + lsp::SymbolKind::INTERFACE => { let text = format!("type {} interface {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..text.len(); (text, filter_range, display_range) } - lsp2::SymbolKind::CLASS => { + lsp::SymbolKind::CLASS => { let text = format!("type {} T", name); let filter_range = 5..5 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::CONSTANT => { + lsp::SymbolKind::CONSTANT => { let text = format!("const {} = nil", name); let filter_range = 6..6 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::VARIABLE => { + lsp::SymbolKind::VARIABLE => { let text = format!("var {} = nil", name); let filter_range = 4..4 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::MODULE => { + lsp::SymbolKind::MODULE => { let text = format!("package {}", name); let filter_range = 8..8 + name.len(); let display_range = 0..filter_range.end; @@ -375,10 +372,10 @@ fn adjust_runs( mod tests { use super::*; use crate::languages::language; - use gpui2::Hsla; - use theme2::SyntaxTheme; + use gpui::Hsla; + use theme::SyntaxTheme; - #[gpui2::test] + #[gpui::test] async fn test_go_label_for_completion() { let language = language( "go", @@ -405,8 +402,8 @@ mod tests { assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FUNCTION), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), label: "Hello".to_string(), detail: Some("func(a B) c.D".to_string()), ..Default::default() @@ -426,8 +423,8 @@ mod tests { // Nested methods assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::METHOD), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::METHOD), label: "one.two.Three".to_string(), detail: Some("func() [3]interface{}".to_string()), ..Default::default() @@ -447,8 +444,8 @@ mod tests { // Nested fields assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FIELD), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), label: "two.Three".to_string(), detail: Some("a.Bcd".to_string()), ..Default::default() diff --git a/crates/zed2/src/languages/html.rs b/crates/zed2/src/languages/html.rs index b46675dd79..b8f1c70cce 100644 --- a/crates/zed2/src/languages/html.rs +++ b/crates/zed2/src/languages/html.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs index cb912f1042..63f909ae2a 100644 --- a/crates/zed2/src/languages/json.rs +++ b/crates/zed2/src/languages/json.rs @@ -1,14 +1,14 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; -use feature_flags2::FeatureFlagAppExt; +use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui2::AppContext; -use language2::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use gpui::AppContext; +use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; -use settings2::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; +use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; use std::{ any::Any, diff --git a/crates/zed2/src/languages/lua.rs b/crates/zed2/src/languages/lua.rs index c92534925c..5fffb37e81 100644 --- a/crates/zed2/src/languages/lua.rs +++ b/crates/zed2/src/languages/lua.rs @@ -3,8 +3,8 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; -use language2::{LanguageServerName, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use smol::fs; use std::{any::Any, env::consts, path::PathBuf}; use util::{ diff --git a/crates/zed2/src/languages/php.rs b/crates/zed2/src/languages/php.rs index d6e462e186..3096fd16e6 100644 --- a/crates/zed2/src/languages/php.rs +++ b/crates/zed2/src/languages/php.rs @@ -3,8 +3,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use smol::{fs, stream::StreamExt}; @@ -91,9 +91,9 @@ impl LspAdapter for IntelephenseLspAdapter { async fn label_for_completion( &self, - _item: &lsp2::CompletionItem, - _language: &Arc, - ) -> Option { + _item: &lsp::CompletionItem, + _language: &Arc, + ) -> Option { None } diff --git a/crates/zed2/src/languages/python.rs b/crates/zed2/src/languages/python.rs index 8bbf022a17..3666237e69 100644 --- a/crates/zed2/src/languages/python.rs +++ b/crates/zed2/src/languages/python.rs @@ -1,7 +1,7 @@ use anyhow::Result; use async_trait::async_trait; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use smol::fs; use std::{ @@ -81,7 +81,7 @@ impl LspAdapter for PythonLspAdapter { get_cached_server_binary(container_dir, &*self.node).await } - async fn process_completion(&self, item: &mut lsp2::CompletionItem) { + async fn process_completion(&self, item: &mut lsp::CompletionItem) { // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. // Where `XX` is the sorting category, `YYYY` is based on most recent usage, // and `name` is the symbol name itself. @@ -104,19 +104,19 @@ impl LspAdapter for PythonLspAdapter { async fn label_for_completion( &self, - item: &lsp2::CompletionItem, - language: &Arc, - ) -> Option { + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { let label = &item.label; let grammar = language.grammar()?; let highlight_id = match item.kind? { - lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, - lsp2::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?, - lsp2::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?, - lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?, + lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?, + lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, _ => return None, }; - Some(language2::CodeLabel { + Some(language::CodeLabel { text: label.clone(), runs: vec![(0..label.len(), highlight_id)], filter_range: 0..label.len(), @@ -126,23 +126,23 @@ impl LspAdapter for PythonLspAdapter { async fn label_for_symbol( &self, name: &str, - kind: lsp2::SymbolKind, - language: &Arc, - ) -> Option { + kind: lsp::SymbolKind, + language: &Arc, + ) -> Option { let (text, filter_range, display_range) = match kind { - lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { let text = format!("def {}():\n", name); let filter_range = 4..4 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::CLASS => { + lsp::SymbolKind::CLASS => { let text = format!("class {}:", name); let filter_range = 6..6 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::CONSTANT => { + lsp::SymbolKind::CONSTANT => { let text = format!("{} = 0", name); let filter_range = 0..name.len(); let display_range = 0..filter_range.end; @@ -151,7 +151,7 @@ impl LspAdapter for PythonLspAdapter { _ => return None, }; - Some(language2::CodeLabel { + Some(language::CodeLabel { runs: language.highlight_text(&text.as_str().into(), display_range.clone()), text: text[display_range].to_string(), filter_range, @@ -177,12 +177,12 @@ async fn get_cached_server_binary( #[cfg(test)] mod tests { - use gpui2::{Context, ModelContext, TestAppContext}; - use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; - use settings2::SettingsStore; + use gpui::{Context, ModelContext, TestAppContext}; + use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings::SettingsStore; use std::num::NonZeroU32; - #[gpui2::test] + #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); let language = @@ -190,7 +190,7 @@ mod tests { cx.update(|cx| { let test_settings = SettingsStore::test(cx); cx.set_global(test_settings); - language2::init(cx); + language::init(cx); cx.update_global::(|store, cx| { store.update_user_settings::(cx, |s| { s.defaults.tab_size = NonZeroU32::new(2); diff --git a/crates/zed2/src/languages/ruby.rs b/crates/zed2/src/languages/ruby.rs index 8718f1c757..3890b90dbd 100644 --- a/crates/zed2/src/languages/ruby.rs +++ b/crates/zed2/src/languages/ruby.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use std::{any::Any, path::PathBuf, sync::Arc}; pub struct RubyLanguageServer; @@ -53,25 +53,25 @@ impl LspAdapter for RubyLanguageServer { async fn label_for_completion( &self, - item: &lsp2::CompletionItem, - language: &Arc, - ) -> Option { + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { let label = &item.label; let grammar = language.grammar()?; let highlight_id = match item.kind? { - lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, - lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, - lsp2::CompletionItemKind::CLASS | lsp2::CompletionItemKind::MODULE => { + lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => { grammar.highlight_id_for_name("type")? } - lsp2::CompletionItemKind::KEYWORD => { + lsp::CompletionItemKind::KEYWORD => { if label.starts_with(':') { grammar.highlight_id_for_name("string.special.symbol")? } else { grammar.highlight_id_for_name("keyword")? } } - lsp2::CompletionItemKind::VARIABLE => { + lsp::CompletionItemKind::VARIABLE => { if label.starts_with('@') { grammar.highlight_id_for_name("property")? } else { @@ -80,7 +80,7 @@ impl LspAdapter for RubyLanguageServer { } _ => return None, }; - Some(language2::CodeLabel { + Some(language::CodeLabel { text: label.clone(), runs: vec![(0..label.len(), highlight_id)], filter_range: 0..label.len(), @@ -90,12 +90,12 @@ impl LspAdapter for RubyLanguageServer { async fn label_for_symbol( &self, label: &str, - kind: lsp2::SymbolKind, - language: &Arc, - ) -> Option { + kind: lsp::SymbolKind, + language: &Arc, + ) -> Option { let grammar = language.grammar()?; match kind { - lsp2::SymbolKind::METHOD => { + lsp::SymbolKind::METHOD => { let mut parts = label.split('#'); let classes = parts.next()?; let method = parts.next()?; @@ -120,21 +120,21 @@ impl LspAdapter for RubyLanguageServer { ix += 1; let end_ix = ix + method.len(); runs.push((ix..end_ix, method_id)); - Some(language2::CodeLabel { + Some(language::CodeLabel { text: label.to_string(), runs, filter_range: 0..label.len(), }) } - lsp2::SymbolKind::CONSTANT => { + lsp::SymbolKind::CONSTANT => { let constant_id = grammar.highlight_id_for_name("constant")?; - Some(language2::CodeLabel { + Some(language::CodeLabel { text: label.to_string(), runs: vec![(0..label.len(), constant_id)], filter_range: 0..label.len(), }) } - lsp2::SymbolKind::CLASS | lsp2::SymbolKind::MODULE => { + lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => { let class_id = grammar.highlight_id_for_name("type")?; let mut ix = 0; @@ -148,7 +148,7 @@ impl LspAdapter for RubyLanguageServer { ix = end_ix; } - Some(language2::CodeLabel { + Some(language::CodeLabel { text: label.to_string(), runs, filter_range: 0..label.len(), diff --git a/crates/zed2/src/languages/rust.rs b/crates/zed2/src/languages/rust.rs index a0abcedd07..961e6fe7f0 100644 --- a/crates/zed2/src/languages/rust.rs +++ b/crates/zed2/src/languages/rust.rs @@ -2,9 +2,9 @@ use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; -pub use language2::*; +pub use language::*; use lazy_static::lazy_static; -use lsp2::LanguageServerBinary; +use lsp::LanguageServerBinary; use regex::Regex; use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; @@ -106,7 +106,7 @@ impl LspAdapter for RustLspAdapter { Some("rust-analyzer/flycheck".into()) } - fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) { + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); } @@ -128,11 +128,11 @@ impl LspAdapter for RustLspAdapter { async fn label_for_completion( &self, - completion: &lsp2::CompletionItem, + completion: &lsp::CompletionItem, language: &Arc, ) -> Option { match completion.kind { - Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => { + Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => { let detail = completion.detail.as_ref().unwrap(); let name = &completion.label; let text = format!("{}: {}", name, detail); @@ -144,9 +144,9 @@ impl LspAdapter for RustLspAdapter { filter_range: 0..name.len(), }); } - Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE) + Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) if completion.detail.is_some() - && completion.insert_text_format != Some(lsp2::InsertTextFormat::SNIPPET) => + && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => { let detail = completion.detail.as_ref().unwrap(); let name = &completion.label; @@ -159,7 +159,7 @@ impl LspAdapter for RustLspAdapter { filter_range: 0..name.len(), }); } - Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD) + Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) if completion.detail.is_some() => { lazy_static! { @@ -188,12 +188,12 @@ impl LspAdapter for RustLspAdapter { } Some(kind) => { let highlight_name = match kind { - lsp2::CompletionItemKind::STRUCT - | lsp2::CompletionItemKind::INTERFACE - | lsp2::CompletionItemKind::ENUM => Some("type"), - lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"), - lsp2::CompletionItemKind::KEYWORD => Some("keyword"), - lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => { + lsp::CompletionItemKind::STRUCT + | lsp::CompletionItemKind::INTERFACE + | lsp::CompletionItemKind::ENUM => Some("type"), + lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp::CompletionItemKind::KEYWORD => Some("keyword"), + lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => { Some("constant") } _ => None, @@ -214,47 +214,47 @@ impl LspAdapter for RustLspAdapter { async fn label_for_symbol( &self, name: &str, - kind: lsp2::SymbolKind, + kind: lsp::SymbolKind, language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { - lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { let text = format!("fn {} () {{}}", name); let filter_range = 3..3 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::STRUCT => { + lsp::SymbolKind::STRUCT => { let text = format!("struct {} {{}}", name); let filter_range = 7..7 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::ENUM => { + lsp::SymbolKind::ENUM => { let text = format!("enum {} {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::INTERFACE => { + lsp::SymbolKind::INTERFACE => { let text = format!("trait {} {{}}", name); let filter_range = 6..6 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::CONSTANT => { + lsp::SymbolKind::CONSTANT => { let text = format!("const {}: () = ();", name); let filter_range = 6..6 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::MODULE => { + lsp::SymbolKind::MODULE => { let text = format!("mod {} {{}}", name); let filter_range = 4..4 + name.len(); let display_range = 0..filter_range.end; (text, filter_range, display_range) } - lsp2::SymbolKind::TYPE_PARAMETER => { + lsp::SymbolKind::TYPE_PARAMETER => { let text = format!("type {} {{}}", name); let filter_range = 5..5 + name.len(); let display_range = 0..filter_range.end; @@ -294,29 +294,29 @@ mod tests { use super::*; use crate::languages::language; - use gpui2::{Context, Hsla, TestAppContext}; - use language2::language_settings::AllLanguageSettings; - use settings2::SettingsStore; - use theme2::SyntaxTheme; + use gpui::{Context, Hsla, TestAppContext}; + use language::language_settings::AllLanguageSettings; + use settings::SettingsStore; + use theme::SyntaxTheme; - #[gpui2::test] + #[gpui::test] async fn test_process_rust_diagnostics() { - let mut params = lsp2::PublishDiagnosticsParams { - uri: lsp2::Url::from_file_path("/a").unwrap(), + let mut params = lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/a").unwrap(), version: None, diagnostics: vec![ // no newlines - lsp2::Diagnostic { + lsp::Diagnostic { message: "use of moved value `a`".to_string(), ..Default::default() }, // newline at the end of a code span - lsp2::Diagnostic { + lsp::Diagnostic { message: "consider importing this struct: `use b::c;\n`".to_string(), ..Default::default() }, // code span starting right after a newline - lsp2::Diagnostic { + lsp::Diagnostic { message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference" .to_string(), ..Default::default() @@ -340,7 +340,7 @@ mod tests { ); } - #[gpui2::test] + #[gpui::test] async fn test_rust_label_for_completion() { let language = language( "rust", @@ -365,8 +365,8 @@ mod tests { assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FUNCTION), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), detail: Some("fn(&mut Option) -> Vec".to_string()), ..Default::default() @@ -387,8 +387,8 @@ mod tests { ); assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FUNCTION), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), detail: Some("async fn(&mut Option) -> Vec".to_string()), ..Default::default() @@ -409,8 +409,8 @@ mod tests { ); assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FIELD), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), label: "len".to_string(), detail: Some("usize".to_string()), ..Default::default() @@ -425,8 +425,8 @@ mod tests { assert_eq!( language - .label_for_completion(&lsp2::CompletionItem { - kind: Some(lsp2::CompletionItemKind::FUNCTION), + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), detail: Some("fn(&mut Option) -> Vec".to_string()), ..Default::default() @@ -447,7 +447,7 @@ mod tests { ); } - #[gpui2::test] + #[gpui::test] async fn test_rust_label_for_symbol() { let language = language( "rust", @@ -471,7 +471,7 @@ mod tests { assert_eq!( language - .label_for_symbol("hello", lsp2::SymbolKind::FUNCTION) + .label_for_symbol("hello", lsp::SymbolKind::FUNCTION) .await, Some(CodeLabel { text: "fn hello".to_string(), @@ -482,7 +482,7 @@ mod tests { assert_eq!( language - .label_for_symbol("World", lsp2::SymbolKind::TYPE_PARAMETER) + .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER) .await, Some(CodeLabel { text: "type World".to_string(), @@ -492,13 +492,13 @@ mod tests { ); } - #[gpui2::test] + #[gpui::test] async fn test_rust_autoindent(cx: &mut TestAppContext) { // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { let test_settings = SettingsStore::test(cx); cx.set_global(test_settings); - language2::init(cx); + language::init(cx); cx.update_global::(|store, cx| { store.update_user_settings::(cx, |s| { s.defaults.tab_size = NonZeroU32::new(2); diff --git a/crates/zed2/src/languages/svelte.rs b/crates/zed2/src/languages/svelte.rs index 53f52a6a30..34dab81772 100644 --- a/crates/zed2/src/languages/svelte.rs +++ b/crates/zed2/src/languages/svelte.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; diff --git a/crates/zed2/src/languages/tailwind.rs b/crates/zed2/src/languages/tailwind.rs index 0aa2154f1e..6d6006dbd4 100644 --- a/crates/zed2/src/languages/tailwind.rs +++ b/crates/zed2/src/languages/tailwind.rs @@ -5,9 +5,9 @@ use futures::{ future::{self, BoxFuture}, FutureExt, StreamExt, }; -use gpui2::AppContext; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use smol::fs; diff --git a/crates/zed2/src/languages/typescript.rs b/crates/zed2/src/languages/typescript.rs index 8eecf25540..de0139b3b2 100644 --- a/crates/zed2/src/languages/typescript.rs +++ b/crates/zed2/src/languages/typescript.rs @@ -3,9 +3,9 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt}; -use gpui2::AppContext; -use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::{CodeActionKind, LanguageServerBinary}; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use smol::{fs, io::BufReader, stream::StreamExt}; @@ -129,10 +129,10 @@ impl LspAdapter for TypeScriptLspAdapter { async fn label_for_completion( &self, - item: &lsp2::CompletionItem, - language: &Arc, - ) -> Option { - use lsp2::CompletionItemKind as Kind; + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; let len = item.label.len(); let grammar = language.grammar()?; let highlight_id = match item.kind? { @@ -149,7 +149,7 @@ impl LspAdapter for TypeScriptLspAdapter { None => item.label.clone(), }; - Some(language2::CodeLabel { + Some(language::CodeLabel { text, runs: vec![(0..len, highlight_id)], filter_range: 0..len, @@ -300,9 +300,9 @@ impl LspAdapter for EsLintLspAdapter { async fn label_for_completion( &self, - _item: &lsp2::CompletionItem, - _language: &Arc, - ) -> Option { + _item: &lsp::CompletionItem, + _language: &Arc, + ) -> Option { None } @@ -335,10 +335,10 @@ async fn get_cached_eslint_server_binary( #[cfg(test)] mod tests { - use gpui2::{Context, TestAppContext}; + use gpui::{Context, TestAppContext}; use unindent::Unindent; - #[gpui2::test] + #[gpui::test] async fn test_outline(cx: &mut TestAppContext) { let language = crate::languages::language( "typescript", @@ -363,7 +363,7 @@ mod tests { .unindent(); let buffer = cx.build_model(|cx| { - language2::Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + language::Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( diff --git a/crates/zed2/src/languages/vue.rs b/crates/zed2/src/languages/vue.rs index 0c87c4bee8..16afd2e299 100644 --- a/crates/zed2/src/languages/vue.rs +++ b/crates/zed2/src/languages/vue.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, ensure, Result}; use async_trait::async_trait; use futures::StreamExt; -pub use language2::*; -use lsp2::{CodeActionKind, LanguageServerBinary}; +pub use language::*; +use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use parking_lot::Mutex; use serde_json::Value; @@ -148,10 +148,10 @@ impl super::LspAdapter for VueLspAdapter { async fn label_for_completion( &self, - item: &lsp2::CompletionItem, - language: &Arc, - ) -> Option { - use lsp2::CompletionItemKind as Kind; + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; let len = item.label.len(); let grammar = language.grammar()?; let highlight_id = match item.kind? { @@ -171,7 +171,7 @@ impl super::LspAdapter for VueLspAdapter { None => item.label.clone(), }; - Some(language2::CodeLabel { + Some(language::CodeLabel { text, runs: vec![(0..len, highlight_id)], filter_range: 0..len, diff --git a/crates/zed2/src/languages/yaml.rs b/crates/zed2/src/languages/yaml.rs index 338a7a7ade..8b438d0949 100644 --- a/crates/zed2/src/languages/yaml.rs +++ b/crates/zed2/src/languages/yaml.rs @@ -1,11 +1,11 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui2::AppContext; -use language2::{ +use gpui::AppContext; +use language::{ language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate, }; -use lsp2::LanguageServerBinary; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::Value; use smol::fs; diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 82eacd9710..6522d97fdc 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -1,3 +1,6 @@ +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + // Allow binary to be called Zed for a nice application menu when running executable directly #![allow(non_snake_case)] @@ -8,19 +11,20 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client2::UserStore; -use db2::kvp::KEY_VALUE_STORE; -use fs2::RealFs; +use client::UserStore; +use collections::HashMap; +use db::kvp::KEY_VALUE_STORE; +use fs::RealFs; use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui2::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; +use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; -use language2::LanguageRegistry; +use language::LanguageRegistry; use log::LevelFilter; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use settings2::{ +use settings::{ default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, }; use simplelog::ConfigBuilder; @@ -39,14 +43,18 @@ use std::{ thread, time::{SystemTime, UNIX_EPOCH}, }; +use text::Point; use util::{ + async_maybe, channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, http::{self, HttpClient}, - paths, ResultExt, + paths::{self, PathLikeWithPosition}, + ResultExt, }; use uuid::Uuid; -use zed2::languages; -use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance}; +use workspace2::{AppState, WorkspaceStore}; +use zed2::{build_window_options, initialize_workspace, languages}; +use zed2::{ensure_only_instance, Assets, IsOnlyInstance}; mod open_listener; @@ -62,20 +70,26 @@ fn main() { log::info!("========== starting zed =========="); let app = App::production(Arc::new(Assets)); - let installation_id = app.executor().block(installation_id()).ok(); + let installation_id = app.background_executor().block(installation_id()).ok(); let session_id = Uuid::new_v4().to_string(); init_panic_hook(&app, installation_id.clone(), session_id.clone()); let fs = Arc::new(RealFs); - let user_settings_file_rx = - watch_config_file(&app.executor(), fs.clone(), paths::SETTINGS.clone()); - let _user_keymap_file_rx = - watch_config_file(&app.executor(), fs.clone(), paths::KEYMAP.clone()); + let user_settings_file_rx = watch_config_file( + &app.background_executor(), + fs.clone(), + paths::SETTINGS.clone(), + ); + let _user_keymap_file_rx = watch_config_file( + &app.background_executor(), + fs.clone(), + paths::KEYMAP.clone(), + ); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) } else { - app.executor().spawn(async { + app.background_executor().spawn(async { load_login_shell_environment().await.log_err(); }) }; @@ -108,27 +122,27 @@ fn main() { handle_settings_file_changes(user_settings_file_rx, cx); // handle_keymap_file_changes(user_keymap_file_rx, cx); - let client = client2::Client::new(http.clone(), cx); + let client = client::Client::new(http.clone(), cx); let mut languages = LanguageRegistry::new(login_shell_env_loaded); let copilot_language_server_id = languages.next_language_server_id(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.background_executor().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); - language2::init(cx); + language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); - theme2::init(cx); + theme::init(cx); // context_menu::init(cx); - project2::Project::init(&client, cx); - client2::init(&client, cx); + project::Project::init(&client, cx); + client::init(&client, cx); // command_palette::init(cx); - language2::init(cx); + language::init(cx); // editor::init(cx); // go_to_line::init(cx); // file_finder::init(cx); @@ -141,7 +155,7 @@ fn main() { // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); // vim::init(cx); // terminal_view::init(cx); - copilot2::init( + copilot::init( copilot_language_server_id, http.clone(), node_runtime.clone(), @@ -164,26 +178,23 @@ fn main() { // client.telemetry().start(installation_id, session_id, cx); - // todo!("app_state") - let app_state = Arc::new(AppState { client, user_store }); - // let app_state = Arc::new(AppState { - // languages, - // client: client.clone(), - // user_store, - // fs, - // build_window_options, - // initialize_workspace, - // background_actions, - // workspace_store, - // node_runtime, - // }); - // cx.set_global(Arc::downgrade(&app_state)); + let app_state = Arc::new(AppState { + languages, + client: client.clone(), + user_store, + fs, + build_window_options, + initialize_workspace, + // background_actions: todo!("ask Mikayla"), + workspace_store, + node_runtime, + }); + cx.set_global(Arc::downgrade(&app_state)); // audio::init(Assets, cx); // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); - // todo!("workspace") - // workspace::init(app_state.clone(), cx); + workspace2::init(app_state.clone(), cx); // recent_projects::init(cx); // journal2::init(app_state.clone(), cx); @@ -191,7 +202,7 @@ fn main() { // theme_selector::init(cx); // activity_indicator::init(cx); // language_tools::init(cx); - call2::init(app_state.client.clone(), app_state.user_store.clone(), cx); + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); // collab_ui::init(&app_state, cx); // feedback::init(cx); // welcome::init(cx); @@ -220,10 +231,8 @@ fn main() { let mut _triggered_authentication = false; match open_rx.try_next() { - Ok(Some(OpenRequest::Paths { paths: _ })) => { - // todo!("workspace") - // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .detach(); + Ok(Some(OpenRequest::Paths { paths })) => { + workspace2::open_paths(&paths, &app_state, None, cx).detach(); } Ok(Some(OpenRequest::CliConnection { connection })) => { let app_state = app_state.clone(); @@ -255,10 +264,10 @@ fn main() { async move { while let Some(request) = open_rx.next().await { match request { - OpenRequest::Paths { paths: _ } => { - // todo!("workspace") - // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .detach(); + OpenRequest::Paths { paths } => { + cx.update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) + .ok() + .map(|t| t.detach()); } OpenRequest::CliConnection { connection } => { let app_state = app_state.clone(); @@ -314,22 +323,30 @@ async fn installation_id() -> Result { } } -async fn restore_or_create_workspace(_app_state: &Arc, mut _cx: AsyncAppContext) { - todo!("workspace") - // if let Some(location) = workspace::last_opened_workspace_paths().await { - // cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) - // .await - // .log_err(); - // } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - // cx.update(|cx| show_welcome_experience(app_state, cx)); - // } else { - // cx.update(|cx| { - // workspace::open_new(app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // }); - // } +async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { + async_maybe!({ + if let Some(location) = workspace2::last_opened_workspace_paths().await { + cx.update(|cx| workspace2::open_paths(location.paths().as_ref(), app_state, None, cx))? + .await + .log_err(); + } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) { + // todo!(welcome) + //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + //todo!() + // cx.update(|cx| show_welcome_experience(app_state, cx)); + } else { + cx.update(|cx| { + workspace2::open_new(app_state, cx, |workspace, cx| { + // todo!(editor) + // Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + })?; + } + anyhow::Ok(()) + }) + .await + .log_err(); } fn init_paths() { @@ -438,7 +455,7 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin std::process::exit(-1); } - let app_version = client2::ZED_APP_VERSION + let app_version = client::ZED_APP_VERSION .or(app_metadata.app_version) .map_or("dev".to_string(), |v| v.to_string()); @@ -506,11 +523,11 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin } fn upload_previous_panics(http: Arc, cx: &mut AppContext) { - let telemetry_settings = *client2::TelemetrySettings::get_global(cx); + let telemetry_settings = *client::TelemetrySettings::get_global(cx); - cx.executor() + cx.background_executor() .spawn(async move { - let panic_report_url = format!("{}/api/panic", &*client2::ZED_SERVER_URL); + let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?; while let Some(child) = children.next().await { let child = child?; @@ -553,7 +570,7 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { if let Some(panic) = panic { let body = serde_json::to_string(&PanicRequest { panic, - token: client2::ZED_SECRET_CLIENT_TOKEN.into(), + token: client::ZED_SECRET_CLIENT_TOKEN.into(), }) .unwrap(); @@ -638,7 +655,7 @@ fn load_embedded_fonts(cx: &AppContext) { let asset_source = cx.asset_source(); let font_paths = asset_source.list("fonts").unwrap(); let embedded_fonts = Mutex::new(Vec::new()); - let executor = cx.executor(); + let executor = cx.background_executor(); executor.block(executor.scoped(|scope| { for font_path in &font_paths { @@ -765,45 +782,45 @@ async fn handle_cli_connection( ) { if let Some(request) = requests.next().await { match request { - CliRequest::Open { paths: _, wait: _ } => { - // let mut caret_positions = HashMap::new(); + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::default(); - // todo!("workspace") - // let paths = if paths.is_empty() { - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - // } else { - // paths - // .into_iter() - // .filter_map(|path_with_position_string| { - // let path_with_position = PathLikeWithPosition::parse_str( - // &path_with_position_string, - // |path_str| { - // Ok::<_, std::convert::Infallible>( - // Path::new(path_str).to_path_buf(), - // ) - // }, - // ) - // .expect("Infallible"); - // let path = path_with_position.path_like; - // if let Some(row) = path_with_position.row { - // if path.is_file() { - // let row = row.saturating_sub(1); - // let col = - // path_with_position.column.unwrap_or(0).saturating_sub(1); - // caret_positions.insert(path.clone(), Point::new(row, col)); - // } - // } - // Some(path) - // }) - // .collect() - // }; + let paths = if paths.is_empty() { + workspace2::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() + }; + // todo!("editor") // let mut errored = false; // match cx - // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + // .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) // .await // { // Ok((workspace, items)) => { @@ -913,11 +930,13 @@ async fn handle_cli_connection( } } -// pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { -// &[ -// ("Go to file", &file_finder::Toggle), -// ("Open command palette", &command_palette::Toggle), -// ("Open recent projects", &recent_projects::OpenRecent), -// ("Change your settings", &zed_actions::OpenSettings), -// ] -// } +pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { + // &[ + // ("Go to file", &file_finder::Toggle), + // ("Open command palette", &command_palette::Toggle), + // ("Open recent projects", &recent_projects::OpenRecent), + // ("Change your settings", &zed_actions::OpenSettings), + // ] + // todo!() + &[] +} diff --git a/crates/zed2/src/only_instance.rs b/crates/zed2/src/only_instance.rs index b252f72ce5..a8c4b30816 100644 --- a/crates/zed2/src/only_instance.rs +++ b/crates/zed2/src/only_instance.rs @@ -37,10 +37,9 @@ pub enum IsOnlyInstance { } pub fn ensure_only_instance() -> IsOnlyInstance { - // todo!("zed_stateless") - // if *db::ZED_STATELESS { - // return IsOnlyInstance::Yes; - // } + if *db::ZED_STATELESS { + return IsOnlyInstance::Yes; + } if check_got_handshake() { return IsOnlyInstance::No; diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 4389f3012a..a33498538e 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,11 +1,17 @@ +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + mod assets; pub mod languages; mod only_instance; mod open_listener; pub use assets::*; -use client2::{Client, UserStore}; -use gpui2::{AsyncAppContext, Model}; +use collections::HashMap; +use gpui::{ + point, px, AppContext, AsyncAppContext, AsyncWindowContext, Point, Task, TitlebarOptions, + WeakView, WindowBounds, WindowKind, WindowOptions, +}; pub use only_instance::*; pub use open_listener::*; @@ -14,8 +20,14 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, }; -use futures::{channel::mpsc, SinkExt, StreamExt}; -use std::{sync::Arc, thread}; +use futures::{ + channel::{mpsc, oneshot}, + FutureExt, SinkExt, StreamExt, +}; +use std::{path::Path, sync::Arc, thread, time::Duration}; +use util::{paths::PathLikeWithPosition, ResultExt}; +use uuid::Uuid; +use workspace2::{AppState, Workspace}; pub fn connect_to_cli( server_name: &str, @@ -46,163 +58,349 @@ pub fn connect_to_cli( Ok((async_request_rx, response_tx)) } -pub struct AppState { - pub client: Arc, - pub user_store: Model, -} - pub async fn handle_cli_connection( - (mut requests, _responses): (mpsc::Receiver, IpcSender), - _app_state: Arc, - mut _cx: AsyncAppContext, + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, ) { if let Some(request) = requests.next().await { match request { - CliRequest::Open { paths: _, wait: _ } => { - // let mut caret_positions = HashMap::new(); + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::default(); - // let paths = if paths.is_empty() { - // todo!() - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - // } else { - // paths - // .into_iter() - // .filter_map(|path_with_position_string| { - // let path_with_position = PathLikeWithPosition::parse_str( - // &path_with_position_string, - // |path_str| { - // Ok::<_, std::convert::Infallible>( - // Path::new(path_str).to_path_buf(), - // ) - // }, - // ) - // .expect("Infallible"); - // let path = path_with_position.path_like; - // if let Some(row) = path_with_position.row { - // if path.is_file() { - // let row = row.saturating_sub(1); - // let col = - // path_with_position.column.unwrap_or(0).saturating_sub(1); - // caret_positions.insert(path.clone(), Point::new(row, col)); - // } - // } - // Some(path) - // }) - // .collect() - // }; + let paths = if paths.is_empty() { + workspace2::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect::>() + }; - // let mut errored = false; - // todo!("workspace") - // match cx - // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .await - // { - // Ok((workspace, items)) => { - // let mut item_release_futures = Vec::new(); + let mut errored = false; - // for (item, path) in items.into_iter().zip(&paths) { - // match item { - // Some(Ok(item)) => { - // if let Some(point) = caret_positions.remove(path) { - // if let Some(active_editor) = item.downcast::() { - // active_editor - // .downgrade() - // .update(&mut cx, |editor, cx| { - // let snapshot = - // editor.snapshot(cx).display_snapshot; - // let point = snapshot - // .buffer_snapshot - // .clip_point(point, Bias::Left); - // editor.change_selections( - // Some(Autoscroll::center()), - // cx, - // |s| s.select_ranges([point..point]), - // ); - // }) - // .log_err(); - // } - // } + if let Some(open_paths_task) = cx + .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) + .log_err() + { + match open_paths_task.await { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); - // let released = oneshot::channel(); - // cx.update(|cx| { - // item.on_release( - // cx, - // Box::new(move |_| { - // let _ = released.0.send(()); - // }), - // ) - // .detach(); - // }); - // item_release_futures.push(released.1); - // } - // Some(Err(err)) => { - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", path, err), - // }) - // .log_err(); - // errored = true; - // } - // None => {} - // } - // } + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(mut item)) => { + if let Some(point) = caret_positions.remove(path) { + todo!("editor") + // if let Some(active_editor) = item.downcast::() { + // active_editor + // .downgrade() + // .update(&mut cx, |editor, cx| { + // let snapshot = + // editor.snapshot(cx).display_snapshot; + // let point = snapshot + // .buffer_snapshot + // .clip_point(point, Bias::Left); + // editor.change_selections( + // Some(Autoscroll::center()), + // cx, + // |s| s.select_ranges([point..point]), + // ); + // }) + // .log_err(); + // } + } - // if wait { - // let background = cx.background(); - // let wait = async move { - // if paths.is_empty() { - // let (done_tx, done_rx) = oneshot::channel(); - // if let Some(workspace) = workspace.upgrade(&cx) { - // let _subscription = cx.update(|cx| { - // cx.observe_release(&workspace, move |_, _| { - // let _ = done_tx.send(()); - // }) - // }); - // drop(workspace); - // let _ = done_rx.await; - // } - // } else { - // let _ = - // futures::future::try_join_all(item_release_futures).await; - // }; - // } - // .fuse(); - // futures::pin_mut!(wait); + let released = oneshot::channel(); + cx.update(move |cx| { + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + }) + .ok(); + item_release_futures.push(released.1); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!( + "error opening {:?}: {}", + path, err + ), + }) + .log_err(); + errored = true; + } + None => {} + } + } - // loop { - // // Repeatedly check if CLI is still open to avoid wasting resources - // // waiting for files or workspaces to close. - // let mut timer = background.timer(Duration::from_secs(1)).fuse(); - // futures::select_biased! { - // _ = wait => break, - // _ = timer => { - // if responses.send(CliResponse::Ping).is_err() { - // break; - // } - // } - // } - // } - // } - // } - // Err(error) => { - // errored = true; - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", paths, error), - // }) - // .log_err(); - // } - // } + if wait { + let executor = cx.background_executor().clone(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + let _subscription = + workspace.update(&mut cx, move |_, cx| { + cx.on_release(|_, _| { + let _ = done_tx.send(()); + }) + }); + let _ = done_rx.await; + } else { + let _ = futures::future::try_join_all(item_release_futures) + .await; + }; + } + .fuse(); + futures::pin_mut!(wait); - // responses - // .send(CliResponse::Exit { - // status: i32::from(errored), - // }) - // .log_err(); + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = executor.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); + } } } } } + +pub fn build_window_options( + bounds: Option, + display_uuid: Option, + cx: &mut AppContext, +) -> WindowOptions { + let bounds = bounds.unwrap_or(WindowBounds::Maximized); + let display = display_uuid.and_then(|uuid| { + cx.displays() + .into_iter() + .find(|display| display.uuid().ok() == Some(uuid)) + }); + + WindowOptions { + bounds, + titlebar: Some(TitlebarOptions { + title: None, + appears_transparent: true, + traffic_light_position: Some(point(px(8.), px(8.))), + }), + center: false, + focus: false, + show: false, + kind: WindowKind::Normal, + is_movable: true, + display_id: display.map(|display| display.id()), + } +} + +pub fn initialize_workspace( + workspace_handle: WeakView, + was_deserialized: bool, + app_state: Arc, + cx: AsyncWindowContext, +) -> Task> { + cx.spawn(|mut cx| async move { + workspace_handle.update(&mut cx, |workspace, cx| { + let workspace_handle = cx.view(); + cx.subscribe(&workspace_handle, { + move |workspace, _, event, cx| { + if let workspace2::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + // todo!() + // let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); + // toolbar.add_item(breadcrumbs, cx); + // let buffer_search_bar = cx.add_view(BufferSearchBar::new); + // toolbar.add_item(buffer_search_bar.clone(), cx); + // let quick_action_bar = cx.add_view(|_| { + // QuickActionBar::new(buffer_search_bar, workspace) + // }); + // toolbar.add_item(quick_action_bar, cx); + // let diagnostic_editor_controls = + // cx.add_view(|_| diagnostics2::ToolbarControls::new()); + // toolbar.add_item(diagnostic_editor_controls, cx); + // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + // toolbar.add_item(project_search_bar, cx); + // let submit_feedback_button = + // cx.add_view(|_| SubmitFeedbackButton::new()); + // toolbar.add_item(submit_feedback_button, cx); + // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + // toolbar.add_item(feedback_info_text, cx); + // let lsp_log_item = + // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); + // toolbar.add_item(lsp_log_item, cx); + // let syntax_tree_item = cx + // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + // toolbar.add_item(syntax_tree_item, cx); + }) + }); + } + } + }) + .detach(); + + // cx.emit(workspace2::Event::PaneAdded( + // workspace.active_pane().clone(), + // )); + + // let collab_titlebar_item = + // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); + // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); + + // let copilot = + // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); + // let diagnostic_summary = + // cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); + // let activity_indicator = activity_indicator::ActivityIndicator::new( + // workspace, + // app_state.languages.clone(), + // cx, + // ); + // let active_buffer_language = + // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); + // let feedback_button = cx.add_view(|_| { + // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) + // }); + // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + workspace.status_bar().update(cx, |status_bar, cx| { + // 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(active_buffer_language, cx); + // status_bar.add_right_item(vim_mode_indicator, cx); + // status_bar.add_right_item(cursor_position, cx); + }); + + // auto_update::notify_of_any_new_update(cx.weak_handle(), cx); + + // vim::observe_keystrokes(cx); + + // cx.on_window_should_close(|workspace, cx| { + // if let Some(task) = workspace.close(&Default::default(), cx) { + // task.detach_and_log_err(cx); + // } + // false + // }); + // })?; + + // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + // let channels_panel = + // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + // let chat_panel = + // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); + // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + // workspace_handle.clone(), + // cx.clone(), + // ); + // let ( + // project_panel, + // terminal_panel, + // assistant_panel, + // channels_panel, + // chat_panel, + // notification_panel, + // ) = futures::try_join!( + // project_panel, + // terminal_panel, + // assistant_panel, + // channels_panel, + // chat_panel, + // notification_panel, + // )?; + // workspace_handle.update(&mut cx, |workspace, cx| { + // let project_panel_position = project_panel.position(cx); + // workspace.add_panel_with_extra_event_handler( + // project_panel, + // cx, + // |workspace, _, event, cx| match event { + // project_panel::Event::NewSearchInDirectory { dir_entry } => { + // search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) + // } + // project_panel::Event::ActivatePanel => { + // workspace.focus_panel::(cx); + // } + // _ => {} + // }, + // ); + // workspace.add_panel(terminal_panel, cx); + // workspace.add_panel(assistant_panel, cx); + // workspace.add_panel(channels_panel, cx); + // workspace.add_panel(chat_panel, cx); + // workspace.add_panel(notification_panel, cx); + + // if !was_deserialized + // && workspace + // .project() + // .read(cx) + // .visible_worktrees(cx) + // .any(|tree| { + // tree.read(cx) + // .root_entry() + // .map_or(false, |entry| entry.is_dir()) + // }) + // { + // workspace.toggle_dock(project_panel_position, cx); + // } + // cx.focus_self(); + })?; + Ok(()) + }) +}