Merge remote-tracking branch 'origin/main' into gpui2-no-send

Nathan Sobo created

Change summary

Cargo.lock                                                                         |  126 
Cargo.toml                                                                         |    2 
crates/assistant/src/codegen.rs                                                    |  108 
crates/assistant/src/prompts.rs                                                    |   18 
crates/call2/Cargo.toml                                                            |    6 
crates/call2/src/participant.rs                                                    |   31 
crates/call2/src/room.rs                                                           |  927 
crates/gpui2/src/subscription.rs                                                   |    2 
crates/gpui2/src/window.rs                                                         |    6 
crates/language2/src/language2.rs                                                  |   10 
crates/live_kit_client/LiveKitBridge/Package.resolved                              |    4 
crates/live_kit_client2/.cargo/config.toml                                         |    2 
crates/live_kit_client2/Cargo.toml                                                 |   71 
crates/live_kit_client2/LiveKitBridge2/Package.resolved                            |   52 
crates/live_kit_client2/LiveKitBridge2/Package.swift                               |   27 
crates/live_kit_client2/LiveKitBridge2/README.md                                   |    3 
crates/live_kit_client2/LiveKitBridge2/Sources/LiveKitBridge2/LiveKitBridge2.swift |  327 
crates/live_kit_client2/build.rs                                                   |  172 
crates/live_kit_client2/examples/test_app.rs                                       |  178 
crates/live_kit_client2/src/live_kit_client2.rs                                    |   11 
crates/live_kit_client2/src/prod.rs                                                |  947 
crates/live_kit_client2/src/test.rs                                                |  651 
crates/multi_buffer2/Cargo.toml                                                    |   78 
crates/multi_buffer2/src/anchor.rs                                                 |  138 
crates/multi_buffer2/src/multi_buffer2.rs                                          | 5393 
crates/storybook2/src/stories/colors.rs                                            |   18 
crates/storybook2/src/stories/focus.rs                                             |   16 
crates/storybook2/src/stories/scroll.rs                                            |   10 
crates/storybook2/src/storybook2.rs                                                |   10 
crates/text2/Cargo.toml                                                            |   37 
crates/text2/src/anchor.rs                                                         |  144 
crates/text2/src/locator.rs                                                        |  125 
crates/text2/src/network.rs                                                        |   69 
crates/text2/src/operation_queue.rs                                                |  153 
crates/text2/src/patch.rs                                                          |  594 
crates/text2/src/selection.rs                                                      |  123 
crates/text2/src/subscription.rs                                                   |   48 
crates/text2/src/tests.rs                                                          |  764 
crates/text2/src/text2.rs                                                          | 2682 
crates/text2/src/undo_map.rs                                                       |  112 
crates/theme2/Cargo.toml                                                           |   12 
crates/theme2/src/colors.rs                                                        |  144 
crates/theme2/src/default_colors.rs                                                |  661 
crates/theme2/src/default_theme.rs                                                 |   58 
crates/theme2/src/registry.rs                                                      |   63 
crates/theme2/src/scale.rs                                                         |  359 
crates/theme2/src/settings.rs                                                      |   14 
crates/theme2/src/syntax.rs                                                        |   37 
crates/theme2/src/theme2.rs                                                        |  163 
crates/theme2/src/themes/andromeda.rs                                              |  130 
crates/theme2/src/themes/atelier_cave_dark.rs                                      |  136 
crates/theme2/src/themes/atelier_cave_light.rs                                     |  136 
crates/theme2/src/themes/atelier_dune_dark.rs                                      |  136 
crates/theme2/src/themes/atelier_dune_light.rs                                     |  136 
crates/theme2/src/themes/atelier_estuary_dark.rs                                   |  136 
crates/theme2/src/themes/atelier_estuary_light.rs                                  |  136 
crates/theme2/src/themes/atelier_forest_dark.rs                                    |  136 
crates/theme2/src/themes/atelier_forest_light.rs                                   |  136 
crates/theme2/src/themes/atelier_heath_dark.rs                                     |  136 
crates/theme2/src/themes/atelier_heath_light.rs                                    |  136 
crates/theme2/src/themes/atelier_lakeside_dark.rs                                  |  136 
crates/theme2/src/themes/atelier_lakeside_light.rs                                 |  136 
crates/theme2/src/themes/atelier_plateau_dark.rs                                   |  136 
crates/theme2/src/themes/atelier_plateau_light.rs                                  |  136 
crates/theme2/src/themes/atelier_savanna_dark.rs                                   |  136 
crates/theme2/src/themes/atelier_savanna_light.rs                                  |  136 
crates/theme2/src/themes/atelier_seaside_dark.rs                                   |  136 
crates/theme2/src/themes/atelier_seaside_light.rs                                  |  136 
crates/theme2/src/themes/atelier_sulphurpool_dark.rs                               |  136 
crates/theme2/src/themes/atelier_sulphurpool_light.rs                              |  136 
crates/theme2/src/themes/ayu_dark.rs                                               |  130 
crates/theme2/src/themes/ayu_light.rs                                              |  130 
crates/theme2/src/themes/ayu_mirage.rs                                             |  130 
crates/theme2/src/themes/gruvbox_dark.rs                                           |  131 
crates/theme2/src/themes/gruvbox_dark_hard.rs                                      |  131 
crates/theme2/src/themes/gruvbox_dark_soft.rs                                      |  131 
crates/theme2/src/themes/gruvbox_light.rs                                          |  131 
crates/theme2/src/themes/gruvbox_light_hard.rs                                     |  131 
crates/theme2/src/themes/gruvbox_light_soft.rs                                     |  131 
crates/theme2/src/themes/mod.rs                                                    |   79 
crates/theme2/src/themes/one_dark.rs                                               |  131 
crates/theme2/src/themes/one_light.rs                                              |  131 
crates/theme2/src/themes/rose_pine.rs                                              |  132 
crates/theme2/src/themes/rose_pine_dawn.rs                                         |  132 
crates/theme2/src/themes/rose_pine_moon.rs                                         |  132 
crates/theme2/src/themes/sandcastle.rs                                             |  130 
crates/theme2/src/themes/solarized_dark.rs                                         |  130 
crates/theme2/src/themes/solarized_light.rs                                        |  130 
crates/theme2/src/themes/summercamp.rs                                             |  130 
crates/theme_converter/Cargo.toml                                                  |   18 
crates/theme_converter/src/main.rs                                                 |  390 
crates/theme_converter/src/theme_printer.rs                                        |  174 
crates/ui2/Cargo.toml                                                              |    1 
crates/ui2/src/components/breadcrumb.rs                                            |   24 
crates/ui2/src/components/buffer.rs                                                |   19 
crates/ui2/src/components/buffer_search.rs                                         |    4 
crates/ui2/src/components/collab_panel.rs                                          |   30 
crates/ui2/src/components/context_menu.rs                                          |    6 
crates/ui2/src/components/icon_button.rs                                           |   20 
crates/ui2/src/components/keybinding.rs                                            |    6 
crates/ui2/src/components/list.rs                                                  |   34 
crates/ui2/src/components/modal.rs                                                 |   10 
crates/ui2/src/components/multi_buffer.rs                                          |   16 
crates/ui2/src/components/notification_toast.rs                                    |    7 
crates/ui2/src/components/notifications_panel.rs                                   |    4 
crates/ui2/src/components/palette.rs                                               |   16 
crates/ui2/src/components/panel.rs                                                 |    6 
crates/ui2/src/components/panes.rs                                                 |    4 
crates/ui2/src/components/player_stack.rs                                          |    4 
crates/ui2/src/components/project_panel.rs                                         |    4 
crates/ui2/src/components/status_bar.rs                                            |    4 
crates/ui2/src/components/tab.rs                                                   |   13 
crates/ui2/src/components/tab_bar.rs                                               |    4 
crates/ui2/src/components/terminal.rs                                              |    6 
crates/ui2/src/components/title_bar.rs                                             |    3 
crates/ui2/src/components/toast.rs                                                 |    4 
crates/ui2/src/components/toolbar.rs                                               |   14 
crates/ui2/src/components/traffic_lights.rs                                        |   10 
crates/ui2/src/components/workspace.rs                                             |   50 
crates/ui2/src/elements/avatar.rs                                                  |    5 
crates/ui2/src/elements/button.rs                                                  |   28 
crates/ui2/src/elements/details.rs                                                 |    7 
crates/ui2/src/elements/icon.rs                                                    |   19 
crates/ui2/src/elements/input.rs                                                   |   16 
crates/ui2/src/elements/label.rs                                                   |   20 
crates/ui2/src/elements/player.rs                                                  |    8 
crates/ui2/src/elements/tool_divider.rs                                            |    4 
crates/ui2/src/prelude.rs                                                          |   27 
crates/ui2/src/settings.rs                                                         |    2 
crates/ui2/src/static_data.rs                                                      |   96 
crates/ui2/src/story.rs                                                            |   12 
crates/zed/Cargo.toml                                                              |    2 
crates/zed2/Cargo.toml                                                             |    4 
133 files changed, 14,913 insertions(+), 7,231 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1169,7 +1169,7 @@ dependencies = [
  "futures 0.3.28",
  "gpui2",
  "language2",
- "live_kit_client",
+ "live_kit_client2",
  "log",
  "media",
  "postage",
@@ -4589,6 +4589,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"
@@ -5035,6 +5068,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",
+ "git",
+ "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_text",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "smallvec",
+ "smol",
+ "snippet",
+ "sum_tree",
+ "text",
+ "theme2",
+ "tree-sitter",
+ "tree-sitter-html",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "multimap"
 version = "0.8.3"
@@ -8726,6 +8806,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"
@@ -8759,6 +8862,7 @@ dependencies = [
  "gpui2",
  "indexmap 1.9.3",
  "parking_lot 0.11.2",
+ "refineable",
  "schemars",
  "serde",
  "serde_derive",
@@ -8768,21 +8872,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"
@@ -9600,6 +9689,7 @@ dependencies = [
  "itertools 0.11.0",
  "rand 0.8.5",
  "serde",
+ "settings2",
  "smallvec",
  "strum",
  "theme2",
@@ -10788,7 +10878,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.111.0"
+version = "0.112.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -10985,7 +11075,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempdir",
- "text",
+ "text2",
  "theme2",
  "thiserror",
  "tiny_http",

Cargo.toml 🔗

@@ -61,6 +61,7 @@ members = [
     "crates/menu",
     "crates/menu2",
     "crates/multi_buffer",
+    "crates/multi_buffer2",
     "crates/node_runtime",
     "crates/notifications",
     "crates/outline",
@@ -92,7 +93,6 @@ members = [
     "crates/text",
     "crates/theme",
     "crates/theme2",
-    "crates/theme_converter",
     "crates/theme_selector",
     "crates/ui2",
     "crates/util",

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<Item = Result<String>>,
 ) -> impl Stream<Item = Result<String>> {
     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)
     })
@@ -558,50 +590,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::<String>()
                 .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::<String>()
                 .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::<String>()
                 .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::<String>()
                 .await,
             "Lorem ipsum dolor"
         );
         assert_eq!(
-            strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
+            strip_invalid_spans_from_codeblock(chunks(
+                "```html\n```js\nLorem ipsum dolor\n```\n```",
+                2
+            ))
+            .map(|chunk| chunk.unwrap())
+            .collect::<String>()
+            .await,
+            "```js\nLorem ipsum dolor\n```"
+        );
+        assert_eq!(
+            strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
                 .map(|chunk| chunk.unwrap())
                 .collect::<String>()
                 .await,
-            "```js\nLorem ipsum dolor\n```"
+            "``\nLorem ipsum dolor\n```"
         );
         assert_eq!(
-            strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
+            strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
                 .map(|chunk| chunk.unwrap())
                 .collect::<String>()
                 .await,
-            "``\nLorem ipsum dolor\n```"
+            "Lorem ipsum"
         );
 
+        assert_eq!(
+            strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum"
+        );
+
+        assert_eq!(
+            strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum"
+        );
+        assert_eq!(
+            strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum"
+        );
         fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
             stream::iter(
                 text.chars()

crates/assistant/src/prompts.rs 🔗

@@ -80,12 +80,12 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> 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<impl ToOffset>) -> 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,
                 }

crates/call2/Cargo.toml 🔗

@@ -13,7 +13,7 @@ test-support = [
     "client2/test-support",
     "collections/test-support",
     "gpui2/test-support",
-    "live_kit_client/test-support",
+    "live_kit_client2/test-support",
     "project2/test-support",
     "util/test-support"
 ]
@@ -24,7 +24,7 @@ client2 = { path = "../client2" }
 collections = { path = "../collections" }
 gpui2 = { path = "../gpui2" }
 log.workspace = true
-live_kit_client = { path = "../live_kit_client" }
+live_kit_client2 = { path = "../live_kit_client2" }
 fs2 = { path = "../fs2" }
 language2 = { path = "../language2" }
 media = { path = "../media" }
