Merge branch 'main' into collab_ui2

Conrad Irwin created

Change summary

Cargo.lock                                     |  21 +
assets/settings/default.json                   |   9 
crates/collab2/src/tests/test_server.rs        |   2 
crates/collab_ui2/src/collab_titlebar_item.rs  |  24 +
crates/command_palette2/src/command_palette.rs |   4 
crates/copilot/src/copilot.rs                  |  20 
crates/editor2/src/display_map/inlay_map.rs    |   2 
crates/editor2/src/editor.rs                   |  50 +--
crates/editor2/src/editor_tests.rs             |   2 
crates/editor2/src/inlay_hint_cache.rs         |   2 
crates/file_finder2/src/file_finder.rs         |   2 
crates/gpui2/Cargo.toml                        |   1 
crates/gpui2/src/action.rs                     | 247 +++++++++----------
crates/gpui2/src/app.rs                        |  27 +
crates/gpui2/src/app/test_context.rs           |  17 +
crates/gpui2/src/elements/div.rs               |  10 
crates/gpui2/src/executor.rs                   |  66 ++++-
crates/gpui2/src/gpui2.rs                      |   2 
crates/gpui2/src/key_dispatch.rs               |  13 
crates/gpui2/src/keymap/keymap.rs              |  30 +-
crates/gpui2/src/platform.rs                   |   6 
crates/gpui2/src/platform/mac/dispatcher.rs    |   6 
crates/gpui2/src/platform/test/dispatcher.rs   |  83 ++++--
crates/gpui2/src/window.rs                     |   4 
crates/gpui2/tests/action_macros.rs            |  45 +++
crates/gpui2_macros/Cargo.toml                 |   2 
crates/gpui2_macros/src/action.rs              | 103 +++++--
crates/gpui2_macros/src/gpui2_macros.rs        |   8 
crates/gpui2_macros/src/register_action.rs     |  78 +++++
crates/language/src/buffer.rs                  |  90 ++++---
crates/language2/src/buffer.rs                 | 215 ++++++++++------
crates/live_kit_client2/examples/test_app.rs   |   4 
crates/project/src/project.rs                  |   4 
crates/project/src/worktree.rs                 |   2 
crates/project2/src/project2.rs                |   4 
crates/project2/src/project_tests.rs           |  67 +++++
crates/project2/src/worktree.rs                |   3 
crates/project_panel2/src/project_panel.rs     |   4 
crates/rope/src/rope.rs                        |   6 
crates/rope2/src/rope2.rs                      |   6 
crates/settings2/src/keymap_file.rs            |   4 
crates/settings2/src/settings_file.rs          |   3 
crates/storybook2/src/storybook2.rs            |   2 
crates/storybook3/src/storybook3.rs            |   4 
crates/terminal_view2/src/terminal_view.rs     |  10 
crates/theme2/src/registry.rs                  |   8 
crates/theme2/src/settings.rs                  |  19 +
crates/theme2/src/theme2.rs                    |  19 +
crates/ui2/src/components/button.rs            |   1 
crates/ui2/src/components/context_menu.rs      |   8 
crates/ui2/src/components/icon_button.rs       |  13 
crates/ui2/src/components/keybinding.rs        |   5 
crates/workspace2/src/dock.rs                  |  25 +
crates/workspace2/src/pane.rs                  |  10 
crates/workspace2/src/workspace2.rs            |  26 +
crates/zed2/src/languages/json.rs              |   2 
crates/zed2/src/main.rs                        |   2 
crates/zed_actions2/src/lib.rs                 |   7 
58 files changed, 940 insertions(+), 519 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3797,6 +3797,7 @@ dependencies = [
  "image",
  "itertools 0.10.5",
  "lazy_static",
+ "linkme",
  "log",
  "media",
  "metal",
@@ -4815,6 +4816,26 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linkme"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.0.42"

assets/settings/default.json 🔗

@@ -35,6 +35,15 @@
   //           "custom": 2
   //         },
   "buffer_line_height": "comfortable",
+  // The name of a font to use for rendering text in the UI
+  "ui_font_family": "Zed Mono",
+  // The OpenType features to enable for text in the UI
+  "ui_font_features": {
+    // Disable ligatures:
+    "calt": false
+  },
+  // The default font size for text in the UI
+  "ui_font_size": 14,
   // The factor to grow the active pane by. Defaults to 1.0
   // which gives the same size as all other panes.
   "active_pane_magnification": 1.0,

crates/collab2/src/tests/test_server.rs 🔗

@@ -224,7 +224,7 @@ impl TestServer {
         });
 
         cx.update(|cx| {
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             Project::init(&client, cx);
             client::init(&client, cx);
             language::init(cx);

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -31,9 +31,9 @@ use std::sync::Arc;
 use call::ActiveCall;
 use client::{Client, UserStore};
 use gpui::{
-    div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render,
-    Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext,
-    WeakView, WindowBounds,
+    div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
+    Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
+    VisualContext, WeakView, WindowBounds,
 };
 use project::Project;
 use theme::ActiveTheme;
@@ -88,12 +88,17 @@ impl Render for CollabTitlebarItem {
         h_stack()
             .id("titlebar")
             .justify_between()
+            .w_full()
+            .h(rems(1.75))
+            // Set a non-scaling min-height here to ensure the titlebar is
+            // always at least the height of the traffic lights.
+            .min_h(px(32.))
             .when(
                 !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
-                |s| s.pl_20(),
+                // Use pixels here instead of a rem-based size because the macOS traffic
+                // lights are a static size, and don't scale with the rest of the UI.
+                |s| s.pl(px(68.)),
             )
-            .w_full()
-            .h(rems(1.75))
             .bg(cx.theme().colors().title_bar_background)
             .on_click(|_, event, cx| {
                 if event.up.click_count == 2 {
@@ -102,6 +107,7 @@ impl Render for CollabTitlebarItem {
             })
             .child(
                 h_stack()
+                    .gap_1()
                     // TODO - Add player menu
                     .child(
                         div()
@@ -130,14 +136,12 @@ impl Render for CollabTitlebarItem {
                                     .color(Some(TextColor::Muted)),
                             )
                             .tooltip(move |_, cx| {
-                                // todo!() Replace with real action.
-                                #[gpui::action]
-                                struct NoAction {}
                                 cx.build_view(|_| {
                                     Tooltip::new("Recent Branches")
                                         .key_binding(KeyBinding::new(gpui::KeyBinding::new(
                                             "cmd-b",
-                                            NoAction {},
+                                            // todo!() Replace with real action.
+                                            gpui::NoAction,
                                             None,
                                         )))
                                         .meta("Only local branches shown")

crates/command_palette2/src/command_palette.rs 🔗

@@ -47,7 +47,7 @@ impl CommandPalette {
             .available_actions()
             .into_iter()
             .filter_map(|action| {
-                let name = action.name();
+                let name = gpui::remove_the_2(action.name());
                 let namespace = name.split("::").next().unwrap_or("malformed action name");
                 if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
                     return None;
@@ -456,7 +456,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init(cx);
             workspace::init(app_state.clone(), cx);

crates/copilot/src/copilot.rs 🔗

@@ -1051,17 +1051,15 @@ mod tests {
         );
 
         // Ensure updates to the file are reflected in the LSP.
-        buffer_1
-            .update(cx, |buffer, cx| {
-                buffer.file_updated(
-                    Arc::new(File {
-                        abs_path: "/root/child/buffer-1".into(),
-                        path: Path::new("child/buffer-1").into(),
-                    }),
-                    cx,
-                )
-            })
-            .await;
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.file_updated(
+                Arc::new(File {
+                    abs_path: "/root/child/buffer-1".into(),
+                    path: Path::new("child/buffer-1").into(),
+                }),
+                cx,
+            )
+        });
         assert_eq!(
             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
                 .await,

crates/editor2/src/display_map/inlay_map.rs 🔗

@@ -1891,6 +1891,6 @@ mod tests {
     fn init_test(cx: &mut AppContext) {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
     }
 }

crates/editor2/src/editor.rs 🔗

@@ -39,7 +39,7 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
+    actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
     EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
     Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 //     //     .with_soft_wrap(true)
 // }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectNext {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectPrevious {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectAllMatches {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageUp {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageDown {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToEndOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleCodeActions {
     #[serde(default)]
     pub deployed_from_indicator: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCodeAction {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleComments {
     #[serde(default)]
     pub advance_downwards: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct FoldAt {
     pub buffer_row: u32,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }
@@ -9379,18 +9379,16 @@ impl Render for Editor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let settings = ThemeSettings::get_global(cx);
         let text_style = match self.mode {
-            EditorMode::SingleLine => {
-                TextStyle {
-                    color: cx.theme().colors().text,
-                    font_family: settings.ui_font.family.clone(), // todo!()
-                    font_features: settings.ui_font.features,
-                    font_size: rems(0.875).into(),
-                    font_weight: FontWeight::NORMAL,
-                    font_style: FontStyle::Normal,
-                    line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
-                    underline: None,
-                }
-            }
+            EditorMode::SingleLine => TextStyle {
+                color: cx.theme().colors().text,
+                font_family: settings.ui_font.family.clone(),
+                font_features: settings.ui_font.features,
+                font_size: rems(0.875).into(),
+                font_weight: FontWeight::NORMAL,
+                font_style: FontStyle::Normal,
+                line_height: relative(1.).into(),
+                underline: None,
+            },
 
             EditorMode::AutoHeight { max_lines } => todo!(),
 

crates/editor2/src/editor_tests.rs 🔗

@@ -8277,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
     cx.update(|cx| {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
         client::init_settings(cx);
         language::init(cx);
         Project::init_settings(cx);

crates/editor2/src/inlay_hint_cache.rs 🔗

@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             client::init_settings(cx);
             language::init(cx);
             Project::init_settings(cx);

crates/file_finder2/src/file_finder.rs 🔗

@@ -1763,7 +1763,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             super::init(cx);
             editor::init(cx);

crates/gpui2/Cargo.toml 🔗

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
 async-task = "4.0.3"
 backtrace = { version = "0.3", optional = true }
 ctor.workspace = true
+linkme = "0.3"
 derive_more.workspace = true
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }

crates/gpui2/src/action.rs 🔗

@@ -1,10 +1,12 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
-use lazy_static::lazy_static;
-use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
-use serde::Deserialize;
-use std::any::{type_name, Any, TypeId};
+pub use no_action::NoAction;
+use serde_json::json;
+use std::{
+    any::{Any, TypeId},
+    ops::Deref,
+};
 
 /// Actions are used to implement keyboard-driven UI.
 /// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
 /// ```rust
 /// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
 /// ```
-/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
-/// it will automatically
+/// More complex data types can also be actions. If you annotate your type with the action derive macro
+/// it will be implemented and registered automatically.
 /// ```
-/// #[action]
+/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
 /// pub struct SelectNext {
 ///     pub replace_newest: bool,
 /// }
 ///
-/// Any type A that satisfies the following bounds is automatically an action:
-///
-/// ```
-/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-/// ```
-///
-/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
-/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
-/// generates the code needed to register your action before `main`. Then you'll need to implement all
-/// the traits manually.
+/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
+/// macro, which only generates the code needed to register your action before `main`.
 ///
 /// ```
 /// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
 ///     pub content: SharedString,
 /// }
 ///
-/// impl std::default::Default for Paste {
-///     fn default() -> Self {
-///         Self {
-///             content: SharedString::from("🍝"),
-///         }
-///     }
+/// impl gpui::Action for Paste {
+///      ///...
 /// }
 /// ```
-pub trait Action: std::fmt::Debug + 'static {
-    fn qualified_name() -> SharedString
-    where
-        Self: Sized;
-    fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+pub trait Action: 'static {
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+    fn partial_eq(&self, action: &dyn Action) -> bool;
+    fn name(&self) -> &str;
+
+    fn debug_name() -> &'static str
     where
         Self: Sized;
-    fn is_registered() -> bool
+    fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
     where
         Self: Sized;
-
-    fn partial_eq(&self, action: &dyn Action) -> bool;
-    fn boxed_clone(&self) -> Box<dyn Action>;
-    fn as_any(&self) -> &dyn Any;
 }
 
-// Types become actions by satisfying a list of trait bounds.
-impl<A> Action for A
-where
-    A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static,
-{
-    fn qualified_name() -> SharedString {
-        let name = type_name::<A>();
-        let mut separator_matches = name.rmatch_indices("::");
-        separator_matches.next().unwrap();
-        let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
-        // todo!() remove the 2 replacement when migration is done
-        name[name_start_ix..].replace("2::", "::").into()
-    }
-
-    fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
-    where
-        Self: Sized,
-    {
-        let action = if let Some(params) = params {
-            serde_json::from_value(params).context("failed to deserialize action")?
-        } else {
-            Self::default()
-        };
-        Ok(Box::new(action))
-    }
-
-    fn is_registered() -> bool {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&TypeId::of::<A>())
-            .is_some()
-    }
-
-    fn partial_eq(&self, action: &dyn Action) -> bool {
-        action
-            .as_any()
-            .downcast_ref::<Self>()
-            .map_or(false, |a| self == a)
-    }
-
-    fn boxed_clone(&self) -> Box<dyn Action> {
-        Box::new(self.clone())
-    }
-
-    fn as_any(&self) -> &dyn Any {
-        self
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("type_name", &self.name())
+            .finish()
     }
 }
 
@@ -119,69 +65,93 @@ impl dyn Action {
     pub fn type_id(&self) -> TypeId {
         self.as_any().type_id()
     }
-
-    pub fn name(&self) -> SharedString {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&self.type_id())
-            .expect("type is not a registered action")
-            .clone()
-    }
 }
 
-type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
-
-lazy_static! {
-    static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
-}
+type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 
-#[derive(Default)]
-struct ActionRegistry {
+pub(crate) struct ActionRegistry {
     builders_by_name: HashMap<SharedString, ActionBuilder>,
     names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
 }
 
-/// Register an action type to allow it to be referenced in keymaps.
-pub fn register_action<A: Action>() {
-    let name = A::qualified_name();
-    let mut lock = ACTION_REGISTRY.write();
-    lock.builders_by_name.insert(name.clone(), A::build);
-    lock.names_by_type_id
-        .insert(TypeId::of::<A>(), name.clone());
-    lock.all_names.push(name);
+impl Default for ActionRegistry {
+    fn default() -> Self {
+        let mut this = ActionRegistry {
+            builders_by_name: Default::default(),
+            names_by_type_id: Default::default(),
+            all_names: Default::default(),
+        };
+
+        this.load_actions();
+
+        this
+    }
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
-    let name = lock
-        .names_by_type_id
-        .get(type_id)
-        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
-        .clone();
-    drop(lock);
-
-    build_action(&name, None)
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub type MacroActionBuilder = fn() -> ActionData;
+
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub struct ActionData {
+    pub name: &'static str,
+    pub type_id: TypeId,
+    pub build: ActionBuilder,
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
+/// This constant must be public to be accessible from other crates.
+/// But it's existence is an implementation detail and should not be used directly.
+#[doc(hidden)]
+#[linkme::distributed_slice]
+pub static __GPUI_ACTIONS: [MacroActionBuilder];
+
+impl ActionRegistry {
+    /// Load all registered actions into the registry.
+    pub(crate) fn load_actions(&mut self) {
+        for builder in __GPUI_ACTIONS {
+            let action = builder();
+            //todo(remove)
+            let name: SharedString = remove_the_2(action.name).into();
+            self.builders_by_name.insert(name.clone(), action.build);
+            self.names_by_type_id.insert(action.type_id, name.clone());
+            self.all_names.push(name);
+        }
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
+        let name = self
+            .names_by_type_id
+            .get(type_id)
+            .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+            .clone();
 
-    let build_action = lock
-        .builders_by_name
-        .get(name)
-        .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
-    (build_action)(params)
-}
+        self.build_action(&name, None)
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action(
+        &self,
+        name: &str,
+        params: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        //todo(remove)
+        let name = remove_the_2(name);
+        let build_action = self
+            .builders_by_name
+            .get(name.deref())
+            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+        (build_action)(params.unwrap_or_else(|| json!({})))
+            .with_context(|| format!("Attempting to build action {}", name))
+    }
 
-pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
-    let lock = ACTION_REGISTRY.read();
-    RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
-        registry.all_names.as_slice()
-    })
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.all_names.as_slice()
+    }
 }
 
 /// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
     () => {};
 
     ( $name:ident ) => {
-        #[gpui::action]
+        #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
         pub struct $name;
     };
 
@@ -200,3 +170,20 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
+
+//todo!(remove)
+pub fn remove_the_2(action_name: &str) -> String {
+    let mut separator_matches = action_name.rmatch_indices("::");
+    separator_matches.next().unwrap();
+    let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
+    // todo!() remove the 2 replacement when migration is done
+    action_name[name_start_ix..]
+        .replace("2::", "::")
+        .to_string()
+}
+
+mod no_action {
+    use crate as gpui;
+
+    actions!(NoAction);
+}

crates/gpui2/src/app.rs 🔗

@@ -14,12 +14,13 @@ use smallvec::SmallVec;
 pub use test_context::*;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
-    AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
-    Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
-    LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
-    Window, WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+    AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+    DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
+    ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
+    PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+    TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
+    WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     flushing_effects: bool,
     pending_updates: usize,
+    pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
                 platform: platform.clone(),
                 app_metadata,
                 text_system,
+                actions: Rc::new(ActionRegistry::default()),
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
     pub fn propagate(&mut self) {
         self.propagate_event = true;
     }
+
+    pub fn build_action(
+        &self,
+        name: &str,
+        data: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        self.actions.build_action(name, data)
+    }
+
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.actions.all_action_names()
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/app/test_context.rs 🔗

@@ -370,10 +370,19 @@ impl<T: Send> Model<T> {
             })
         });
 
-        cx.executor().run_until_parked();
-        rx.try_next()
-            .expect("no event received")
-            .expect("model was dropped")
+        // Run other tasks until the event is emitted.
+        loop {
+            match rx.try_next() {
+                Ok(Some(event)) => return event,
+                Ok(None) => panic!("model was dropped"),
+                Err(_) => {
+                    if !cx.executor().tick() {
+                        break;
+                    }
+                }
+            }
+        }
+        panic!("no event received")
     }
 }
 

crates/gpui2/src/elements/div.rs 🔗

@@ -237,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         //
         // if we are relying on this side-effect still, removing the debug_assert!
         // likely breaks the command_palette tests.
-        debug_assert!(
-            A::is_registered(),
-            "{:?} is not registered as an action",
-            A::qualified_name()
-        );
+        // debug_assert!(
+        //     A::is_registered(),
+        //     "{:?} is not registered as an action",
+        //     A::qualified_name()
+        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {

crates/gpui2/src/executor.rs 🔗

@@ -5,10 +5,11 @@ use std::{
     fmt::Debug,
     marker::PhantomData,
     mem,
+    num::NonZeroUsize,
     pin::Pin,
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     task::{Context, Poll},
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
         }
     }
 }
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct TaskLabel(NonZeroUsize);
+
+impl TaskLabel {
+    pub fn new() -> Self {
+        static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
+        Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
+    }
+}
+
 type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
+
 type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
+
 impl BackgroundExecutor {
     pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
         Self { dispatcher }
     }
 
-    /// Enqueues the given closure to be run on any thread. The closure returns
-    /// a future which will be run to completion on any available thread.
+    /// Enqueues the given future to be run to completion on a background thread.
     pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
     where
         R: Send + 'static,
     {
+        self.spawn_internal::<R>(Box::pin(future), None)
+    }
+
+    /// Enqueues the given future to be run to completion on a background thread.
+    /// The given label can be used to control the priority of the task in tests.
+    pub fn spawn_labeled<R>(
+        &self,
+        label: TaskLabel,
+        future: impl Future<Output = R> + Send + 'static,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        self.spawn_internal::<R>(Box::pin(future), Some(label))
+    }
+
+    fn spawn_internal<R: Send + 'static>(
+        &self,
+        future: AnyFuture<R>,
+        label: Option<TaskLabel>,
+    ) -> Task<R> {
         let dispatcher = self.dispatcher.clone();
-        fn inner<R: Send + 'static>(
-            dispatcher: Arc<dyn PlatformDispatcher>,
-            future: AnyFuture<R>,
-        ) -> Task<R> {
-            let (runnable, task) =
-                async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
-            runnable.schedule();
-            Task::Spawned(task)
-        }
-        inner::<R>(dispatcher, Box::pin(future))
+        let (runnable, task) =
+            async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
+        runnable.schedule();
+        Task::Spawned(task)
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
             match future.as_mut().poll(&mut cx) {
                 Poll::Ready(result) => return result,
                 Poll::Pending => {
-                    if !self.dispatcher.poll(background_only) {
+                    if !self.dispatcher.tick(background_only) {
                         if awoken.swap(false, SeqCst) {
                             continue;
                         }
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
         self.dispatcher.as_test().unwrap().simulate_random_delay()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.dispatcher.as_test().unwrap().deprioritize(task_label)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn advance_clock(&self, duration: Duration) {
         self.dispatcher.as_test().unwrap().advance_clock(duration)
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn tick(&self) -> bool {
+        self.dispatcher.as_test().unwrap().tick(false)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn run_until_parked(&self) {
         self.dispatcher.as_test().unwrap().run_until_parked()

crates/gpui2/src/gpui2.rs 🔗

@@ -49,11 +49,13 @@ pub use input::*;
 pub use interactive::*;
 pub use key_dispatch::*;
 pub use keymap::*;
+pub use linkme;
 pub use platform::*;
 use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
 pub use serde;
+pub use serde_derive;
 pub use serde_json;
 pub use smallvec;
 pub use smol::Timer;

crates/gpui2/src/key_dispatch.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
-    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
+    Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
-use util::ResultExt;
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
     focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
     keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Arc<Mutex<Keymap>>,
+    action_registry: Rc<ActionRegistry>,
 }
 
 #[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
 }
 
 impl DispatchTree {
-    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+    pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
         Self {
             node_stack: Vec::new(),
             context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
             focusable_node_ids: HashMap::default(),
             keystroke_matchers: HashMap::default(),
             keymap,
+            action_registry,
         }
     }
 
@@ -153,7 +154,9 @@ impl DispatchTree {
             for node_id in self.dispatch_path(*node) {
                 let node = &self.nodes[node_id.0];
                 for DispatchActionListener { action_type, .. } in &node.action_listeners {
-                    actions.extend(build_action_from_type(action_type).log_err());
+                    // Intentionally silence these errors without logging.
+                    // If an action cannot be built by default, it's not available.
+                    actions.extend(self.action_registry.build_action_type(action_type).ok());
                 }
             }
         }

crates/gpui2/src/keymap/keymap.rs 🔗

@@ -1,7 +1,10 @@
-use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
 use collections::HashSet;
 use smallvec::SmallVec;
-use std::{any::TypeId, collections::HashMap};
+use std::{
+    any::{Any, TypeId},
+    collections::HashMap,
+};
 
 #[derive(Copy, Clone, Eq, PartialEq, Default)]
 pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
     }
 
     pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
-        // todo!("no action")
-        // let no_action_id = (NoAction {}).id();
+        let no_action_id = &(NoAction {}).type_id();
         let mut new_bindings = Vec::new();
-        let has_new_disabled_keystrokes = false;
+        let mut has_new_disabled_keystrokes = false;
         for binding in bindings {
-            // if binding.action().id() == no_action_id {
-            //     has_new_disabled_keystrokes |= self
-            //         .disabled_keystrokes
-            //         .entry(binding.keystrokes)
-            //         .or_default()
-            //         .insert(binding.context_predicate);
-            // } else {
-            new_bindings.push(binding);
-            // }
+            if binding.action.type_id() == *no_action_id {
+                has_new_disabled_keystrokes |= self
+                    .disabled_keystrokes
+                    .entry(binding.keystrokes)
+                    .or_default()
+                    .insert(binding.context_predicate);
+            } else {
+                new_bindings.push(binding);
+            }
         }
 
         if has_new_disabled_keystrokes {

crates/gpui2/src/platform.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
     FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
     Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
-    SharedString, Size,
+    SharedString, Size, TaskLabel,
 };
 use anyhow::{anyhow, bail};
 use async_task::Runnable;
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
 
 pub trait PlatformDispatcher: Send + Sync {
     fn is_main_thread(&self) -> bool;
-    fn dispatch(&self, runnable: Runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
     fn dispatch_on_main_thread(&self, runnable: Runnable);
     fn dispatch_after(&self, duration: Duration, runnable: Runnable);
-    fn poll(&self, background_only: bool) -> bool;
+    fn tick(&self, background_only: bool) -> bool;
     fn park(&self);
     fn unparker(&self) -> Unparker;
 

crates/gpui2/src/platform/mac/dispatcher.rs 🔗

@@ -2,7 +2,7 @@
 #![allow(non_camel_case_types)]
 #![allow(non_snake_case)]
 
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use objc::{
     class, msg_send,
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
         is_main_thread == YES
     }
 
-    fn dispatch(&self, runnable: Runnable) {
+    fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
         unsafe {
             dispatch_async_f(
                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
         }
     }
 
-    fn poll(&self, _background_only: bool) -> bool {
+    fn tick(&self, _background_only: bool) -> bool {
         false
     }
 

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -1,7 +1,7 @@
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use backtrace::Backtrace;
-use collections::{HashMap, VecDeque};
+use collections::{HashMap, HashSet, VecDeque};
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
 use rand::prelude::*;
@@ -28,12 +28,14 @@ struct TestDispatcherState {
     random: StdRng,
     foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
     background: Vec<Runnable>,
+    deprioritized_background: Vec<Runnable>,
     delayed: Vec<(Duration, Runnable)>,
     time: Duration,
     is_main_thread: bool,
     next_id: TestDispatcherId,
     allow_parking: bool,
     waiting_backtrace: Option<Backtrace>,
+    deprioritized_task_labels: HashSet<TaskLabel>,
 }
 
 impl TestDispatcher {
@@ -43,12 +45,14 @@ impl TestDispatcher {
             random,
             foreground: HashMap::default(),
             background: Vec::new(),
+            deprioritized_background: Vec::new(),
             delayed: Vec::new(),
             time: Duration::ZERO,
             is_main_thread: true,
             next_id: TestDispatcherId(1),
             allow_parking: false,
             waiting_backtrace: None,
+            deprioritized_task_labels: Default::default(),
         };
 
         TestDispatcher {
@@ -101,8 +105,15 @@ impl TestDispatcher {
         }
     }
 
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.state
+            .lock()
+            .deprioritized_task_labels
+            .insert(task_label);
+    }
+
     pub fn run_until_parked(&self) {
-        while self.poll(false) {}
+        while self.tick(false) {}
     }
 
     pub fn parking_allowed(&self) -> bool {
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
         self.state.lock().is_main_thread
     }
 
-    fn dispatch(&self, runnable: Runnable) {
-        self.state.lock().background.push(runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+        {
+            let mut state = self.state.lock();
+            if label.map_or(false, |label| {
+                state.deprioritized_task_labels.contains(&label)
+            }) {
+                state.deprioritized_background.push(runnable);
+            } else {
+                state.background.push(runnable);
+            }
+        }
         self.unparker.unpark();
     }
 
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
         state.delayed.insert(ix, (next_time, runnable));
     }
 
-    fn poll(&self, background_only: bool) -> bool {
+    fn tick(&self, background_only: bool) -> bool {
         let mut state = self.state.lock();
 
         while let Some((deadline, _)) = state.delayed.first() {
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
         };
         let background_len = state.background.len();
 
+        let runnable;
+        let main_thread;
         if foreground_len == 0 && background_len == 0 {
-            return false;
-        }
-
-        let main_thread = state.random.gen_ratio(
-            foreground_len as u32,
-            (foreground_len + background_len) as u32,
-        );
-        let was_main_thread = state.is_main_thread;
-        state.is_main_thread = main_thread;
-
-        let runnable = if main_thread {
-            let state = &mut *state;
-            let runnables = state
-                .foreground
-                .values_mut()
-                .filter(|runnables| !runnables.is_empty())
-                .choose(&mut state.random)
-                .unwrap();
-            runnables.pop_front().unwrap()
+            let deprioritized_background_len = state.deprioritized_background.len();
+            if deprioritized_background_len == 0 {
+                return false;
+            }
+            let ix = state.random.gen_range(0..deprioritized_background_len);
+            main_thread = false;
+            runnable = state.deprioritized_background.swap_remove(ix);
         } else {
-            let ix = state.random.gen_range(0..background_len);
-            state.background.swap_remove(ix)
+            main_thread = state.random.gen_ratio(
+                foreground_len as u32,
+                (foreground_len + background_len) as u32,
+            );
+            if main_thread {
+                let state = &mut *state;
+                runnable = state
+                    .foreground
+                    .values_mut()
+                    .filter(|runnables| !runnables.is_empty())
+                    .choose(&mut state.random)
+                    .unwrap()
+                    .pop_front()
+                    .unwrap();
+            } else {
+                let ix = state.random.gen_range(0..background_len);
+                runnable = state.background.swap_remove(ix);
+            };
         };
 
+        let was_main_thread = state.is_main_thread;
+        state.is_main_thread = main_thread;
         drop(state);
         runnable.run();
-
         self.state.lock().is_main_thread = was_main_thread;
 
         true

crates/gpui2/src/window.rs 🔗

@@ -311,8 +311,8 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
-            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,

crates/gpui2/tests/action_macros.rs 🔗

@@ -0,0 +1,45 @@
+use serde_derive::Deserialize;
+
+#[test]
+fn test_derive() {
+    use gpui2 as gpui;
+
+    #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
+    struct AnotherTestAction;
+
+    #[gpui2_macros::register_action]
+    #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+    struct RegisterableAction {}
+
+    impl gpui::Action for RegisterableAction {
+        fn boxed_clone(&self) -> Box<dyn gpui::Action> {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
+            todo!()
+        }
+
+        fn name(&self) -> &str {
+            todo!()
+        }
+
+        fn debug_name() -> &'static str
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+
+        fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+    }
+}

crates/gpui2_macros/Cargo.toml 🔗

@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
 proc-macro = true
 
 [dependencies]
-syn = "1.0.72"
+syn = { version = "1.0.72", features = ["full"] }
 quote = "1.0.9"
 proc-macro2 = "1.0.66"

crates/gpui2_macros/src/action.rs 🔗

@@ -15,48 +15,81 @@
 
 use proc_macro::TokenStream;
 use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
+
+use crate::register_action::register_action;
+
+pub fn action(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
 
-pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(item as DeriveInput);
     let name = &input.ident;
-    let attrs = input
-        .attrs
-        .into_iter()
-        .filter(|attr| !attr.path.is_ident("action"))
-        .collect::<Vec<_>>();
-
-    let attributes = quote! {
-        #[gpui::register_action]
-        #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
-        #(#attrs)*
+
+    if input.generics.lt_token.is_some() {
+        return Error::new(name.span(), "Actions must be a concrete type")
+            .into_compile_error()
+            .into();
+    }
+
+    let is_unit_struct = match input.data {
+        syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
+        syn::Data::Enum(_) => false,
+        syn::Data::Union(_) => false,
+    };
+
+    let build_impl = if is_unit_struct {
+        quote! {
+            Ok(std::boxed::Box::new(Self {}))
+        }
+    } else {
+        quote! {
+            Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+        }
     };
-    let visibility = input.vis;
-
-    let output = match input.data {
-        syn::Data::Struct(ref struct_data) => match &struct_data.fields {
-            syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
-                let fields = &struct_data.fields;
-                quote! {
-                    #attributes
-                    #visibility struct #name #fields
-                }
+
+    let register_action = register_action(&name);
+
+    let output = quote! {
+        const _: fn() = || {
+            fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> +  ::std::cmp::PartialEq + ::std::clone::Clone>() {}
+            assert_impl::<#name>();
+        };
+
+        impl gpui::Action for #name {
+            fn name(&self) -> &'static str
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn debug_name() -> &'static str
+            where
+                Self: ::std::marker::Sized
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
+            where
+                Self: ::std::marker::Sized {
+                    #build_impl
             }
-            syn::Fields::Unit => {
-                quote! {
-                    #attributes
-                    #visibility struct #name;
-                }
+
+            fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
+                action
+                    .as_any()
+                    .downcast_ref::<Self>()
+                    .map_or(false, |a| self == a)
             }
-        },
-        syn::Data::Enum(ref enum_data) => {
-            let variants = &enum_data.variants;
-            quote! {
-                #attributes
-                #visibility enum #name { #variants }
+
+            fn boxed_clone(&self) ->  std::boxed::Box<dyn gpui::Action> {
+                ::std::boxed::Box::new(self.clone())
+            }
+
+            fn as_any(&self) -> &dyn ::std::any::Any {
+                self
             }
         }
-        _ => panic!("Expected a struct or an enum."),
+
+        #register_action
     };
 
     TokenStream::from(output)

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
     style_helpers::style_helpers(args)
 }
 
-#[proc_macro_attribute]
-pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    action::action(attr, item)
+#[proc_macro_derive(Action)]
+pub fn action(input: TokenStream) -> TokenStream {
+    action::action(input)
 }
 
 #[proc_macro_attribute]
 pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    register_action::register_action(attr, item)
+    register_action::register_action_macro(attr, item)
 }
 
 #[proc_macro_derive(Component, attributes(component))]

crates/gpui2_macros/src/register_action.rs 🔗

@@ -12,22 +12,76 @@
 //     gpui2::register_action_builder::<Foo>()
 // }
 use proc_macro::TokenStream;
+use proc_macro2::Ident;
 use quote::{format_ident, quote};
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
 
-pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
     let input = parse_macro_input!(item as DeriveInput);
-    let type_name = &input.ident;
-    let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
+    let registration = register_action(&input.ident);
 
-    let expanded = quote! {
+    let has_action_derive = input
+        .attrs
+        .iter()
+        .find(|attr| {
+            (|| {
+                let meta = attr.parse_meta().ok()?;
+                meta.path().is_ident("derive").then(|| match meta {
+                    syn::Meta::Path(_) => None,
+                    syn::Meta::NameValue(_) => None,
+                    syn::Meta::List(list) => list
+                        .nested
+                        .iter()
+                        .find(|list| match list {
+                            syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
+                            syn::NestedMeta::Lit(_) => false,
+                        })
+                        .map(|_| true),
+                })?
+            })()
+            .unwrap_or(false)
+        })
+        .is_some();
+
+    if has_action_derive {
+        return Error::new(
+            input.ident.span(),
+            "The Action derive macro has already registered this action",
+        )
+        .into_compile_error()
+        .into();
+    }
+
+    TokenStream::from(quote! {
         #input
-        #[allow(non_snake_case)]
-        #[gpui::ctor]
-        fn #ctor_fn_name() {
-            gpui::register_action::<#type_name>()
-        }
-    };
 
-    TokenStream::from(expanded)
+        #registration
+    })
+}
+
+pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
+    let static_slice_name =
+        format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
+
+    let action_builder_fn_name = format_ident!(
+        "__gpui_actions_builder_{}",
+        type_name.to_string().to_lowercase()
+    );
+
+    quote! {
+        #[doc(hidden)]
+        #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+        #[linkme(crate = gpui::linkme)]
+        static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
+
+        /// This is an auto generated function, do not use.
+        #[doc(hidden)]
+        fn #action_builder_fn_name() -> gpui::ActionData {
+            gpui::ActionData {
+                name: ::std::any::type_name::<#type_name>(),
+                type_id: ::std::any::TypeId::of::<#type_name>(),
+                build: <#type_name as gpui::Action>::build,
+            }
+        }
+    }
 }

crates/language/src/buffer.rs 🔗

@@ -17,7 +17,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
+use futures::channel::oneshot;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -62,6 +62,7 @@ pub struct Buffer {
     saved_mtime: SystemTime,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -509,6 +510,7 @@ impl Buffer {
             saved_mtime,
             saved_version: buffer.version(),
             saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -608,37 +610,52 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            }) {
-                let new_text = new_text.await?;
-                let diff = this
-                    .read_with(&cx, |this, cx| this.diff(new_text, cx))
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Ok(Some(transaction));
-                        }
-                    }
-                    Ok(None)
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            }) else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            });
+            Ok(())
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -667,13 +684,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -693,8 +705,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.foreground().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -708,7 +719,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {

crates/language2/src/buffer.rs 🔗

@@ -16,8 +16,9 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
-use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
+use futures::channel::oneshot;
+use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
+use lazy_static::lazy_static;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use similar::{ChangeTag, TextDiff};
@@ -44,23 +45,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
 
 pub use lsp::DiagnosticSeverity;
 
+lazy_static! {
+    pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
+}
+
 pub struct Buffer {
     text: TextBuffer,
     diff_base: Option<String>,
     git_diff: git::diff::BufferDiff,
     file: Option<Arc<dyn File>>,
-    saved_version: clock::Global,
-    saved_version_fingerprint: RopeFingerprint,
+    /// The mtime of the file when this buffer was last loaded from
+    /// or saved to disk.
     saved_mtime: SystemTime,
+    /// The version vector when this buffer was last loaded from
+    /// or saved to disk.
+    saved_version: clock::Global,
+    /// A hash of the current contents of the buffer's file.
+    file_fingerprint: RopeFingerprint,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -380,8 +391,7 @@ impl Buffer {
                 .ok_or_else(|| anyhow!("missing line_ending"))?,
         ));
         this.saved_version = proto::deserialize_version(&message.saved_version);
-        this.saved_version_fingerprint =
-            proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
+        this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
         this.saved_mtime = message
             .saved_mtime
             .ok_or_else(|| anyhow!("invalid saved_mtime"))?
@@ -397,7 +407,7 @@ impl Buffer {
             diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
             line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
             saved_version: proto::serialize_version(&self.saved_version),
-            saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
+            saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
             saved_mtime: Some(self.saved_mtime.into()),
         }
     }
@@ -467,7 +477,8 @@ impl Buffer {
         Self {
             saved_mtime,
             saved_version: buffer.version(),
-            saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            file_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -533,7 +544,7 @@ impl Buffer {
     }
 
     pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
-        self.saved_version_fingerprint
+        self.file_fingerprint
     }
 
     pub fn saved_mtime(&self) -> SystemTime {
@@ -561,43 +572,58 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.saved_mtime = mtime;
         cx.emit(Event::Saved);
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            })? {
-                let new_text = new_text.await?;
-                let diff = this
-                    .update(&mut cx, |this, cx| this.diff(new_text, cx))?
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Some(transaction);
-                        }
-                    }
-                    None
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            })?
+            else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            })
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -609,14 +635,14 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.text.set_line_ending(line_ending);
         self.saved_mtime = mtime;
         if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
             file.buffer_reloaded(
                 self.remote_id(),
                 &self.saved_version,
-                self.saved_version_fingerprint,
+                self.file_fingerprint,
                 self.line_ending(),
                 self.saved_mtime,
                 cx,
@@ -626,13 +652,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -652,8 +673,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.background_executor().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -667,7 +687,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {
@@ -1118,36 +1137,72 @@ impl Buffer {
     pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
         let old_text = self.as_rope().clone();
         let base_version = self.version();
-        cx.background_executor().spawn(async move {
-            let old_text = old_text.to_string();
-            let line_ending = LineEnding::detect(&new_text);
-            LineEnding::normalize(&mut new_text);
-            let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
-            let mut edits = Vec::new();
-            let mut offset = 0;
-            let empty: Arc<str> = "".into();
-            for change in diff.iter_all_changes() {
-                let value = change.value();
-                let end_offset = offset + value.len();
-                match change.tag() {
-                    ChangeTag::Equal => {
-                        offset = end_offset;
-                    }
-                    ChangeTag::Delete => {
-                        edits.push((offset..end_offset, empty.clone()));
-                        offset = end_offset;
+        cx.background_executor()
+            .spawn_labeled(*BUFFER_DIFF_TASK, async move {
+                let old_text = old_text.to_string();
+                let line_ending = LineEnding::detect(&new_text);
+                LineEnding::normalize(&mut new_text);
+
+                let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
+                let empty: Arc<str> = "".into();
+
+                let mut edits = Vec::new();
+                let mut old_offset = 0;
+                let mut new_offset = 0;
+                let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
+                for change in diff.iter_all_changes().map(Some).chain([None]) {
+                    if let Some(change) = &change {
+                        let len = change.value().len();
+                        match change.tag() {
+                            ChangeTag::Equal => {
+                                old_offset += len;
+                                new_offset += len;
+                            }
+                            ChangeTag::Delete => {
+                                let old_end_offset = old_offset + len;
+                                if let Some((last_old_range, _)) = &mut last_edit {
+                                    last_old_range.end = old_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_end_offset, new_offset..new_offset));
+                                }
+                                old_offset = old_end_offset;
+                            }
+                            ChangeTag::Insert => {
+                                let new_end_offset = new_offset + len;
+                                if let Some((_, last_new_range)) = &mut last_edit {
+                                    last_new_range.end = new_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_offset, new_offset..new_end_offset));
+                                }
+                                new_offset = new_end_offset;
+                            }
+                        }
                     }
-                    ChangeTag::Insert => {
-                        edits.push((offset..offset, value.into()));
+
+                    if let Some((old_range, new_range)) = &last_edit {
+                        if old_offset > old_range.end
+                            || new_offset > new_range.end
+                            || change.is_none()
+                        {
+                            let text = if new_range.is_empty() {
+                                empty.clone()
+                            } else {
+                                new_text[new_range.clone()].into()
+                            };
+                            edits.push((old_range.clone(), text));
+                            last_edit.take();
+                        }
                     }
                 }
-            }
-            Diff {
-                base_version,
-                line_ending,
-                edits,
-            }
-        })
+
+                Diff {
+                    base_version,
+                    line_ending,
+                    edits,
+                }
+            })
     }
 
     /// Spawn a background task that searches the buffer for any whitespace
@@ -1231,12 +1286,12 @@ impl Buffer {
     }
 
     pub fn is_dirty(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             || self.file.as_ref().map_or(false, |file| file.is_deleted())
     }
 
     pub fn has_conflict(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             && self
                 .file
                 .as_ref()

crates/live_kit_client2/examples/test_app.rs 🔗

@@ -1,7 +1,7 @@
 use std::{sync::Arc, time::Duration};
 
 use futures::StreamExt;
-use gpui::KeyBinding;
+use gpui::{Action, KeyBinding};
 use live_kit_client2::{
     LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
 };
@@ -10,7 +10,7 @@ use log::LevelFilter;
 use serde_derive::Deserialize;
 use simplelog::SimpleLogger;
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
 struct Quit;
 
 fn main() {

crates/project/src/project.rs 🔗

@@ -6190,7 +6190,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7182,7 +7182,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project/src/worktree.rs 🔗

@@ -959,7 +959,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 });
             }

crates/project2/src/project2.rs 🔗

@@ -6262,7 +6262,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7256,7 +7256,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project2/src/project_tests.rs 🔗

@@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
     assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
 }
 
+#[gpui::test(iterations = 30)]
+async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "file1": "the original contents",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+        .await
+        .unwrap();
+
+    // Simulate buffer diffs being slow, so that they don't complete before
+    // the next file change occurs.
+    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+    // Change the buffer's file on disk, and then wait for the file change
+    // to be detected by the worktree, so that the buffer starts reloading.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the first contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    // Change the buffer's file again. Depending on the random seed, the
+    // previous file change may still be in progress.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the second contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    cx.executor().run_until_parked();
+    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+    buffer.read_with(cx, |buffer, _| {
+        let buffer_text = buffer.text();
+        if buffer_text == on_disk_text {
+            assert!(
+                !buffer.is_dirty() && !buffer.has_conflict(),
+                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
+            );
+        }
+        // If the file change occurred while the buffer was processing the first
+        // change, the buffer will be in a conflicting state.
+        else {
+            assert!(
+                buffer.is_dirty() && buffer.has_conflict(),
+                "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
+            );
+        }
+    });
+}
+
 #[gpui::test]
 async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/project2/src/worktree.rs 🔗

@@ -276,6 +276,7 @@ struct ShareState {
     _maintain_remote_snapshot: Task<Option<()>>,
 }
 
+#[derive(Clone)]
 pub enum Event {
     UpdatedEntries(UpdatedEntriesSet),
     UpdatedGitRepositories(UpdatedGitRepositoriesSet),
@@ -961,7 +962,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 })?;
             }

crates/project_panel2/src/project_panel.rs 🔗

@@ -2785,7 +2785,7 @@ mod tests {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
             init_settings(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init_settings(cx);
             crate::init((), cx);
@@ -2798,7 +2798,7 @@ mod tests {
     fn init_test_with_editor(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             init_settings(cx);
             language::init(cx);
             editor::init(cx);

crates/rope/src/rope.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/rope2/src/rope2.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/settings2/src/keymap_file.rs 🔗

@@ -73,9 +73,9 @@ impl KeymapFile {
                                     "Expected first item in array to be a string."
                                 )));
                             };
-                            gpui::build_action(&name, Some(data))
+                            cx.build_action(&name, Some(data))
                         }
-                        Value::String(name) => gpui::build_action(&name, None),
+                        Value::String(name) => cx.build_action(&name, None),
                         Value::Null => Ok(no_action()),
                         _ => {
                             return Some(Err(anyhow!("Expected two-element array, got {action:?}")))

crates/settings2/src/settings_file.rs 🔗

@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
     .unwrap();
     util::merge_non_null_json_value_into(
         serde_json::json!({
+            "ui_font_family": "Courier",
+            "ui_font_features": {},
+            "ui_font_size": 14,
             "buffer_font_family": "Courier",
             "buffer_font_features": {},
             "buffer_font_size": 14,

crates/storybook2/src/storybook2.rs 🔗

@@ -60,7 +60,7 @@ fn main() {
             .unwrap();
         cx.set_global(store);
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::All, cx);
 
         let selector =
             story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));

crates/storybook3/src/storybook3.rs 🔗

@@ -31,7 +31,7 @@ fn main() {
             .unwrap();
         cx.set_global(store);
         ui::settings::init(cx);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
 
         cx.open_window(
             WindowOptions {
@@ -64,8 +64,6 @@ impl Render for TestView {
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         div()
-            .p(px(10.))
-            .bg(hsla(1., 1., 1., 0.))
             .flex()
             .flex_col()
             .size_full()

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -9,7 +9,7 @@ pub mod terminal_panel;
 // use crate::terminal_element::TerminalElement;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
+    actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
     EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
     InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
     ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
@@ -55,12 +55,10 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 #[derive(Clone, Debug, PartialEq)]
 pub struct ScrollTerminal(pub i32);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendText(String);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendKeystroke(String);
 
 actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
@@ -1130,7 +1128,7 @@ mod tests {
     pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
         let params = cx.update(AppState::test);
         cx.update(|cx| {
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             Project::init_settings(cx);
             language::init(cx);
         });

crates/theme2/src/registry.rs 🔗

@@ -100,6 +100,11 @@ impl ThemeRegistry {
             .ok_or_else(|| anyhow!("theme not found: {}", name))
             .cloned()
     }
+
+    pub fn load_user_themes(&mut self) {
+        #[cfg(not(feature = "importing-themes"))]
+        self.insert_user_theme_familes(crate::all_user_themes());
+    }
 }
 
 impl Default for ThemeRegistry {
@@ -110,9 +115,6 @@ impl Default for ThemeRegistry {
 
         this.insert_theme_families([one_family()]);
 
-        #[cfg(not(feature = "importing-themes"))]
-        this.insert_user_theme_familes(crate::all_user_themes());
-
         this
     }
 }

crates/theme2/src/settings.rs 🔗

@@ -34,6 +34,10 @@ pub struct ThemeSettingsContent {
     #[serde(default)]
     pub ui_font_size: Option<f32>,
     #[serde(default)]
+    pub ui_font_family: Option<String>,
+    #[serde(default)]
+    pub ui_font_features: Option<FontFeatures>,
+    #[serde(default)]
     pub buffer_font_family: Option<String>,
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
@@ -117,13 +121,13 @@ impl settings::Settings for ThemeSettings {
         user_values: &[&Self::FileContent],
         cx: &mut AppContext,
     ) -> Result<Self> {
-        let themes = cx.default_global::<Arc<ThemeRegistry>>();
+        let themes = cx.default_global::<ThemeRegistry>();
 
         let mut this = Self {
-            ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font_size: defaults.ui_font_size.unwrap().into(),
             ui_font: Font {
-                family: "Helvetica".into(),
-                features: Default::default(),
+                family: defaults.ui_font_family.clone().unwrap().into(),
+                features: defaults.ui_font_features.clone().unwrap(),
                 weight: Default::default(),
                 style: Default::default(),
             },
@@ -149,6 +153,13 @@ impl settings::Settings for ThemeSettings {
                 this.buffer_font.features = value;
             }
 
+            if let Some(value) = value.ui_font_family {
+                this.ui_font.family = value.into();
+            }
+            if let Some(value) = value.ui_font_features {
+                this.ui_font.features = value;
+            }
+
             if let Some(value) = &value.theme {
                 if let Some(theme) = themes.get(value).log_err() {
                     this.active_theme = theme;

crates/theme2/src/theme2.rs 🔗

@@ -31,8 +31,25 @@ pub enum Appearance {
     Dark,
 }
 
-pub fn init(cx: &mut AppContext) {
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LoadThemes {
+    /// Only load the base theme.
+    ///
+    /// No user themes will be loaded.
+    JustBase,
+
+    /// Load all of the built-in themes.
+    All,
+}
+
+pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
     cx.set_global(ThemeRegistry::default());
+
+    match themes_to_load {
+        LoadThemes::JustBase => (),
+        LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
+    }
+
     ThemeSettings::register(cx);
 }
 

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

@@ -178,6 +178,7 @@ impl<V: 'static> Button<V> {
             .text_ui()
             .rounded_md()
             .bg(self.variant.bg_color(cx))
+            .cursor_pointer()
             .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
             .active(|style| style.bg(self.variant.bg_color_active(cx)));
 

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

@@ -7,7 +7,6 @@ use gpui::{
     overlay, px, Action, AnchorCorner, AnyElement, Bounds, DispatchPhase, Div, EventEmitter,
     FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
 };
-use smallvec::SmallVec;
 
 pub struct ContextMenu {
     items: Vec<ListItem>,
@@ -269,16 +268,15 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{action, Div, Render, VisualContext};
+    use gpui::{actions, Div, Render, VisualContext};
 
-    #[action]
-    struct PrintCurrentDate {}
+    actions!(PrintCurrentDate);
 
     fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
         cx.build_view(|cx| {
             ContextMenu::new(cx).header(header).separator().entry(
                 Label::new("Print current time"),
-                PrintCurrentDate {}.boxed_clone(),
+                PrintCurrentDate.boxed_clone(),
             )
         })
     }

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

@@ -83,7 +83,7 @@ impl<V: 'static> IconButton<V> {
     fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => TextColor::Disabled,
-            (InteractionState::Active, _) => TextColor::Error,
+            (InteractionState::Active, _) => TextColor::Selected,
             _ => self.color,
         };
 
@@ -110,17 +110,16 @@ impl<V: 'static> IconButton<V> {
             .rounded_md()
             .p_1()
             .bg(bg_color)
+            .cursor_pointer()
             .hover(|style| style.bg(bg_hover_color))
             .active(|style| style.bg(bg_active_color))
             .child(IconElement::new(self.icon).color(icon_color));
 
         if let Some(click_handler) = self.handlers.click.clone() {
-            button = button
-                .on_mouse_down(MouseButton::Left, move |state, event, cx| {
-                    cx.stop_propagation();
-                    click_handler(state, cx);
-                })
-                .cursor_pointer();
+            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+                cx.stop_propagation();
+                click_handler(state, cx);
+            })
         }
 
         if let Some(tooltip) = self.tooltip.take() {

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

@@ -81,13 +81,12 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{actions, Div, Render};
     use itertools::Itertools;
 
     pub struct KeybindingStory;
 
-    #[action]
-    struct NoAction {}
+    actions!(NoAction);
 
     pub fn binding(key: &str) -> gpui::KeyBinding {
         gpui::KeyBinding::new(key, NoAction {}, None)

crates/workspace2/src/dock.rs 🔗

@@ -1,19 +1,14 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, overlay, point, px, Action, AnchorCorner, AnyElement, AnyView, AppContext, Component,
-    DispatchPhase, Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent,
-    Pixels, Point, Render, SharedString, Style, Styled, Subscription, View, ViewContext,
-    VisualContext, WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use smallvec::SmallVec;
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-use ui::{
-    h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, MenuEvent, MenuHandle,
-    Tooltip,
-};
+use std::sync::Arc;
+use theme2::ActiveTheme;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -457,10 +452,16 @@ impl Render for Dock {
             let size = entry.panel.size(cx);
 
             div()
+                .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
                     Axis::Horizontal => this.w(px(size)).h_full(),
                     Axis::Vertical => this.h(px(size)).w_full(),
                 })
+                .map(|this| match self.position() {
+                    DockPosition::Left => this.border_r(),
+                    DockPosition::Right => this.border_l(),
+                    DockPosition::Bottom => this.border_t(),
+                })
                 .child(entry.panel.to_any())
         } else {
             div()
@@ -715,7 +716,7 @@ impl Render for PanelButtons {
                 )
             });
 
-        h_stack().children(buttons)
+        h_stack().gap_0p5().children(buttons)
     }
 }
 

crates/workspace2/src/pane.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
     EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
     ViewContext, VisualContext, WeakView, WindowContext,
 };
@@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize);
 //     pub pane: WeakView<Pane>,
 // }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
@@ -1917,7 +1915,7 @@ impl Render for Pane {
             .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
             .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
             .size_full()
-            .on_action(|pane: &mut Self, action, cx| {
+            .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
                 pane.close_active_item(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             })

crates/workspace2/src/workspace2.rs 🔗

@@ -29,11 +29,11 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
-    ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext,
+    ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -194,8 +194,7 @@ impl Clone for Toast {
     }
 }
 
-#[register_action]
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)]
 pub struct OpenTerminal {
     pub working_directory: PathBuf,
 }
@@ -355,7 +354,7 @@ impl AppState {
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::JustBase, cx);
         client2::init(&client, cx);
         crate::init_settings(cx);
 
@@ -3614,7 +3613,16 @@ impl Render for Workspace {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let mut context = KeyContext::default();
         context.add("Workspace");
-        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+
+        let (ui_font, ui_font_size) = {
+            let theme_settings = ThemeSettings::get_global(cx);
+            (
+                theme_settings.ui_font.family.clone(),
+                theme_settings.ui_font_size.clone(),
+            )
+        };
+
+        cx.set_rem_size(ui_font_size);
 
         self.actions(div())
             .key_context(context)

crates/zed2/src/languages/json.rs 🔗

@@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter {
         &self,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
-        let action_names = gpui::all_action_names();
+        let action_names = cx.all_action_names();
         let staff_mode = cx.is_staff();
         let language_names = &self.languages.language_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(

crates/zed2/src/main.rs 🔗

@@ -140,7 +140,7 @@ fn main() {
 
         cx.set_global(client.clone());
 
-        theme::init(cx);
+        theme::init(theme::LoadThemes::All, cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
         command_palette::init(cx);

crates/zed_actions2/src/lib.rs 🔗

@@ -1,4 +1,5 @@
-use gpui::action;
+use gpui::Action;
+use serde::Deserialize;
 
 // If the zed binary doesn't use anything in this crate, it will be optimized away
 // and the actions won't initialize. So we just provide an empty initialization function
@@ -9,12 +10,12 @@ use gpui::action;
 // https://github.com/mmastrac/rust-ctor/issues/280
 pub fn init() {}
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenBrowser {
     pub url: String,
 }
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenZedURL {
     pub url: String,
 }