@@ -47,6 +47,6 @@ fs2 = { path = "../fs2", features = ["test-support"] }
 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"] }
+live_kit_client2 = { path = "../live_kit_client2", features = ["test-support"] }
 project2 = { path = "../project2", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

crates/call2/src/participant.rs 🔗

@@ -1,10 +1,12 @@
 use anyhow::{anyhow, Result};
 use client2::ParticipantIndex;
 use client2::{proto, User};
+use collections::HashMap;
 use gpui2::WeakModel;
-pub use live_kit_client::Frame;
+pub use live_kit_client2::Frame;
+use live_kit_client2::{RemoteAudioTrack, RemoteVideoTrack};
 use project2::Project;
-use std::{fmt, sync::Arc};
+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<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
-    // pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
-}
-
-#[derive(Clone)]
-pub struct RemoteVideoTrack {
-    pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
-}
-
-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<Frame> {
-        self.live_kit_track.frames()
-    }
+    pub video_tracks: HashMap<live_kit_client2::Sid, Arc<RemoteVideoTrack>>,
+    pub audio_tracks: HashMap<live_kit_client2::Sid, Arc<RemoteAudioTrack>>,
 }

crates/call2/src/room.rs 🔗

@@ -1,9 +1,6 @@
-#![allow(dead_code, unused)]
-// todo!()
-
 use crate::{
     call_settings::CallSettings,
-    participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
+    participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
     IncomingCall,
 };
 use anyhow::{anyhow, Result};
@@ -19,12 +16,15 @@ use gpui2::{
     AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
 };
 use language2::LanguageRegistry;
-use live_kit_client::{LocalTrackPublication, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate};
+use live_kit_client2::{
+    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 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<u64>,
-    // live_kit: Option<LiveKitRoom>,
+    live_kit: Option<LiveKitRoom>,
     status: RoomStatus,
     shared_projects: HashSet<WeakModel<Project>>,
     joined_projects: HashSet<WeakModel<Project>>,
@@ -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_client2::ConnectionState::Connected { .. }
+            )
+        } else {
+            false
+        }
     }
 
     fn new(
@@ -114,125 +113,130 @@ impl Room {
         user_store: Model<UserStore>,
         cx: &mut ModelContext<Self>,
     ) -> 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;
-        //             };
-
-        //             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;
-        //             };
-
-        //             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;
-        //             };
-
-        //             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?;
-
-        //         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);
-
-        //     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()
-        // });
-
-        // Audio::play_sound(Sound::Joined, cx);
-
-        // 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,
-        // }
+        let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
+            let room = live_kit_client2::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_client2::ConnectionState::Disconnected {
+                        this.update(&mut cx, |this, cx| this.leave(cx).log_err())
+                            .ok();
+                        break;
+                    }
+                }
+            });
+
+            let _maintain_video_tracks = cx.spawn_on_main({
+                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();
+                    }
+                }
+            });
+
+            let _maintain_audio_tracks = cx.spawn_on_main({
+                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();
+                    }
+                }
+            });
+
+            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?;
+                }
+
+                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
+        };
+
+        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);
+
+        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_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(
@@ -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();
     }
@@ -795,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());
-
-                            //     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();
-                            //     }
-                            // }
+                            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, publication) in
+                                    audio_tracks.iter().zip(publications.iter())
+                                {
+                                    this.remote_audio_track_updated(
+                                        RemoteAudioTrackUpdate::Subscribed(
+                                            track.clone(),
+                                            publication.clone(),
+                                        ),
+                                        cx,
+                                    )
+                                    .log_err();
+                                }
+                            }
                         }
                     }
 
@@ -919,7 +923,6 @@ impl Room {
         change: RemoteVideoTrackUpdate,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
-        todo!();
         match change {
             RemoteVideoTrackUpdate::Subscribed(track) => {
                 let user_id = track.publisher_id().parse()?;
@@ -928,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,
                 });
@@ -947,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,
                 });
@@ -977,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();
-
-                // cx.emit(Event::RemoteAudioTracksChanged {
-                //     participant_id: participant.peer_id,
-                // });
+                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,
+                });
             }
             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,
+                });
             }
         }
 
@@ -1216,278 +1210,269 @@ 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<bool> {
-        // 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<Self>) -> Task<Result<()>> {
-        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")));
-        // }
-
-        // 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
-        //     };
-
-        //     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)
-        //             };
-
-        //             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)
-        //                     }
-        //                 }
-        //             }
-        //         })?
-        // })
+        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")));
+        };
+
+        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 (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)
+                            }
+                        }
+                    }
+                })?
+        })
     }
 
     pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        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")));
-        // }
-
-        // 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
-        //     };
-
-        //     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)
-        //             };
-
-        //             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();
-        //                     }
-
-        //                     Audio::play_sound(Sound::StartScreenshare, cx);
-
-        //                     Ok(())
-        //                 }
-        //                 Err(error) => {
-        //                     if canceled {
-        //                         Ok(())
-        //                     } else {
-        //                         live_kit.screen_track = LocalTrack::None;
-        //                         cx.notify();
-        //                         Err(error)
-        //                     }
-        //                 }
-        //             }
-        //         })?
-        // })
+        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")));
+        };
+
+        cx.spawn_on_main(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 (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();
+                            }
+
+                            Audio::play_sound(Sound::StartScreenshare, cx);
+
+                            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<Self>) -> Result<Task<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 (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();
-        //         }
-        //     }
-
-        //     Ok(ret_task)
-        // } else {
-        //     Err(anyhow!("LiveKit not started"))
-        // }
+        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;
+
+            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"))
+        }
     }
 
     pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
-        todo!()
-        // 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)),
-        //             );
-        //         }
-        //     }
-
-        //     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"))
-        // }
+        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)),
+                    );
+                }
+            }
+
+            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"))
+        }
     }
 
     pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {

crates/gpui2/src/subscription.rs 🔗

@@ -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

crates/gpui2/src/window.rs 🔗

@@ -506,6 +506,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<Pixels>) {
+        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();

crates/language2/src/language2.rs 🔗

@@ -42,7 +42,7 @@ use std::{
     },
 };
 use syntax_map::SyntaxSnapshot;
-use theme2::{SyntaxTheme, Theme};
+use theme2::{SyntaxTheme, ThemeVariant};
 use tree_sitter::{self, Query};
 use unicase::UniCase;
 use util::{http::HttpClient, paths::PathExt};
@@ -642,7 +642,7 @@ struct LanguageRegistryState {
     next_available_language_id: AvailableLanguageId,
     loading_languages: HashMap<AvailableLanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
     subscription: (watch::Sender<()>, watch::Receiver<()>),
-    theme: Option<Arc<Theme>>,
+    theme: Option<Arc<ThemeVariant>>,
     version: usize,
     reload_count: usize,
 }
@@ -743,11 +743,11 @@ impl LanguageRegistry {
         self.state.read().reload_count
     }
 
-    pub fn set_theme(&self, theme: Arc<Theme>) {
+    pub fn set_theme(&self, theme: Arc<ThemeVariant>) {
         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());
         }
     }
 
@@ -1048,7 +1048,7 @@ impl LanguageRegistryState {
 
     fn add(&mut self, language: Arc<Language>) {
         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;

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"
         }
       }
     ]

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 = { 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 = { 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

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
+}

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")]),
+    ]
+)

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<LKRoomDelegate>.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<Room>.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<Room>.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<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalVideoTrack>.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<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalAudioTrack>.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<Room>.fromOpaque(room).takeUnretainedValue()
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+    let _ = room.localParticipant?.unpublish(publication: publication)
+}
+
+@_cdecl("LKRoomAudioTracksForRemoteParticipant")
+public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.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<Room>.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<Room>.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<MacOSDisplay>.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<Track>.fromOpaque(track).takeUnretainedValue() as! VideoTrack
+    let renderer = Unmanaged<LKVideoRenderer>.fromOpaque(renderer).takeRetainedValue()
+    renderer.track = track
+    track.add(videoRenderer: renderer)
+}
+
+@_cdecl("LKRemoteVideoTrackGetSid")
+public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString {
+    let track = Unmanaged<RemoteVideoTrack>.fromOpaque(track).takeUnretainedValue()
+    return track.sid! as CFString
+}
+
+@_cdecl("LKRemoteAudioTrackGetSid")
+public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString {
+    let track = Unmanaged<RemoteAudioTrack>.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<LocalTrackPublication>.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<RemoteTrackPublication>.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<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.muted
+}
+
+@_cdecl("LKRemoteTrackPublicationGetSid")
+public func LKRemoteTrackPublicationGetSid(
+    publication: UnsafeRawPointer
+) -> CFString {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.sid as CFString
+}

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<String>,
+    pub runtime_library_import_paths: Vec<String>,
+    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())
+    }
+}

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_on_main(|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.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();
+}

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::*;

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<swift::Room>,
+    connection: Mutex<(
+        watch::Sender<ConnectionState>,
+        watch::Receiver<ConnectionState>,
+    )>,
+    remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
+    remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
+    _delegate: Mutex<RoomDelegate>,
+}
+
+trait AssertSendSync: Send {}
+impl AssertSendSync for Room {}
+
+impl Room {
+    pub fn new() -> Arc<Self> {
+        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<ConnectionState> {
+        self.connection.lock().1.clone()
+    }
+
+    pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
+        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<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
+        extern "C" fn callback(tx: *mut c_void, sources: CFArrayRef, error: CFStringRef) {
+            unsafe {
+                let tx = Box::from_raw(tx as *mut oneshot::Sender<Result<Vec<MacOSDisplay>>>);
+
+                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<Self>,
+        track: LocalVideoTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
+        extern "C" fn callback(
+            tx: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            error: CFStringRef,
+        ) {
+            let tx =
+                unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
+            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<Self>,
+        track: LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
+        extern "C" fn callback(
+            tx: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            error: CFStringRef,
+        ) {
+            let tx =
+                unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
+            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<Arc<RemoteVideoTrack>> {
+        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<Arc<RemoteAudioTrack>> {
+        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<Arc<RemoteTrackPublication>> {
+        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<RemoteAudioTrackUpdate> {
+        let (tx, rx) = mpsc::unbounded();
+        self.remote_audio_track_subscribers.lock().push(tx);
+        rx
+    }
+
+    pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackUpdate> {
+        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<String>) {
+        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<Result<()>>,
+    ) {
+        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<Result<()>>) };
+            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<MacOSDisplay>) {
+        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<Room>,
+}
+
+impl RoomDelegate {
+    fn new(weak_room: Weak<Room>) -> 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<Output = Result<()>> {
+        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<Result<()>>) };
+            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<swift::RemoteTrackPublication>,
+}
+
+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<Output = Result<()>> {
+        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<Result<()>>) };
+            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<swift::RemoteAudioTrack>,
+    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<Output = Result<()>> {
+        async { Ok(()) }
+    }
+
+    pub fn disable(&self) -> impl Future<Output = Result<()>> {
+        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<swift::RemoteVideoTrack>,
+    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<Frame> {
+        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<Frame>);
+                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<Frame>);
+            }
+        }
+
+        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<RemoteVideoTrack>),
+    Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
+pub enum RemoteAudioTrackUpdate {
+    ActiveSpeakersChanged { speakers: Vec<Sid> },
+    MuteChanged { track_id: Sid, muted: bool },
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
+    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()
+    }
+}

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::Executor;
+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<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
+
+pub struct TestServer {
+    pub url: String,
+    pub api_key: String,
+    pub secret_key: String,
+    rooms: Mutex<HashMap<String, TestServerRoom>>,
+    executor: Arc<Executor>,
+}
+
+impl TestServer {
+    pub fn create(
+        url: String,
+        api_key: String,
+        secret_key: String,
+        executor: Arc<Executor>,
+    ) -> Result<Arc<TestServer>> {
+        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<Arc<TestServer>> {
+        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<Room>) -> 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<Vec<Arc<RemoteVideoTrack>>> {
+        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<Vec<Arc<RemoteAudioTrack>>> {
+        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<Sid, Arc<Room>>,
+    video_tracks: Vec<Arc<RemoteVideoTrack>>,
+    audio_tracks: Vec<Arc<RemoteAudioTrack>>,
+}
+
+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<String> {
+        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<String> {
+        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<ConnectionState>,
+        watch::Receiver<ConnectionState>,
+    ),
+    display_sources: Vec<MacOSDisplay>,
+    audio_track_updates: (
+        async_broadcast::Sender<RemoteAudioTrackUpdate>,
+        async_broadcast::Receiver<RemoteAudioTrackUpdate>,
+    ),
+    video_track_updates: (
+        async_broadcast::Sender<RemoteVideoTrackUpdate>,
+        async_broadcast::Receiver<RemoteVideoTrackUpdate>,
+    ),
+}
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum ConnectionState {
+    Disconnected,
+    Connected { url: String, token: String },
+}
+
+pub struct Room(Mutex<RoomState>);
+
+impl Room {
+    pub fn new() -> Arc<Self> {
+        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<ConnectionState> {
+        self.0.lock().connection.1.clone()
+    }
+
+    pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
+        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<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
+        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<Self>,
+        track: LocalVideoTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        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<Self>,
+        track: LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        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<Arc<RemoteAudioTrack>> {
+        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<Arc<RemoteTrackPublication>> {
+        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<Arc<RemoteVideoTrack>> {
+        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<Item = RemoteAudioTrackUpdate> {
+        self.0.lock().audio_track_updates.1.clone()
+    }
+
+    pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
+        self.0.lock().video_track_updates.1.clone()
+    }
+
+    pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {
+        self.0.lock().display_sources = sources;
+    }
+
+    fn test_server(&self) -> Arc<TestServer> {
+        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<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
+pub struct RemoteTrackPublication;
+
+impl RemoteTrackPublication {
+    pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
+        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<Frame>,
+}
+
+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<Frame>,
+}
+
+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<Frame> {
+        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<Output = Result<()>> {
+        async { Ok(()) }
+    }
+
+    pub fn disable(&self) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
+#[derive(Clone)]
+pub enum RemoteVideoTrackUpdate {
+    Subscribed(Arc<RemoteVideoTrack>),
+    Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
+#[derive(Clone)]
+pub enum RemoteAudioTrackUpdate {
+    ActiveSpeakersChanged { speakers: Vec<Sid> },
+    MuteChanged { track_id: Sid, muted: bool },
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
+    Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
+#[derive(Clone)]
+pub struct MacOSDisplay {
+    frames: (
+        async_broadcast::Sender<Frame>,
+        async_broadcast::Receiver<Frame>,
+    ),
+}
+
+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")
+    }
+}

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 = [
+    "copilot2/test-support",
+    "text/test-support",
+    "language2/test-support",
+    "gpui2/test-support",
+    "util/test-support",
+    "tree-sitter-rust",
+    "tree-sitter-typescript"
+]
+
+[dependencies]
+client2 = { path = "../client2" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+git = { path = "../git" }
+gpui2 = { path = "../gpui2" }
+language2 = { path = "../language2" }
+lsp2 = { path = "../lsp2" }
+rich_text = { path = "../rich_text" }
+settings2 = { path = "../settings2" }
+snippet = { path = "../snippet" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+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]
+copilot2 = { path = "../copilot2", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+lsp2 = { path = "../lsp2", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+project2 = { path = "../project2", features = ["test-support"] }
+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

crates/multi_buffer2/src/anchor.rs 🔗

@@ -0,0 +1,138 @@
+use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
+use language2::{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<u64>,
+    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<D>(&self, snapshot: &MultiBufferSnapshot) -> D
+    where
+        D: TextDimension + Ord + Sub<D, Output = D>,
+    {
+        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<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
+    fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
+    fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
+}
+
+impl AnchorRangeExt for Range<Anchor> {
+    fn cmp(&self, other: &Range<Anchor>, 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<usize> {
+        self.start.to_offset(content)..self.end.to_offset(content)
+    }
+
+    fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
+        self.start.to_point(content)..self.end.to_point(content)
+    }
+}

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 gpui2::{AppContext, EventEmitter, Model, ModelContext};
+pub use language2::Completion;
+use language2::{
+    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 theme2::SyntaxTheme;
+use util::post_inc;
+
+#[cfg(any(test, feature = "test-support"))]
+use gpui2::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<MultiBufferSnapshot>,
+    buffers: RefCell<HashMap<u64, BufferState>>,
+    next_excerpt_id: usize,
+    subscriptions: Topic,
+    singleton: bool,
+    replica_id: ReplicaId,
+    history: History,
+    title: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+    ExcerptsAdded {
+        buffer: Model<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language2::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
+    ExcerptsEdited {
+        ids: Vec<ExcerptId>,
+    },
+    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<Transaction>,
+    redo_stack: Vec<Transaction>,
+    transaction_depth: usize,
+    group_interval: Duration,
+}
+
+#[derive(Clone)]
+struct Transaction {
+    id: TransactionId,
+    buffer_transactions: HashMap<u64, text::TransactionId>,
+    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<Buffer>,
+    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<Locator>,
+    _subscriptions: [gpui2::Subscription; 2],
+}
+
+#[derive(Clone, Default)]
+pub struct MultiBufferSnapshot {
+    singleton: bool,
+    excerpts: SumTree<Excerpt>,
+    excerpt_ids: SumTree<ExcerptIdMapping>,
+    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<text::Anchor>,
+    pub starts_new_buffer: bool,
+}
+
+#[derive(Clone)]
+struct Excerpt {
+    id: ExcerptId,
+    locator: Locator,
+    buffer_id: u64,
+    buffer: BufferSnapshot,
+    range: ExcerptRange<text::Anchor>,
+    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<T> {
+    pub context: Range<T>,
+    pub primary: Option<Range<T>>,
+}
+
+#[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<u32>,
+    excerpts: Cursor<'a, Excerpt, Point>,
+}
+
+pub struct MultiBufferChunks<'a> {
+    range: Range<usize>,
+    excerpts: Cursor<'a, Excerpt, usize>,
+    excerpt_chunks: Option<ExcerptChunks<'a>>,
+    language_aware: bool,
+}
+
+pub struct MultiBufferBytes<'a> {
+    range: Range<usize>,
+    excerpts: Cursor<'a, Excerpt, usize>,
+    excerpt_bytes: Option<ExcerptBytes<'a>>,
+    chunk: &'a [u8],
+}
+
+pub struct ReversedMultiBufferBytes<'a> {
+    range: Range<usize>,
+    excerpts: Cursor<'a, Excerpt, usize>,
+    excerpt_bytes: Option<ExcerptBytes<'a>>,
+    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>) -> 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<Buffer>, cx: &mut ModelContext<Self>) -> 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<MultiBufferSnapshot> {
+        self.sync(cx);
+        self.snapshot.borrow()
+    }
+
+    pub fn as_singleton(&self) -> Option<Model<Buffer>> {
+        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<T: ToOffset>(
+        &self,
+        offset: T,
+        theme: Option<&SyntaxTheme>,
+        cx: &AppContext,
+    ) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
+        self.read(cx).symbols_containing(offset, theme)
+    }
+
+    pub fn edit<I, S, T>(
+        &mut self,
+        edits: I,
+        mut autoindent_mode: Option<AutoindentMode>,
+        cx: &mut ModelContext<Self>,
+    ) where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        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<usize>,
+            new_text: Arc<str>,
+            is_insertion: bool,
+            original_indent_column: u32,
+        }
+        let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
+        let mut edited_excerpt_ids = Vec::new();
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        for (ix, (range, new_text)) in edits.enumerate() {
+            let new_text: Arc<str> = 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<u64, Vec<BufferEdit>>,
+            autoindent_mode: Option<AutoindentMode>,
+            edited_excerpt_ids: Vec<ExcerptId>,
+            cx: &mut ModelContext<MultiBuffer>,
+        ) {
+            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<str> = "".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<Self>) -> Option<TransactionId> {
+        self.start_transaction_at(Instant::now(), cx)
+    }
+
+    pub fn start_transaction_at(
+        &mut self,
+        now: Instant,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<TransactionId> {
+        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<Self>) -> Option<TransactionId> {
+        self.end_transaction_at(Instant::now(), cx)
+    }
+
+    pub fn end_transaction_at(
+        &mut self,
+        now: Instant,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<TransactionId> {
+        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<Self>,
+    ) {
+        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>) {
+        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<Self>)
+    where
+        T: IntoIterator<Item = (&'a Model<Buffer>, &'a language2::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<Self>,
+    ) {
+        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<Anchor>],
+        line_mode: bool,
+        cursor_shape: CursorShape,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
+            Default::default();
+        let snapshot = self.read(cx);
+        let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
+        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<Self>) {
+        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<Self>) -> Option<TransactionId> {
+        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<Self>) -> Option<TransactionId> {
+        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<Self>) {
+        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<Buffer>,
+        ranges: Vec<Range<text::Anchor>>,
+        context_line_count: u32,
+        cx: &mut ModelContext<Self>,
+    ) -> mpsc::Receiver<Range<Anchor>> {
+        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.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<O>(
+        &mut self,
+        buffer: Model<Buffer>,
+        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Vec<ExcerptId>
+    where
+        O: text::ToOffset,
+    {
+        self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
+    }
+
+    pub fn push_excerpts_with_context_lines<O>(
+        &mut self,
+        buffer: Model<Buffer>,
+        ranges: Vec<Range<O>>,
+        context_line_count: u32,
+        cx: &mut ModelContext<Self>,
+    ) -> Vec<Range<Anchor>>
+    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<O>(
+        &mut self,
+        prev_excerpt_id: ExcerptId,
+        buffer: Model<Buffer>,
+        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Vec<ExcerptId>
+    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<O>(
+        &mut self,
+        prev_excerpt_id: ExcerptId,
+        buffer: Model<Buffer>,
+        ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
+        cx: &mut ModelContext<Self>,
+    ) 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::<Option<&Locator>>();
+        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>) {
+        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<Buffer>,
+        cx: &AppContext,
+    ) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
+        let mut excerpts = Vec::new();
+        let snapshot = self.read(cx);
+        let buffers = self.buffers.borrow();
+        let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
+        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<ExcerptId> {
+        self.snapshot
+            .borrow()
+            .excerpts
+            .iter()
+            .map(|entry| entry.id)
+            .collect()
+    }
+
+    pub fn excerpt_containing(
+        &self,
+        position: impl ToOffset,
+        cx: &AppContext,
+    ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
+        let snapshot = self.read(cx);
+        let position = position.to_offset(&snapshot);
+
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        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<T: ToOffset>(
+        &self,
+        point: T,
+        cx: &AppContext,
+    ) -> Option<(Model<Buffer>, usize, ExcerptId)> {
+        let snapshot = self.read(cx);
+        let offset = point.to_offset(&snapshot);
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        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<T: ToOffset>(
+        &self,
+        range: Range<T>,
+        cx: &AppContext,
+    ) -> Vec<(Model<Buffer>, Range<usize>, 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::<usize>();
+        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<Item = ExcerptId>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.sync(cx);
+        let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
+        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<Item = Anchor>,
+        cx: &mut ModelContext<Self>,
+    ) -> impl 'static + Future<Output = Result<()>> {
+        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<T: ToOffset>(
+        &self,
+        position: T,
+        cx: &AppContext,
+    ) -> Option<(Model<Buffer>, language2::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<Buffer>,
+        event: &language2::Event,
+        cx: &mut ModelContext<Self>,
+    ) {
+        cx.emit(match event {
+            language2::Event::Edited => Event::Edited {
+                sigleton_buffer_edited: true,
+            },
+            language2::Event::DirtyChanged => Event::DirtyChanged,
+            language2::Event::Saved => Event::Saved,
+            language2::Event::FileHandleChanged => Event::FileHandleChanged,
+            language2::Event::Reloaded => Event::Reloaded,
+            language2::Event::DiffBaseChanged => Event::DiffBaseChanged,
+            language2::Event::LanguageChanged => Event::LanguageChanged,
+            language2::Event::Reparsed => Event::Reparsed,
+            language2::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
+            language2::Event::Closed => Event::Closed,
+
+            //
+            language2::Event::Operation(_) => return,
+        });
+    }
+
+    pub fn all_buffers(&self) -> HashSet<Model<Buffer>> {
+        self.buffers
+            .borrow()
+            .values()
+            .map(|state| state.buffer.clone())
+            .collect()
+    }
+
+    pub fn buffer(&self, buffer_id: u64) -> Option<Model<Buffer>> {
+        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<Arc<Language>> {
+        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<Buffer>)) {
+        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::<usize>(
+                            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 gpui2::AppContext) -> Model<Self> {
+        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<const COUNT: usize>(
+        excerpts: [(&str, Vec<Range<Point>>); COUNT],
+        cx: &mut gpui2::AppContext,
+    ) -> Model<Self> {
+        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<Buffer>, cx: &mut gpui2::AppContext) -> Model<Self> {
+        cx.build_model(|cx| Self::singleton(buffer, cx))
+    }
+
+    pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui2::AppContext) -> Model<Self> {
+        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<Self>,
+    ) {
+        use util::RandomCharIter;
+
+        let snapshot = self.read(cx);
+        let mut edits: Vec<(Range<usize>, Arc<str>)> = 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<Self>,
+    ) {
+        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::<String>();
+                    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::<Vec<_>>();
+                log::info!(
+                    "Inserting excerpts from buffer {} and ranges {:?}: {:?}",
+                    buffer_handle.read(cx).remote_id(),
+                    ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
+                    ranges
+                        .iter()
+                        .map(|r| &buffer_text[r.context.clone()])
+                        .collect::<Vec<_>>()
+                );
+
+                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::<Vec<_>>();
+                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<Self>,
+    ) {
+        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<Self>) {
+        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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
+        let mut offset = position.to_offset(self);
+        let mut cursor = self.excerpts.cursor::<usize>();
+        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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
+        let offset = position.to_offset(self);
+        self.text_for_range(offset..self.len())
+            .flat_map(|chunk| chunk.chars())
+    }
+
+    pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = &str> + '_ {
+        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<T>(&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<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
+        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::<usize>();
+        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::<Point>();
+        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::<OffsetUtf16>();
+        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<PointUtf16>, bias: Bias) -> PointUtf16 {
+        if let Some((_, _, buffer)) = self.as_singleton() {
+            return buffer.clip_point_utf16(point, bias);
+        }
+
+        let mut cursor = self.excerpts.cursor::<PointUtf16>();
+        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<T: ToOffset>(&self, range: Range<T>) -> MultiBufferBytes {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let mut excerpts = self.excerpts.cursor::<usize>();
+        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<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> ReversedMultiBufferBytes {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let mut excerpts = self.excerpts.cursor::<usize>();
+        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<T: ToOffset>(&self, range: Range<T>, 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<T: ToOffset>(
+        &self,
+        point: T,
+    ) -> Option<(&BufferSnapshot, usize)> {
+        let offset = point.to_offset(&self);
+        let mut cursor = self.excerpts.cursor::<usize>();
+        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<Item = u32>,
+        cx: &AppContext,
+    ) -> BTreeMap<u32, IndentSize> {
+        let mut result = BTreeMap::new();
+
+        let mut rows_for_excerpt = Vec::new();
+        let mut cursor = self.excerpts.cursor::<Point>();
+        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<u32> {
+        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<Point>)> {
+        let mut cursor = self.excerpts.cursor::<Point>();
+        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<D, O>(&self, range: Range<O>) -> 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::<usize>();
+        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<D>(&self, anchor: &Anchor) -> D
+    where
+        D: TextDimension + Ord + Sub<D, Output = D>,
+    {
+        let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
+        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::<D>(&excerpt.buffer);
+                let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
+                let buffer_position = cmp::min(
+                    excerpt_buffer_end,
+                    anchor.text_anchor.summary::<D>(&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<D>
+    where
+        D: TextDimension + Ord + Sub<D, Output = D>,
+        I: 'a + IntoIterator<Item = &'a Anchor>,
+    {
+        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::<ExcerptSummary>();
+        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::<D>(&excerpt.buffer);
+                    let excerpt_buffer_end =
+                        excerpt.range.context.end.summary::<D>(&excerpt.buffer);
+                    summaries.extend(
+                        excerpt
+                            .buffer
+                            .summaries_for_anchors::<D, _>(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<Item = &'a Anchor>,
+    {
+        let mut anchors = anchors.into_iter().enumerate().peekable();
+        let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
+        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<T: ToOffset>(&self, position: T) -> Anchor {
+        self.anchor_at(position, Bias::Left)
+    }
+
+    pub fn anchor_after<T: ToOffset>(&self, position: T) -> Anchor {
+        self.anchor_at(position, Bias::Right)
+    }
+
+    pub fn anchor_at<T: ToOffset>(&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<ExcerptId>)>();
+        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::<Option<&Locator>>();
+        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<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
+        self.excerpts
+            .iter()
+            .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
+    }
+
+    pub fn excerpt_boundaries_in_range<R, T>(
+        &self,
+        range: R,
+    ) -> impl Iterator<Item = ExcerptBoundary> + '_
+    where
+        R: RangeBounds<T>,
+        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<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> Option<(Range<usize>, Range<usize>)> {
+        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<usize>, Range<usize>)> = 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<T>,
+    ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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<T>,
+    ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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<dyn File>> {
+        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<Language>> {
+        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<LanguageScope> {
+        self.point_to_buffer_offset(point)
+            .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
+    }
+
+    pub fn language_indent_size_at<T: ToOffset>(
+        &self,
+        position: T,
+        cx: &AppContext,
+    ) -> Option<IndentSize> {
+        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<Item = DiagnosticEntry<O>> + '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<T>,
+        reversed: bool,
+    ) -> impl Iterator<Item = DiagnosticEntry<O>> + '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<u32>,
+    ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+        let mut cursor = self.excerpts.cursor::<Point>();
+
+        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<u32>,
+    ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+        let mut cursor = self.excerpts.cursor::<Point>();
+
+        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<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
+        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<Outline<Anchor>> {
+        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<T: ToOffset>(
+        &self,
+        offset: T,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
+        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::<ExcerptId>();
+            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<u64> {
+        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::<Option<&Locator>>();
+        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<T>,
+    ) -> Option<(&'a Excerpt, usize)> {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+
+        let mut cursor = self.excerpts.cursor::<usize>();
+        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<Anchor>,
+    ) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
+        let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
+        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<usize> {
+        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<TransactionId> {
+        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<u64, TransactionId>,
+    ) -> 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<MultiBuffer>,
+    ) where
+        T: IntoIterator<Item = (&'a Model<Buffer>, &'a language2::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<Transaction> {
+        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<TransactionId> {
+        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<TransactionId> {
+        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<text::Anchor>,
+        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::<TextSummary, _>(range.context.to_offset(&buffer)),
+            buffer_id,
+            buffer,
+            range,
+            has_trailing_newline,
+        }
+    }
+
+    fn chunks_in_range(&self, range: Range<usize>, 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<usize>) -> 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<usize>) -> 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<usize> 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<ExcerptId> {
+    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<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        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<Self::Item> {
+        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<Self::Item> {
+        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<usize> {
+        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<usize> {
+        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<Self::Item> {
+        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<Self::Item> {
+        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<T>(
+    buffer: &BufferSnapshot,
+    ranges: &[Range<T>],
+    context_line_count: u32,
+) -> (Vec<ExcerptRange<Point>>, Vec<usize>)
+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 gpui2::{AppContext, Context, TestAppContext};
+    use language2::{Buffer, Rope};
+    use parking_lot::RwLock;
+    use rand::prelude::*;
+    use settings2::SettingsStore;
+    use std::env;
+    use util::test::sample_text;
+
+    #[gpui2::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::<Vec<_>>(),
+            (0..buffer.read(cx).row_count())
+                .map(Some)
+                .collect::<Vec<_>>()
+        );
+
+        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::<Vec<_>>(),
+            (0..buffer.read(cx).row_count())
+                .map(Some)
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[gpui2::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
+                .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| language2::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");
+    }
+
+    #[gpui2::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::<Event>::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::<Vec<_>>(),
+            [Some(1), Some(2), Some(3), Some(4), Some(3)]
+        );
+        assert_eq!(
+            snapshot.buffer_rows(2).collect::<Vec<_>>(),
+            [Some(3), Some(4), Some(3)]
+        );
+        assert_eq!(snapshot.buffer_rows(4).collect::<Vec<_>>(), [Some(3)]);
+        assert_eq!(snapshot.buffer_rows(5).collect::<Vec<_>>(), []);
+
+        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<Point>,
+            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::<String>(),
+                        boundary.starts_new_buffer,
+                    )
+                })
+                .collect::<Vec<_>>()
+        }
+    }
+
+    #[gpui2::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::<usize>(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);
+    }
+
+    #[gpui2::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<_>>(),
+            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)
+            ]
+        );
+    }
+
+    #[gpui2::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::<Vec<_>>().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<_>>(),
+            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)
+            ]
+        );
+    }
+
+    #[gpui2::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::<Vec<_>>(), &[Some(0)]);
+        assert_eq!(snapshot.buffer_rows(1).collect::<Vec<_>>(), &[]);
+    }
+
+    #[gpui2::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);
+    }
+
+    #[gpui2::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);
+    }
+
+    #[gpui2::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::<usize>(&snapshot_1.anchor_before(2)),
+            0
+        );
+        assert_eq!(
+            snapshot_2.summaries_for_anchors::<usize, _>(&[
+                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::<usize, _>(&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::<Vec<_>>(),
+            &[(0, true), (1, true), (2, true), (3, true)]
+        );
+        assert_eq!(
+            snapshot_3.summaries_for_anchors::<usize, _>(new_anchors.iter().map(|a| &a.1)),
+            &[0, 2, 7, 13]
+        );
+    }
+
+    #[gpui2::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<Model<Buffer>> = Vec::new();
+        let multibuffer = cx.build_model(|_| MultiBuffer::new(0));
+        let mut excerpt_ids = Vec::<ExcerptId>::new();
+        let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::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::<String>(),
+                        );
+                    }
+                    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::<String>();
+                        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::<Vec<_>>(),
+                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::<Vec<_>>(),
+                    &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::<PointUtf16, _>(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::<String>();
+                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::<String>()
+                    })
+                    .collect::<Vec<_>>()
+                    .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::<TextSummary, _>(start_ix..end_ix),
+                    expected_summary,
+                    "incorrect summary for range {:?}",
+                    start_ix..end_ix
+                );
+            }
+
+            // Anchor resolution
+            let summaries = snapshot.summaries_for_anchors::<usize, _>(&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::<usize>(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::<String>(),
+                    expected_text[..end_ix].chars().rev().collect::<String>(),
+                );
+            }
+
+            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::<Vec<_>>(),
+                    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());
+        }
+    }
+
+    #[gpui2::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");
+        });
+    }
+}

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<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> 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))),
-                            ))
                     })),
             )
     }

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<Self, StatefulInteraction<Self>, FocusEnabled<Self>>;
 
     fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> 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();
 

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<Self, StatefulInteraction<Self>>;
 
     fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> 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| {

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,7 +71,6 @@ 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(
@@ -82,7 +81,12 @@ fn main() {
                 }),
                 ..Default::default()
             },
-            move |cx| cx.build_view(|cx| StoryWrapper::new(selector.story(cx))),
+            move |cx| {
+                let theme_settings = ThemeSettings::get_global(cx);
+                cx.set_rem_size(theme_settings.ui_font_size);
+
+                cx.build_view(|cx| StoryWrapper::new(selector.story(cx)))
+            },
         );
 
         cx.activate(true);

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"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true
+rand.workspace = true

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<u64>,
+}
+
+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<D>(&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<usize>;
+    fn to_point(&self, snapshot: &BufferSnapshot) -> Range<Point>;
+    fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range<PointUtf16>;
+}
+
+impl<T> OffsetRangeExt for Range<T>
+where
+    T: ToOffset,
+{
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> Range<usize> {
+        self.start.to_offset(snapshot)..self.end.to_offset(snapshot)
+    }
+
+    fn to_point(&self, snapshot: &BufferSnapshot) -> Range<Point> {
+        self.start.to_offset(snapshot).to_point(snapshot)
+            ..self.end.to_offset(snapshot).to_point(snapshot)
+    }
+
+    fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range<PointUtf16> {
+        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<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>;
+}
+
+impl AnchorRangeExt for Range<Anchor> {
+    fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
+        Ok(match self.start.cmp(&other.start, buffer) {
+            Ordering::Equal => other.end.cmp(&self.end, buffer),
+            ord => ord,
+        })
+    }
+}

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;
+
+    #[gpui2::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)
+            );
+        }
+    }
+}

crates/text2/src/network.rs 🔗

@@ -0,0 +1,69 @@
+use clock::ReplicaId;
+
+pub struct Network<T: Clone, R: rand::Rng> {
+    inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
+    all_messages: Vec<T>,
+    rng: R,
+}
+
+#[derive(Clone)]
+struct Envelope<T: Clone> {
+    message: T,
+}
+
+impl<T: Clone, R: rand::Rng> Network<T, R> {
+    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<T>) {
+        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<T> {
+        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()
+    }
+}

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>(T);
+
+#[derive(Clone, Debug)]
+pub struct OperationQueue<T: Operation>(SumTree<OperationItem<T>>);
+
+#[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<T: Operation> Default for OperationQueue<T> {
+    fn default() -> Self {
+        OperationQueue::new()
+    }
+}
+
+impl<T: Operation> OperationQueue<T> {
+    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<T>) {
+        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<Item = &T> {
+        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<T: Operation> Item for OperationItem<T> {
+    type Summary = OperationSummary;
+
+    fn summary(&self) -> Self::Summary {
+        OperationSummary {
+            key: OperationKey::new(self.0.lamport_timestamp()),
+            len: 1,
+        }
+    }
+}
+
+impl<T: Operation> KeyedItem for OperationItem<T> {
+    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
+        }
+    }
+}

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<T>(Vec<Edit<T>>);
+
+impl<T> Patch<T>
+where
+    T: 'static
+        + Clone
+        + Copy
+        + Ord
+        + Sub<T, Output = T>
+        + Add<T, Output = T>
+        + AddAssign
+        + Default
+        + PartialEq,
+{
+    pub fn new(edits: Vec<Edit<T>>) -> Self {
+        #[cfg(debug_assertions)]
+        {
+            let mut last_edit: Option<&Edit<T>> = 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<T>] {
+        &self.0
+    }
+
+    pub fn into_inner(self) -> Vec<Edit<T>> {
+        self.0
+    }
+
+    pub fn compose(&self, new_edits_iter: impl IntoIterator<Item = Edit<T>>) -> 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<T>) {
+        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<T: Clone> IntoIterator for Patch<T> {
+    type Item = Edit<T>;
+    type IntoIter = std::vec::IntoIter<Edit<T>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
+impl<'a, T: Clone> IntoIterator for &'a Patch<T> {
+    type Item = Edit<T>;
+    type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter().cloned()
+    }
+}
+
+impl<'a, T: Clone> IntoIterator for &'a mut Patch<T> {
+    type Item = Edit<T>;
+    type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter().cloned()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::prelude::*;
+    use std::env;
+
+    #[gpui2::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,
+                },
+            ]),
+        );
+    }
+
+    #[gpui2::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,
+            }]),
+        );
+    }
+
+    #[gpui2::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,
+                },
+            ]),
+        );
+    }
+
+    #[gpui2::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,
+            }]),
+        );
+    }
+
+    #[gpui2::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,
+                },
+            ]),
+        );
+    }
+
+    #[gpui2::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);
+    }
+
+    #[gpui2::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::<Vec<_>>();
+        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::<Vec<_>>();
+                log::info!(
+                    "  editing {:?}: {:?}",
+                    start..end,
+                    new_chars.iter().collect::<String>()
+                );
+                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<u32>, new: Patch<u32>, composed: Patch<u32>) {
+        let original = ('a'..'z').collect::<Vec<_>>();
+        let inserted = ('A'..'Z').collect::<Vec<_>>();
+
+        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::<String>(),
+            expected.into_iter().collect::<String>(),
+            "expected patch is incorrect"
+        );
+
+        assert_eq!(old.compose(&new), composed);
+    }
+
+    fn apply_patch(text: &mut Vec<char>, patch: &Patch<u32>, 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(),
+            );
+        }
+    }
+}

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<T> {
+    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<T: Clone> Selection<T> {
+    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<F, S>(&self, f: F) -> Selection<S>
+    where
+        F: Fn(T) -> S,
+    {
+        Selection::<S> {
+            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<T: Copy + Ord> Selection<T> {
+    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<T> {
+        self.start..self.end
+    }
+}
+
+impl Selection<usize> {
+    #[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<usize>) -> bool {
+        self.start == offset_range.start && self.end == offset_range.end
+    }
+}
+
+impl Selection<Anchor> {
+    pub fn resolve<'a, D: 'a + TextDimension>(
+        &'a self,
+        snapshot: &'a BufferSnapshot,
+    ) -> Selection<D> {
+        Selection {
+            id: self.id,
+            start: snapshot.summary_for_anchor(&self.start),
+            end: snapshot.summary_for_anchor(&self.end),
+            reversed: self.reversed,
+            goal: self.goal,
+        }
+    }
+}

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<Vec<Weak<Mutex<Patch<usize>>>>>);
+
+pub struct Subscription(Arc<Mutex<Patch<usize>>>);
+
+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<Item = Edit<usize>>) {
+        publish(&mut *self.0.lock(), edits);
+    }
+
+    pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator<Item = Edit<usize>>) {
+        publish(self.0.get_mut(), edits);
+    }
+}
+
+impl Subscription {
+    pub fn consume(&self) -> Patch<usize> {
+        mem::take(&mut *self.0.lock())
+    }
+}
+
+fn publish(
+    subscriptions: &mut Vec<Weak<Mutex<Patch<usize>>>>,
+    edits: impl Clone + IntoIterator<Item = Edit<usize>>,
+) {
+    subscriptions.retain(|subscription| {
+        if let Some(subscription) = subscription.upgrade() {
+            let mut patch = subscription.lock();
+            *patch = patch.compose(edits.clone());
+            true
+        } else {
+            false
+        }
+    });
+}

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");
+}
+
+#[gpui2::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::<String>();
+    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::<TextSummary, _>(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::<usize>(&old_buffer.version)
+            .collect::<Vec<_>>();
+
+        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::<String>();
+            let edits = buffer
+                .edits_since_in_range::<usize>(&old_buffer.version, range.clone())
+                .collect::<Vec<_>>();
+            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::<String>();
+            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<usize> {
+        let offset = offset_after(text, part);
+        offset..offset
+    }
+
+    fn range_of(text: &str, part: &str) -> Range<usize> {
+        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::<TextSummary, _>(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::<TextSummary, _>(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::<TextSummary, _>(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::<TextSummary, _>(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::<TextSummary, _>(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::<String>(), "abcd\nefgh\nijkl\nmno\nPQrs");
+
+    let chars = buffer.chars_at(Point::new(1, 0));
+    assert_eq!(chars.collect::<String>(), "efgh\nijkl\nmno\nPQrs");
+
+    let chars = buffer.chars_at(Point::new(2, 0));
+    assert_eq!(chars.collect::<String>(), "ijkl\nmno\nPQrs");
+
+    let chars = buffer.chars_at(Point::new(3, 0));
+    assert_eq!(chars.collect::<String>(), "mno\nPQrs");
+
+    let chars = buffer.chars_at(Point::new(4, 0));
+    assert_eq!(chars.collect::<String>(), "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::<String>(), "    \"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::<usize>(&tx)
+            .collect::<Vec<_>>(),
+        [2..4, 6..9]
+    );
+
+    buffer.edit([(5..5, "hijk")]);
+    assert_eq!(buffer.text(), "12cd5hijk6efg7");
+    assert_eq!(
+        buffer
+            .edited_ranges_for_transaction::<usize>(&tx)
+            .collect::<Vec<_>>(),
+        [2..4, 10..13]
+    );
+
+    buffer.edit([(4..4, "l")]);
+    assert_eq!(buffer.text(), "12cdl5hijk6efg7");
+    assert_eq!(
+        buffer
+            .edited_ranges_for_transaction::<usize>(&tx)
+            .collect::<Vec<_>>(),
+        [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");
+}
+
+#[gpui2::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::<String>();
+    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();
+    }
+}

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<Operation>,
+    deferred_replicas: HashSet<ReplicaId>,
+    pub lamport_clock: clock::Lamport,
+    subscriptions: Topic,
+    edit_id_resolvers: HashMap<clock::Lamport, Vec<oneshot::Sender<()>>>,
+    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<Fragment>,
+    insertions: SumTree<InsertionFragment>,
+    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<clock::Lamport>,
+    pub start: clock::Global,
+}
+
+impl HistoryEntry {
+    pub fn transaction_id(&self) -> TransactionId {
+        self.transaction.id
+    }
+}
+
+struct History {
+    base_text: Rope,
+    operations: TreeMap<clock::Lamport, Operation>,
+    insertion_slices: HashMap<clock::Lamport, Vec<InsertionSlice>>,
+    undo_stack: Vec<HistoryEntry>,
+    redo_stack: Vec<HistoryEntry>,
+    transaction_depth: usize,
+    group_interval: Duration,
+}
+
+#[derive(Clone, Debug)]
+struct InsertionSlice {
+    insertion_id: clock::Lamport,
+    range: Range<usize>,
+}
+
+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<TransactionId> {
+        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<TransactionId> {
+        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<TransactionId> {
+        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<Transaction> {
+        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<FilterCursor<'a, F, Fragment, FragmentTextSummary>>,
+    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<D> {
+    pub old: Range<D>,
+    pub new: Range<D>,
+}
+
+impl<D> Edit<D>
+where
+    D: Sub<D, Output = D> + 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<D1, D2> Edit<(D1, D2)> {
+    pub fn flatten(self) -> (Edit<D1>, Edit<D2>) {
+        (
+            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<clock::Lamport>,
+    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<clock::Global>) {
+        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<Range<FullOffset>>,
+    pub new_text: Vec<Arc<str>>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct UndoOperation {
+    pub timestamp: clock::Lamport,
+    pub version: clock::Global,
+    pub counts: HashMap<clock::Lamport, u32>,
+}
+
+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<R, I, S, T>(&mut self, edits: R) -> Operation
+    where
+        R: IntoIterator<IntoIter = I>,
+        I: ExactSizeIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        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<S: ToOffset, T: Into<Arc<str>>>(
+        &mut self,
+        edits: impl ExactSizeIterator<Item = (Range<S>, 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::<FragmentTextSummary>();
+        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<I: IntoIterator<Item = Operation>>(&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<FullOffset>],
+        new_text: &[Arc<str>],
+        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<Item = &'a clock::Lamport>,
+    ) -> 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::<InsertionFragmentKey>();
+        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<TransactionId> {
+        self.start_transaction_at(Instant::now())
+    }
+
+    pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
+        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<clock::Lamport, Operation> {
+        &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<Operation> {
+        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<Operation> {
+        let transactions = self
+            .history
+            .remove_from_undo_until(transaction_id)
+            .iter()
+            .map(|entry| entry.transaction.clone())
+            .collect::<Vec<_>>();
+
+        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<Operation> {
+        let transactions = self
+            .history
+            .remove_from_redo(transaction_id)
+            .iter()
+            .map(|entry| entry.transaction.clone())
+            .collect::<Vec<_>>();
+
+        transactions
+            .into_iter()
+            .map(|transaction| self.undo_or_redo(transaction).unwrap())
+            .collect()
+    }
+
+    fn undo_or_redo(&mut self, transaction: Transaction) -> Result<Operation> {
+        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<Item = Range<D>>
+    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<Range<usize>> = 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<Item = clock::Lamport>,
+    ) -> impl 'static + Future<Output = Result<()>> {
+        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<Item = Anchor>,
+    ) -> impl 'static + Future<Output = Result<()>> {
+        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<Output = Result<()>> {
+        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<usize>, 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::<Option<&Locator>>();
+        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<usize> {
+        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<T>(
+        &self,
+        rng: &mut T,
+        edit_count: usize,
+    ) -> Vec<(Range<usize>, Arc<str>)>
+    where
+        T: rand::Rng,
+    {
+        let mut edits: Vec<(Range<usize>, Arc<str>)> = 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<T>(
+        &mut self,
+        rng: &mut T,
+        edit_count: usize,
+    ) -> (Vec<(Range<usize>, Arc<str>)>, 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<Operation> {
+        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<Item = char> + '_ {
+        self.chars_at(0)
+    }
+
+    pub fn chars_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = char> + '_ {
+        self.text_for_range(range).flat_map(str::chars)
+    }
+
+    pub fn reversed_chars_for_range<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = char> + '_ {
+        self.reversed_chunks_in_range(range)
+            .flat_map(|chunk| chunk.chars().rev())
+    }
+
+    pub fn contains_str_at<T>(&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<T>(&self, position: T, needle: &str) -> Range<T>
+    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<Item = &Fragment> {
+        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<PointUtf16>) -> usize {
+        self.visible_text.unclipped_point_utf16_to_offset(point)
+    }
+
+    pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> 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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
+        let offset = position.to_offset(self);
+        self.visible_text.chars_at(offset)
+    }
+
+    pub fn reversed_chars_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
+        let offset = position.to_offset(self);
+        self.visible_text.reversed_chars_at(offset)
+    }
+
+    pub fn reversed_chunks_in_range<T: ToOffset>(&self, range: Range<T>) -> 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<T: ToOffset>(&self, range: Range<T>) -> 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<T: ToOffset>(&self, range: Range<T>) -> 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<T: ToOffset>(&self, range: Range<T>) -> 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<D, O: ToOffset>(&self, range: Range<O>) -> 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<Item = D>
+    where
+        D: 'a + TextDimension,
+        A: 'a + IntoIterator<Item = &'a Anchor>,
+    {
+        let anchors = anchors.into_iter();
+        self.summaries_for_anchors_with_payload::<D, _, ()>(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<Item = (D, T)>
+    where
+        D: 'a + TextDimension,
+        A: 'a + IntoIterator<Item = (&'a Anchor, T)>,
+    {
+        let anchors = anchors.into_iter();
+        let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>();
+        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<D>(&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::<InsertionFragmentKey>();
+            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::<InsertionFragmentKey>();
+            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<T: ToOffset>(&self, position: T) -> Anchor {
+        self.anchor_at(position, Bias::Left)
+    }
+
+    pub fn anchor_after<T: ToOffset>(&self, position: T) -> Anchor {
+        self.anchor_at(position, Bias::Right)
+    }
+
+    pub fn anchor_at<T: ToOffset>(&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::<usize>();
+            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<PointUtf16>, 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<Item = Edit<D>>
+    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<Item = (Edit<D>, Range<Anchor>)>
+    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<Anchor>,
+    ) -> impl 'a + Iterator<Item = Edit<D>>
+    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<Anchor>,
+    ) -> impl 'a + Iterator<Item = (Edit<D>, Range<Anchor>)>
+    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<D>, Range<Anchor>);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut pending_edit: Option<Self::Item> = 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<clock::Global>;
+
+    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<Self> {
+        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<usize> for FullOffset {
+    fn add_assign(&mut self, rhs: usize) {
+        self.0 += rhs;
+    }
+}
+
+impl ops::Add<usize> 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<clock::Global>) {
+        *self += summary.text.visible;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FullOffset {
+    fn add_summary(&mut self, summary: &FragmentSummary, _: &Option<clock::Global>) {
+        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<clock::Global>) {
+        *self = Some(&summary.max_id);
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, FragmentSummary, FragmentTextSummary> for usize {
+    fn cmp(
+        &self,
+        cursor_location: &FragmentTextSummary,
+        _: &Option<clock::Global>,
+    ) -> 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<clock::Global>) {
+        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<clock::Global>) -> 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<PointUtf16> {
+    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<PointUtf16> {
+    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<str>) -> Arc<str> {
+        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
+            replaced.into()
+        } else {
+            text
+        }
+    }
+}

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<UndoMapEntry>);
+
+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::<Vec<_>>();
+        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::<UndoMapKey>();
+        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::<UndoMapKey>();
+        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
+    }
+}

crates/theme2/Cargo.toml 🔗

@@ -16,19 +16,19 @@ 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 = { path = "../fs" }
+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
+settings2 = { path = "../settings2" }
 toml.workspace = true
+util = { path = "../util" }
 
 [dev-dependencies]
 gpui2 = { path = "../gpui2", features = ["test-support"] }

crates/theme2/src/colors.rs 🔗

@@ -0,0 +1,144 @@
+use gpui2::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<PlayerColor>);
+
+#[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 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 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 = gpui2::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 = gpui2::rgb(0xff00ff);
+        let green: Hsla = gpui2::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);
+    }
+}

crates/theme2/src/default.rs → crates/theme2/src/default_colors.rs 🔗

@@ -1,79 +1,350 @@
-use gpui2::Rgba;
-use indexmap::IndexMap;
+use std::num::ParseIntError;
 
-use crate::scale::{ColorScaleName, ColorScaleSet, ColorScales};
+use gpui2::{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,
+};
+
+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 From<DefaultColorScaleSet> 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 StatusColors {
+    fn default() -> Self {
+        Self {
+            conflict: red().dark().step_11(),
+            created: gpui2::black(),
+            deleted: gpui2::black(),
+            error: gpui2::black(),
+            hidden: gpui2::black(),
+            ignored: gpui2::black(),
+            info: gpui2::black(),
+            modified: gpui2::black(),
+            renamed: gpui2::black(),
+            success: gpui2::black(),
+            warning: gpui2::black(),
+        }
     }
 }
 
-pub fn default_color_scales() -> ColorScales {
-    use ColorScaleName::*;
+impl Default for GitStatusColors {
+    fn default() -> Self {
+        Self {
+            conflict: gpui2::rgba(0xdec184ff).into(),
+            created: gpui2::rgba(0xa1c181ff).into(),
+            deleted: gpui2::rgba(0xd07277ff).into(),
+            ignored: gpui2::rgba(0x555a63ff).into(),
+            modified: gpui2::rgba(0x74ade8ff).into(),
+            renamed: gpui2::rgba(0xdec184ff).into(),
+        }
+    }
+}
+
+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![
+                (
+                    "string.special.symbol".into(),
+                    gpui2::rgba(0xad6e26ff).into(),
+                ),
+                ("hint".into(), gpui2::rgba(0x9294beff).into()),
+                ("link_uri".into(), gpui2::rgba(0x3882b7ff).into()),
+                ("type".into(), gpui2::rgba(0x3882b7ff).into()),
+                ("string.regex".into(), gpui2::rgba(0xad6e26ff).into()),
+                ("constant".into(), gpui2::rgba(0x669f59ff).into()),
+                ("function".into(), gpui2::rgba(0x5b79e3ff).into()),
+                ("string.special".into(), gpui2::rgba(0xad6e26ff).into()),
+                ("punctuation.bracket".into(), gpui2::rgba(0x4d4f52ff).into()),
+                ("variable".into(), gpui2::rgba(0x383a41ff).into()),
+                ("punctuation".into(), gpui2::rgba(0x383a41ff).into()),
+                ("property".into(), gpui2::rgba(0xd3604fff).into()),
+                ("string".into(), gpui2::rgba(0x649f57ff).into()),
+                ("predictive".into(), gpui2::rgba(0x9b9ec6ff).into()),
+                ("attribute".into(), gpui2::rgba(0x5c78e2ff).into()),
+                ("number".into(), gpui2::rgba(0xad6e25ff).into()),
+                ("constructor".into(), gpui2::rgba(0x5c78e2ff).into()),
+                ("embedded".into(), gpui2::rgba(0x383a41ff).into()),
+                ("title".into(), gpui2::rgba(0xd3604fff).into()),
+                ("tag".into(), gpui2::rgba(0x5c78e2ff).into()),
+                ("boolean".into(), gpui2::rgba(0xad6e25ff).into()),
+                (
+                    "punctuation.list_marker".into(),
+                    gpui2::rgba(0xd3604fff).into(),
+                ),
+                ("variant".into(), gpui2::rgba(0x5b79e3ff).into()),
+                ("emphasis".into(), gpui2::rgba(0x5c78e2ff).into()),
+                ("link_text".into(), gpui2::rgba(0x5b79e3ff).into()),
+                ("comment".into(), gpui2::rgba(0xa2a3a7ff).into()),
+                ("punctuation.special".into(), gpui2::rgba(0xb92b46ff).into()),
+                ("emphasis.strong".into(), gpui2::rgba(0xad6e25ff).into()),
+                ("primary".into(), gpui2::rgba(0x383a41ff).into()),
+                (
+                    "punctuation.delimiter".into(),
+                    gpui2::rgba(0x4d4f52ff).into(),
+                ),
+                ("label".into(), gpui2::rgba(0x5c78e2ff).into()),
+                ("keyword".into(), gpui2::rgba(0xa449abff).into()),
+                ("string.escape".into(), gpui2::rgba(0x7c7e86ff).into()),
+                ("text.literal".into(), gpui2::rgba(0x649f57ff).into()),
+                ("variable.special".into(), gpui2::rgba(0xad6e25ff).into()),
+                ("comment.doc".into(), gpui2::rgba(0x7c7e86ff).into()),
+                ("enum".into(), gpui2::rgba(0xd3604fff).into()),
+                ("operator".into(), gpui2::rgba(0x3882b7ff).into()),
+                ("preproc".into(), gpui2::rgba(0x383a41ff).into()),
+            ],
+        }
+    }
+
+    pub fn default_dark() -> Self {
+        Self {
+            highlights: vec![
+                ("keyword".into(), gpui2::rgba(0xb477cfff).into()),
+                ("comment.doc".into(), gpui2::rgba(0x878e98ff).into()),
+                ("variant".into(), gpui2::rgba(0x73ade9ff).into()),
+                ("property".into(), gpui2::rgba(0xd07277ff).into()),
+                ("function".into(), gpui2::rgba(0x73ade9ff).into()),
+                ("type".into(), gpui2::rgba(0x6eb4bfff).into()),
+                ("tag".into(), gpui2::rgba(0x74ade8ff).into()),
+                ("string.escape".into(), gpui2::rgba(0x878e98ff).into()),
+                ("punctuation.bracket".into(), gpui2::rgba(0xb2b9c6ff).into()),
+                ("hint".into(), gpui2::rgba(0x5a6f89ff).into()),
+                ("punctuation".into(), gpui2::rgba(0xacb2beff).into()),
+                ("comment".into(), gpui2::rgba(0x5d636fff).into()),
+                ("emphasis".into(), gpui2::rgba(0x74ade8ff).into()),
+                ("punctuation.special".into(), gpui2::rgba(0xb1574bff).into()),
+                ("link_uri".into(), gpui2::rgba(0x6eb4bfff).into()),
+                ("string.regex".into(), gpui2::rgba(0xbf956aff).into()),
+                ("constructor".into(), gpui2::rgba(0x73ade9ff).into()),
+                ("operator".into(), gpui2::rgba(0x6eb4bfff).into()),
+                ("constant".into(), gpui2::rgba(0xdfc184ff).into()),
+                ("string.special".into(), gpui2::rgba(0xbf956aff).into()),
+                ("emphasis.strong".into(), gpui2::rgba(0xbf956aff).into()),
+                (
+                    "string.special.symbol".into(),
+                    gpui2::rgba(0xbf956aff).into(),
+                ),
+                ("primary".into(), gpui2::rgba(0xacb2beff).into()),
+                ("preproc".into(), gpui2::rgba(0xc8ccd4ff).into()),
+                ("string".into(), gpui2::rgba(0xa1c181ff).into()),
+                (
+                    "punctuation.delimiter".into(),
+                    gpui2::rgba(0xb2b9c6ff).into(),
+                ),
+                ("embedded".into(), gpui2::rgba(0xc8ccd4ff).into()),
+                ("enum".into(), gpui2::rgba(0xd07277ff).into()),
+                ("variable.special".into(), gpui2::rgba(0xbf956aff).into()),
+                ("text.literal".into(), gpui2::rgba(0xa1c181ff).into()),
+                ("attribute".into(), gpui2::rgba(0x74ade8ff).into()),
+                ("link_text".into(), gpui2::rgba(0x73ade9ff).into()),
+                ("title".into(), gpui2::rgba(0xd07277ff).into()),
+                ("predictive".into(), gpui2::rgba(0x5a6a87ff).into()),
+                ("number".into(), gpui2::rgba(0xbf956aff).into()),
+                ("label".into(), gpui2::rgba(0x74ade8ff).into()),
+                ("variable".into(), gpui2::rgba(0xc8ccd4ff).into()),
+                ("boolean".into(), gpui2::rgba(0xbf956aff).into()),
+                (
+                    "punctuation.list_marker".into(),
+                    gpui2::rgba(0xd07277ff).into(),
+                ),
+            ],
+        }
+    }
+}
+
+impl ThemeColors {
+    pub fn default_light() -> Self {
+        Self {
+            border: gpui2::white(),
+            border_variant: gpui2::white(),
+            border_focused: gpui2::white(),
+            border_transparent: gpui2::white(),
+            elevated_surface: gpui2::white(),
+            surface: gpui2::white(),
+            background: gpui2::white(),
+            element: gpui2::white(),
+            element_hover: gpui2::white(),
+            element_active: gpui2::white(),
+            element_selected: gpui2::white(),
+            element_disabled: gpui2::white(),
+            element_placeholder: gpui2::white(),
+            ghost_element: gpui2::white(),
+            ghost_element_hover: gpui2::white(),
+            ghost_element_active: gpui2::white(),
+            ghost_element_selected: gpui2::white(),
+            ghost_element_disabled: gpui2::white(),
+            text: gpui2::white(),
+            text_muted: gpui2::white(),
+            text_placeholder: gpui2::white(),
+            text_disabled: gpui2::white(),
+            text_accent: gpui2::white(),
+            icon: gpui2::white(),
+            icon_muted: gpui2::white(),
+            icon_disabled: gpui2::white(),
+            icon_placeholder: gpui2::white(),
+            icon_accent: gpui2::white(),
+            status_bar: gpui2::white(),
+            title_bar: gpui2::white(),
+            toolbar: gpui2::white(),
+            tab_bar: gpui2::white(),
+            editor: gpui2::white(),
+            editor_subheader: gpui2::white(),
+            editor_active_line: gpui2::white(),
+        }
+    }
+
+    pub fn default_dark() -> Self {
+        Self {
+            border: gpui2::rgba(0x464b57ff).into(),
+            border_variant: gpui2::rgba(0x464b57ff).into(),
+            border_focused: gpui2::rgba(0x293b5bff).into(),
+            border_transparent: gpui2::rgba(0x00000000).into(),
+            elevated_surface: gpui2::rgba(0x3b414dff).into(),
+            surface: gpui2::rgba(0x2f343eff).into(),
+            background: gpui2::rgba(0x3b414dff).into(),
+            element: gpui2::rgba(0x3b414dff).into(),
+            element_hover: gpui2::rgba(0xffffff1e).into(),
+            element_active: gpui2::rgba(0xffffff28).into(),
+            element_selected: gpui2::rgba(0x18243dff).into(),
+            element_disabled: gpui2::rgba(0x00000000).into(),
+            element_placeholder: gpui2::black(),
+            ghost_element: gpui2::rgba(0x00000000).into(),
+            ghost_element_hover: gpui2::rgba(0xffffff14).into(),
+            ghost_element_active: gpui2::rgba(0xffffff1e).into(),
+            ghost_element_selected: gpui2::rgba(0x18243dff).into(),
+            ghost_element_disabled: gpui2::rgba(0x00000000).into(),
+            text: gpui2::rgba(0xc8ccd4ff).into(),
+            text_muted: gpui2::rgba(0x838994ff).into(),
+            text_placeholder: gpui2::rgba(0xd07277ff).into(),
+            text_disabled: gpui2::rgba(0x555a63ff).into(),
+            text_accent: gpui2::rgba(0x74ade8ff).into(),
+            icon: gpui2::black(),
+            icon_muted: gpui2::rgba(0x838994ff).into(),
+            icon_disabled: gpui2::black(),
+            icon_placeholder: gpui2::black(),
+            icon_accent: gpui2::black(),
+            status_bar: gpui2::rgba(0x3b414dff).into(),
+            title_bar: gpui2::rgba(0x3b414dff).into(),
+            toolbar: gpui2::rgba(0x282c33ff).into(),
+            tab_bar: gpui2::rgba(0x2f343eff).into(),
+            editor: gpui2::rgba(0x282c33ff).into(),
+            editor_subheader: gpui2::rgba(0x2f343eff).into(),
+            editor_active_line: gpui2::rgba(0x2f343eff).into(),
+        }
+    }
+}
+
+type StaticColorScale = [&'static str; 12];
+
+struct StaticColorScaleSet {
+    scale: &'static str,
+    light: StaticColorScale,
+    light_alpha: StaticColorScale,
+    dark: StaticColorScale,
+    dark_alpha: StaticColorScale,
+}
 
-    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()),
-    ])
+impl TryFrom<StaticColorScaleSet> for ColorScaleSet {
+    type Error = ParseIntError;
+
+    fn try_from(value: StaticColorScaleSet) -> Result<Self, Self::Error> {
+        fn to_color_scale(scale: StaticColorScale) -> Result<ColorScale, ParseIntError> {
+            scale
+                .into_iter()
+                .map(|color| Rgba::try_from(color).map(Hsla::from))
+                .collect::<Result<Vec<_>, _>>()
+                .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 {
+    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 +402,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 +466,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 +530,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 +594,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 +658,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 +722,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 +786,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 +850,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 +914,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 +978,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 +1042,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 +1106,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 +1170,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 +1234,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 +1298,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 +1362,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 +1426,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 +1490,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 +1554,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 +1618,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 +1682,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 +1746,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 +1810,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 +1874,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 +1938,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 +2002,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 +2066,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 +2130,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 +2194,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 +2258,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 +2322,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 +2386,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 +2450,6 @@ fn white() -> DefaultColorScaleSet {
             "#fffffff2",
         ],
     }
+    .try_into()
+    .unwrap()
 }

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()
+    }
+}

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 std::{collections::HashMap, sync::Arc};
 
 pub struct ThemeRegistry {
-    themes: HashMap<SharedString, Arc<Theme>>,
+    themes: HashMap<SharedString, Arc<ThemeVariant>>,
 }
 
 impl ThemeRegistry {
-    fn insert_themes(&mut self, themes: impl IntoIterator<Item = Theme>) {
+    fn insert_theme_families(&mut self, families: impl IntoIterator<Item = ThemeFamily>) {
+        for family in families.into_iter() {
+            self.insert_themes(family.themes);
+        }
+    }
+
+    fn insert_themes(&mut self, themes: impl IntoIterator<Item = ThemeVariant>) {
         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<Item = ThemeMetadata> + '_ {
-        self.themes.values().map(|theme| theme.metadata.clone())
+    pub fn list(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
+        self.themes.values().map(|theme| theme.name.clone())
     }
 
-    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
+    pub fn get(&self, name: &str) -> Result<Arc<ThemeVariant>> {
         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
     }

crates/theme2/src/scale.rs 🔗

@@ -1,98 +1,237 @@
-use gpui2::{AppContext, Hsla};
-use indexmap::IndexMap;
-
-use crate::{theme, 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,
+use gpui2::{AppContext, Hsla, SharedString};
+
+use crate::{ActiveTheme, Appearance};
+
+/// 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<Hsla>);
+
+impl FromIterator<Hsla> for ColorScale {
+    fn from_iter<T: IntoIterator<Item = Hsla>>(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<ColorScaleName, ColorScaleSet>;
+    /// 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<Self::Item>;
+
+    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<SharedString>,
         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 light(&self, step: ColorScaleStep) -> Hsla {
-        self.light[step - 1]
+    pub fn name(&self) -> &SharedString {
+        &self.name
     }
 
-    pub fn light_alpha(&self, step: ColorScaleStep) -> Hsla {
-        self.light_alpha[step - 1]
+    pub fn light(&self) -> &ColorScale {
+        &self.light
     }
 
-    pub fn dark(&self, step: ColorScaleStep) -> Hsla {
-        self.dark[step - 1]
+    pub fn light_alpha(&self) -> &ColorScale {
+        &self.light_alpha
     }
 
-    pub fn dark_alpha(&self, step: ColorScaleStep) -> Hsla {
-        self.dark_alpha[step - 1]
+    pub fn dark(&self) -> &ColorScale {
+        &self.dark
     }
 
-    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),
         }
     }
 }

crates/theme2/src/settings.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Theme, ThemeRegistry};
+use crate::{ThemeRegistry, ThemeVariant};
 use anyhow::Result;
 use gpui2::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
 use schemars::{
@@ -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<Theme>,
+    pub active_theme: Arc<ThemeVariant>,
 }
 
 #[derive(Default)]
@@ -28,6 +29,8 @@ pub struct AdjustedBufferFontSize(Option<Pixels>);
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeSettingsContent {
+    #[serde(default)]
+    pub ui_font_size: Option<f32>,
     #[serde(default)]
     pub buffer_font_family: Option<String>,
     #[serde(default)]
@@ -115,6 +118,7 @@ impl settings2::Settings for ThemeSettings {
         let themes = cx.default_global::<Arc<ThemeRegistry>>();
 
         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),

crates/theme2/src/syntax.rs 🔗

@@ -0,0 +1,37 @@
+use gpui2::{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<Item = (&'static str, Hsla)>) -> 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()
+    }
+}

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::*;
+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 gpui2::{AppContext, Hsla, SharedString};
 use settings2::Settings;
-use std::sync::Arc;
 
 #[derive(Debug, Clone, PartialEq)]
 pub enum Appearance {
@@ -24,132 +28,51 @@ pub fn init(cx: &mut AppContext) {
     ThemeSettings::register(cx);
 }
 
-pub fn active_theme<'a>(cx: &'a AppContext) -> &'a Arc<Theme> {
-    &ThemeSettings::get_global(cx).active_theme
+pub trait ActiveTheme {
+    fn theme(&self) -> &ThemeVariant;
 }
 
-pub fn theme(cx: &AppContext) -> Arc<Theme> {
-    active_theme(cx).clone()
+impl ActiveTheme for AppContext {
+    fn theme(&self) -> &ThemeVariant {
+        &ThemeSettings::get_global(self).active_theme
+    }
 }
 
-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],
+pub struct ThemeFamily {
+    #[allow(dead_code)]
+    pub(crate) id: String,
+    pub name: SharedString,
+    pub author: SharedString,
+    pub themes: Vec<ThemeVariant>,
+    pub scales: ColorScales,
 }
 
-#[derive(Clone)]
-pub struct SyntaxTheme {
-    pub highlights: Vec<(String, HighlightStyle)>,
+impl ThemeFamily {}
+
+pub struct ThemeVariant {
+    #[allow(dead_code)]
+    pub(crate) id: String,
+    pub name: SharedString,
+    pub appearance: Appearance,
+    pub styles: ThemeStyles,
 }
 
-impl SyntaxTheme {
-    // TOOD: Get this working with `#[cfg(test)]`. Why isn't it?
-    pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
-        SyntaxTheme {
-            highlights: colors
-                .into_iter()
-                .map(|(key, color)| {
-                    (
-                        key.to_owned(),
-                        HighlightStyle {
-                            color: Some(color),
-                            ..Default::default()
-                        },
-                    )
-                })
-                .collect(),
-        }
+impl ThemeVariant {
+    /// Returns the [`ThemeColors`] for the theme.
+    #[inline(always)]
+    pub fn colors(&self) -> &ThemeColors {
+        &self.styles.colors
     }
 
-    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()
+    /// Returns the [`SyntaxTheme`] for the theme.
+    #[inline(always)]
+    pub fn syntax(&self) -> &SyntaxTheme {
+        &self.styles.syntax
     }
 
-    pub fn color(&self, name: &str) -> Hsla {
-        self.get(name).color.unwrap_or_default()
+    /// 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)
     }
 }
-
-#[derive(Clone, Copy)]
-pub struct PlayerTheme {
-    pub cursor: Hsla,
-    pub selection: Hsla,
-}
-
-#[derive(Clone)]
-pub struct ThemeMetadata {
-    pub name: SharedString,
-    pub is_light: bool,
-}
-
-pub struct Editor {
-    pub syntax: Arc<SyntaxTheme>,
-}

crates/theme2/src/themes/andromeda.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_cave_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_cave_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_dune_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_dune_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_estuary_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_estuary_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_forest_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_forest_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_heath_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_heath_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_lakeside_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_lakeside_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_plateau_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_plateau_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_savanna_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_savanna_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_seaside_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_seaside_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_sulphurpool_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/atelier_sulphurpool_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/ayu_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/ayu_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/ayu_mirage.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_dark_hard.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_dark_soft.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_light_hard.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/gruvbox_light_soft.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/mod.rs 🔗

@@ -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::*;

crates/theme2/src/themes/one_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/one_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/rose_pine.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/rose_pine_dawn.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/rose_pine_moon.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/sandcastle.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/solarized_dark.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/solarized_light.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme2/src/themes/summercamp.rs 🔗

@@ -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(),
-            },
-        ],
-    }
-}

crates/theme_converter/Cargo.toml 🔗

@@ -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" }

crates/theme_converter/src/main.rs 🔗

@@ -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::<Vec<_>>()
-            .join("\n"),
-        use_statements = theme_modules
-            .iter()
-            .map(|module| format!("pub use {module}::*;"))
-            .collect::<Vec<_>>()
-            .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<Cow<[u8]>> {
-        Self::get(path)
-            .map(|f| f.data)
-            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
-    }
-
-    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
-        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::<Hsla>(0xff00ff),
-                selection: rgb::<Hsla>(0xff00ff),
-            }
-        }
-    }
-}
-
-impl From<PlayerThemeColors> for PlayerTheme {
-    fn from(value: PlayerThemeColors) -> Self {
-        Self {
-            cursor: value.cursor,
-            selection: value.selection,
-        }
-    }
-}
-
-fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result<theme2::Theme> {
-    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::<Hsla>(0xEC695E),
-        mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
-        mac_os_traffic_light_green: rgb::<Hsla>(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<String, JsonSyntaxStyle>,
-}
-
-#[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<PlayerColors>,
-    #[serde(deserialize_with = "deserialize_syntax_colors")]
-    pub syntax: HashMap<String, Hsla>,
-}
-
-#[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<u8>,
-}
-
-fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    struct PlayerArrayVisitor;
-
-    impl<'de> Visitor<'de> for PlayerArrayVisitor {
-        type Value = Vec<PlayerColors>;
-
-        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-            formatter.write_str("an object with integer keys")
-        }
-
-        fn visit_map<A: serde::de::MapAccess<'de>>(
-            self,
-            mut map: A,
-        ) -> Result<Self::Value, A::Error> {
-            let mut players = Vec::with_capacity(8);
-            while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
-                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<HashMap<String, Hsla>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    struct ColorWrapper {
-        color: Hsla,
-    }
-
-    struct SyntaxVisitor;
-
-    impl<'de> Visitor<'de> for SyntaxVisitor {
-        type Value = HashMap<String, Hsla>;
-
-        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<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, 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)
-}

crates/theme_converter/src/theme_printer.rs 🔗

@@ -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<T>);
-
-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()
-    }
-}

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" }

crates/ui2/src/components/breadcrumb.rs 🔗

@@ -19,24 +19,22 @@ impl Breadcrumb {
     }
 
     fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
-        let theme = theme(cx);
-
-        div().child(" › ").text_color(theme.text_muted)
+        div()
+            .child(" › ")
+            .text_color(cx.theme().colors().text_muted)
     }
 
     fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> 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"),
                             },
                         ]),
                     ],

crates/ui2/src/components/buffer.rs 🔗

@@ -155,18 +155,16 @@ impl Buffer {
     }
 
     fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
-        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<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> 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)),
                 )
         }
     }

crates/ui2/src/components/buffer_search.rs 🔗

@@ -30,9 +30,7 @@ impl Render for BufferSearch {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
-        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::<Self>::new("replace", Icon::Replace)
                     .when(self.is_replace_open, |this| this.color(IconColor::Accent))

crates/ui2/src/components/collab_panel.rs 🔗

@@ -15,27 +15,29 @@ impl CollabPanel {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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..."),
                     ),
             )

crates/ui2/src/components/context_menu.rs 🔗

@@ -44,13 +44,11 @@ impl ContextMenu {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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

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<V: 'static> IconButton<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => IconColor::Disabled,
             _ => self.color,
@@ -75,14 +73,14 @@ impl<V: 'static> IconButton<V> {
 
         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<V: 'static> IconButton<V> {
             .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))

crates/ui2/src/components/keybinding.rs 🔗

@@ -60,15 +60,13 @@ impl Key {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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())
     }
 }

crates/ui2/src/components/list.rs 🔗

@@ -89,8 +89,6 @@ impl ListHeader {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
         let is_toggled = self.toggleable.is_toggled();
 
@@ -99,9 +97,10 @@ impl ListHeader {
         h_stack()
             .flex_1()
             .w_full()
-            .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)
             })
             .relative()
             .child(
@@ -363,7 +362,6 @@ impl ListEntry {
 
     fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let settings = user_settings(cx);
-        let theme = theme(cx);
 
         let left_content = match self.left_content.clone() {
             Some(LeftContent::Icon(i)) => Some(
@@ -385,9 +383,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 +398,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,19 +471,18 @@ impl<V: 'static> ListDetailsEntry<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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,
+                cx.theme().colors().ghost_element,
+                cx.theme().colors().ghost_element_hover,
+                cx.theme().colors().ghost_element_active,
             ),
             false => (
-                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,
             ),
         };
 
@@ -524,9 +522,7 @@ impl ListSeparator {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
-        div().h_px().w_full().bg(theme.border)
+        div().h_px().w_full().bg(cx.theme().colors().border)
     }
 }
 

crates/ui2/src/components/modal.rs 🔗

@@ -39,22 +39,20 @@ impl<V: 'static> Modal<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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<V: 'static> Modal<V> {
                     this.child(
                         h_stack()
                             .border_t()
-                            .border_color(theme.border)
+                            .border_color(cx.theme().colors().border)
                             .p_1()
                             .justify_end()
                             .children(self.secondary_action)

crates/ui2/src/components/multi_buffer.rs 🔗

@@ -12,8 +12,6 @@ impl MultiBuffer {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> 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),
                 ]))
         }
     }

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<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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()))
     }
 }

crates/ui2/src/components/notifications_panel.rs 🔗

@@ -12,15 +12,13 @@ impl NotificationsPanel {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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("header")

crates/ui2/src/components/palette.rs 🔗

@@ -43,22 +43,20 @@ impl Palette {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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)
                             })),
                     ),

crates/ui2/src/components/panel.rs 🔗

@@ -93,8 +93,6 @@ impl<V: 'static> Panel<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         let current_size = self.width.unwrap_or(self.initial_width);
 
         v_stack()
@@ -111,8 +109,8 @@ impl<V: 'static> Panel<V> {
             .when(self.current_side == PanelSide::Bottom, |this| {
                 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)
     }
 }

crates/ui2/src/components/panes.rs 🔗

@@ -90,8 +90,6 @@ impl<V: 'static> PaneGroup<V> {
     }
 
     fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         if !self.panes.is_empty() {
             let el = div()
                 .flex()
@@ -115,7 +113,7 @@ impl<V: 'static> PaneGroup<V> {
                 .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 {

crates/ui2/src/components/player_stack.rs 🔗

@@ -14,9 +14,7 @@ impl PlayerStack {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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)
                     })

crates/ui2/src/components/project_panel.rs 🔗

@@ -14,15 +14,13 @@ impl ProjectPanel {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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")

crates/ui2/src/components/status_bar.rs 🔗

@@ -86,8 +86,6 @@ impl StatusBar {
         view: &mut Workspace,
         cx: &mut ViewContext<Workspace>,
     ) -> impl Component<Workspace> {
-        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))
     }

crates/ui2/src/components/tab.rs 🔗

@@ -87,7 +87,6 @@ impl Tab {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
         let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
         let is_deleted = self.fs_status == FileSystemStatus::Deleted;
 
@@ -110,14 +109,14 @@ impl Tab {
 
         let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
             true => (
-                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,
             ),
             false => (
-                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,
             ),
         };
 

crates/ui2/src/components/tab_bar.rs 🔗

@@ -24,15 +24,13 @@ impl TabBar {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         let (can_navigate_back, can_navigate_forward) = self.can_navigate;
 
         div()
             .id(self.id.clone())
             .w_full()
             .flex()
-            .bg(theme.tab_bar)
+            .bg(cx.theme().colors().tab_bar)
             // Left Side
             .child(
                 div()

crates/ui2/src/components/terminal.rs 🔗

@@ -12,8 +12,6 @@ impl Terminal {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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)),
             )
     }
 }

crates/ui2/src/components/title_bar.rs 🔗

@@ -89,7 +89,6 @@ impl Render for TitleBar {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
-        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()

crates/ui2/src/components/toast.rs 🔗

@@ -37,8 +37,6 @@ impl<V: 'static> Toast<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
         let mut div = div();
 
         if self.origin == ToastOrigin::Bottom {
@@ -56,7 +54,7 @@ impl<V: 'static> Toast<V> {
             .rounded_lg()
             .shadow_md()
             .overflow_hidden()
-            .bg(theme.elevated_surface)
+            .bg(cx.theme().colors().elevated_surface)
             .children(self.children)
     }
 }

crates/ui2/src/components/toolbar.rs 🔗

@@ -55,10 +55,8 @@ impl<V: 'static> Toolbar<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            let theme = theme(cx);
-
             Story::container(cx)
                 .child(Story::title_for::<_, Toolbar<Self>>(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"),
                                     },
                                 ]),
                             ],

crates/ui2/src/components/traffic_lights.rs 🔗

@@ -22,13 +22,13 @@ impl TrafficLight {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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)

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>) {
         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<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
-        let theme = theme(cx);
-
-        // HACK: This should happen inside of `debug_toggle_user_settings`, but
-        // we don't have `cx.global::<FakeSettings>()` 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()

crates/ui2/src/elements/avatar.rs 🔗

@@ -22,8 +22,6 @@ impl Avatar {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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())
     }
 }
 

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<V: 'static> Button<V> {
             .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<V: 'static> ButtonGroup<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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));

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<V: 'static> {
@@ -27,13 +28,11 @@ impl<V: 'static> Details<V> {
     }
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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))

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,13 +26,14 @@ pub enum IconColor {
 
 impl IconColor {
     pub fn color(self, cx: &WindowContext) -> Hsla {
-        let theme = theme(cx);
+        let theme_colors = cx.theme().colors();
+
         match self {
-            IconColor::Default => gpui2::red(),
-            IconColor::Muted => gpui2::red(),
-            IconColor::Disabled => gpui2::red(),
-            IconColor::Placeholder => gpui2::red(),
-            IconColor::Accent => gpui2::red(),
+            IconColor::Default => theme_colors.icon,
+            IconColor::Muted => theme_colors.icon_muted,
+            IconColor::Disabled => theme_colors.icon_disabled,
+            IconColor::Placeholder => theme_colors.icon_placeholder,
+            IconColor::Accent => theme_colors.icon_accent,
             IconColor::Error => gpui2::red(),
             IconColor::Warning => gpui2::red(),
             IconColor::Success => gpui2::red(),
@@ -174,8 +175,8 @@ impl IconElement {
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         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()

crates/ui2/src/elements/input.rs 🔗

@@ -57,18 +57,16 @@ impl Input {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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,7 +88,7 @@ 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))

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::Default => cx.theme().colors().text,
+            Self::Muted => cx.theme().colors().text_muted,
             Self::Created => gpui2::red(),
             Self::Modified => gpui2::red(),
             Self::Deleted => gpui2::red(),
-            Self::Disabled => theme.text_disabled,
+            Self::Disabled => cx.theme().colors().text_disabled,
             Self::Hidden => gpui2::red(),
-            Self::Placeholder => theme.text_placeholder,
-            Self::Accent => gpui2::red(),
+            Self::Placeholder => cx.theme().colors().text_placeholder,
+            Self::Accent => cx.theme().colors().text_accent,
         }
     }
 }
@@ -88,7 +86,7 @@ impl Label {
                         .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 +124,7 @@ impl HighlightedLabel {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        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();
 

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<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
-        let theme = theme(cx);
-        theme.players[self.index].cursor
+        cx.theme().styles.player.0[self.index].cursor
     }
 
     pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
-        let theme = theme(cx);
-        theme.players[self.index].selection
+        cx.theme().styles.player.0[self.index].selection
     }
 
     pub fn avatar_src(&self) -> &str {

crates/ui2/src/elements/tool_divider.rs 🔗

@@ -9,8 +9,6 @@ impl ToolDivider {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let theme = theme(cx);
-
-        div().w_px().h_3().bg(theme.border)
+        div().w_px().h_3().bg(cx.theme().colors().border)
     }
 }

crates/ui2/src/prelude.rs 🔗

@@ -4,21 +4,12 @@ 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;
-
-    let settings = user_settings(cx);
-
-    rems(*settings.ui_scale * UI_SCALE_RATIO * size)
-}
-
 #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum FileSystemStatus {
     #[default]
@@ -54,15 +45,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,
         }
     }
 }

crates/ui2/src/settings.rs 🔗

@@ -58,7 +58,6 @@ pub struct FakeSettings {
     pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
     pub list_indent_depth: SettingValue<AbsoluteLength>,
     pub titlebar: TitlebarSettings,
-    pub ui_scale: SettingValue<f32>,
 }
 
 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.),
         }
     }
 }

crates/ui2/src/static_data.rs 🔗

@@ -1,12 +1,12 @@
 use std::path::PathBuf;
 use std::str::FromStr;
 
-use gpui2::ViewContext;
+use gpui2::{AppContext, ViewContext};
 use rand::Rng;
-use theme2::Theme;
+use theme2::ActiveTheme;
 
 use crate::{
-    theme, Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
+    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,
@@ -643,8 +643,6 @@ pub fn empty_buffer_example() -> Buffer {
 }
 
 pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
-    let theme = theme(cx);
-
     EditorPane::new(
         cx,
         static_tabs_example(),
@@ -652,29 +650,29 @@ pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> 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<BufferRow> {
+pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
     let show_line_number = true;
 
     vec![
@@ -686,15 +684,15 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 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 +708,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 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 +731,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
             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 +746,15 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 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 +769,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "}".to_string(),
-                    color: theme.text,
+                    color: cx.theme().colors().text,
                 }],
             }),
             cursors: None,
@@ -782,8 +780,6 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
 }
 
 pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
-    let theme = theme(cx);
-
     EditorPane::new(
         cx,
         static_tabs_example(),
@@ -791,29 +787,29 @@ pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext<EditorPa
         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_with_status_example(&theme),
+        hello_world_rust_buffer_with_status_example(cx),
     )
 }
 
-pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> 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<BufferRow> {
+pub fn hello_world_rust_with_status_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
     let show_line_number = true;
 
     vec![
@@ -825,15 +821,15 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                 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 +845,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                 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 +868,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             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 +883,15 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                 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 +906,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "}".to_string(),
-                    color: theme.text,
+                    color: cx.theme().colors().text,
                 }],
             }),
             cursors: None,
@@ -924,7 +920,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "".to_string(),
-                    color: theme.text,
+                    color: cx.theme().colors().text,
                 }],
             }),
             cursors: None,
@@ -938,7 +934,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             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 +944,16 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
     ]
 }
 
-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<BufferRow> {
+pub fn terminal_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
     let show_line_number = false;
 
     vec![
@@ -969,31 +965,31 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 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 +1004,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "λ ".to_string(),
-                    color: theme.syntax.color("string"),
+                    color: cx.theme().syntax_color("string"),
                 }],
             }),
             cursors: None,

crates/ui2/src/story.rs 🔗

@@ -6,8 +6,6 @@ pub struct Story {}
 
 impl Story {
     pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
-        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<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
-        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<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
-        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())
     }
 }

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.111.0"
+version = "0.112.0"
 publish = false
 
 [lib]

crates/zed2/Cargo.toml 🔗

@@ -63,7 +63,7 @@ settings2 = { path = "../settings2" }
 feature_flags2 = { path = "../feature_flags2" }
 sum_tree = { path = "../sum_tree" }
 shellexpand = "2.1.0"
-text = { path = "../text" }
+text2 = { path = "../text2" }
 # terminal_view = { path = "../terminal_view" }
 theme2 = { path = "../theme2" }
 # theme_selector = { path = "../theme_selector" }
@@ -152,7 +152,7 @@ language2 = { path = "../language2", features = ["test-support"] }
 project2 = { path = "../project2", features = ["test-support"] }
 # rpc = { path = "../rpc", features = ["test-support"] }
 # settings = { path = "../settings", features = ["test-support"] }
-# text = { path = "../text", features = ["test-support"] }
+text2 = { path = "../text2", features = ["test-support"] }
 # util = { path = "../util", features = ["test-support"] }
 # workspace = { path = "../workspace", features = ["test-support"] }
 unindent.workspace = true