Merge branch 'main' into migrations-on-server-start

Conrad Irwin created

Change summary

Cargo.lock                                                        |  21 
Cargo.toml                                                        |   3 
assets/icons/arrow_circle.svg                                     |   7 
assets/settings/default.json                                      |   6 
crates/assets/Cargo.toml                                          |  12 
crates/assets/src/lib.rs                                          |   1 
crates/assistant/src/assistant_panel.rs                           |  34 
crates/assistant/src/assistant_settings.rs                        |  16 
crates/auto_update/Cargo.toml                                     |   1 
crates/auto_update/src/auto_update.rs                             |  18 
crates/auto_update/src/update_notification.rs                     |   4 
crates/breadcrumbs/src/breadcrumbs.rs                             |   5 
crates/call/src/call_settings.rs                                  |   4 
crates/client/src/client.rs                                       |   7 
crates/client/src/user.rs                                         |   4 
crates/collab/Cargo.toml                                          |   2 
crates/collab/src/db/ids.rs                                       |  16 
crates/collab/src/db/queries/projects.rs                          | 118 
crates/collab/src/db/tests/db_tests.rs                            |   4 
crates/collab/src/lib.rs                                          |   4 
crates/collab/src/rpc.rs                                          | 205 
crates/collab/src/tests/channel_guest_tests.rs                    |  10 
crates/collab/src/tests/integration_tests.rs                      |   6 
crates/collab/src/tests/test_server.rs                            |  16 
crates/collab_ui/Cargo.toml                                       |   3 
crates/collab_ui/src/chat_panel.rs                                | 103 
crates/collab_ui/src/chat_panel/message_editor.rs                 |  59 
crates/collab_ui/src/collab_panel.rs                              |  97 
crates/collab_ui/src/collab_panel/channel_modal.rs                |   4 
crates/collab_ui/src/collab_panel/contact_finder.rs               |   4 
crates/collab_ui/src/collab_titlebar_item.rs                      |  20 
crates/collab_ui/src/notification_panel.rs                        |  12 
crates/collab_ui/src/notifications.rs                             |  11 
crates/collab_ui/src/notifications/collab_notification.rs         |  52 
crates/collab_ui/src/notifications/incoming_call_notification.rs  |  52 
crates/collab_ui/src/notifications/project_shared_notification.rs |  74 
crates/collab_ui/src/notifications/stories.rs                     |   3 
crates/collab_ui/src/notifications/stories/collab_notification.rs |  50 
crates/collab_ui/src/panel_settings.rs                            |   9 
crates/copilot/Cargo.toml                                         |   1 
crates/copilot/src/copilot.rs                                     |   3 
crates/copilot/src/sign_in.rs                                     | 211 -
crates/copilot_ui/Cargo.toml                                      |   5 
crates/copilot_ui/src/copilot_button.rs                           |  26 
crates/copilot_ui/src/copilot_ui.rs                               |   5 
crates/copilot_ui/src/sign_in.rs                                  | 183 
crates/diagnostics/src/diagnostics.rs                             |  12 
crates/diagnostics/src/items.rs                                   |  39 
crates/diagnostics/src/project_diagnostics_settings.rs            |   4 
crates/diagnostics/src/toolbar_controls.rs                        |   4 
crates/editor/src/editor.rs                                       |  12 
crates/editor/src/editor_settings.rs                              |  48 
crates/editor/src/element.rs                                      |   4 
crates/editor/src/test.rs                                         |   6 
crates/feedback/src/deploy_feedback_button.rs                     |   4 
crates/feedback/src/feedback_modal.rs                             |  64 
crates/file_finder/src/file_finder.rs                             |   2 
crates/gpui/docs/key_dispatch.md                                  |   4 
crates/gpui/src/action.rs                                         |   1 
crates/gpui/src/app/test_context.rs                               |   5 
crates/gpui/src/elements/img.rs                                   |   8 
crates/gpui/src/elements/overlay.rs                               |   2 
crates/gpui/src/gpui.rs                                           |   2 
crates/gpui/src/image_cache.rs                                    |   6 
crates/gpui/src/key_dispatch.rs                                   |   4 
crates/gpui/src/platform/test/display.rs                          |   2 
crates/gpui/src/platform/test/platform.rs                         |   1 
crates/gpui/src/shared_url.rs                                     |  25 
crates/gpui/tests/action_macros.rs                                |  12 
crates/journal/src/journal.rs                                     |   7 
crates/language/src/language_settings.rs                          |  68 
crates/language/src/syntax_map/syntax_map_tests.rs                |  28 
crates/outline/src/outline.rs                                     |   8 
crates/project/src/project.rs                                     |  26 
crates/project/src/project_settings.rs                            |  26 
crates/project/src/project_tests.rs                               |  69 
crates/project/src/worktree.rs                                    |  32 
crates/project_panel/src/project_panel.rs                         |  23 
crates/project_panel/src/project_panel_settings.rs                |  23 
crates/quick_action_bar/src/quick_action_bar.rs                   |  13 
crates/rpc/src/macros.rs                                          |   4 
crates/rpc/src/proto.rs                                           |   5 
crates/search/src/buffer_search.rs                                |  16 
crates/search/src/project_search.rs                               |  27 
crates/search/src/search.rs                                       |   8 
crates/search/src/search_bar.rs                                   |   2 
crates/semantic_index/src/semantic_index.rs                       |  12 
crates/semantic_index/src/semantic_index_settings.rs              |   5 
crates/storybook/Cargo.toml                                       |   1 
crates/storybook/src/story_selector.rs                            |   4 
crates/terminal/src/terminal.rs                                   |  12 
crates/terminal/src/terminal_settings.rs                          |  72 
crates/terminal_view/src/terminal_element.rs                      |  12 
crates/terminal_view/src/terminal_panel.rs                        |  12 
crates/terminal_view/src/terminal_view.rs                         |   6 
crates/theme/src/theme.rs                                         |   7 
crates/ui/src/components/button/button.rs                         |  10 
crates/ui/src/components/button/button_icon.rs                    |  12 
crates/ui/src/components/button/icon_button.rs                    |  10 
crates/ui/src/components/checkbox.rs                              |   6 
crates/ui/src/components/context_menu.rs                          |  10 
crates/ui/src/components/disclosure.rs                            |   6 
crates/ui/src/components/icon.rs                                  | 202 
crates/ui/src/components/keybinding.rs                            |  44 
crates/ui/src/components/list/list_sub_header.rs                  |  15 
crates/ui/src/components/stories/button.rs                        |   6 
crates/ui/src/components/stories/icon.rs                          |   8 
crates/ui/src/components/stories/icon_button.rs                   |  18 
crates/ui/src/components/stories/list_header.rs                   |  14 
crates/ui/src/components/stories/list_item.rs                     |  26 
crates/ui/src/components/stories/tab.rs                           |   2 
crates/ui/src/components/stories/tab_bar.rs                       |  11 
crates/ui/src/prelude.rs                                          |   3 
crates/ui/src/styles/typography.rs                                |  72 
crates/vim/src/vim.rs                                             |   3 
crates/welcome/src/base_keymap_setting.rs                         |   3 
crates/workspace/src/dock.rs                                      |   9 
crates/workspace/src/item.rs                                      |   6 
crates/workspace/src/notifications.rs                             | 103 
crates/workspace/src/pane.rs                                      |  18 
crates/workspace/src/pane_group.rs                                |  68 
crates/workspace/src/shared_screen.rs                             |   4 
crates/workspace/src/toolbar.rs                                   |  83 
crates/workspace/src/workspace.rs                                 |  79 
crates/workspace/src/workspace_settings.rs                        |  32 
crates/zed/Cargo.toml                                             |   3 
crates/zed/build.rs                                               |   2 
crates/zed/src/main.rs                                            |   5 
crates/zed/src/zed.rs                                             |   8 
script/lib/bump-version.sh                                        |   2 
130 files changed, 1,924 insertions(+), 1,289 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -292,6 +292,15 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
 
+[[package]]
+name = "assets"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "rust-embed",
+]
+
 [[package]]
 name = "assistant"
 version = "0.1.0"
@@ -677,6 +686,7 @@ dependencies = [
  "log",
  "menu",
  "project",
+ "schemars",
  "serde",
  "serde_derive",
  "serde_json",
@@ -1442,7 +1452,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.34.0"
+version = "0.35.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1549,6 +1559,7 @@ dependencies = [
  "serde_json",
  "settings",
  "smallvec",
+ "story",
  "theme",
  "theme_selector",
  "time",
@@ -1687,12 +1698,11 @@ dependencies = [
  "settings",
  "smol",
  "theme",
- "ui",
  "util",
 ]
 
 [[package]]
-name = "copilot_button"
+name = "copilot_ui"
 version = "0.1.0"
 dependencies = [
  "anyhow",
@@ -1705,6 +1715,7 @@ dependencies = [
  "settings",
  "smol",
  "theme",
+ "ui",
  "util",
  "workspace",
  "zed_actions",
@@ -7437,6 +7448,7 @@ dependencies = [
  "backtrace-on-stack-overflow",
  "chrono",
  "clap 4.4.4",
+ "collab_ui",
  "dialoguer",
  "editor",
  "fuzzy",
@@ -9528,6 +9540,7 @@ dependencies = [
  "activity_indicator",
  "ai",
  "anyhow",
+ "assets",
  "assistant",
  "async-compression",
  "async-recursion 0.3.2",
@@ -9546,7 +9559,7 @@ dependencies = [
  "collections",
  "command_palette",
  "copilot",
- "copilot_button",
+ "copilot_ui",
  "ctor",
  "db",
  "diagnostics",

Cargo.toml 🔗

@@ -1,5 +1,6 @@
 [workspace]
 members = [
+    "crates/assets",
     "crates/activity_indicator",
     "crates/ai",
     "crates/assistant",
@@ -16,7 +17,7 @@ members = [
     "crates/collections",
     "crates/command_palette",
     "crates/copilot",
-    "crates/copilot_button",
+    "crates/copilot_ui",
     "crates/db",
     "crates/refineable",
     "crates/refineable/derive_refineable",

assets/icons/arrow_circle.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/settings/default.json 🔗

@@ -76,7 +76,7 @@
   // or waits for a `copilot::Toggle`
   "show_copilot_suggestions": true,
   // Whether to show tabs and spaces in the editor.
-  // This setting can take two values:
+  // This setting can take three values:
   //
   // 1. Draw tabs and spaces only for the selected text (default):
   //    "selection"
@@ -183,7 +183,7 @@
     // Default height when the assistant is docked to the bottom.
     "default_height": 320,
     // The default OpenAI model to use when starting new conversations. This
-    // setting can take two values:
+    // setting can take three values:
     //
     // 1. "gpt-3.5-turbo-0613""
     // 2. "gpt-4-0613""
@@ -351,7 +351,7 @@
     //      }
     "working_directory": "current_project_directory",
     // Set the cursor blinking behavior in the terminal.
-    // May take 4 values:
+    // May take 3 values:
     //  1. Never blink the cursor, ignoring the terminal mode
     //         "blinking": "off",
     //  2. Default the cursor blink to off, but allow the terminal to

crates/assets/Cargo.toml 🔗

@@ -0,0 +1,12 @@
+[package]
+name = "assets"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gpui = {path = "../gpui"}
+rust-embed.workspace = true
+anyhow.workspace = true

crates/zed/src/assets.rs → crates/assets/src/lib.rs 🔗

@@ -1,3 +1,4 @@
+// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
 use anyhow::anyhow;
 
 use gpui::{AssetSource, Result, SharedString};

crates/assistant/src/assistant_panel.rs 🔗

@@ -933,7 +933,7 @@ impl AssistantPanel {
     }
 
     fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("hamburger_button", Icon::Menu)
+        IconButton::new("hamburger_button", IconName::Menu)
             .on_click(cx.listener(|this, _event, cx| {
                 if this.active_editor().is_some() {
                     this.set_active_editor_index(None, cx);
@@ -957,7 +957,7 @@ impl AssistantPanel {
     }
 
     fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("split_button", Icon::Snip)
+        IconButton::new("split_button", IconName::Snip)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(active_editor) = this.active_editor() {
                     active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
@@ -968,7 +968,7 @@ impl AssistantPanel {
     }
 
     fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("assist_button", Icon::MagicWand)
+        IconButton::new("assist_button", IconName::MagicWand)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(active_editor) = this.active_editor() {
                     active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
@@ -979,7 +979,7 @@ impl AssistantPanel {
     }
 
     fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("quote_button", Icon::Quote)
+        IconButton::new("quote_button", IconName::Quote)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(workspace) = this.workspace.upgrade() {
                     cx.window_context().defer(move |cx| {
@@ -994,7 +994,7 @@ impl AssistantPanel {
     }
 
     fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("plus_button", Icon::Plus)
+        IconButton::new("plus_button", IconName::Plus)
             .on_click(cx.listener(|this, _event, cx| {
                 this.new_conversation(cx);
             }))
@@ -1004,12 +1004,12 @@ impl AssistantPanel {
 
     fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let zoomed = self.zoomed;
-        IconButton::new("zoom_button", Icon::Maximize)
+        IconButton::new("zoom_button", IconName::Maximize)
             .on_click(cx.listener(|this, _event, cx| {
                 this.toggle_zoom(&ToggleZoom, cx);
             }))
             .selected(zoomed)
-            .selected_icon(Icon::Minimize)
+            .selected_icon(IconName::Minimize)
             .icon_size(IconSize::Small)
             .tooltip(move |cx| {
                 Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
@@ -1286,8 +1286,8 @@ impl Panel for AssistantPanel {
         }
     }
 
-    fn icon(&self, cx: &WindowContext) -> Option<Icon> {
-        Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button)
+    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
+        Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -2349,7 +2349,7 @@ impl ConversationEditor {
                                             div()
                                                 .id("error")
                                                 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
-                                                .child(IconElement::new(Icon::XCircle)),
+                                                .child(Icon::new(IconName::XCircle)),
                                         )
                                     } else {
                                         None
@@ -2645,7 +2645,7 @@ impl Render for InlineAssistant {
                     .justify_center()
                     .w(measurements.gutter_width)
                     .child(
-                        IconButton::new("include_conversation", Icon::Ai)
+                        IconButton::new("include_conversation", IconName::Ai)
                             .on_click(cx.listener(|this, _, cx| {
                                 this.toggle_include_conversation(&ToggleIncludeConversation, cx)
                             }))
@@ -2660,7 +2660,7 @@ impl Render for InlineAssistant {
                     )
                     .children(if SemanticIndex::enabled(cx) {
                         Some(
-                            IconButton::new("retrieve_context", Icon::MagnifyingGlass)
+                            IconButton::new("retrieve_context", IconName::MagnifyingGlass)
                                 .on_click(cx.listener(|this, _, cx| {
                                     this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
                                 }))
@@ -2682,7 +2682,7 @@ impl Render for InlineAssistant {
                             div()
                                 .id("error")
                                 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
-                                .child(IconElement::new(Icon::XCircle).color(Color::Error)),
+                                .child(Icon::new(IconName::XCircle).color(Color::Error)),
                         )
                     } else {
                         None
@@ -2957,7 +2957,7 @@ impl InlineAssistant {
                 div()
                     .id("error")
                     .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
-                    .child(IconElement::new(Icon::XCircle))
+                    .child(Icon::new(IconName::XCircle))
                     .into_any_element()
             ),
 
@@ -2965,7 +2965,7 @@ impl InlineAssistant {
                 div()
                     .id("error")
                     .tooltip(|cx| Tooltip::text("Not Indexed", cx))
-                    .child(IconElement::new(Icon::XCircle))
+                    .child(Icon::new(IconName::XCircle))
                     .into_any_element()
             ),
 
@@ -2996,7 +2996,7 @@ impl InlineAssistant {
                     div()
                         .id("update")
                         .tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
-                        .child(IconElement::new(Icon::Update).color(Color::Info))
+                        .child(Icon::new(IconName::Update).color(Color::Info))
                         .into_any_element()
                 )
             }
@@ -3005,7 +3005,7 @@ impl InlineAssistant {
                 div()
                     .id("check")
                     .tooltip(|cx| Tooltip::text("Index up to date", cx))
-                    .child(IconElement::new(Icon::Check).color(Color::Success))
+                    .child(Icon::new(IconName::Check).color(Color::Success))
                     .into_any_element()
             ),
         }

crates/assistant/src/assistant_settings.rs 🔗

@@ -57,12 +57,28 @@ pub struct AssistantSettings {
     pub default_open_ai_model: OpenAIModel,
 }
 
+/// Assistant panel settings
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AssistantSettingsContent {
+    /// Whether to show the assistant panel button in the status bar.
+    ///
+    /// Default: true
     pub button: Option<bool>,
+    /// Where to dock the assistant.
+    ///
+    /// Default: right
     pub dock: Option<AssistantDockPosition>,
+    /// Default width in pixels when the assistant is docked to the left or right.
+    ///
+    /// Default: 640
     pub default_width: Option<f32>,
+    /// Default height in pixels when the assistant is docked to the bottom.
+    ///
+    /// Default: 320
     pub default_height: Option<f32>,
+    /// The default OpenAI model to use when starting new conversations.
+    ///
+    /// Default: gpt-4-1106-preview
     pub default_open_ai_model: Option<OpenAIModel>,
 }
 

crates/auto_update/Cargo.toml 🔗

@@ -22,6 +22,7 @@ anyhow.workspace = true
 isahc.workspace = true
 lazy_static.workspace = true
 log.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/auto_update/src/auto_update.rs 🔗

@@ -10,6 +10,7 @@ use gpui::{
 };
 use isahc::AsyncBody;
 
+use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_derive::Serialize;
 use smol::io::AsyncReadExt;
@@ -61,18 +62,27 @@ struct JsonRelease {
 
 struct AutoUpdateSetting(bool);
 
+/// Whether or not to automatically check for updates.
+///
+/// Default: true
+#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)]
+#[serde(transparent)]
+struct AutoUpdateSettingOverride(Option<bool>);
+
 impl Settings for AutoUpdateSetting {
     const KEY: Option<&'static str> = Some("auto_update");
 
-    type FileContent = Option<bool>;
+    type FileContent = AutoUpdateSettingOverride;
 
     fn load(
-        default_value: &Option<bool>,
-        user_values: &[&Option<bool>],
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
         _: &mut AppContext,
     ) -> Result<Self> {
         Ok(Self(
-            Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
+            Self::json_merge(default_value, user_values)?
+                .0
+                .ok_or_else(Self::missing_default)?,
         ))
     }
 }

crates/auto_update/src/update_notification.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
 };
 use menu::Cancel;
 use util::channel::ReleaseChannel;
-use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
+use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
 
 pub struct UpdateNotification {
     version: SemanticVersion,
@@ -30,7 +30,7 @@ impl Render for UpdateNotification {
                     .child(
                         div()
                             .id("cancel")
-                            .child(IconElement::new(Icon::Close))
+                            .child(Icon::new(IconName::Close))
                             .cursor_pointer()
                             .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
                     ),

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -67,7 +67,10 @@ impl Render for Breadcrumbs {
                     })
                     .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
             ),
-            None => element.child(breadcrumbs_stack),
+            None => element
+                // Match the height of the `ButtonLike` in the other arm.
+                .h(rems(22. / 16.))
+                .child(breadcrumbs_stack),
         }
     }
 }

crates/call/src/call_settings.rs 🔗

@@ -9,8 +9,12 @@ pub struct CallSettings {
     pub mute_on_join: bool,
 }
 
+/// Configuration of voice calls in Zed.
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct CallSettingsContent {
+    /// Whether the microphone should be muted when joining a channel or a call.
+    ///
+    /// Default: false
     pub mute_on_join: Option<bool>,
 }
 

crates/client/src/client.rs 🔗

@@ -352,9 +352,16 @@ pub struct TelemetrySettings {
     pub metrics: bool,
 }
 
+/// Control what info is collected by Zed.
 #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct TelemetrySettingsContent {
+    /// Send debug info like crash reports.
+    ///
+    /// Default: true
     pub diagnostics: Option<bool>,
+    /// Send anonymized usage data like what languages you're using Zed with.
+    ///
+    /// Default: true
     pub metrics: Option<bool>,
 }
 

crates/client/src/user.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
 use feature_flags::FeatureFlagAppExt;
 use futures::{channel::mpsc, Future, StreamExt};
-use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
+use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
@@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
 pub struct User {
     pub id: UserId,
     pub github_login: String,
-    pub avatar_uri: SharedString,
+    pub avatar_uri: SharedUrl,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.34.0"
+version = "0.35.0"
 publish = false
 
 [[bin]]

crates/collab/src/db/ids.rs 🔗

@@ -140,6 +140,22 @@ impl ChannelRole {
             Guest | Banned => false,
         }
     }
+
+    pub fn can_edit_projects(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Admin | Member => true,
+            Guest | Banned => false,
+        }
+    }
+
+    pub fn can_read_projects(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Admin | Member | Guest => true,
+            Banned => false,
+        }
+    }
 }
 
 impl From<proto::ChannelRole> for ChannelRole {

crates/collab/src/db/queries/projects.rs 🔗

@@ -777,13 +777,129 @@ impl Database {
         .await
     }
 
-    pub async fn project_collaborators(
+    pub async fn check_user_is_project_host(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<()> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            project_collaborator::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(project_collaborator::Column::ProjectId.eq(project_id))
+                        .add(project_collaborator::Column::IsHost.eq(true))
+                        .add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
+                        .add(
+                            project_collaborator::Column::ConnectionServerId
+                                .eq(connection_id.owner_id),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("failed to read project host"))?;
+
+            Ok(())
+        })
+        .await
+        .map(|guard| guard.into_inner())
+    }
+
+    pub async fn host_for_read_only_project_request(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<ConnectionId> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let current_participant = room_participant::Entity::find()
+                .filter(room_participant::Column::RoomId.eq(room_id))
+                .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such room"))?;
+
+            if !current_participant
+                .role
+                .map_or(false, |role| role.can_read_projects())
+            {
+                Err(anyhow!("not authorized to read projects"))?;
+            }
+
+            let host = project_collaborator::Entity::find()
+                .filter(
+                    project_collaborator::Column::ProjectId
+                        .eq(project_id)
+                        .and(project_collaborator::Column::IsHost.eq(true)),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("failed to read project host"))?;
+
+            Ok(host.connection())
+        })
+        .await
+        .map(|guard| guard.into_inner())
+    }
+
+    pub async fn host_for_mutating_project_request(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<ConnectionId> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let current_participant = room_participant::Entity::find()
+                .filter(room_participant::Column::RoomId.eq(room_id))
+                .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such room"))?;
+
+            if !current_participant
+                .role
+                .map_or(false, |role| role.can_edit_projects())
+            {
+                Err(anyhow!("not authorized to edit projects"))?;
+            }
+
+            let host = project_collaborator::Entity::find()
+                .filter(
+                    project_collaborator::Column::ProjectId
+                        .eq(project_id)
+                        .and(project_collaborator::Column::IsHost.eq(true)),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("failed to read project host"))?;
+
+            Ok(host.connection())
+        })
+        .await
+        .map(|guard| guard.into_inner())
+    }
+
+    pub async fn project_collaborators_for_buffer_update(
         &self,
         project_id: ProjectId,
         connection_id: ConnectionId,
     ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
         let room_id = self.room_id_for_project(project_id).await?;
         self.room_transaction(room_id, |tx| async move {
+            let current_participant = room_participant::Entity::find()
+                .filter(room_participant::Column::RoomId.eq(room_id))
+                .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such room"))?;
+
+            if !current_participant
+                .role
+                .map_or(false, |role| role.can_edit_projects())
+            {
+                Err(anyhow!("not authorized to edit projects"))?;
+            }
+
             let collaborators = project_collaborator::Entity::find()
                 .filter(project_collaborator::Column::ProjectId.eq(project_id))
                 .all(&*tx)

crates/collab/src/db/tests/db_tests.rs 🔗

@@ -455,7 +455,7 @@ async fn test_project_count(db: &Arc<Database>) {
         .unwrap();
 
     let room_id = RoomId::from_proto(
-        db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
+        db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "test")
             .await
             .unwrap()
             .id,
@@ -473,7 +473,7 @@ async fn test_project_count(db: &Arc<Database>) {
         room_id,
         user2.user_id,
         ConnectionId { owner_id, id: 1 },
-        "dev",
+        "test",
     )
     .await
     .unwrap();

crates/collab/src/lib.rs 🔗

@@ -88,7 +88,7 @@ impl std::fmt::Display for Error {
 
 impl std::error::Error for Error {}
 
-#[derive(Default, Deserialize)]
+#[derive(Deserialize)]
 pub struct Config {
     pub http_port: u16,
     pub database_url: String,
@@ -100,7 +100,7 @@ pub struct Config {
     pub live_kit_secret: Option<String>,
     pub rust_log: Option<String>,
     pub log_json: Option<bool>,
-    pub zed_environment: String,
+    pub zed_environment: Arc<str>,
 }
 
 impl Config {

crates/collab/src/rpc.rs 🔗

@@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
         self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
-        RequestMessage, UpdateChannelBufferCollaborators,
+        RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -66,7 +66,6 @@ use time::OffsetDateTime;
 use tokio::sync::{watch, Semaphore};
 use tower::ServiceBuilder;
 use tracing::{info_span, instrument, Instrument};
-use util::channel::RELEASE_CHANNEL_NAME;
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -104,6 +103,7 @@ impl<R: RequestMessage> Response<R> {
 
 #[derive(Clone)]
 struct Session {
+    zed_environment: Arc<str>,
     user_id: UserId,
     connection_id: ConnectionId,
     db: Arc<tokio::sync::Mutex<DbHandle>>,
@@ -216,40 +216,45 @@ impl Server {
             .add_message_handler(update_language_server)
             .add_message_handler(update_diagnostic_summary)
             .add_message_handler(update_worktree_settings)
-            .add_message_handler(refresh_inlay_hints)
-            .add_request_handler(forward_project_request::<proto::GetHover>)
-            .add_request_handler(forward_project_request::<proto::GetDefinition>)
-            .add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
-            .add_request_handler(forward_project_request::<proto::GetReferences>)
-            .add_request_handler(forward_project_request::<proto::SearchProject>)
-            .add_request_handler(forward_project_request::<proto::GetDocumentHighlights>)
-            .add_request_handler(forward_project_request::<proto::GetProjectSymbols>)
-            .add_request_handler(forward_project_request::<proto::OpenBufferForSymbol>)
-            .add_request_handler(forward_project_request::<proto::OpenBufferById>)
-            .add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
-            .add_request_handler(forward_project_request::<proto::GetCompletions>)
-            .add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
-            .add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
-            .add_request_handler(forward_project_request::<proto::GetCodeActions>)
-            .add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
-            .add_request_handler(forward_project_request::<proto::PrepareRename>)
-            .add_request_handler(forward_project_request::<proto::PerformRename>)
-            .add_request_handler(forward_project_request::<proto::ReloadBuffers>)
-            .add_request_handler(forward_project_request::<proto::SynchronizeBuffers>)
-            .add_request_handler(forward_project_request::<proto::FormatBuffers>)
-            .add_request_handler(forward_project_request::<proto::CreateProjectEntry>)
-            .add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
-            .add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
-            .add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
-            .add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
-            .add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
-            .add_request_handler(forward_project_request::<proto::InlayHints>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetHover>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetDefinition>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetTypeDefinition>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
+            .add_request_handler(forward_read_only_project_request::<proto::SearchProject>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
+            .add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
+            .add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
+            .add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
+            .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
+            .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
+            .add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
+            .add_request_handler(
+                forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
+            )
+            .add_request_handler(
+                forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
+            )
+            .add_request_handler(forward_mutating_project_request::<proto::GetCodeActions>)
+            .add_request_handler(forward_mutating_project_request::<proto::ApplyCodeAction>)
+            .add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
+            .add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
+            .add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
+            .add_request_handler(forward_mutating_project_request::<proto::FormatBuffers>)
+            .add_request_handler(forward_mutating_project_request::<proto::CreateProjectEntry>)
+            .add_request_handler(forward_mutating_project_request::<proto::RenameProjectEntry>)
+            .add_request_handler(forward_mutating_project_request::<proto::CopyProjectEntry>)
+            .add_request_handler(forward_mutating_project_request::<proto::DeleteProjectEntry>)
+            .add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
+            .add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
+            .add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
             .add_message_handler(create_buffer_for_peer)
             .add_request_handler(update_buffer)
-            .add_message_handler(update_buffer_file)
-            .add_message_handler(buffer_reloaded)
-            .add_message_handler(buffer_saved)
-            .add_request_handler(forward_project_request::<proto::SaveBuffer>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::UpdateBufferFile>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBase>)
             .add_request_handler(get_users)
             .add_request_handler(fuzzy_search_users)
             .add_request_handler(request_contact)
@@ -281,7 +286,6 @@ impl Server {
             .add_request_handler(follow)
             .add_message_handler(unfollow)
             .add_message_handler(update_followers)
-            .add_message_handler(update_diff_base)
             .add_request_handler(get_private_user_info)
             .add_message_handler(acknowledge_channel_message)
             .add_message_handler(acknowledge_buffer_version);
@@ -609,6 +613,7 @@ impl Server {
                 user_id,
                 connection_id,
                 db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))),
+                zed_environment: this.app_state.config.zed_environment.clone(),
                 peer: this.peer.clone(),
                 connection_pool: this.connection_pool.clone(),
                 live_kit_client: this.app_state.live_kit_client.clone(),
@@ -965,7 +970,7 @@ async fn create_room(
             session.user_id,
             session.connection_id,
             &live_kit_room,
-            RELEASE_CHANNEL_NAME.as_str(),
+            &session.zed_environment,
         )
         .await?;
 
@@ -999,7 +1004,7 @@ async fn join_room(
                 room_id,
                 session.user_id,
                 session.connection_id,
-                RELEASE_CHANNEL_NAME.as_str(),
+                session.zed_environment.as_ref(),
             )
             .await?;
         room_updated(&room.room, &session.peer);
@@ -1693,10 +1698,6 @@ async fn update_worktree_settings(
     Ok(())
 }
 
-async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
-    broadcast_project_message(request.project_id, request, session).await
-}
-
 async fn start_language_server(
     request: proto::StartLanguageServer,
     session: Session,
@@ -1741,7 +1742,7 @@ async fn update_language_server(
     Ok(())
 }
 
-async fn forward_project_request<T>(
+async fn forward_read_only_project_request<T>(
     request: T,
     response: Response<T>,
     session: Session,
@@ -1750,24 +1751,37 @@ where
     T: EntityMessage + RequestMessage,
 {
     let project_id = ProjectId::from_proto(request.remote_entity_id());
-    let host_connection_id = {
-        let collaborators = session
-            .db()
-            .await
-            .project_collaborators(project_id, session.connection_id)
-            .await?;
-        collaborators
-            .iter()
-            .find(|collaborator| collaborator.is_host)
-            .ok_or_else(|| anyhow!("host not found"))?
-            .connection_id
-    };
-
+    let host_connection_id = session
+        .db()
+        .await
+        .host_for_read_only_project_request(project_id, session.connection_id)
+        .await?;
     let payload = session
         .peer
         .forward_request(session.connection_id, host_connection_id, request)
         .await?;
+    response.send(payload)?;
+    Ok(())
+}
 
+async fn forward_mutating_project_request<T>(
+    request: T,
+    response: Response<T>,
+    session: Session,
+) -> Result<()>
+where
+    T: EntityMessage + RequestMessage,
+{
+    let project_id = ProjectId::from_proto(request.remote_entity_id());
+    let host_connection_id = session
+        .db()
+        .await
+        .host_for_mutating_project_request(project_id, session.connection_id)
+        .await?;
+    let payload = session
+        .peer
+        .forward_request(session.connection_id, host_connection_id, request)
+        .await?;
     response.send(payload)?;
     Ok(())
 }
@@ -1776,6 +1790,14 @@ async fn create_buffer_for_peer(
     request: proto::CreateBufferForPeer,
     session: Session,
 ) -> Result<()> {
+    session
+        .db()
+        .await
+        .check_user_is_project_host(
+            ProjectId::from_proto(request.project_id),
+            session.connection_id,
+        )
+        .await?;
     let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
     session
         .peer
@@ -1791,11 +1813,12 @@ async fn update_buffer(
     let project_id = ProjectId::from_proto(request.project_id);
     let mut guest_connection_ids;
     let mut host_connection_id = None;
+
     {
         let collaborators = session
             .db()
             .await
-            .project_collaborators(project_id, session.connection_id)
+            .project_collaborators_for_buffer_update(project_id, session.connection_id)
             .await?;
         guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
         for collaborator in collaborators.iter() {
@@ -1828,60 +1851,17 @@ async fn update_buffer(
     Ok(())
 }
 
-async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
-
-    broadcast(
-        Some(session.connection_id),
-        project_connection_ids.iter().copied(),
-        |connection_id| {
-            session
-                .peer
-                .forward_send(session.connection_id, connection_id, request.clone())
-        },
-    );
-    Ok(())
-}
-
-async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
-    broadcast(
-        Some(session.connection_id),
-        project_connection_ids.iter().copied(),
-        |connection_id| {
-            session
-                .peer
-                .forward_send(session.connection_id, connection_id, request.clone())
-        },
-    );
-    Ok(())
-}
-
-async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
-    broadcast_project_message(request.project_id, request, session).await
-}
-
-async fn broadcast_project_message<T: EnvelopedMessage>(
-    project_id: u64,
+async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
     request: T,
     session: Session,
 ) -> Result<()> {
-    let project_id = ProjectId::from_proto(project_id);
+    let project_id = ProjectId::from_proto(request.remote_entity_id());
     let project_connection_ids = session
         .db()
         .await
         .project_connection_ids(project_id, session.connection_id)
         .await?;
+
     broadcast(
         Some(session.connection_id),
         project_connection_ids.iter().copied(),
@@ -2608,7 +2588,7 @@ async fn join_channel_internal(
                 channel_id,
                 session.user_id,
                 session.connection_id,
-                RELEASE_CHANNEL_NAME.as_str(),
+                session.zed_environment.as_ref(),
             )
             .await?;
 
@@ -3110,25 +3090,6 @@ async fn mark_notification_as_read(
     Ok(())
 }
 
-async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
-    broadcast(
-        Some(session.connection_id),
-        project_connection_ids.iter().copied(),
-        |connection_id| {
-            session
-                .peer
-                .forward_send(session.connection_id, connection_id, request.clone())
-        },
-    );
-    Ok(())
-}
-
 async fn get_private_user_info(
     _request: proto::GetPrivateUserInfo,
     response: Response<proto::GetPrivateUserInfo>,

crates/collab/src/tests/channel_guest_tests.rs 🔗

@@ -82,5 +82,13 @@ async fn test_channel_guests(
         project_b.read_with(cx_b, |project, _| project.remote_id()),
         Some(project_id),
     );
-    assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()))
+    assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
+
+    assert!(project_b
+        .update(cx_b, |project, cx| {
+            let worktree_id = project.worktrees().next().unwrap().read(cx).id();
+            project.create_entry((worktree_id, "b.txt"), false, cx)
+        })
+        .await
+        .is_err())
 }

crates/collab/src/tests/integration_tests.rs 🔗

@@ -4936,10 +4936,10 @@ async fn test_project_symbols(
         .await
         .unwrap();
 
-    buffer_b_2.read_with(cx_b, |buffer, _| {
+    buffer_b_2.read_with(cx_b, |buffer, cx| {
         assert_eq!(
-            buffer.file().unwrap().path().as_ref(),
-            Path::new("../crate-2/two.rs")
+            buffer.file().unwrap().full_path(cx),
+            Path::new("/code/crate-2/two.rs")
         );
     });
 

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

@@ -2,7 +2,7 @@ use crate::{
     db::{tests::TestDb, NewUserParams, UserId},
     executor::Executor,
     rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    AppState,
+    AppState, Config,
 };
 use anyhow::anyhow;
 use call::ActiveCall;
@@ -414,7 +414,19 @@ impl TestServer {
         Arc::new(AppState {
             db: test_db.db().clone(),
             live_kit_client: Some(Arc::new(fake_server.create_api_client())),
-            config: Default::default(),
+            config: Config {
+                http_port: 0,
+                database_url: "".into(),
+                database_max_connections: 0,
+                api_token: "".into(),
+                invite_link_prefix: "".into(),
+                live_kit_server: None,
+                live_kit_key: None,
+                live_kit_secret: None,
+                rust_log: None,
+                log_json: None,
+                zed_environment: "test".into(),
+            },
         })
     }
 }

crates/collab_ui/Cargo.toml 🔗

@@ -9,6 +9,8 @@ path = "src/collab_ui.rs"
 doctest = false
 
 [features]
+default = []
+stories = ["dep:story"]
 test-support = [
     "call/test-support",
     "client/test-support",
@@ -44,6 +46,7 @@ project = { path = "../project" }
 recent_projects = { path = "../recent_projects" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }
+story = { path = "../story", optional = true }
 feature_flags = { path = "../feature_flags"}
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }

crates/collab_ui/src/chat_panel.rs 🔗

@@ -19,9 +19,8 @@ use rich_text::RichText;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::sync::Arc;
-use theme::ActiveTheme as _;
 use time::{OffsetDateTime, UtcOffset};
-use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
+use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -48,7 +47,7 @@ pub struct ChatPanel {
     languages: Arc<LanguageRegistry>,
     message_list: ListState,
     active_chat: Option<(Model<ChannelChat>, Subscription)>,
-    input_editor: View<MessageEditor>,
+    message_editor: View<MessageEditor>,
     local_timezone: UtcOffset,
     fs: Arc<dyn Fs>,
     width: Option<Pixels>,
@@ -120,7 +119,7 @@ impl ChatPanel {
                 message_list,
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
-                input_editor,
+                message_editor: input_editor,
                 local_timezone: cx.local_timezone(),
                 subscriptions: Vec::new(),
                 workspace: workspace_handle,
@@ -209,7 +208,7 @@ impl ChatPanel {
                 self.message_list.reset(chat.message_count());
 
                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-                self.input_editor.update(cx, |editor, cx| {
+                self.message_editor.update(cx, |editor, cx| {
                     editor.set_channel(channel_id, channel_name, cx);
                 });
             };
@@ -282,12 +281,12 @@ impl ChatPanel {
                                 )),
                         )
                         .end_child(
-                            IconButton::new("notes", Icon::File)
+                            IconButton::new("notes", IconName::File)
                                 .on_click(cx.listener(Self::open_notes))
                                 .tooltip(|cx| Tooltip::text("Open notes", cx)),
                         )
                         .end_child(
-                            IconButton::new("call", Icon::AudioOn)
+                            IconButton::new("call", IconName::AudioOn)
                                 .on_click(cx.listener(Self::join_call))
                                 .tooltip(|cx| Tooltip::text("Join call", cx)),
                         ),
@@ -300,13 +299,7 @@ impl ChatPanel {
                     this
                 }
             }))
-            .child(
-                div()
-                    .z_index(1)
-                    .p_2()
-                    .bg(cx.theme().colors().background)
-                    .child(self.input_editor.clone()),
-            )
+            .child(h_stack().p_2().child(self.message_editor.clone()))
             .into_any()
     }
 
@@ -402,7 +395,7 @@ impl ChatPanel {
                     .w_8()
                     .visible_on_hover("")
                     .children(message_id_to_remove.map(|message_id| {
-                        IconButton::new(("remove", message_id), Icon::XCircle).on_click(
+                        IconButton::new(("remove", message_id), IconName::XCircle).on_click(
                             cx.listener(move |this, _, cx| {
                                 this.remove_message(message_id, cx);
                             }),
@@ -428,32 +421,48 @@ impl ChatPanel {
         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
     }
 
-    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-        Button::new("sign-in", "Sign in to use chat")
-            .on_click(cx.listener(move |this, _, cx| {
-                let client = this.client.clone();
-                cx.spawn(|this, mut cx| async move {
-                    if client
-                        .authenticate_and_connect(true, &cx)
-                        .log_err()
-                        .await
-                        .is_some()
-                    {
-                        this.update(&mut cx, |_, cx| {
-                            cx.focus_self();
+    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack()
+            .gap_2()
+            .p_4()
+            .child(
+                Button::new("sign-in", "Sign in")
+                    .style(ButtonStyle::Filled)
+                    .icon_color(Color::Muted)
+                    .icon(IconName::Github)
+                    .icon_position(IconPosition::Start)
+                    .full_width()
+                    .on_click(cx.listener(move |this, _, cx| {
+                        let client = this.client.clone();
+                        cx.spawn(|this, mut cx| async move {
+                            if client
+                                .authenticate_and_connect(true, &cx)
+                                .log_err()
+                                .await
+                                .is_some()
+                            {
+                                this.update(&mut cx, |_, cx| {
+                                    cx.focus_self();
+                                })
+                                .ok();
+                            }
                         })
-                        .ok();
-                    }
-                })
-                .detach();
-            }))
-            .into_any_element()
+                        .detach();
+                    })),
+            )
+            .child(
+                div().flex().w_full().items_center().child(
+                    Label::new("Sign in to chat.")
+                        .color(Color::Muted)
+                        .size(LabelSize::Small),
+                ),
+            )
     }
 
     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = self.active_chat.as_ref() {
             let message = self
-                .input_editor
+                .message_editor
                 .update(cx, |editor, cx| editor.take_message(cx));
 
             if let Some(task) = chat
@@ -550,12 +559,18 @@ impl EventEmitter<Event> for ChatPanel {}
 
 impl Render for ChatPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        div()
-            .full()
-            .child(if self.client.user_id().is_some() {
-                self.render_channel(cx)
-            } else {
-                self.render_sign_in_prompt(cx)
+        v_stack()
+            .size_full()
+            .map(|this| match (self.client.user_id(), self.active_chat()) {
+                (Some(_), Some(_)) => this.child(self.render_channel(cx)),
+                (Some(_), None) => this.child(
+                    div().p_4().child(
+                        Label::new("Select a channel to chat in.")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted),
+                    ),
+                ),
+                (None, _) => this.child(self.render_sign_in_prompt(cx)),
             })
             .min_w(px(150.))
     }
@@ -563,7 +578,7 @@ impl Render for ChatPanel {
 
 impl FocusableView for ChatPanel {
     fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.input_editor.read(cx).focus_handle(cx)
+        self.message_editor.read(cx).focus_handle(cx)
     }
 }
 
@@ -607,12 +622,12 @@ impl Panel for ChatPanel {
         "ChatPanel"
     }
 
-    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
         if !is_channels_feature_enabled(cx) {
             return None;
         }
 
-        Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
+        Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -1,16 +1,19 @@
+use std::{sync::Arc, time::Duration};
+
 use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
 use client::UserId;
 use collections::HashMap;
-use editor::{AnchorRangeExt, Editor};
+use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
 use gpui::{
-    AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
-    ViewContext, WeakView,
+    AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
+    Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
 };
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
-use std::{sync::Arc, time::Duration};
-use workspace::item::ItemHandle;
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::prelude::*;
 
 const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 
@@ -181,7 +184,14 @@ impl MessageEditor {
                 }
 
                 editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
+                editor.highlight_text::<Self>(
+                    anchor_ranges,
+                    HighlightStyle {
+                        font_weight: Some(FontWeight::BOLD),
+                        ..Default::default()
+                    },
+                    cx,
+                )
             });
 
             this.mentions = mentioned_user_ids;
@@ -196,8 +206,39 @@ impl MessageEditor {
 }
 
 impl Render for MessageEditor {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.editor.to_any()
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let settings = ThemeSettings::get_global(cx);
+        let text_style = TextStyle {
+            color: if self.editor.read(cx).read_only(cx) {
+                cx.theme().colors().text_disabled
+            } else {
+                cx.theme().colors().text
+            },
+            font_family: settings.ui_font.family.clone(),
+            font_features: settings.ui_font.features,
+            font_size: rems(0.875).into(),
+            font_weight: FontWeight::NORMAL,
+            font_style: FontStyle::Normal,
+            line_height: relative(1.3).into(),
+            background_color: None,
+            underline: None,
+            white_space: WhiteSpace::Normal,
+        };
+
+        div()
+            .w_full()
+            .px_2()
+            .py_1()
+            .bg(cx.theme().colors().editor_background)
+            .rounded_md()
+            .child(EditorElement::new(
+                &self.editor,
+                EditorStyle {
+                    local_player: cx.theme().players().local(),
+                    text: text_style,
+                    ..Default::default()
+                },
+            ))
     }
 }
 
@@ -205,7 +246,7 @@ impl Render for MessageEditor {
 mod tests {
     use super::*;
     use client::{Client, User, UserStore};
-    use gpui::{Context as _, TestAppContext, VisualContext as _};
+    use gpui::TestAppContext;
     use language::{Language, LanguageConfig};
     use rpc::proto;
     use settings::SettingsStore;

crates/collab_ui/src/collab_panel.rs 🔗

@@ -31,7 +31,7 @@ use smallvec::SmallVec;
 use std::{mem, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{
-    prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label,
+    prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
     ListHeader, ListItem, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
@@ -848,7 +848,7 @@ impl CollabPanel {
             .end_slot(if is_pending {
                 Label::new("Calling").color(Color::Muted).into_any_element()
             } else if is_current_user {
-                IconButton::new("leave-call", Icon::Exit)
+                IconButton::new("leave-call", IconName::Exit)
                     .style(ButtonStyle::Subtle)
                     .on_click(move |_, cx| Self::leave_call(cx))
                     .tooltip(|cx| Tooltip::text("Leave Call", cx))
@@ -896,8 +896,8 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(IconButton::new(0, Icon::Folder)),
+                    .child(render_tree_branch(is_last, false, cx))
+                    .child(IconButton::new(0, IconName::Folder)),
             )
             .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
@@ -917,8 +917,8 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(IconButton::new(0, Icon::Screen)),
+                    .child(render_tree_branch(is_last, false, cx))
+                    .child(IconButton::new(0, IconName::Screen)),
             )
             .child(Label::new("Screen"))
             .when_some(peer_id, |this, _| {
@@ -958,8 +958,8 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(false, cx))
-                    .child(IconButton::new(0, Icon::File)),
+                    .child(render_tree_branch(false, true, cx))
+                    .child(IconButton::new(0, IconName::File)),
             )
             .child(div().h_7().w_full().child(Label::new("notes")))
             .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
@@ -979,8 +979,8 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(false, cx))
-                    .child(IconButton::new(0, Icon::MessageBubbles)),
+                    .child(render_tree_branch(false, false, cx))
+                    .child(IconButton::new(0, IconName::MessageBubbles)),
             )
             .child(Label::new("chat"))
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
@@ -1007,7 +1007,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(!has_visible_participants, cx))
+                    .child(render_tree_branch(!has_visible_participants, false, cx))
                     .child(""),
             )
             .child(Label::new(if count == 1 {
@@ -1724,7 +1724,7 @@ impl CollabPanel {
                     .child(
                         Button::new("sign_in", "Sign in")
                             .icon_color(Color::Muted)
-                            .icon(Icon::Github)
+                            .icon(IconName::Github)
                             .icon_position(IconPosition::Start)
                             .style(ButtonStyle::Filled)
                             .full_width()
@@ -1921,7 +1921,7 @@ impl CollabPanel {
         let button = match section {
             Section::ActiveCall => channel_link.map(|channel_link| {
                 let channel_link_copy = channel_link.clone();
-                IconButton::new("channel-link", Icon::Copy)
+                IconButton::new("channel-link", IconName::Copy)
                     .icon_size(IconSize::Small)
                     .size(ButtonSize::None)
                     .visible_on_hover("section-header")
@@ -1933,13 +1933,13 @@ impl CollabPanel {
                     .into_any_element()
             }),
             Section::Contacts => Some(
-                IconButton::new("add-contact", Icon::Plus)
+                IconButton::new("add-contact", IconName::Plus)
                     .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
                     .tooltip(|cx| Tooltip::text("Search for new contact", cx))
                     .into_any_element(),
             ),
             Section::Channels => Some(
-                IconButton::new("add-channel", Icon::Plus)
+                IconButton::new("add-channel", IconName::Plus)
                     .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
                     .tooltip(|cx| Tooltip::text("Create a channel", cx))
                     .into_any_element(),
@@ -2010,7 +2010,7 @@ impl CollabPanel {
                         })
                         .when(!calling, |el| {
                             el.child(
-                                IconButton::new("remove_contact", Icon::Close)
+                                IconButton::new("remove_contact", IconName::Close)
                                     .icon_color(Color::Muted)
                                     .visible_on_hover("")
                                     .tooltip(|cx| Tooltip::text("Remove Contact", cx))
@@ -2071,13 +2071,13 @@ impl CollabPanel {
 
         let controls = if is_incoming {
             vec![
-                IconButton::new("decline-contact", Icon::Close)
+                IconButton::new("decline-contact", IconName::Close)
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, false, cx);
                     }))
                     .icon_color(color)
                     .tooltip(|cx| Tooltip::text("Decline invite", cx)),
-                IconButton::new("accept-contact", Icon::Check)
+                IconButton::new("accept-contact", IconName::Check)
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, true, cx);
                     }))
@@ -2086,7 +2086,7 @@ impl CollabPanel {
             ]
         } else {
             let github_login = github_login.clone();
-            vec![IconButton::new("remove_contact", Icon::Close)
+            vec![IconButton::new("remove_contact", IconName::Close)
                 .on_click(cx.listener(move |this, _, cx| {
                     this.remove_contact(user_id, &github_login, cx);
                 }))
@@ -2126,13 +2126,13 @@ impl CollabPanel {
         };
 
         let controls = [
-            IconButton::new("reject-invite", Icon::Close)
+            IconButton::new("reject-invite", IconName::Close)
                 .on_click(cx.listener(move |this, _, cx| {
                     this.respond_to_channel_invite(channel_id, false, cx);
                 }))
                 .icon_color(color)
                 .tooltip(|cx| Tooltip::text("Decline invite", cx)),
-            IconButton::new("accept-invite", Icon::Check)
+            IconButton::new("accept-invite", IconName::Check)
                 .on_click(cx.listener(move |this, _, cx| {
                     this.respond_to_channel_invite(channel_id, true, cx);
                 }))
@@ -2150,7 +2150,7 @@ impl CollabPanel {
                     .child(h_stack().children(controls)),
             )
             .start_slot(
-                IconElement::new(Icon::Hash)
+                Icon::new(IconName::Hash)
                     .size(IconSize::Small)
                     .color(Color::Muted),
             )
@@ -2162,7 +2162,7 @@ impl CollabPanel {
         cx: &mut ViewContext<Self>,
     ) -> ListItem {
         ListItem::new("contact-placeholder")
-            .child(IconElement::new(Icon::Plus))
+            .child(Icon::new(IconName::Plus))
             .child(Label::new("Add a Contact"))
             .selected(is_selected)
             .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
@@ -2211,8 +2211,12 @@ impl CollabPanel {
                     .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
                     .take(FACEPILE_LIMIT)
                     .chain(if extra_count > 0 {
-                        // todo!() @nate - this label looks wrong.
-                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
+                        Some(
+                            div()
+                                .ml_1()
+                                .child(Label::new(format!("+{extra_count}")))
+                                .into_any_element(),
+                        )
                     } else {
                         None
                     })
@@ -2242,7 +2246,7 @@ impl CollabPanel {
         };
 
         let messages_button = |cx: &mut ViewContext<Self>| {
-            IconButton::new("channel_chat", Icon::MessageBubbles)
+            IconButton::new("channel_chat", IconName::MessageBubbles)
                 .icon_size(IconSize::Small)
                 .icon_color(if has_messages_notification {
                     Color::Default
@@ -2254,7 +2258,7 @@ impl CollabPanel {
         };
 
         let notes_button = |cx: &mut ViewContext<Self>| {
-            IconButton::new("channel_notes", Icon::File)
+            IconButton::new("channel_notes", IconName::File)
                 .icon_size(IconSize::Small)
                 .icon_color(if has_notes_notification {
                     Color::Default
@@ -2311,9 +2315,13 @@ impl CollabPanel {
                         },
                     ))
                     .start_slot(
-                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
+                        Icon::new(if is_public {
+                            IconName::Public
+                        } else {
+                            IconName::Hash
+                        })
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
                     )
                     .child(
                         h_stack()
@@ -2382,7 +2390,7 @@ impl CollabPanel {
             .indent_level(depth + 1)
             .indent_step_size(px(20.))
             .start_slot(
-                IconElement::new(Icon::Hash)
+                Icon::new(IconName::Hash)
                     .size(IconSize::Small)
                     .color(Color::Muted),
             );
@@ -2394,21 +2402,16 @@ impl CollabPanel {
         {
             item.child(Label::new(pending_name))
         } else {
-            item.child(
-                div()
-                    .w_full()
-                    .py_1() // todo!() @nate this is a px off at the default font size.
-                    .child(self.channel_name_editor.clone()),
-            )
+            item.child(self.channel_name_editor.clone())
         }
     }
 }
 
-fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
+fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
     let rem_size = cx.rem_size();
     let line_height = cx.text_style().line_height_in_pixels(rem_size);
     let width = rem_size * 1.5;
-    let thickness = px(2.);
+    let thickness = px(1.);
     let color = cx.theme().colors().text;
 
     canvas(move |bounds, cx| {
@@ -2422,7 +2425,11 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
                 point(start_x, top),
                 point(
                     start_x + thickness,
-                    if is_last { start_y } else { bounds.bottom() },
+                    if is_last {
+                        start_y
+                    } else {
+                        bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
+                    },
                 ),
             ),
             color,
@@ -2497,10 +2504,10 @@ impl Panel for CollabPanel {
         cx.notify();
     }
 
-    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
+    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
         CollaborationPanelSettings::get_global(cx)
             .button
-            .then(|| ui::Icon::Collab)
+            .then(|| ui::IconName::Collab)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -2643,11 +2650,11 @@ impl Render for DraggedChannelView {
             .p_1()
             .gap_1()
             .child(
-                IconElement::new(
+                Icon::new(
                     if self.channel.visibility == proto::ChannelVisibility::Public {
-                        Icon::Public
+                        IconName::Public
                     } else {
-                        Icon::Hash
+                        IconName::Hash
                     },
                 )
                 .size(IconSize::Small)

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -168,7 +168,7 @@ impl Render for ChannelModal {
                             .w_px()
                             .flex_1()
                             .gap_1()
-                            .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
+                            .child(Icon::new(IconName::Hash).size(IconSize::Medium))
                             .child(Label::new(channel_name)),
                     )
                     .child(
@@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
                                 Some(ChannelRole::Guest) => Some(Label::new("Guest")),
                                 _ => None,
                             })
-                            .child(IconButton::new("ellipsis", Icon::Ellipsis))
+                            .child(IconButton::new("ellipsis", IconName::Ellipsis))
                             .children(
                                 if let (Some((menu, _)), true) = (&self.context_menu, selected) {
                                     Some(

crates/collab_ui/src/collab_panel/contact_finder.rs 🔗

@@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate {
                 .selected(selected)
                 .start_slot(Avatar::new(user.avatar_uri.clone()))
                 .child(Label::new(user.github_login.clone()))
-                .end_slot::<IconElement>(
-                    icon_path.map(|icon_path| IconElement::from_path(icon_path)),
-                ),
+                .end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
         )
     }
 }

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -15,7 +15,7 @@ use std::sync::Arc;
 use theme::{ActiveTheme, PlayerColors};
 use ui::{
     h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconElement, TintColor, Tooltip,
+    IconButton, IconName, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -213,7 +213,7 @@ impl Render for CollabTitlebarItem {
                         .child(
                             div()
                                 .child(
-                                    IconButton::new("leave-call", ui::Icon::Exit)
+                                    IconButton::new("leave-call", ui::IconName::Exit)
                                         .style(ButtonStyle::Subtle)
                                         .tooltip(|cx| Tooltip::text("Leave call", cx))
                                         .icon_size(IconSize::Small)
@@ -230,9 +230,9 @@ impl Render for CollabTitlebarItem {
                                 IconButton::new(
                                     "mute-microphone",
                                     if is_muted {
-                                        ui::Icon::MicMute
+                                        ui::IconName::MicMute
                                     } else {
-                                        ui::Icon::Mic
+                                        ui::IconName::Mic
                                     },
                                 )
                                 .tooltip(move |cx| {
@@ -256,9 +256,9 @@ impl Render for CollabTitlebarItem {
                             IconButton::new(
                                 "mute-sound",
                                 if is_deafened {
-                                    ui::Icon::AudioOff
+                                    ui::IconName::AudioOff
                                 } else {
-                                    ui::Icon::AudioOn
+                                    ui::IconName::AudioOn
                                 },
                             )
                             .style(ButtonStyle::Subtle)
@@ -281,7 +281,7 @@ impl Render for CollabTitlebarItem {
                         )
                         .when(!read_only, |this| {
                             this.child(
-                                IconButton::new("screen-share", ui::Icon::Screen)
+                                IconButton::new("screen-share", ui::IconName::Screen)
                                     .style(ButtonStyle::Subtle)
                                     .icon_size(IconSize::Small)
                                     .selected(is_screen_sharing)
@@ -573,7 +573,7 @@ impl CollabTitlebarItem {
             | client::Status::ReconnectionError { .. } => Some(
                 div()
                     .id("disconnected")
-                    .child(IconElement::new(Icon::Disconnected).size(IconSize::Small))
+                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
                     .tooltip(|cx| Tooltip::text("Disconnected", cx))
                     .into_any_element(),
             ),
@@ -643,7 +643,7 @@ impl CollabTitlebarItem {
                             h_stack()
                                 .gap_0p5()
                                 .child(Avatar::new(user.avatar_uri.clone()))
-                                .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+                                .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
                         )
                         .style(ButtonStyle::Subtle)
                         .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@@ -665,7 +665,7 @@ impl CollabTitlebarItem {
                         .child(
                             h_stack()
                                 .gap_0p5()
-                                .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+                                .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
                         )
                         .style(ButtonStyle::Subtle)
                         .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),

crates/collab_ui/src/notification_panel.rs 🔗

@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::{sync::Arc, time::Duration};
 use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
+use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -553,7 +553,7 @@ impl Render for NotificationPanel {
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
                     .child(Label::new("Notifications"))
-                    .child(IconElement::new(Icon::Envelope)),
+                    .child(Icon::new(IconName::Envelope)),
             )
             .map(|this| {
                 if self.client.user_id().is_none() {
@@ -564,7 +564,7 @@ impl Render for NotificationPanel {
                             .child(
                                 Button::new("sign_in_prompt_button", "Sign in")
                                     .icon_color(Color::Muted)
-                                    .icon(Icon::Github)
+                                    .icon(IconName::Github)
                                     .icon_position(IconPosition::Start)
                                     .style(ButtonStyle::Filled)
                                     .full_width()
@@ -655,10 +655,10 @@ impl Panel for NotificationPanel {
         }
     }
 
-    fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+    fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
         (NotificationPanelSettings::get_global(cx).button
             && self.notification_store.read(cx).notification_count() > 0)
-            .then(|| Icon::Bell)
+            .then(|| IconName::Bell)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -716,7 +716,7 @@ impl Render for NotificationToast {
             .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
             .child(Label::new(self.text.clone()))
             .child(
-                IconButton::new("close", Icon::Close)
+                IconButton::new("close", IconName::Close)
                     .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
             )
             .on_click(cx.listener(|this, _, cx| {

crates/collab_ui/src/notifications.rs 🔗

@@ -1,9 +1,16 @@
+mod collab_notification;
+pub mod incoming_call_notification;
+pub mod project_shared_notification;
+
+#[cfg(feature = "stories")]
+mod stories;
+
 use gpui::AppContext;
 use std::sync::Arc;
 use workspace::AppState;
 
-pub mod incoming_call_notification;
-pub mod project_shared_notification;
+#[cfg(feature = "stories")]
+pub use stories::*;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     incoming_call_notification::init(app_state, cx);

crates/collab_ui/src/notifications/collab_notification.rs 🔗

@@ -0,0 +1,52 @@
+use gpui::{img, prelude::*, AnyElement, SharedUrl};
+use smallvec::SmallVec;
+use ui::prelude::*;
+
+#[derive(IntoElement)]
+pub struct CollabNotification {
+    avatar_uri: SharedUrl,
+    accept_button: Button,
+    dismiss_button: Button,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl CollabNotification {
+    pub fn new(
+        avatar_uri: impl Into<SharedUrl>,
+        accept_button: Button,
+        dismiss_button: Button,
+    ) -> Self {
+        Self {
+            avatar_uri: avatar_uri.into(),
+            accept_button,
+            dismiss_button,
+            children: SmallVec::new(),
+        }
+    }
+}
+
+impl ParentElement for CollabNotification {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for CollabNotification {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_stack()
+            .text_ui()
+            .justify_between()
+            .size_full()
+            .overflow_hidden()
+            .elevation_3(cx)
+            .p_2()
+            .gap_2()
+            .child(img(self.avatar_uri).w_12().h_12().rounded_full())
+            .child(v_stack().overflow_hidden().children(self.children))
+            .child(
+                v_stack()
+                    .child(self.accept_button)
+                    .child(self.dismiss_button),
+            )
+    }
+}

crates/collab_ui/src/notifications/incoming_call_notification.rs 🔗

@@ -1,15 +1,12 @@
 use crate::notification_window_options;
+use crate::notifications::collab_notification::CollabNotification;
 use call::{ActiveCall, IncomingCall};
 use futures::StreamExt;
-use gpui::{
-    img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
-    VisualContext as _, WindowHandle,
-};
+use gpui::{prelude::*, AppContext, WindowHandle};
 use settings::Settings;
 use std::sync::{Arc, Weak};
 use theme::ThemeSettings;
-use ui::prelude::*;
-use ui::{h_stack, v_stack, Button, Label};
+use ui::{prelude::*, Button, Label};
 use util::ResultExt;
 use workspace::AppState;
 
@@ -31,8 +28,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             if let Some(incoming_call) = incoming_call {
                 let unique_screens = cx.update(|cx| cx.displays()).unwrap();
                 let window_size = gpui::Size {
-                    width: px(380.),
-                    height: px(64.),
+                    width: px(400.),
+                    height: px(72.),
                 };
 
                 for screen in unique_screens {
@@ -129,35 +126,22 @@ impl Render for IncomingCallNotification {
 
         cx.set_rem_size(ui_font_size);
 
-        h_stack()
-            .font(ui_font)
-            .text_ui()
-            .justify_between()
-            .size_full()
-            .overflow_hidden()
-            .elevation_3(cx)
-            .p_2()
-            .gap_2()
-            .child(
-                img(self.state.call.calling_user.avatar_uri.clone())
-                    .w_12()
-                    .h_12()
-                    .rounded_full(),
+        div().size_full().font(ui_font).child(
+            CollabNotification::new(
+                self.state.call.calling_user.avatar_uri.clone(),
+                Button::new("accept", "Accept").on_click({
+                    let state = self.state.clone();
+                    move |_, cx| state.respond(true, cx)
+                }),
+                Button::new("decline", "Decline").on_click({
+                    let state = self.state.clone();
+                    move |_, cx| state.respond(false, cx)
+                }),
             )
             .child(v_stack().overflow_hidden().child(Label::new(format!(
                 "{} is sharing a project in Zed",
                 self.state.call.calling_user.github_login
-            ))))
-            .child(
-                v_stack()
-                    .child(Button::new("accept", "Accept").render(cx).on_click({
-                        let state = self.state.clone();
-                        move |_, cx| state.respond(true, cx)
-                    }))
-                    .child(Button::new("decline", "Decline").render(cx).on_click({
-                        let state = self.state.clone();
-                        move |_, cx| state.respond(false, cx)
-                    })),
-            )
+            )))),
+        )
     }
 }

crates/collab_ui/src/notifications/project_shared_notification.rs 🔗

@@ -1,12 +1,13 @@
 use crate::notification_window_options;
+use crate::notifications::collab_notification::CollabNotification;
 use call::{room, ActiveCall};
 use client::User;
 use collections::HashMap;
-use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
+use gpui::{AppContext, Size};
 use settings::Settings;
 use std::sync::{Arc, Weak};
 use theme::ThemeSettings;
-use ui::{h_stack, prelude::*, v_stack, Button, Label};
+use ui::{prelude::*, Button, Label};
 use workspace::AppState;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -130,51 +131,30 @@ impl Render for ProjectSharedNotification {
 
         cx.set_rem_size(ui_font_size);
 
-        h_stack()
-            .font(ui_font)
-            .text_ui()
-            .justify_between()
-            .size_full()
-            .overflow_hidden()
-            .elevation_3(cx)
-            .p_2()
-            .gap_2()
-            .child(
-                img(self.owner.avatar_uri.clone())
-                    .w_12()
-                    .h_12()
-                    .rounded_full(),
-            )
-            .child(
-                v_stack()
-                    .overflow_hidden()
-                    .child(Label::new(self.owner.github_login.clone()))
-                    .child(Label::new(format!(
-                        "is sharing a project in Zed{}",
-                        if self.worktree_root_names.is_empty() {
-                            ""
-                        } else {
-                            ":"
-                        }
-                    )))
-                    .children(if self.worktree_root_names.is_empty() {
-                        None
-                    } else {
-                        Some(Label::new(self.worktree_root_names.join(", ")))
-                    }),
-            )
-            .child(
-                v_stack()
-                    .child(Button::new("open", "Open").on_click(cx.listener(
-                        move |this, _event, cx| {
-                            this.join(cx);
-                        },
-                    )))
-                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
-                        move |this, _event, cx| {
-                            this.dismiss(cx);
-                        },
-                    ))),
+        div().size_full().font(ui_font).child(
+            CollabNotification::new(
+                self.owner.avatar_uri.clone(),
+                Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
+                    this.join(cx);
+                })),
+                Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
+                    this.dismiss(cx);
+                })),
             )
+            .child(Label::new(self.owner.github_login.clone()))
+            .child(Label::new(format!(
+                "is sharing a project in Zed{}",
+                if self.worktree_root_names.is_empty() {
+                    ""
+                } else {
+                    ":"
+                }
+            )))
+            .children(if self.worktree_root_names.is_empty() {
+                None
+            } else {
+                Some(Label::new(self.worktree_root_names.join(", ")))
+            }),
+        )
     }
 }

crates/collab_ui/src/notifications/stories/collab_notification.rs 🔗

@@ -0,0 +1,50 @@
+use gpui::prelude::*;
+use story::{StoryContainer, StoryItem, StorySection};
+use ui::prelude::*;
+
+use crate::notifications::collab_notification::CollabNotification;
+
+pub struct CollabNotificationStory;
+
+impl Render for CollabNotificationStory {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let window_container = |width, height| div().w(px(width)).h(px(height));
+
+        StoryContainer::new(
+            "CollabNotification Story",
+            "crates/collab_ui/src/notifications/stories/collab_notification.rs",
+        )
+        .child(
+            StorySection::new().child(StoryItem::new(
+                "Incoming Call Notification",
+                window_container(400., 72.).child(
+                    CollabNotification::new(
+                        "https://avatars.githubusercontent.com/u/1486634?v=4",
+                        Button::new("accept", "Accept"),
+                        Button::new("decline", "Decline"),
+                    )
+                    .child(
+                        v_stack()
+                            .overflow_hidden()
+                            .child(Label::new("maxdeviant is sharing a project in Zed")),
+                    ),
+                ),
+            )),
+        )
+        .child(
+            StorySection::new().child(StoryItem::new(
+                "Project Shared Notification",
+                window_container(400., 72.).child(
+                    CollabNotification::new(
+                        "https://avatars.githubusercontent.com/u/1714999?v=4",
+                        Button::new("open", "Open"),
+                        Button::new("dismiss", "Dismiss"),
+                    )
+                    .child(Label::new("iamnbutler"))
+                    .child(Label::new("is sharing a project in Zed:"))
+                    .child(Label::new("zed")),
+                ),
+            )),
+        )
+    }
+}

crates/collab_ui/src/panel_settings.rs 🔗

@@ -28,8 +28,17 @@ pub struct NotificationPanelSettings {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct PanelSettingsContent {
+    /// Whether to show the panel button in the status bar.
+    ///
+    /// Default: true
     pub button: Option<bool>,
+    /// Where to dock the panel.
+    ///
+    /// Default: left
     pub dock: Option<DockPosition>,
+    /// Default width of the panel in pixels.
+    ///
+    /// Default: 240
     pub default_width: Option<f32>,
 }
 

crates/copilot/Cargo.toml 🔗

@@ -28,7 +28,6 @@ theme = { path = "../theme" }
 lsp = { path = "../lsp" }
 node_runtime = { path = "../node_runtime"}
 util = { path = "../util" }
-ui = { path = "../ui" }
 async-compression.workspace = true
 async-tar = "0.4.2"
 anyhow.workspace = true

crates/copilot/src/copilot.rs 🔗

@@ -1,6 +1,4 @@
 pub mod request;
-mod sign_in;
-
 use anyhow::{anyhow, Context as _, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
@@ -98,7 +96,6 @@ pub fn init(
     })
     .detach();
 
-    sign_in::init(cx);
     cx.on_action(|_: &SignIn, cx| {
         if let Some(copilot) = Copilot::global(cx) {
             copilot

crates/copilot/src/sign_in.rs 🔗

@@ -1,211 +0,0 @@
-use crate::{request::PromptUserDeviceFlow, Copilot, Status};
-use gpui::{
-    div, size, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement,
-    IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds,
-    WindowHandle, WindowKind, WindowOptions,
-};
-use theme::ActiveTheme;
-use ui::{prelude::*, Button, Icon, IconElement, Label};
-
-const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
-
-pub fn init(cx: &mut AppContext) {
-    if let Some(copilot) = Copilot::global(cx) {
-        let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
-        cx.observe(&copilot, move |copilot, cx| {
-            let status = copilot.read(cx).status();
-
-            match &status {
-                crate::Status::SigningIn { prompt } => {
-                    if let Some(window) = verification_window.as_mut() {
-                        let updated = window
-                            .update(cx, |verification, cx| {
-                                verification.set_status(status.clone(), cx);
-                                cx.activate_window();
-                            })
-                            .is_ok();
-                        if !updated {
-                            verification_window = Some(create_copilot_auth_window(cx, &status));
-                        }
-                    } else if let Some(_prompt) = prompt {
-                        verification_window = Some(create_copilot_auth_window(cx, &status));
-                    }
-                }
-                Status::Authorized | Status::Unauthorized => {
-                    if let Some(window) = verification_window.as_ref() {
-                        window
-                            .update(cx, |verification, cx| {
-                                verification.set_status(status, cx);
-                                cx.activate(true);
-                                cx.activate_window();
-                            })
-                            .ok();
-                    }
-                }
-                _ => {
-                    if let Some(code_verification) = verification_window.take() {
-                        code_verification
-                            .update(cx, |_, cx| cx.remove_window())
-                            .ok();
-                    }
-                }
-            }
-        })
-        .detach();
-    }
-}
-
-fn create_copilot_auth_window(
-    cx: &mut AppContext,
-    status: &Status,
-) -> WindowHandle<CopilotCodeVerification> {
-    let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
-    let window_options = WindowOptions {
-        bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
-        titlebar: None,
-        center: true,
-        focus: true,
-        show: true,
-        kind: WindowKind::PopUp,
-        is_movable: true,
-        display_id: None,
-    };
-    let window = cx.open_window(window_options, |cx| {
-        cx.new_view(|_| CopilotCodeVerification::new(status.clone()))
-    });
-    window
-}
-
-pub struct CopilotCodeVerification {
-    status: Status,
-    connect_clicked: bool,
-}
-
-impl CopilotCodeVerification {
-    pub fn new(status: Status) -> Self {
-        Self {
-            status,
-            connect_clicked: false,
-        }
-    }
-
-    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
-        self.status = status;
-        cx.notify();
-    }
-
-    fn render_device_code(
-        data: &PromptUserDeviceFlow,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let copied = cx
-            .read_from_clipboard()
-            .map(|item| item.text() == &data.user_code)
-            .unwrap_or(false);
-        h_stack()
-            .cursor_pointer()
-            .justify_between()
-            .on_mouse_down(gpui::MouseButton::Left, {
-                let user_code = data.user_code.clone();
-                move |_, cx| {
-                    cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
-                    cx.notify();
-                }
-            })
-            .child(Label::new(data.user_code.clone()))
-            .child(div())
-            .child(Label::new(if copied { "Copied!" } else { "Copy" }))
-    }
-
-    fn render_prompting_modal(
-        connect_clicked: bool,
-        data: &PromptUserDeviceFlow,
-        cx: &mut ViewContext<Self>,
-    ) -> impl Element {
-        let connect_button_label = if connect_clicked {
-            "Waiting for connection..."
-        } else {
-            "Connect to Github"
-        };
-        v_stack()
-            .flex_1()
-            .items_center()
-            .justify_between()
-            .w_full()
-            .child(Label::new(
-                "Enable Copilot by connecting your existing license",
-            ))
-            .child(Self::render_device_code(data, cx))
-            .child(
-                Label::new("Paste this code into GitHub after clicking the button below.")
-                    .size(ui::LabelSize::Small),
-            )
-            .child(
-                Button::new("connect-button", connect_button_label).on_click({
-                    let verification_uri = data.verification_uri.clone();
-                    cx.listener(move |this, _, cx| {
-                        cx.open_url(&verification_uri);
-                        this.connect_clicked = true;
-                    })
-                }),
-            )
-    }
-    fn render_enabled_modal() -> impl Element {
-        v_stack()
-            .child(Label::new("Copilot Enabled!"))
-            .child(Label::new(
-                "You can update your settings or sign out from the Copilot menu in the status bar.",
-            ))
-            .child(
-                Button::new("copilot-enabled-done-button", "Done")
-                    .on_click(|_, cx| cx.remove_window()),
-            )
-    }
-
-    fn render_unauthorized_modal() -> impl Element {
-        v_stack()
-            .child(Label::new(
-                "Enable Copilot by connecting your existing license.",
-            ))
-            .child(
-                Label::new("You must have an active Copilot license to use it in Zed.")
-                    .color(Color::Warning),
-            )
-            .child(
-                Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
-                    cx.remove_window();
-                    cx.open_url(COPILOT_SIGN_UP_URL)
-                }),
-            )
-    }
-}
-
-impl Render for CopilotCodeVerification {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let prompt = match &self.status {
-            Status::SigningIn {
-                prompt: Some(prompt),
-            } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
-            Status::Unauthorized => {
-                self.connect_clicked = false;
-                Self::render_unauthorized_modal().into_any_element()
-            }
-            Status::Authorized => {
-                self.connect_clicked = false;
-                Self::render_enabled_modal().into_any_element()
-            }
-            _ => div().into_any_element(),
-        };
-        div()
-            .id("copilot code verification")
-            .flex()
-            .flex_col()
-            .size_full()
-            .items_center()
-            .p_10()
-            .bg(cx.theme().colors().element_background)
-            .child(ui::Label::new("Connect Copilot to Zed"))
-            .child(IconElement::new(Icon::ZedXCopilot))
-            .child(prompt)
-    }
-}

crates/copilot_button/Cargo.toml → crates/copilot_ui/Cargo.toml 🔗

@@ -1,11 +1,11 @@
 [package]
-name = "copilot_button"
+name = "copilot_ui"
 version = "0.1.0"
 edition = "2021"
 publish = false
 
 [lib]
-path = "src/copilot_button.rs"
+path = "src/copilot_ui.rs"
 doctest = false
 
 [dependencies]
@@ -17,6 +17,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
+ui = { path = "../ui" }
 util = { path = "../util" }
 workspace = {path = "../workspace" }
 anyhow.workspace = true

crates/copilot_button/src/copilot_button.rs → crates/copilot_ui/src/copilot_button.rs 🔗

@@ -1,3 +1,4 @@
+use crate::sign_in::CopilotCodeVerification;
 use anyhow::Result;
 use copilot::{Copilot, SignOut, Status};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
@@ -16,7 +17,9 @@ use util::{paths, ResultExt};
 use workspace::{
     create_and_open_local_file,
     item::ItemHandle,
-    ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
+    ui::{
+        popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
+    },
     StatusItemView, Toast, Workspace,
 };
 use zed_actions::OpenBrowser;
@@ -50,15 +53,15 @@ impl Render for CopilotButton {
             .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
 
         let icon = match status {
-            Status::Error(_) => Icon::CopilotError,
+            Status::Error(_) => IconName::CopilotError,
             Status::Authorized => {
                 if enabled {
-                    Icon::Copilot
+                    IconName::Copilot
                 } else {
-                    Icon::CopilotDisabled
+                    IconName::CopilotDisabled
                 }
             }
-            _ => Icon::CopilotInit,
+            _ => IconName::CopilotInit,
         };
 
         if let Status::Error(e) = status {
@@ -331,7 +334,9 @@ fn initiate_sign_in(cx: &mut WindowContext) {
         return;
     };
     let status = copilot.read(cx).status();
-
+    let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+        return;
+    };
     match status {
         Status::Starting { task } => {
             let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
@@ -370,9 +375,12 @@ fn initiate_sign_in(cx: &mut WindowContext) {
             .detach();
         }
         _ => {
-            copilot
-                .update(cx, |copilot, cx| copilot.sign_in(cx))
-                .detach_and_log_err(cx);
+            copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
+            workspace
+                .update(cx, |this, cx| {
+                    this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
+                })
+                .ok();
         }
     }
 }

crates/copilot_ui/src/sign_in.rs 🔗

@@ -0,0 +1,183 @@
+use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
+use gpui::{
+    div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
+    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
+    Subscription, ViewContext,
+};
+use ui::{prelude::*, Button, IconName, Label};
+use workspace::ModalView;
+
+const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
+
+pub struct CopilotCodeVerification {
+    status: Status,
+    connect_clicked: bool,
+    focus_handle: FocusHandle,
+    _subscription: Subscription,
+}
+
+impl FocusableView for CopilotCodeVerification {
+    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
+impl ModalView for CopilotCodeVerification {}
+
+impl CopilotCodeVerification {
+    pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
+        let status = copilot.read(cx).status();
+        Self {
+            status,
+            connect_clicked: false,
+            focus_handle: cx.focus_handle(),
+            _subscription: cx.observe(copilot, |this, copilot, cx| {
+                let status = copilot.read(cx).status();
+                match status {
+                    Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
+                        this.set_status(status, cx)
+                    }
+                    _ => cx.emit(DismissEvent),
+                }
+            }),
+        }
+    }
+
+    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
+        self.status = status;
+        cx.notify();
+    }
+
+    fn render_device_code(
+        data: &PromptUserDeviceFlow,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let copied = cx
+            .read_from_clipboard()
+            .map(|item| item.text() == &data.user_code)
+            .unwrap_or(false);
+        h_stack()
+            .w_full()
+            .p_1()
+            .border()
+            .border_muted(cx)
+            .rounded_md()
+            .cursor_pointer()
+            .justify_between()
+            .on_mouse_down(gpui::MouseButton::Left, {
+                let user_code = data.user_code.clone();
+                move |_, cx| {
+                    cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
+                    cx.notify();
+                }
+            })
+            .child(div().flex_1().child(Label::new(data.user_code.clone())))
+            .child(div().flex_none().px_1().child(Label::new(if copied {
+                "Copied!"
+            } else {
+                "Copy"
+            })))
+    }
+
+    fn render_prompting_modal(
+        connect_clicked: bool,
+        data: &PromptUserDeviceFlow,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element {
+        let connect_button_label = if connect_clicked {
+            "Waiting for connection..."
+        } else {
+            "Connect to Github"
+        };
+        v_stack()
+            .flex_1()
+            .gap_2()
+            .items_center()
+            .child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large))
+            .child(
+                Label::new("Using Copilot requres an active subscription on Github.")
+                    .color(Color::Muted),
+            )
+            .child(Self::render_device_code(data, cx))
+            .child(
+                Label::new("Paste this code into GitHub after clicking the button below.")
+                    .size(ui::LabelSize::Small),
+            )
+            .child(
+                Button::new("connect-button", connect_button_label)
+                    .on_click({
+                        let verification_uri = data.verification_uri.clone();
+                        cx.listener(move |this, _, cx| {
+                            cx.open_url(&verification_uri);
+                            this.connect_clicked = true;
+                        })
+                    })
+                    .full_width()
+                    .style(ButtonStyle::Filled),
+            )
+    }
+    fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
+        v_stack()
+            .gap_2()
+            .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
+            .child(Label::new(
+                "You can update your settings or sign out from the Copilot menu in the status bar.",
+            ))
+            .child(
+                Button::new("copilot-enabled-done-button", "Done")
+                    .full_width()
+                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
+            )
+    }
+
+    fn render_unauthorized_modal() -> impl Element {
+        v_stack()
+            .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
+
+            .child(Label::new(
+                "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
+            ).color(Color::Warning))
+            .child(
+                Button::new("copilot-subscribe-button", "Subscibe on Github")
+                    .full_width()
+                    .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
+            )
+    }
+}
+
+impl Render for CopilotCodeVerification {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let prompt = match &self.status {
+            Status::SigningIn {
+                prompt: Some(prompt),
+            } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
+            Status::Unauthorized => {
+                self.connect_clicked = false;
+                Self::render_unauthorized_modal().into_any_element()
+            }
+            Status::Authorized => {
+                self.connect_clicked = false;
+                Self::render_enabled_modal(cx).into_any_element()
+            }
+            _ => div().into_any_element(),
+        };
+
+        v_stack()
+            .id("copilot code verification")
+            .elevation_3(cx)
+            .w_96()
+            .items_center()
+            .p_4()
+            .gap_2()
+            .child(
+                svg()
+                    .w_32()
+                    .h_16()
+                    .flex_none()
+                    .path(IconName::ZedXCopilot.path())
+                    .text_color(cx.theme().colors().icon),
+            )
+            .child(prompt)
+    }
+}

crates/diagnostics/src/diagnostics.rs 🔗

@@ -36,7 +36,7 @@ use std::{
 };
 use theme::ActiveTheme;
 pub use toolbar_controls::ToolbarControls;
-use ui::{h_stack, prelude::*, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, Icon, IconName, Label};
 use util::TryFutureExt;
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor {
                     then.child(
                         h_stack()
                             .gap_1()
-                            .child(IconElement::new(Icon::XCircle).color(Color::Error))
+                            .child(Icon::new(IconName::XCircle).color(Color::Error))
                             .child(Label::new(self.summary.error_count.to_string()).color(
                                 if selected {
                                     Color::Default
@@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor {
                     then.child(
                         h_stack()
                             .gap_1()
-                            .child(
-                                IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
-                            )
+                            .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
                             .child(Label::new(self.summary.warning_count.to_string()).color(
                                 if selected {
                                     Color::Default
@@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                                 .flex_none()
                                 .map(|icon| {
                                     if diagnostic.severity == DiagnosticSeverity::ERROR {
-                                        icon.path(Icon::XCircle.path())
+                                        icon.path(IconName::XCircle.path())
                                             .text_color(Color::Error.color(cx))
                                     } else {
-                                        icon.path(Icon::ExclamationTriangle.path())
+                                        icon.path(IconName::ExclamationTriangle.path())
                                             .text_color(Color::Warning.color(cx))
                                     }
                                 }),

crates/diagnostics/src/items.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 };
 use language::Diagnostic;
 use lsp::LanguageServerId;
-use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
+use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 
 use crate::{Deploy, ProjectDiagnosticsEditor};
@@ -24,24 +24,16 @@ impl Render for DiagnosticIndicator {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
             (0, 0) => h_stack().map(|this| {
-                if !self.in_progress_checks.is_empty() {
-                    this.child(
-                        IconElement::new(Icon::ArrowCircle)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                } else {
-                    this.child(
-                        IconElement::new(Icon::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Default),
-                    )
-                }
+                this.child(
+                    Icon::new(IconName::Check)
+                        .size(IconSize::Small)
+                        .color(Color::Default),
+                )
             }),
             (0, warning_count) => h_stack()
                 .gap_1()
                 .child(
-                    IconElement::new(Icon::ExclamationTriangle)
+                    Icon::new(IconName::ExclamationTriangle)
                         .size(IconSize::Small)
                         .color(Color::Warning),
                 )
@@ -49,7 +41,7 @@ impl Render for DiagnosticIndicator {
             (error_count, 0) => h_stack()
                 .gap_1()
                 .child(
-                    IconElement::new(Icon::XCircle)
+                    Icon::new(IconName::XCircle)
                         .size(IconSize::Small)
                         .color(Color::Error),
                 )
@@ -57,13 +49,13 @@ impl Render for DiagnosticIndicator {
             (error_count, warning_count) => h_stack()
                 .gap_1()
                 .child(
-                    IconElement::new(Icon::XCircle)
+                    Icon::new(IconName::XCircle)
                         .size(IconSize::Small)
                         .color(Color::Error),
                 )
                 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
                 .child(
-                    IconElement::new(Icon::ExclamationTriangle)
+                    Icon::new(IconName::ExclamationTriangle)
                         .size(IconSize::Small)
                         .color(Color::Warning),
                 )
@@ -72,9 +64,14 @@ impl Render for DiagnosticIndicator {
 
         let status = if !self.in_progress_checks.is_empty() {
             Some(
-                Label::new("Checking…")
-                    .size(LabelSize::Small)
-                    .color(Color::Muted)
+                h_stack()
+                    .gap_2()
+                    .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
+                    .child(
+                        Label::new("Checking…")
+                            .size(LabelSize::Small)
+                            .into_any_element(),
+                    )
                     .into_any_element(),
             )
         } else if let Some(diagnostic) = &self.current_diagnostic {

crates/diagnostics/src/project_diagnostics_settings.rs 🔗

@@ -6,8 +6,12 @@ pub struct ProjectDiagnosticsSettings {
     pub include_warnings: bool,
 }
 
+/// Diagnostics configuration.
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct ProjectDiagnosticsSettingsContent {
+    /// Whether to show warnings or not by default.
+    ///
+    /// Default: true
     include_warnings: Option<bool>,
 }
 

crates/diagnostics/src/toolbar_controls.rs 🔗

@@ -1,7 +1,7 @@
 use crate::ProjectDiagnosticsEditor;
 use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
 use ui::prelude::*;
-use ui::{Icon, IconButton, Tooltip};
+use ui::{IconButton, IconName, Tooltip};
 use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 
 pub struct ToolbarControls {
@@ -24,7 +24,7 @@ impl Render for ToolbarControls {
         };
 
         div().child(
-            IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
+            IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
                 .tooltip(move |cx| Tooltip::text(tooltip, cx))
                 .on_click(cx.listener(|this, _, cx| {
                     if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {

crates/editor/src/editor.rs 🔗

@@ -99,8 +99,8 @@ use sum_tree::TreeMap;
 use text::{OffsetUtf16, Rope};
 use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
 use ui::{
-    h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover,
-    Tooltip,
+    h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
+    Popover, Tooltip,
 };
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
@@ -4223,7 +4223,7 @@ impl Editor {
     ) -> Option<IconButton> {
         if self.available_code_actions.is_some() {
             Some(
-                IconButton::new("code_actions_indicator", ui::Icon::Bolt)
+                IconButton::new("code_actions_indicator", ui::IconName::Bolt)
                     .icon_size(IconSize::Small)
                     .icon_color(Color::Muted)
                     .selected(is_active)
@@ -4257,7 +4257,7 @@ impl Editor {
                 fold_data
                     .map(|(fold_status, buffer_row, active)| {
                         (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
-                            IconButton::new(ix as usize, ui::Icon::ChevronDown)
+                            IconButton::new(ix as usize, ui::IconName::ChevronDown)
                                 .on_click(cx.listener(move |editor, _e, cx| match fold_status {
                                     FoldStatus::Folded => {
                                         editor.unfold_at(&UnfoldAt { buffer_row }, cx);
@@ -4269,7 +4269,7 @@ impl Editor {
                                 .icon_color(ui::Color::Muted)
                                 .icon_size(ui::IconSize::Small)
                                 .selected(fold_status == FoldStatus::Folded)
-                                .selected_icon(ui::Icon::ChevronRight)
+                                .selected_icon(ui::IconName::ChevronRight)
                                 .size(ui::ButtonSize::None)
                         })
                     })
@@ -9739,7 +9739,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
                 ),
             )
             .child(
-                IconButton::new(("copy-block", cx.block_id), Icon::Copy)
+                IconButton::new(("copy-block", cx.block_id), IconName::Copy)
                     .icon_color(Color::Muted)
                     .size(ButtonSize::Compact)
                     .style(ButtonStyle::Transparent)

crates/editor/src/editor_settings.rs 🔗

@@ -14,11 +14,15 @@ pub struct EditorSettings {
     pub seed_search_query_from_cursor: SeedQuerySetting,
 }
 
+/// When to populate a new search's query based on the text under the cursor.
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum SeedQuerySetting {
+    /// Always populate the search query with the word under the cursor.
     Always,
+    /// Only populate the search query when there is text selected.
     Selection,
+    /// Never populate the search query
     Never,
 }
 
@@ -29,31 +33,75 @@ pub struct Scrollbar {
     pub selections: bool,
 }
 
+/// When to show the scrollbar in the editor.
+///
+/// Default: auto
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowScrollbar {
+    /// Show the scrollbar if there's important information or
+    /// follow the system's configured behavior.
     Auto,
+    /// Match the system's configured behavior.
     System,
+    /// Always show the scrollbar.
     Always,
+    /// Never show the scrollbar.
     Never,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct EditorSettingsContent {
+    /// Whether the cursor blinks in the editor.
+    ///
+    /// Default: true
     pub cursor_blink: Option<bool>,
+    /// Whether to show the informational hover box when moving the mouse
+    /// over symbols in the editor.
+    ///
+    /// Default: true
     pub hover_popover_enabled: Option<bool>,
+    /// Whether to pop the completions menu while typing in an editor without
+    /// explicitly requesting it.
+    ///
+    /// Default: true
     pub show_completions_on_input: Option<bool>,
+    /// Whether to display inline and alongside documentation for items in the
+    /// completions menu.
+    ///
+    /// Default: true
     pub show_completion_documentation: Option<bool>,
+    /// Whether to use additional LSP queries to format (and amend) the code after
+    /// every "trigger" symbol input, defined by LSP server capabilities.
+    ///
+    /// Default: true
     pub use_on_type_format: Option<bool>,
+    /// Scrollbar related settings
     pub scrollbar: Option<ScrollbarContent>,
+    /// Whether the line numbers on editors gutter are relative or not.
+    ///
+    /// Default: false
     pub relative_line_numbers: Option<bool>,
+    /// When to populate a new search's query based on the text under the cursor.
+    ///
+    /// Default: always
     pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
 }
 
+/// Scrollbar related settings
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct ScrollbarContent {
+    /// When to show the scrollbar in the editor.
+    ///
+    /// Default: auto
     pub show: Option<ShowScrollbar>,
+    /// Whether to show git diff indicators in the scrollbar.
+    ///
+    /// Default: true
     pub git_diff: Option<bool>,
+    /// Whether to show buffer search result markers in the scrollbar.
+    ///
+    /// Default: true
     pub selections: Option<bool>,
 }
 

crates/editor/src/element.rs 🔗

@@ -795,7 +795,7 @@ impl EditorElement {
                     cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
-                        gpui::yellow(), // todo!("use the right color")
+                        cx.theme().status().modified,
                         Edges::default(),
                         transparent_black(),
                     ));
@@ -850,7 +850,7 @@ impl EditorElement {
             cx.paint_quad(quad(
                 highlight_bounds,
                 Corners::all(0.05 * line_height),
-                color, // todo!("use the right color")
+                color,
                 Edges::default(),
                 transparent_black(),
             ));

crates/editor/src/test.rs 🔗

@@ -60,8 +60,7 @@ pub fn assert_text_with_selections(
 #[allow(dead_code)]
 #[cfg(any(test, feature = "test-support"))]
 pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
-    // todo!()
-    Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
+    Editor::new(EditorMode::Full, buffer, None, cx)
 }
 
 pub(crate) fn build_editor_with_project(
@@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project(
     buffer: Model<MultiBuffer>,
     cx: &mut ViewContext<Editor>,
 ) -> Editor {
-    // todo!()
-    Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
+    Editor::new(EditorMode::Full, buffer, Some(project), cx)
 }

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{Render, ViewContext, WeakView};
-use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
+use ui::{prelude::*, ButtonCommon, IconButton, IconName, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 use crate::{feedback_modal::FeedbackModal, GiveFeedback};
@@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton {
                 })
             })
             .is_some();
-        IconButton::new("give-feedback", Icon::Envelope)
+        IconButton::new("give-feedback", IconName::Envelope)
             .style(ui::ButtonStyle::Subtle)
             .icon_size(IconSize::Small)
             .selected(is_open)

crates/feedback/src/feedback_modal.rs 🔗

@@ -7,7 +7,7 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
 use futures::AsyncReadExt;
 use gpui::{
-    div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+    div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
     PromptLevel, Render, Task, View, ViewContext,
 };
 use isahc::Request;
@@ -179,14 +179,13 @@ impl FeedbackModal {
             editor
         });
 
-        // Moved here because providing it inline breaks rustfmt
-        let placeholder_text =
-            "You can use markdown to organize your feedback with code and links.";
-
         let feedback_editor = cx.new_view(|cx| {
             let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
-            editor.set_placeholder_text(placeholder_text, cx);
-            // editor.set_show_gutter(false, cx);
+            editor.set_placeholder_text(
+                "You can use markdown to organize your feedback with code and links.",
+                cx,
+            );
+            editor.set_show_gutter(false, cx);
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
@@ -422,10 +421,6 @@ impl Render for FeedbackModal {
         let open_community_repo =
             cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
 
-        // Moved this here because providing it inline breaks rustfmt
-        let provide_an_email_address =
-            "Provide an email address if you want us to be able to reply.";
-
         v_stack()
             .elevation_3(cx)
             .key_context("GiveFeedback")
@@ -434,11 +429,8 @@ impl Render for FeedbackModal {
             .max_w(rems(96.))
             .h(rems(32.))
             .p_4()
-            .gap_4()
-            .child(v_stack().child(
-                // TODO: Add Headline component to `ui2`
-                div().text_xl().child("Share Feedback"),
-            ))
+            .gap_2()
+            .child(Headline::new("Share Feedback"))
             .child(
                 Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
                     format!(
@@ -468,17 +460,26 @@ impl Render for FeedbackModal {
                     .child(self.feedback_editor.clone()),
             )
             .child(
-                h_stack()
-                    .bg(cx.theme().colors().editor_background)
-                    .p_2()
-                    .border()
-                    .rounded_md()
-                    .border_color(if self.valid_email_address() {
-                        cx.theme().colors().border
-                    } else {
-                        red()
-                    })
-                    .child(self.email_address_editor.clone()),
+                v_stack()
+                    .gap_1()
+                    .child(
+                        h_stack()
+                            .bg(cx.theme().colors().editor_background)
+                            .p_2()
+                            .border()
+                            .rounded_md()
+                            .border_color(if self.valid_email_address() {
+                                cx.theme().colors().border
+                            } else {
+                                cx.theme().status().error_border
+                            })
+                            .child(self.email_address_editor.clone()),
+                    )
+                    .child(
+                        Label::new("Provide an email address if you want us to be able to reply.")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted),
+                    ),
             )
             .child(
                 h_stack()
@@ -487,7 +488,7 @@ impl Render for FeedbackModal {
                     .child(
                         Button::new("community_repository", "Community Repository")
                             .style(ButtonStyle::Transparent)
-                            .icon(Icon::ExternalLink)
+                            .icon(IconName::ExternalLink)
                             .icon_position(IconPosition::End)
                             .icon_size(IconSize::Small)
                             .on_click(open_community_repo),
@@ -515,12 +516,7 @@ impl Render for FeedbackModal {
                                         this.submit(cx).detach();
                                     }))
                                     .tooltip(move |cx| {
-                                        Tooltip::with_meta(
-                                            "Submit feedback to the Zed team.",
-                                            None,
-                                            provide_an_email_address,
-                                            cx,
-                                        )
+                                        Tooltip::text("Submit feedback to the Zed team.", cx)
                                     })
                                     .when(!self.can_submit(), |this| this.disabled(true)),
                             ),

crates/file_finder/src/file_finder.rs 🔗

@@ -1297,7 +1297,7 @@ mod tests {
         // so that one should be sorted earlier
         let b_path = ProjectPath {
             worktree_id,
-            path: Arc::from(Path::new("/root/dir2/b.txt")),
+            path: Arc::from(Path::new("dir2/b.txt")),
         };
         workspace
             .update(cx, |workspace, cx| {

crates/gpui/docs/key_dispatch.md 🔗

@@ -50,7 +50,7 @@ impl Render for Menu {
             .on_action(|this, move: &MoveDown, cx| {
                 // ...
             })
-            .children(todo!())
+            .children(unimplemented!())
     }
 }
 ```
@@ -68,7 +68,7 @@ impl Render for Menu {
             .on_action(|this, move: &MoveDown, cx| {
                 // ...
             })
-            .children(todo!())
+            .children(unimplemented!())
     }
 }
 ```

crates/gpui/src/action.rs 🔗

@@ -203,7 +203,6 @@ macro_rules! __impl_action {
                 )
             }
 
-            // todo!() why is this needed in addition to name?
             fn debug_name() -> &'static str
             where
                 Self: ::std::marker::Sized

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

@@ -467,12 +467,11 @@ impl<V> View<V> {
                         }
                     }
 
-                    // todo!(start_waiting)
-                    // cx.borrow().foreground_executor().start_waiting();
+                    cx.borrow().background_executor().start_waiting();
                     rx.recv()
                         .await
                         .expect("view dropped with pending condition");
-                    // cx.borrow().foreground_executor().finish_waiting();
+                    cx.borrow().background_executor().finish_waiting();
                 }
             })
             .await

crates/gpui/src/elements/img.rs 🔗

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::{
     point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
-    InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
+    InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
     StyleRefinement, Styled, WindowContext,
 };
 use futures::FutureExt;
@@ -12,13 +12,13 @@ use util::ResultExt;
 #[derive(Clone, Debug)]
 pub enum ImageSource {
     /// Image content will be loaded from provided URI at render time.
-    Uri(SharedString),
+    Uri(SharedUrl),
     Data(Arc<ImageData>),
     Surface(CVImageBuffer),
 }
 
-impl From<SharedString> for ImageSource {
-    fn from(value: SharedString) -> Self {
+impl From<SharedUrl> for ImageSource {
+    fn from(value: SharedUrl) -> Self {
         Self::Uri(value)
     }
 }

crates/gpui/src/elements/overlay.rs 🔗

@@ -14,8 +14,8 @@ pub struct Overlay {
     children: SmallVec<[AnyElement; 2]>,
     anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
-    // todo!();
     anchor_position: Option<Point<Pixels>>,
+    // todo!();
     // position_mode: OverlayPositionMode,
 }
 

crates/gpui/src/gpui.rs 🔗

@@ -18,6 +18,7 @@ mod platform;
 pub mod prelude;
 mod scene;
 mod shared_string;
+mod shared_url;
 mod style;
 mod styled;
 mod subscription;
@@ -67,6 +68,7 @@ pub use refineable::*;
 pub use scene::*;
 use seal::Sealed;
 pub use shared_string::*;
+pub use shared_url::*;
 pub use smol::Timer;
 pub use style::*;
 pub use styled::*;

crates/gpui/src/image_cache.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ImageData, ImageId, SharedString};
+use crate::{ImageData, ImageId, SharedUrl};
 use collections::HashMap;
 use futures::{
     future::{BoxFuture, Shared},
@@ -44,7 +44,7 @@ impl From<ImageError> for Error {
 
 pub struct ImageCache {
     client: Arc<dyn HttpClient>,
-    images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
+    images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
 }
 
 type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
@@ -59,7 +59,7 @@ impl ImageCache {
 
     pub fn get(
         &self,
-        uri: impl Into<SharedString>,
+        uri: impl Into<SharedUrl>,
     ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
         let uri = uri.into();
         let mut images = self.images.lock();

crates/gpui/src/key_dispatch.rs 🔗

@@ -192,8 +192,8 @@ impl DispatchTree {
         keymap
             .bindings_for_action(action)
             .filter(|binding| {
-                for i in 1..context_stack.len() {
-                    let context = &context_stack[0..i];
+                for i in 0..context_stack.len() {
+                    let context = &context_stack[0..=i];
                     if keymap.binding_enabled(binding, context) {
                         return true;
                     }

crates/gpui/src/platform/test/display.rs 🔗

@@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
     }
 
     fn as_any(&self) -> &dyn std::any::Any {
-        todo!()
+        unimplemented!()
     }
 
     fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {

crates/gpui/src/platform/test/platform.rs 🔗

@@ -103,7 +103,6 @@ impl TestPlatform {
     }
 }
 
-// todo!("implement out what our tests needed in GPUI 1")
 impl Platform for TestPlatform {
     fn background_executor(&self) -> BackgroundExecutor {
         self.background_executor.clone()

crates/gpui/src/shared_url.rs 🔗

@@ -0,0 +1,25 @@
+use derive_more::{Deref, DerefMut};
+
+use crate::SharedString;
+
+/// A [`SharedString`] containing a URL.
+#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)]
+pub struct SharedUrl(SharedString);
+
+impl std::fmt::Debug for SharedUrl {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::fmt::Display for SharedUrl {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_ref())
+    }
+}
+
+impl<T: Into<SharedString>> From<T> for SharedUrl {
+    fn from(value: T) -> Self {
+        Self(value.into())
+    }
+}

crates/gpui/tests/action_macros.rs 🔗

@@ -18,33 +18,33 @@ fn test_action_macros() {
 
     impl gpui::Action for RegisterableAction {
         fn boxed_clone(&self) -> Box<dyn gpui::Action> {
-            todo!()
+            unimplemented!()
         }
 
         fn as_any(&self) -> &dyn std::any::Any {
-            todo!()
+            unimplemented!()
         }
 
         fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
-            todo!()
+            unimplemented!()
         }
 
         fn name(&self) -> &str {
-            todo!()
+            unimplemented!()
         }
 
         fn debug_name() -> &'static str
         where
             Self: Sized,
         {
-            todo!()
+            unimplemented!()
         }
 
         fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
         where
             Self: Sized,
         {
-            todo!()
+            unimplemented!()
         }
     }
 }

crates/journal/src/journal.rs 🔗

@@ -15,9 +15,16 @@ use workspace::{AppState, OpenVisible, Workspace};
 
 actions!(journal, [NewJournalEntry]);
 
+/// Settings specific to journaling
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 pub struct JournalSettings {
+    /// The path of the directory where journal entries are stored.
+    ///
+    /// Default: `~`
     pub path: Option<String>,
+    /// What format to display the hours in.
+    ///
+    /// Default: hour12
     pub hour_format: Option<HourFormat>,
 }
 

crates/language/src/language_settings.rs 🔗

@@ -79,36 +79,90 @@ pub struct AllLanguageSettingsContent {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct LanguageSettingsContent {
+    /// How many columns a tab should occupy.
+    ///
+    /// Default: 4
     #[serde(default)]
     pub tab_size: Option<NonZeroU32>,
+    /// Whether to indent lines using tab characters, as opposed to multiple
+    /// spaces.
+    ///
+    /// Default: false
     #[serde(default)]
     pub hard_tabs: Option<bool>,
+    /// How to soft-wrap long lines of text.
+    ///
+    /// Default: none
     #[serde(default)]
     pub soft_wrap: Option<SoftWrap>,
+    /// The column at which to soft-wrap lines, for buffers where soft-wrap
+    /// is enabled.
+    ///
+    /// Default: 80
     #[serde(default)]
     pub preferred_line_length: Option<u32>,
+    /// Whether to show wrap guides in the editor. Setting this to true will
+    /// show a guide at the 'preferred_line_length' value if softwrap is set to
+    /// 'preferred_line_length', and will show any additional guides as specified
+    /// by the 'wrap_guides' setting.
+    ///
+    /// Default: true
     #[serde(default)]
     pub show_wrap_guides: Option<bool>,
+    /// Character counts at which to show wrap guides in the editor.
+    ///
+    /// Default: []
     #[serde(default)]
     pub wrap_guides: Option<Vec<usize>>,
+    /// Whether or not to perform a buffer format before saving.
+    ///
+    /// Default: on
     #[serde(default)]
     pub format_on_save: Option<FormatOnSave>,
+    /// Whether or not to remove any trailing whitespace from lines of a buffer
+    /// before saving it.
+    ///
+    /// Default: true
     #[serde(default)]
     pub remove_trailing_whitespace_on_save: Option<bool>,
+    /// Whether or not to ensure there's a single newline at the end of a buffer
+    /// when saving it.
+    ///
+    /// Default: true
     #[serde(default)]
     pub ensure_final_newline_on_save: Option<bool>,
+    /// How to perform a buffer format.
+    ///
+    /// Default: auto
     #[serde(default)]
     pub formatter: Option<Formatter>,
+    /// Zed's Prettier integration settings.
+    /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
+    /// project has no other Prettier installed.
+    ///
+    /// Default: {}
     #[serde(default)]
     pub prettier: Option<HashMap<String, serde_json::Value>>,
+    /// Whether to use language servers to provide code intelligence.
+    ///
+    /// Default: true
     #[serde(default)]
     pub enable_language_server: Option<bool>,
+    /// Controls whether copilot provides suggestion immediately (true)
+    /// or waits for a `copilot::Toggle` (false).
+    ///
+    /// Default: true
     #[serde(default)]
     pub show_copilot_suggestions: Option<bool>,
+    /// Whether to show tabs and spaces in the editor.
     #[serde(default)]
     pub show_whitespaces: Option<ShowWhitespaceSetting>,
+    /// Whether to start a new line with a comment when a previous line is a comment as well.
+    ///
+    /// Default: true
     #[serde(default)]
     pub extend_comment_on_newline: Option<bool>,
+    /// Inlay hint related settings.
     #[serde(default)]
     pub inlay_hints: Option<InlayHintSettings>,
 }
@@ -128,8 +182,11 @@ pub struct FeaturesContent {
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum SoftWrap {
+    /// Do not soft wrap.
     None,
+    /// Soft wrap lines that overflow the editor
     EditorWidth,
+    /// Soft wrap lines at the preferred line length
     PreferredLineLength,
 }
 
@@ -148,18 +205,26 @@ pub enum FormatOnSave {
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowWhitespaceSetting {
+    /// Draw tabs and spaces only for the selected text.
     Selection,
+    /// Do not draw any tabs or spaces
     None,
+    /// Draw all invisible symbols
     All,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum Formatter {
+    /// Format files using Zed's Prettier integration (if applicable),
+    /// or falling back to formatting via language server.
     #[default]
     Auto,
+    /// Format code using the current language server.
     LanguageServer,
+    /// Format code using Zed's Prettier integration.
     Prettier,
+    /// Format code using an external command.
     External {
         command: Arc<str>,
         arguments: Arc<[String]>,
@@ -168,6 +233,9 @@ pub enum Formatter {
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct InlayHintSettings {
+    /// Global switch to toggle hints on and off.
+    ///
+    /// Default: false
     #[serde(default)]
     pub enabled: bool,
     #[serde(default = "default_true")]

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() {
     let (buffer, syntax_map) = test_edit_sequence(
         "Rust",
         &[
-            "fn a() { dbg }",
-            "fn a() { dbg«!» }",
-            "fn a() { dbg!«()» }",
-            "fn a() { dbg!(«b») }",
-            "fn a() { dbg!(b«.») }",
-            "fn a() { dbg!(b.«c») }",
-            "fn a() { dbg!(b.c«()») }",
-            "fn a() { dbg!(b.c(«vec»)) }",
-            "fn a() { dbg!(b.c(vec«!»)) }",
-            "fn a() { dbg!(b.c(vec!«[]»)) }",
-            "fn a() { dbg!(b.c(vec![«d»])) }",
-            "fn a() { dbg!(b.c(vec![d«.»])) }",
-            "fn a() { dbg!(b.c(vec![d.«e»])) }",
+            "fn a() { test_macro }",
+            "fn a() { test_macro«!» }",
+            "fn a() { test_macro!«()» }",
+            "fn a() { test_macro!(«b») }",
+            "fn a() { test_macro!(b«.») }",
+            "fn a() { test_macro!(b.«c») }",
+            "fn a() { test_macro!(b.c«()») }",
+            "fn a() { test_macro!(b.c(«vec»)) }",
+            "fn a() { test_macro!(b.c(vec«!»)) }",
+            "fn a() { test_macro!(b.c(vec!«[]»)) }",
+            "fn a() { test_macro!(b.c(vec![«d»])) }",
+            "fn a() { test_macro!(b.c(vec![d«.»])) }",
+            "fn a() { test_macro!(b.c(vec![d.«e»])) }",
         ],
     );
 
@@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() {
         &syntax_map,
         &buffer,
         &["field"],
-        "fn a() { dbg!(b.«c»(vec![d.«e»])) }",
+        "fn a() { test_macro!(b.«c»(vec![d.«e»])) }",
     );
 }
 

crates/outline/src/outline.rs 🔗

@@ -54,7 +54,13 @@ impl FocusableView for OutlineView {
 }
 
 impl EventEmitter<DismissEvent> for OutlineView {}
-impl ModalView for OutlineView {}
+impl ModalView for OutlineView {
+    fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.picker
+            .update(cx, |picker, cx| picker.delegate.restore_active_editor(cx));
+        true
+    }
+}
 
 impl Render for OutlineView {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {

crates/project/src/project.rs 🔗

@@ -4732,7 +4732,8 @@ impl Project {
             } else {
                 return Task::ready(Err(anyhow!("worktree not found for symbol")));
             };
-            let symbol_abs_path = worktree_abs_path.join(&symbol.path.path);
+
+            let symbol_abs_path = resolve_path(worktree_abs_path, &symbol.path.path);
             let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
                 uri
             } else {
@@ -6581,7 +6582,14 @@ impl Project {
                 let removed = *change == PathChange::Removed;
                 let abs_path = worktree.absolutize(path);
                 settings_contents.push(async move {
-                    (settings_dir, (!removed).then_some(fs.load(&abs_path).await))
+                    (
+                        settings_dir,
+                        if removed {
+                            None
+                        } else {
+                            Some(async move { fs.load(&abs_path?).await }.await)
+                        },
+                    )
                 });
             }
         }
@@ -8718,6 +8726,20 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
     components.iter().map(|c| c.as_os_str()).collect()
 }
 
+fn resolve_path(base: &Path, path: &Path) -> PathBuf {
+    let mut result = base.to_path_buf();
+    for component in path.components() {
+        match component {
+            Component::ParentDir => {
+                result.pop();
+            }
+            Component::CurDir => (),
+            _ => result.push(component),
+        }
+    }
+    result
+}
+
 impl Item for Buffer {
     fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
         File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))

crates/project/src/project_settings.rs 🔗

@@ -7,16 +7,40 @@ use std::sync::Arc;
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ProjectSettings {
+    /// Configuration for language servers.
+    ///
+    /// The following settings can be overriden for specific language servers:
+    /// - initialization_options
+    /// To override settings for a language, add an entry for that language server's
+    /// name to the lsp value.
+    /// Default: null
     #[serde(default)]
     pub lsp: HashMap<Arc<str>, LspSettings>,
+
+    /// Configuration for Git-related features
     #[serde(default)]
     pub git: GitSettings,
+    /// Completely ignore files matching globs from `file_scan_exclusions`
+    ///
+    /// Default: [
+    ///   "**/.git",
+    ///   "**/.svn",
+    ///   "**/.hg",
+    ///   "**/CVS",
+    ///   "**/.DS_Store",
+    ///   "**/Thumbs.db",
+    ///   "**/.classpath",
+    ///   "**/.settings"
+    /// ]
     #[serde(default)]
     pub file_scan_exclusions: Option<Vec<String>>,
 }
 
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct GitSettings {
+    /// Whether or not to show the git gutter.
+    ///
+    /// Default: tracked_files
     pub git_gutter: Option<GitGutterSetting>,
     pub gutter_debounce: Option<u64>,
 }
@@ -24,8 +48,10 @@ pub struct GitSettings {
 #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum GitGutterSetting {
+    /// Show git gutter in tracked files.
     #[default]
     TrackedFiles,
+    /// Hide git gutter
     Hide,
 }
 

crates/project/src/project_tests.rs 🔗

@@ -4278,6 +4278,75 @@ fn test_glob_literal_prefix() {
     assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
 }
 
+#[gpui::test]
+async fn test_create_entry(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/one/two",
+        json!({
+            "three": {
+                "a.txt": "",
+                "four": {}
+            },
+            "c.rs": ""
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
+    project
+        .update(cx, |project, cx| {
+            let id = project.worktrees().next().unwrap().read(cx).id();
+            project.create_entry((id, "b.."), true, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+
+    // Can't create paths outside the project
+    let result = project
+        .update(cx, |project, cx| {
+            let id = project.worktrees().next().unwrap().read(cx).id();
+            project.create_entry((id, "../../boop"), true, cx)
+        })
+        .await;
+    assert!(result.is_err());
+
+    // Can't create paths with '..'
+    let result = project
+        .update(cx, |project, cx| {
+            let id = project.worktrees().next().unwrap().read(cx).id();
+            project.create_entry((id, "four/../beep"), true, cx)
+        })
+        .await;
+    assert!(result.is_err());
+
+    assert_eq!(
+        fs.paths(true),
+        vec![
+            PathBuf::from("/"),
+            PathBuf::from("/one"),
+            PathBuf::from("/one/two"),
+            PathBuf::from("/one/two/c.rs"),
+            PathBuf::from("/one/two/three"),
+            PathBuf::from("/one/two/three/a.txt"),
+            PathBuf::from("/one/two/three/b.."),
+            PathBuf::from("/one/two/three/four"),
+        ]
+    );
+
+    // And we cannot open buffers with '..'
+    let result = project
+        .update(cx, |project, cx| {
+            let id = project.worktrees().next().unwrap().read(cx).id();
+            project.open_buffer((id, "../c.rs"), cx)
+        })
+        .await;
+    assert!(result.is_err())
+}
+
 async fn search(
     project: &Model<Project>,
     query: SearchQuery,

crates/project/src/worktree.rs 🔗

@@ -965,6 +965,7 @@ impl LocalWorktree {
         let entry = self.refresh_entry(path.clone(), None, cx);
 
         cx.spawn(|this, mut cx| async move {
+            let abs_path = abs_path?;
             let text = fs.load(&abs_path).await?;
             let mut index_task = None;
             let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
@@ -1050,6 +1051,7 @@ impl LocalWorktree {
 
         cx.spawn(move |this, mut cx| async move {
             let entry = save.await?;
+            let abs_path = abs_path?;
             let this = this.upgrade().context("worktree dropped")?;
 
             let (entry_id, mtime, path) = match entry {
@@ -1139,9 +1141,9 @@ impl LocalWorktree {
         let fs = self.fs.clone();
         let write = cx.background_executor().spawn(async move {
             if is_dir {
-                fs.create_dir(&abs_path).await
+                fs.create_dir(&abs_path?).await
             } else {
-                fs.save(&abs_path, &Default::default(), Default::default())
+                fs.save(&abs_path?, &Default::default(), Default::default())
                     .await
             }
         });
@@ -1188,7 +1190,7 @@ impl LocalWorktree {
         let fs = self.fs.clone();
         let write = cx
             .background_executor()
-            .spawn(async move { fs.save(&abs_path, &text, line_ending).await });
+            .spawn(async move { fs.save(&abs_path?, &text, line_ending).await });
 
         cx.spawn(|this, mut cx| async move {
             write.await?;
@@ -1210,10 +1212,10 @@ impl LocalWorktree {
 
         let delete = cx.background_executor().spawn(async move {
             if entry.is_file() {
-                fs.remove_file(&abs_path, Default::default()).await?;
+                fs.remove_file(&abs_path?, Default::default()).await?;
             } else {
                 fs.remove_dir(
-                    &abs_path,
+                    &abs_path?,
                     RemoveOptions {
                         recursive: true,
                         ignore_if_not_exists: false,
@@ -1252,7 +1254,7 @@ impl LocalWorktree {
         let abs_new_path = self.absolutize(&new_path);
         let fs = self.fs.clone();
         let rename = cx.background_executor().spawn(async move {
-            fs.rename(&abs_old_path, &abs_new_path, Default::default())
+            fs.rename(&abs_old_path?, &abs_new_path?, Default::default())
                 .await
         });
 
@@ -1284,8 +1286,8 @@ impl LocalWorktree {
         let copy = cx.background_executor().spawn(async move {
             copy_recursive(
                 fs.as_ref(),
-                &abs_old_path,
-                &abs_new_path,
+                &abs_old_path?,
+                &abs_new_path?,
                 Default::default(),
             )
             .await
@@ -1609,11 +1611,17 @@ impl Snapshot {
         &self.abs_path
     }
 
-    pub fn absolutize(&self, path: &Path) -> PathBuf {
+    pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
+        if path
+            .components()
+            .any(|component| !matches!(component, std::path::Component::Normal(_)))
+        {
+            return Err(anyhow!("invalid path"));
+        }
         if path.file_name().is_some() {
-            self.abs_path.join(path)
+            Ok(self.abs_path.join(path))
         } else {
-            self.abs_path.to_path_buf()
+            Ok(self.abs_path.to_path_buf())
         }
     }
 
@@ -2823,7 +2831,7 @@ impl language::LocalFile for File {
         let abs_path = worktree.absolutize(&self.path);
         let fs = worktree.fs.clone();
         cx.background_executor()
-            .spawn(async move { fs.load(&abs_path).await })
+            .spawn(async move { fs.load(&abs_path?).await })
     }
 
     fn buffer_reloaded(

crates/project_panel/src/project_panel.rs 🔗

@@ -30,7 +30,7 @@ use std::{
     sync::Arc,
 };
 use theme::ThemeSettings;
-use ui::{prelude::*, v_stack, ContextMenu, IconElement, KeyBinding, Label, ListItem};
+use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -1403,7 +1403,7 @@ impl ProjectPanel {
                     .indent_step_size(px(settings.indent_size))
                     .selected(is_selected)
                     .child(if let Some(icon) = &icon {
-                        div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
+                        div().child(Icon::from_path(icon.to_string()).color(Color::Muted))
                     } else {
                         div().size(IconSize::default().rems()).invisible()
                     })
@@ -1433,6 +1433,9 @@ impl ProjectPanel {
                     }))
                     .on_secondary_mouse_down(cx.listener(
                         move |this, event: &MouseDownEvent, cx| {
+                            // Stop propagation to prevent the catch-all context menu for the project
+                            // panel from being deployed.
+                            cx.stop_propagation();
                             this.deploy_context_menu(event.position, entry_id, cx);
                         },
                     )),
@@ -1515,6 +1518,16 @@ impl Render for ProjectPanel {
                     el.on_action(cx.listener(Self::reveal_in_finder))
                         .on_action(cx.listener(Self::open_in_terminal))
                 })
+                .on_mouse_down(
+                    MouseButton::Right,
+                    cx.listener(move |this, event: &MouseDownEvent, cx| {
+                        // When deploying the context menu anywhere below the last project entry,
+                        // act as if the user clicked the root of the last worktree.
+                        if let Some(entry_id) = this.last_worktree_root_id {
+                            this.deploy_context_menu(event.position, entry_id, cx);
+                        }
+                    }),
+                )
                 .track_focus(&self.focus_handle)
                 .child(
                     uniform_list(
@@ -1577,7 +1590,7 @@ impl Render for DraggedProjectEntryView {
                     .indent_level(self.details.depth)
                     .indent_step_size(px(settings.indent_size))
                     .child(if let Some(icon) = &self.details.icon {
-                        div().child(IconElement::from_path(icon.to_string()))
+                        div().child(Icon::from_path(icon.to_string()))
                     } else {
                         div()
                     })
@@ -1627,8 +1640,8 @@ impl Panel for ProjectPanel {
         cx.notify();
     }
 
-    fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
-        Some(ui::Icon::FileTree)
+    fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
+        Some(ui::IconName::FileTree)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -24,12 +24,35 @@ pub struct ProjectPanelSettings {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct ProjectPanelSettingsContent {
+    /// Customise default width (in pixels) taken by project panel
+    ///
+    /// Default: 240
     pub default_width: Option<f32>,
+    /// The position of project panel
+    ///
+    /// Default: left
     pub dock: Option<ProjectPanelDockPosition>,
+    /// Whether to show file icons in the project panel.
+    ///
+    /// Default: true
     pub file_icons: Option<bool>,
+    /// Whether to show folder icons or chevrons for directories in the project panel.
+    ///
+    /// Default: true
     pub folder_icons: Option<bool>,
+    /// Whether to show the git status in the project panel.
+    ///
+    /// Default: true
     pub git_status: Option<bool>,
+    /// Amount of indentation (in pixels) for nested items.
+    ///
+    /// Default: 20
     pub indent_size: Option<f32>,
+    /// Whether to reveal it in the project panel automatically,
+    /// when a corresponding project entry becomes active.
+    /// Gitignored entries are never auto revealed.
+    ///
+    /// Default: true
     pub auto_reveal_entries: Option<bool>,
 }
 

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     Subscription, View, ViewContext, WeakView,
 };
 use search::{buffer_search, BufferSearchBar};
-use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
 use workspace::{
     item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
@@ -43,7 +43,7 @@ impl Render for QuickActionBar {
 
         let inlay_hints_button = Some(QuickActionBarButton::new(
             "toggle inlay hints",
-            Icon::InlayHint,
+            IconName::InlayHint,
             editor.read(cx).inlay_hints_enabled(),
             Box::new(editor::ToggleInlayHints),
             "Toggle Inlay Hints",
@@ -60,7 +60,7 @@ impl Render for QuickActionBar {
 
         let search_button = Some(QuickActionBarButton::new(
             "toggle buffer search",
-            Icon::MagnifyingGlass,
+            IconName::MagnifyingGlass,
             !self.buffer_search_bar.read(cx).is_dismissed(),
             Box::new(buffer_search::Deploy { focus: false }),
             "Buffer Search",
@@ -77,7 +77,7 @@ impl Render for QuickActionBar {
 
         let assistant_button = QuickActionBarButton::new(
             "toggle inline assistant",
-            Icon::MagicWand,
+            IconName::MagicWand,
             false,
             Box::new(InlineAssist),
             "Inline Assist",
@@ -95,7 +95,6 @@ impl Render for QuickActionBar {
 
         h_stack()
             .id("quick action bar")
-            .p_1()
             .gap_2()
             .children(inlay_hints_button)
             .children(search_button)
@@ -108,7 +107,7 @@ impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
 #[derive(IntoElement)]
 struct QuickActionBarButton {
     id: ElementId,
-    icon: Icon,
+    icon: IconName,
     toggled: bool,
     action: Box<dyn Action>,
     tooltip: SharedString,
@@ -118,7 +117,7 @@ struct QuickActionBarButton {
 impl QuickActionBarButton {
     fn new(
         id: impl Into<ElementId>,
-        icon: Icon,
+        icon: IconName,
         toggled: bool,
         action: Box<dyn Action>,
         tooltip: impl Into<SharedString>,

crates/rpc/src/macros.rs 🔗

@@ -60,8 +60,10 @@ macro_rules! request_messages {
 
 #[macro_export]
 macro_rules! entity_messages {
-    ($id_field:ident, $($name:ident),* $(,)?) => {
+    ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => {
         $(impl EntityMessage for $name {
+            type Entity = $entity_type;
+
             fn remote_entity_id(&self) -> u64 {
                 self.$id_field
             }

crates/rpc/src/proto.rs 🔗

@@ -31,6 +31,7 @@ pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 's
 }
 
 pub trait EntityMessage: EnvelopedMessage {
+    type Entity;
     fn remote_entity_id(&self) -> u64;
 }
 
@@ -369,7 +370,7 @@ request_messages!(
 );
 
 entity_messages!(
-    project_id,
+    {project_id, ShareProject},
     AddProjectCollaborator,
     ApplyCodeAction,
     ApplyCompletionAdditionalEdits,
@@ -422,7 +423,7 @@ entity_messages!(
 );
 
 entity_messages!(
-    channel_id,
+    {channel_id, Channel},
     ChannelMessageSent,
     RemoveChannelMessage,
     UpdateChannelBuffer,

crates/search/src/buffer_search.rs 🔗

@@ -21,7 +21,7 @@ use settings::Settings;
 use std::{any::Any, sync::Arc};
 use theme::ThemeSettings;
 
-use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip};
+use ui::{h_stack, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip};
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
@@ -225,7 +225,7 @@ impl Render for BufferSearchBar {
                     .border_color(editor_border)
                     .min_w(rems(384. / 16.))
                     .rounded_lg()
-                    .child(IconElement::new(Icon::MagnifyingGlass))
+                    .child(Icon::new(IconName::MagnifyingGlass))
                     .child(self.render_text_input(&self.query_editor, cx))
                     .children(supported_options.case.then(|| {
                         self.render_search_option_button(
@@ -287,7 +287,7 @@ impl Render for BufferSearchBar {
                         this.child(
                             IconButton::new(
                                 "buffer-search-bar-toggle-replace-button",
-                                Icon::Replace,
+                                IconName::Replace,
                             )
                             .style(ButtonStyle::Subtle)
                             .when(self.replace_enabled, |button| {
@@ -323,7 +323,7 @@ impl Render for BufferSearchBar {
                         )
                         .when(should_show_replace_input, |this| {
                             this.child(
-                                IconButton::new("search-replace-next", ui::Icon::ReplaceNext)
+                                IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
                                     .tooltip(move |cx| {
                                         Tooltip::for_action("Replace next", &ReplaceNext, cx)
                                     })
@@ -332,7 +332,7 @@ impl Render for BufferSearchBar {
                                     })),
                             )
                             .child(
-                                IconButton::new("search-replace-all", ui::Icon::ReplaceAll)
+                                IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
                                     .tooltip(move |cx| {
                                         Tooltip::for_action("Replace all", &ReplaceAll, cx)
                                     })
@@ -350,7 +350,7 @@ impl Render for BufferSearchBar {
                     .gap_0p5()
                     .flex_none()
                     .child(
-                        IconButton::new("select-all", ui::Icon::SelectAll)
+                        IconButton::new("select-all", ui::IconName::SelectAll)
                             .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
                             .tooltip(|cx| {
                                 Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
@@ -358,13 +358,13 @@ impl Render for BufferSearchBar {
                     )
                     .children(match_count)
                     .child(render_nav_button(
-                        ui::Icon::ChevronLeft,
+                        ui::IconName::ChevronLeft,
                         self.active_match_index.is_some(),
                         "Select previous match",
                         &SelectPrevMatch,
                     ))
                     .child(render_nav_button(
-                        ui::Icon::ChevronRight,
+                        ui::IconName::ChevronRight,
                         self.active_match_index.is_some(),
                         "Select next match",
                         &SelectNextMatch,

crates/search/src/project_search.rs 🔗

@@ -38,7 +38,7 @@ use std::{
 use theme::ThemeSettings;
 
 use ui::{
-    h_stack, prelude::*, v_stack, Icon, IconButton, IconElement, Label, LabelCommon, LabelSize,
+    h_stack, prelude::*, v_stack, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
     Selectable, ToggleButton, Tooltip,
 };
 use util::{paths::PathMatcher, ResultExt as _};
@@ -424,7 +424,8 @@ impl Item for ProjectSearchView {
             .current()
             .as_ref()
             .map(|query| {
-                let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+                let query = query.replace('\n', "");
+                let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN);
                 query_text.into()
             });
         let tab_name = last_query
@@ -432,7 +433,7 @@ impl Item for ProjectSearchView {
             .unwrap_or_else(|| "Project search".into());
         h_stack()
             .gap_2()
-            .child(IconElement::new(Icon::MagnifyingGlass).color(if selected {
+            .child(Icon::new(IconName::MagnifyingGlass).color(if selected {
                 Color::Default
             } else {
                 Color::Muted
@@ -1616,12 +1617,12 @@ impl Render for ProjectSearchBar {
                 .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
                 .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
                 .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
-                .child(IconElement::new(Icon::MagnifyingGlass))
+                .child(Icon::new(IconName::MagnifyingGlass))
                 .child(self.render_text_input(&search.query_editor, cx))
                 .child(
                     h_stack()
                         .child(
-                            IconButton::new("project-search-filter-button", Icon::Filter)
+                            IconButton::new("project-search-filter-button", IconName::Filter)
                                 .tooltip(|cx| {
                                     Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
                                 })
@@ -1639,7 +1640,7 @@ impl Render for ProjectSearchBar {
                             this.child(
                                 IconButton::new(
                                     "project-search-case-sensitive",
-                                    Icon::CaseSensitive,
+                                    IconName::CaseSensitive,
                                 )
                                 .tooltip(|cx| {
                                     Tooltip::for_action(
@@ -1659,7 +1660,7 @@ impl Render for ProjectSearchBar {
                                 )),
                             )
                             .child(
-                                IconButton::new("project-search-whole-word", Icon::WholeWord)
+                                IconButton::new("project-search-whole-word", IconName::WholeWord)
                                     .tooltip(|cx| {
                                         Tooltip::for_action(
                                             "Toggle whole word",
@@ -1738,7 +1739,7 @@ impl Render for ProjectSearchBar {
                         }),
                 )
                 .child(
-                    IconButton::new("project-search-toggle-replace", Icon::Replace)
+                    IconButton::new("project-search-toggle-replace", IconName::Replace)
                         .on_click(cx.listener(|this, _, cx| {
                             this.toggle_replace(&ToggleReplace, cx);
                         }))
@@ -1755,7 +1756,7 @@ impl Render for ProjectSearchBar {
                 .border_1()
                 .border_color(cx.theme().colors().border)
                 .rounded_lg()
-                .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small))
+                .child(Icon::new(IconName::Replace).size(ui::IconSize::Small))
                 .child(self.render_text_input(&search.replacement_editor, cx))
         } else {
             // Fill out the space if we don't have a replacement editor.
@@ -1764,7 +1765,7 @@ impl Render for ProjectSearchBar {
         let actions_column = h_stack()
             .when(search.replace_enabled, |this| {
                 this.child(
-                    IconButton::new("project-search-replace-next", Icon::ReplaceNext)
+                    IconButton::new("project-search-replace-next", IconName::ReplaceNext)
                         .on_click(cx.listener(|this, _, cx| {
                             if let Some(search) = this.active_project_search.as_ref() {
                                 search.update(cx, |this, cx| {
@@ -1775,7 +1776,7 @@ impl Render for ProjectSearchBar {
                         .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)),
                 )
                 .child(
-                    IconButton::new("project-search-replace-all", Icon::ReplaceAll)
+                    IconButton::new("project-search-replace-all", IconName::ReplaceAll)
                         .on_click(cx.listener(|this, _, cx| {
                             if let Some(search) = this.active_project_search.as_ref() {
                                 search.update(cx, |this, cx| {
@@ -1796,7 +1797,7 @@ impl Render for ProjectSearchBar {
                 this
             })
             .child(
-                IconButton::new("project-search-prev-match", Icon::ChevronLeft)
+                IconButton::new("project-search-prev-match", IconName::ChevronLeft)
                     .disabled(search.active_match_index.is_none())
                     .on_click(cx.listener(|this, _, cx| {
                         if let Some(search) = this.active_project_search.as_ref() {
@@ -1810,7 +1811,7 @@ impl Render for ProjectSearchBar {
                     }),
             )
             .child(
-                IconButton::new("project-search-next-match", Icon::ChevronRight)
+                IconButton::new("project-search-next-match", IconName::ChevronRight)
                     .disabled(search.active_match_index.is_none())
                     .on_click(cx.listener(|this, _, cx| {
                         if let Some(search) = this.active_project_search.as_ref() {

crates/search/src/search.rs 🔗

@@ -60,11 +60,11 @@ impl SearchOptions {
         }
     }
 
-    pub fn icon(&self) -> ui::Icon {
+    pub fn icon(&self) -> ui::IconName {
         match *self {
-            SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
-            SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
-            SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit,
+            SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
+            SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
+            SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit,
             _ => panic!("{:?} is not a named SearchOption", self),
         }
     }

crates/search/src/search_bar.rs 🔗

@@ -3,7 +3,7 @@ use ui::IconButton;
 use ui::{prelude::*, Tooltip};
 
 pub(super) fn render_nav_button(
-    icon: ui::Icon,
+    icon: ui::IconName,
     active: bool,
     tooltip: &'static str,
     action: &'static dyn Action,

crates/semantic_index/src/semantic_index.rs 🔗

@@ -559,7 +559,7 @@ impl SemanticIndex {
                         .spawn(async move {
                             let mut changed_paths = BTreeMap::new();
                             for file in worktree.files(false, 0) {
-                                let absolute_path = worktree.absolutize(&file.path);
+                                let absolute_path = worktree.absolutize(&file.path)?;
 
                                 if file.is_external || file.is_ignored || file.is_symlink {
                                     continue;
@@ -1068,11 +1068,10 @@ impl SemanticIndex {
                                 return true;
                             };
 
-                        worktree_state.changed_paths.retain(|path, info| {
+                        for (path, info) in &worktree_state.changed_paths {
                             if info.is_deleted {
                                 files_to_delete.push((worktree_state.db_id, path.clone()));
-                            } else {
-                                let absolute_path = worktree.read(cx).absolutize(path);
+                            } else if let Ok(absolute_path) = worktree.read(cx).absolutize(path) {
                                 let job_handle = JobHandle::new(pending_file_count_tx);
                                 pending_files.push(PendingFile {
                                     absolute_path,
@@ -1083,9 +1082,8 @@ impl SemanticIndex {
                                     worktree_db_id: worktree_state.db_id,
                                 });
                             }
-
-                            false
-                        });
+                        }
+                        worktree_state.changed_paths.clear();
                         true
                     });
 

crates/semantic_index/src/semantic_index_settings.rs 🔗

@@ -8,8 +8,13 @@ pub struct SemanticIndexSettings {
     pub enabled: bool,
 }
 
+/// Configuration of semantic index, an alternate search engine available in
+/// project search.
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct SemanticIndexSettingsContent {
+    /// Whether or not to display the Semantic mode in project search.
+    ///
+    /// Default: true
     pub enabled: Option<bool>,
 }
 

crates/storybook/Cargo.toml 🔗

@@ -14,6 +14,7 @@ anyhow.workspace = true
 backtrace-on-stack-overflow = "0.3.0"
 chrono = "0.4"
 clap = { version = "4.4", features = ["derive", "string"] }
+collab_ui = { path = "../collab_ui", features = ["stories"] }
 strum = { version = "0.25.0", features = ["derive"] }
 dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
 editor = { path = "../editor" }

crates/storybook/src/story_selector.rs 🔗

@@ -16,6 +16,7 @@ pub enum ComponentStory {
     Avatar,
     Button,
     Checkbox,
+    CollabNotification,
     ContextMenu,
     Cursor,
     Disclosure,
@@ -45,6 +46,9 @@ impl ComponentStory {
             Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
             Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
             Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
+            Self::CollabNotification => cx
+                .new_view(|_| collab_ui::notifications::CollabNotificationStory)
+                .into(),
             Self::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(),
             Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(),
             Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(),

crates/terminal/src/terminal.rs 🔗

@@ -1459,14 +1459,16 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla {
     }
 }
 
-///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
-///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
+/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube.
+/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
 ///
-///Wikipedia gives a formula for calculating the index for a given color:
+/// Wikipedia gives a formula for calculating the index for a given color:
 ///
-///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
+/// ```
+/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
+/// ```
 ///
-///This function does the reverse, calculating the r, g, and b components from a given index.
+/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index.
 fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
     debug_assert!((&16..=&231).contains(&i));
     let i = i - 16;

crates/terminal/src/terminal_settings.rs 🔗

@@ -36,6 +36,9 @@ pub enum VenvSettings {
     #[default]
     Off,
     On {
+        /// Default directories to search for virtual environments, relative
+        /// to the current working directory. We recommend overriding this
+        /// in your project's settings, rather than globally.
         activate_script: Option<ActivateScript>,
         directories: Option<Vec<PathBuf>>,
     },
@@ -73,20 +76,68 @@ pub enum ActivateScript {
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct TerminalSettingsContent {
+    /// What shell to use when opening a terminal.
+    ///
+    /// Default: system
     pub shell: Option<Shell>,
+    /// What working directory to use when launching the terminal
+    ///
+    /// Default: current_project_directory
     pub working_directory: Option<WorkingDirectory>,
+    /// Set the terminal's font size.
+    ///
+    /// If this option is not included,
+    /// the terminal will default to matching the buffer's font size.
     pub font_size: Option<f32>,
+    /// Set the terminal's font family.
+    ///
+    /// If this option is not included,
+    /// the terminal will default to matching the buffer's font family.
     pub font_family: Option<String>,
+    /// Set the terminal's line height.
+    ///
+    /// Default: comfortable
     pub line_height: Option<TerminalLineHeight>,
     pub font_features: Option<FontFeatures>,
+    /// Any key-value pairs added to this list will be added to the terminal's
+    /// environment. Use `:` to separate multiple values.
+    ///
+    /// Default: {}
     pub env: Option<HashMap<String, String>>,
+    /// Set the cursor blinking behavior in the terminal.
+    ///
+    /// Default: terminal_controlled
     pub blinking: Option<TerminalBlink>,
+    /// Set whether Alternate Scroll mode (code: ?1007) is active by default.
+    /// Alternate Scroll mode converts mouse scroll events into up / down key
+    /// presses when in the alternate screen (e.g. when running applications
+    /// like vim or  less). The terminal can still set and unset this mode.
+    ///
+    /// Default: off
     pub alternate_scroll: Option<AlternateScroll>,
+    /// Set whether the option key behaves as the meta key.
+    ///
+    /// Default: false
     pub option_as_meta: Option<bool>,
+    /// Whether or not selecting text in the terminal will automatically
+    /// copy to the system clipboard.
+    ///
+    /// Default: false
     pub copy_on_select: Option<bool>,
     pub dock: Option<TerminalDockPosition>,
+    /// Default width when the terminal is docked to the left or right.
+    ///
+    /// Default: 640
     pub default_width: Option<f32>,
+    /// Default height when the terminal is docked to the bottom.
+    ///
+    /// Default: 320
     pub default_height: Option<f32>,
+    /// Activate the python virtual environment, if one is found, in the
+    /// terminal's working directory (as resolved by the working_directory
+    /// setting). Set this to "off" to disable this behavior.
+    ///
+    /// Default: on
     pub detect_venv: Option<VenvSettings>,
 }
 
@@ -107,9 +158,13 @@ impl settings::Settings for TerminalSettings {
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum TerminalLineHeight {
+    /// Use a line height that's comfortable for reading, 1.618
     #[default]
     Comfortable,
+    /// Use a standard line height, 1.3. This option is useful for TUIs,
+    /// particularly if they use box characters
     Standard,
+    /// Use a custom line height.
     Custom(f32),
 }
 
@@ -127,17 +182,25 @@ impl TerminalLineHeight {
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum TerminalBlink {
+    /// Never blink the cursor, ignoring the terminal mode.
     Off,
+    /// Default the cursor blink to off, but allow the terminal to
+    /// set blinking.
     TerminalControlled,
+    /// Always blink the cursor, ignoring the terminal mode.
     On,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum Shell {
+    /// Use the system's default terminal configuration in /etc/passwd
     System,
     Program(String),
-    WithArguments { program: String, args: Vec<String> },
+    WithArguments {
+        program: String,
+        args: Vec<String>,
+    },
 }
 
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -150,8 +213,15 @@ pub enum AlternateScroll {
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum WorkingDirectory {
+    /// Use the current file's project directory.  Will Fallback to the
+    /// first project directory strategy if unsuccessful.
     CurrentProjectDirectory,
+    /// Use the first project in this workspace's directory.
     FirstProjectDirectory,
+    /// Always use this platform's home directory (if it can be found).
     AlwaysHome,
+    /// Slways use a specific directory. This value will be shell expanded.
+    /// If this path is not a valid directory the terminal will default to
+    /// this platform's home directory  (if it can be found).
     Always { directory: String },
 }

crates/terminal_view/src/terminal_element.rs 🔗

@@ -451,6 +451,18 @@ impl TerminalElement {
             }
         });
 
+        let interactive_text_bounds = InteractiveBounds {
+            bounds,
+            stacking_order: cx.stacking_order().clone(),
+        };
+        if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
+            if self.can_navigate_to_selected_word && last_hovered_word.is_some() {
+                cx.set_cursor_style(gpui::CursorStyle::PointingHand)
+            } else {
+                cx.set_cursor_style(gpui::CursorStyle::IBeam)
+            }
+        }
+
         let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
             div()
                 .size_full()

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -19,7 +19,7 @@ use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     item::Item,
     pane,
-    ui::Icon,
+    ui::IconName,
     DraggedTab, Pane, Workspace,
 };
 
@@ -71,7 +71,7 @@ impl TerminalPanel {
                 h_stack()
                     .gap_2()
                     .child(
-                        IconButton::new("plus", Icon::Plus)
+                        IconButton::new("plus", IconName::Plus)
                             .icon_size(IconSize::Small)
                             .on_click(move |_, cx| {
                                 terminal_panel
@@ -82,10 +82,10 @@ impl TerminalPanel {
                     )
                     .child({
                         let zoomed = pane.is_zoomed();
-                        IconButton::new("toggle_zoom", Icon::Maximize)
+                        IconButton::new("toggle_zoom", IconName::Maximize)
                             .icon_size(IconSize::Small)
                             .selected(zoomed)
-                            .selected_icon(Icon::Minimize)
+                            .selected_icon(IconName::Minimize)
                             .on_click(cx.listener(|pane, _, cx| {
                                 pane.toggle_zoom(&workspace::ToggleZoom, cx);
                             }))
@@ -477,8 +477,8 @@ impl Panel for TerminalPanel {
         "TerminalPanel"
     }
 
-    fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
-        Some(Icon::Terminal)
+    fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
+        Some(IconName::Terminal)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -2,8 +2,6 @@ mod persistence;
 pub mod terminal_element;
 pub mod terminal_panel;
 
-// todo!()
-// use crate::terminal_element::TerminalElement;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
     div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
@@ -22,7 +20,7 @@ use terminal::{
     Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
 };
 use terminal_element::TerminalElement;
-use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label};
 use util::{paths::PathLikeWithPosition, ResultExt};
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent},
@@ -692,7 +690,7 @@ impl Item for TerminalView {
         let title = self.terminal().read(cx).title(true);
         h_stack()
             .gap_2()
-            .child(IconElement::new(Icon::Terminal))
+            .child(Icon::new(IconName::Terminal))
             .child(Label::new(title).color(if selected {
                 Color::Default
             } else {

crates/theme/src/theme.rs 🔗

@@ -73,13 +73,6 @@ impl ActiveTheme for AppContext {
     }
 }
 
-// todo!()
-// impl<'a> ActiveTheme for WindowContext<'a> {
-//     fn theme(&self) -> &Arc<Theme> {
-//         &ThemeSettings::get_global(self.app()).active_theme
-//     }
-// }
-
 pub struct ThemeFamily {
     pub id: String,
     pub name: SharedString,

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

@@ -2,7 +2,7 @@ use gpui::{AnyView, DefiniteLength};
 
 use crate::{prelude::*, IconPosition, KeyBinding};
 use crate::{
-    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
+    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
 };
 
 use super::button_icon::ButtonIcon;
@@ -14,11 +14,11 @@ pub struct Button {
     label_color: Option<Color>,
     label_size: Option<LabelSize>,
     selected_label: Option<SharedString>,
-    icon: Option<Icon>,
+    icon: Option<IconName>,
     icon_position: Option<IconPosition>,
     icon_size: Option<IconSize>,
     icon_color: Option<Color>,
-    selected_icon: Option<Icon>,
+    selected_icon: Option<IconName>,
     key_binding: Option<KeyBinding>,
 }
 
@@ -54,7 +54,7 @@ impl Button {
         self
     }
 
-    pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+    pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
         self.icon = icon.into();
         self
     }
@@ -74,7 +74,7 @@ impl Button {
         self
     }
 
-    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
         self.selected_icon = icon.into();
         self
     }

crates/ui/src/components/button/button_icon.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{prelude::*, Icon, IconElement, IconSize};
+use crate::{prelude::*, Icon, IconName, IconSize};
 
 /// An icon that appears within a button.
 ///
@@ -6,17 +6,17 @@ use crate::{prelude::*, Icon, IconElement, IconSize};
 /// or as a standalone icon, like in [`IconButton`](crate::IconButton).
 #[derive(IntoElement)]
 pub(super) struct ButtonIcon {
-    icon: Icon,
+    icon: IconName,
     size: IconSize,
     color: Color,
     disabled: bool,
     selected: bool,
-    selected_icon: Option<Icon>,
+    selected_icon: Option<IconName>,
     selected_style: Option<ButtonStyle>,
 }
 
 impl ButtonIcon {
-    pub fn new(icon: Icon) -> Self {
+    pub fn new(icon: IconName) -> Self {
         Self {
             icon,
             size: IconSize::default(),
@@ -44,7 +44,7 @@ impl ButtonIcon {
         self
     }
 
-    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
         self.selected_icon = icon.into();
         self
     }
@@ -88,6 +88,6 @@ impl RenderOnce for ButtonIcon {
             self.color
         };
 
-        IconElement::new(icon).size(self.size).color(icon_color)
+        Icon::new(icon).size(self.size).color(icon_color)
     }
 }

crates/ui/src/components/button/icon_button.rs 🔗

@@ -1,21 +1,21 @@
 use gpui::{AnyView, DefiniteLength};
 
 use crate::{prelude::*, SelectableButton};
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize};
 
 use super::button_icon::ButtonIcon;
 
 #[derive(IntoElement)]
 pub struct IconButton {
     base: ButtonLike,
-    icon: Icon,
+    icon: IconName,
     icon_size: IconSize,
     icon_color: Color,
-    selected_icon: Option<Icon>,
+    selected_icon: Option<IconName>,
 }
 
 impl IconButton {
-    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+    pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
         Self {
             base: ButtonLike::new(id),
             icon,
@@ -35,7 +35,7 @@ impl IconButton {
         self
     }
 
-    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
         self.selected_icon = icon.into();
         self
     }

crates/ui/src/components/checkbox.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
 
 use crate::prelude::*;
-use crate::{Color, Icon, IconElement, Selection};
+use crate::{Color, Icon, IconName, Selection};
 
 pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
 
@@ -47,7 +47,7 @@ impl RenderOnce for Checkbox {
         let group_id = format!("checkbox_group_{:?}", self.id);
 
         let icon = match self.checked {
-            Selection::Selected => Some(IconElement::new(Icon::Check).size(IconSize::Small).color(
+            Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
                 if self.disabled {
                     Color::Disabled
                 } else {
@@ -55,7 +55,7 @@ impl RenderOnce for Checkbox {
                 },
             )),
             Selection::Indeterminate => Some(
-                IconElement::new(Icon::Dash)
+                Icon::new(IconName::Dash)
                     .size(IconSize::Small)
                     .color(if self.disabled {
                         Color::Disabled

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

@@ -1,6 +1,6 @@
 use crate::{
-    h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
-    ListSeparator, ListSubHeader,
+    h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
+    ListSubHeader,
 };
 use gpui::{
     px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
@@ -14,7 +14,7 @@ enum ContextMenuItem {
     Header(SharedString),
     Entry {
         label: SharedString,
-        icon: Option<Icon>,
+        icon: Option<IconName>,
         handler: Rc<dyn Fn(&mut WindowContext)>,
         action: Option<Box<dyn Action>>,
     },
@@ -117,7 +117,7 @@ impl ContextMenu {
             label: label.into(),
             action: Some(action.boxed_clone()),
             handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
-            icon: Some(Icon::Link),
+            icon: Some(IconName::Link),
         });
         self
     }
@@ -280,7 +280,7 @@ impl Render for ContextMenu {
                                 h_stack()
                                     .gap_1()
                                     .child(Label::new(label.clone()))
-                                    .child(IconElement::new(*icon))
+                                    .child(Icon::new(*icon))
                                     .into_any_element()
                             } else {
                                 Label::new(label.clone()).into_any_element()

crates/ui/src/components/disclosure.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::ClickEvent;
 
-use crate::{prelude::*, Color, Icon, IconButton, IconSize};
+use crate::{prelude::*, Color, IconButton, IconName, IconSize};
 
 #[derive(IntoElement)]
 pub struct Disclosure {
@@ -32,8 +32,8 @@ impl RenderOnce for Disclosure {
         IconButton::new(
             self.id,
             match self.is_open {
-                true => Icon::ChevronDown,
-                false => Icon::ChevronRight,
+                true => IconName::ChevronDown,
+                false => IconName::ChevronRight,
             },
         )
         .icon_color(Color::Muted)

crates/ui/src/components/icon.rs 🔗

@@ -22,7 +22,7 @@ impl IconSize {
 }
 
 #[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
-pub enum Icon {
+pub enum IconName {
     Ai,
     ArrowDown,
     ArrowLeft,
@@ -111,118 +111,108 @@ pub enum Icon {
     ZedXCopilot,
 }
 
-impl Icon {
+impl IconName {
     pub fn path(self) -> &'static str {
         match self {
-            Icon::Ai => "icons/ai.svg",
-            Icon::ArrowDown => "icons/arrow_down.svg",
-            Icon::ArrowLeft => "icons/arrow_left.svg",
-            Icon::ArrowRight => "icons/arrow_right.svg",
-            Icon::ArrowUp => "icons/arrow_up.svg",
-            Icon::ArrowUpRight => "icons/arrow_up_right.svg",
-            Icon::ArrowCircle => "icons/arrow_circle.svg",
-            Icon::AtSign => "icons/at_sign.svg",
-            Icon::AudioOff => "icons/speaker_off.svg",
-            Icon::AudioOn => "icons/speaker_loud.svg",
-            Icon::Backspace => "icons/backspace.svg",
-            Icon::Bell => "icons/bell.svg",
-            Icon::BellOff => "icons/bell_off.svg",
-            Icon::BellRing => "icons/bell_ring.svg",
-            Icon::Bolt => "icons/bolt.svg",
-            Icon::CaseSensitive => "icons/case_insensitive.svg",
-            Icon::Check => "icons/check.svg",
-            Icon::ChevronDown => "icons/chevron_down.svg",
-            Icon::ChevronLeft => "icons/chevron_left.svg",
-            Icon::ChevronRight => "icons/chevron_right.svg",
-            Icon::ChevronUp => "icons/chevron_up.svg",
-            Icon::Close => "icons/x.svg",
-            Icon::Collab => "icons/user_group_16.svg",
-            Icon::Command => "icons/command.svg",
-            Icon::Control => "icons/control.svg",
-            Icon::Copilot => "icons/copilot.svg",
-            Icon::CopilotDisabled => "icons/copilot_disabled.svg",
-            Icon::CopilotError => "icons/copilot_error.svg",
-            Icon::CopilotInit => "icons/copilot_init.svg",
-            Icon::Copy => "icons/copy.svg",
-            Icon::Dash => "icons/dash.svg",
-            Icon::Delete => "icons/delete.svg",
-            Icon::Disconnected => "icons/disconnected.svg",
-            Icon::Ellipsis => "icons/ellipsis.svg",
-            Icon::Envelope => "icons/feedback.svg",
-            Icon::Escape => "icons/escape.svg",
-            Icon::ExclamationTriangle => "icons/warning.svg",
-            Icon::Exit => "icons/exit.svg",
-            Icon::ExternalLink => "icons/external_link.svg",
-            Icon::File => "icons/file.svg",
-            Icon::FileDoc => "icons/file_icons/book.svg",
-            Icon::FileGeneric => "icons/file_icons/file.svg",
-            Icon::FileGit => "icons/file_icons/git.svg",
-            Icon::FileLock => "icons/file_icons/lock.svg",
-            Icon::FileRust => "icons/file_icons/rust.svg",
-            Icon::FileToml => "icons/file_icons/toml.svg",
-            Icon::FileTree => "icons/project.svg",
-            Icon::Filter => "icons/filter.svg",
-            Icon::Folder => "icons/file_icons/folder.svg",
-            Icon::FolderOpen => "icons/file_icons/folder_open.svg",
-            Icon::FolderX => "icons/stop_sharing.svg",
-            Icon::Github => "icons/github.svg",
-            Icon::Hash => "icons/hash.svg",
-            Icon::InlayHint => "icons/inlay_hint.svg",
-            Icon::Link => "icons/link.svg",
-            Icon::MagicWand => "icons/magic_wand.svg",
-            Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
-            Icon::MailOpen => "icons/mail_open.svg",
-            Icon::Maximize => "icons/maximize.svg",
-            Icon::Menu => "icons/menu.svg",
-            Icon::MessageBubbles => "icons/conversations.svg",
-            Icon::Mic => "icons/mic.svg",
-            Icon::MicMute => "icons/mic_mute.svg",
-            Icon::Minimize => "icons/minimize.svg",
-            Icon::Option => "icons/option.svg",
-            Icon::PageDown => "icons/page_down.svg",
-            Icon::PageUp => "icons/page_up.svg",
-            Icon::Plus => "icons/plus.svg",
-            Icon::Public => "icons/public.svg",
-            Icon::Quote => "icons/quote.svg",
-            Icon::Replace => "icons/replace.svg",
-            Icon::ReplaceAll => "icons/replace_all.svg",
-            Icon::ReplaceNext => "icons/replace_next.svg",
-            Icon::Return => "icons/return.svg",
-            Icon::Screen => "icons/desktop.svg",
-            Icon::SelectAll => "icons/select_all.svg",
-            Icon::Shift => "icons/shift.svg",
-            Icon::Snip => "icons/snip.svg",
-            Icon::Space => "icons/space.svg",
-            Icon::Split => "icons/split.svg",
-            Icon::Tab => "icons/tab.svg",
-            Icon::Terminal => "icons/terminal.svg",
-            Icon::Update => "icons/update.svg",
-            Icon::WholeWord => "icons/word_search.svg",
-            Icon::XCircle => "icons/error.svg",
-            Icon::ZedXCopilot => "icons/zed_x_copilot.svg",
+            IconName::Ai => "icons/ai.svg",
+            IconName::ArrowDown => "icons/arrow_down.svg",
+            IconName::ArrowLeft => "icons/arrow_left.svg",
+            IconName::ArrowRight => "icons/arrow_right.svg",
+            IconName::ArrowUp => "icons/arrow_up.svg",
+            IconName::ArrowUpRight => "icons/arrow_up_right.svg",
+            IconName::ArrowCircle => "icons/arrow_circle.svg",
+            IconName::AtSign => "icons/at_sign.svg",
+            IconName::AudioOff => "icons/speaker_off.svg",
+            IconName::AudioOn => "icons/speaker_loud.svg",
+            IconName::Backspace => "icons/backspace.svg",
+            IconName::Bell => "icons/bell.svg",
+            IconName::BellOff => "icons/bell_off.svg",
+            IconName::BellRing => "icons/bell_ring.svg",
+            IconName::Bolt => "icons/bolt.svg",
+            IconName::CaseSensitive => "icons/case_insensitive.svg",
+            IconName::Check => "icons/check.svg",
+            IconName::ChevronDown => "icons/chevron_down.svg",
+            IconName::ChevronLeft => "icons/chevron_left.svg",
+            IconName::ChevronRight => "icons/chevron_right.svg",
+            IconName::ChevronUp => "icons/chevron_up.svg",
+            IconName::Close => "icons/x.svg",
+            IconName::Collab => "icons/user_group_16.svg",
+            IconName::Command => "icons/command.svg",
+            IconName::Control => "icons/control.svg",
+            IconName::Copilot => "icons/copilot.svg",
+            IconName::CopilotDisabled => "icons/copilot_disabled.svg",
+            IconName::CopilotError => "icons/copilot_error.svg",
+            IconName::CopilotInit => "icons/copilot_init.svg",
+            IconName::Copy => "icons/copy.svg",
+            IconName::Dash => "icons/dash.svg",
+            IconName::Delete => "icons/delete.svg",
+            IconName::Disconnected => "icons/disconnected.svg",
+            IconName::Ellipsis => "icons/ellipsis.svg",
+            IconName::Envelope => "icons/feedback.svg",
+            IconName::Escape => "icons/escape.svg",
+            IconName::ExclamationTriangle => "icons/warning.svg",
+            IconName::Exit => "icons/exit.svg",
+            IconName::ExternalLink => "icons/external_link.svg",
+            IconName::File => "icons/file.svg",
+            IconName::FileDoc => "icons/file_icons/book.svg",
+            IconName::FileGeneric => "icons/file_icons/file.svg",
+            IconName::FileGit => "icons/file_icons/git.svg",
+            IconName::FileLock => "icons/file_icons/lock.svg",
+            IconName::FileRust => "icons/file_icons/rust.svg",
+            IconName::FileToml => "icons/file_icons/toml.svg",
+            IconName::FileTree => "icons/project.svg",
+            IconName::Filter => "icons/filter.svg",
+            IconName::Folder => "icons/file_icons/folder.svg",
+            IconName::FolderOpen => "icons/file_icons/folder_open.svg",
+            IconName::FolderX => "icons/stop_sharing.svg",
+            IconName::Github => "icons/github.svg",
+            IconName::Hash => "icons/hash.svg",
+            IconName::InlayHint => "icons/inlay_hint.svg",
+            IconName::Link => "icons/link.svg",
+            IconName::MagicWand => "icons/magic_wand.svg",
+            IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
+            IconName::MailOpen => "icons/mail_open.svg",
+            IconName::Maximize => "icons/maximize.svg",
+            IconName::Menu => "icons/menu.svg",
+            IconName::MessageBubbles => "icons/conversations.svg",
+            IconName::Mic => "icons/mic.svg",
+            IconName::MicMute => "icons/mic_mute.svg",
+            IconName::Minimize => "icons/minimize.svg",
+            IconName::Option => "icons/option.svg",
+            IconName::PageDown => "icons/page_down.svg",
+            IconName::PageUp => "icons/page_up.svg",
+            IconName::Plus => "icons/plus.svg",
+            IconName::Public => "icons/public.svg",
+            IconName::Quote => "icons/quote.svg",
+            IconName::Replace => "icons/replace.svg",
+            IconName::ReplaceAll => "icons/replace_all.svg",
+            IconName::ReplaceNext => "icons/replace_next.svg",
+            IconName::Return => "icons/return.svg",
+            IconName::Screen => "icons/desktop.svg",
+            IconName::SelectAll => "icons/select_all.svg",
+            IconName::Shift => "icons/shift.svg",
+            IconName::Snip => "icons/snip.svg",
+            IconName::Space => "icons/space.svg",
+            IconName::Split => "icons/split.svg",
+            IconName::Tab => "icons/tab.svg",
+            IconName::Terminal => "icons/terminal.svg",
+            IconName::Update => "icons/update.svg",
+            IconName::WholeWord => "icons/word_search.svg",
+            IconName::XCircle => "icons/error.svg",
+            IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
         }
     }
 }
 
 #[derive(IntoElement)]
-pub struct IconElement {
+pub struct Icon {
     path: SharedString,
     color: Color,
     size: IconSize,
 }
 
-impl RenderOnce for IconElement {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        svg()
-            .size(self.size.rems())
-            .flex_none()
-            .path(self.path)
-            .text_color(self.color.color(cx))
-    }
-}
-
-impl IconElement {
-    pub fn new(icon: Icon) -> Self {
+impl Icon {
+    pub fn new(icon: IconName) -> Self {
         Self {
             path: icon.path().into(),
             color: Color::default(),
@@ -248,3 +238,13 @@ impl IconElement {
         self
     }
 }
+
+impl RenderOnce for Icon {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        svg()
+            .size(self.size.rems())
+            .flex_none()
+            .path(self.path)
+            .text_color(self.color.color(cx))
+    }
+}

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

@@ -1,4 +1,4 @@
-use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
+use crate::{h_stack, prelude::*, Icon, IconName, IconSize};
 use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
 
 #[derive(IntoElement, Clone)]
@@ -26,16 +26,16 @@ impl RenderOnce for KeyBinding {
                     .text_color(cx.theme().colors().text_muted)
                     .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
                     .when(keystroke.modifiers.control, |el| {
-                        el.child(KeyIcon::new(Icon::Control))
+                        el.child(KeyIcon::new(IconName::Control))
                     })
                     .when(keystroke.modifiers.alt, |el| {
-                        el.child(KeyIcon::new(Icon::Option))
+                        el.child(KeyIcon::new(IconName::Option))
                     })
                     .when(keystroke.modifiers.command, |el| {
-                        el.child(KeyIcon::new(Icon::Command))
+                        el.child(KeyIcon::new(IconName::Command))
                     })
                     .when(keystroke.modifiers.shift, |el| {
-                        el.child(KeyIcon::new(Icon::Shift))
+                        el.child(KeyIcon::new(IconName::Shift))
                     })
                     .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
                     .when(key_icon.is_none(), |el| {
@@ -62,21 +62,21 @@ impl KeyBinding {
         Some(Self::new(key_binding))
     }
 
-    fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
+    fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
         match keystroke.key.as_str() {
-            "left" => Some(Icon::ArrowLeft),
-            "right" => Some(Icon::ArrowRight),
-            "up" => Some(Icon::ArrowUp),
-            "down" => Some(Icon::ArrowDown),
-            "backspace" => Some(Icon::Backspace),
-            "delete" => Some(Icon::Delete),
-            "return" => Some(Icon::Return),
-            "enter" => Some(Icon::Return),
-            "tab" => Some(Icon::Tab),
-            "space" => Some(Icon::Space),
-            "escape" => Some(Icon::Escape),
-            "pagedown" => Some(Icon::PageDown),
-            "pageup" => Some(Icon::PageUp),
+            "left" => Some(IconName::ArrowLeft),
+            "right" => Some(IconName::ArrowRight),
+            "up" => Some(IconName::ArrowUp),
+            "down" => Some(IconName::ArrowDown),
+            "backspace" => Some(IconName::Backspace),
+            "delete" => Some(IconName::Delete),
+            "return" => Some(IconName::Return),
+            "enter" => Some(IconName::Return),
+            "tab" => Some(IconName::Tab),
+            "space" => Some(IconName::Space),
+            "escape" => Some(IconName::Escape),
+            "pagedown" => Some(IconName::PageDown),
+            "pageup" => Some(IconName::PageUp),
             _ => None,
         }
     }
@@ -120,13 +120,13 @@ impl Key {
 
 #[derive(IntoElement)]
 pub struct KeyIcon {
-    icon: Icon,
+    icon: IconName,
 }
 
 impl RenderOnce for KeyIcon {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         div().w(rems(14. / 16.)).child(
-            IconElement::new(self.icon)
+            Icon::new(self.icon)
                 .size(IconSize::Small)
                 .color(Color::Muted),
         )
@@ -134,7 +134,7 @@ impl RenderOnce for KeyIcon {
 }
 
 impl KeyIcon {
-    pub fn new(icon: Icon) -> Self {
+    pub fn new(icon: IconName) -> Self {
         Self { icon }
     }
 }

crates/ui/src/components/list/list_sub_header.rs 🔗

@@ -1,10 +1,10 @@
 use crate::prelude::*;
-use crate::{h_stack, Icon, IconElement, IconSize, Label};
+use crate::{h_stack, Icon, IconName, IconSize, Label};
 
 #[derive(IntoElement)]
 pub struct ListSubHeader {
     label: SharedString,
-    start_slot: Option<Icon>,
+    start_slot: Option<IconName>,
     inset: bool,
 }
 
@@ -17,7 +17,7 @@ impl ListSubHeader {
         }
     }
 
-    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+    pub fn left_icon(mut self, left_icon: Option<IconName>) -> Self {
         self.start_slot = left_icon;
         self
     }
@@ -40,11 +40,10 @@ impl RenderOnce for ListSubHeader {
                         .flex()
                         .gap_1()
                         .items_center()
-                        .children(self.start_slot.map(|i| {
-                            IconElement::new(i)
-                                .color(Color::Muted)
-                                .size(IconSize::Small)
-                        }))
+                        .children(
+                            self.start_slot
+                                .map(|i| Icon::new(i).color(Color::Muted).size(IconSize::Small)),
+                        )
                         .child(Label::new(self.label.clone()).color(Color::Muted)),
                 ),
         )

crates/ui/src/components/stories/button.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::Render;
 use story::Story;
 
-use crate::{prelude::*, Icon};
+use crate::{prelude::*, IconName};
 use crate::{Button, ButtonStyle};
 
 pub struct ButtonStory;
@@ -23,12 +23,12 @@ impl Render for ButtonStory {
             .child(Story::label("With `label_color`"))
             .child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
             .child(Story::label("With `icon`"))
-            .child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
+            .child(Button::new("filled_with_icon", "Click me").icon(IconName::FileGit))
             .child(Story::label("Selected with `icon`"))
             .child(
                 Button::new("filled_and_selected_with_icon", "Click me")
                     .selected(true)
-                    .icon(Icon::FileGit),
+                    .icon(IconName::FileGit),
             )
             .child(Story::label("Default (Subtle)"))
             .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))

crates/ui/src/components/stories/icon.rs 🔗

@@ -3,17 +3,17 @@ use story::Story;
 use strum::IntoEnumIterator;
 
 use crate::prelude::*;
-use crate::{Icon, IconElement};
+use crate::{Icon, IconName};
 
 pub struct IconStory;
 
 impl Render for IconStory {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let icons = Icon::iter();
+        let icons = IconName::iter();
 
         Story::container()
-            .child(Story::title_for::<IconElement>())
+            .child(Story::title_for::<Icon>())
             .child(Story::label("All Icons"))
-            .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+            .child(div().flex().gap_3().children(icons.map(Icon::new)))
     }
 }

crates/ui/src/components/stories/icon_button.rs 🔗

@@ -2,7 +2,7 @@ use gpui::Render;
 use story::{StoryContainer, StoryItem, StorySection};
 
 use crate::{prelude::*, Tooltip};
-use crate::{Icon, IconButton};
+use crate::{IconButton, IconName};
 
 pub struct IconButtonStory;
 
@@ -10,7 +10,7 @@ impl Render for IconButtonStory {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
         let default_button = StoryItem::new(
             "Default",
-            IconButton::new("default_icon_button", Icon::Hash),
+            IconButton::new("default_icon_button", IconName::Hash),
         )
         .description("Displays an icon button.")
         .usage(
@@ -21,7 +21,7 @@ impl Render for IconButtonStory {
 
         let selected_button = StoryItem::new(
             "Selected",
-            IconButton::new("selected_icon_button", Icon::Hash).selected(true),
+            IconButton::new("selected_icon_button", IconName::Hash).selected(true),
         )
         .description("Displays an icon button that is selected.")
         .usage(
@@ -32,9 +32,9 @@ impl Render for IconButtonStory {
 
         let selected_with_selected_icon = StoryItem::new(
             "Selected with `selected_icon`",
-            IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+            IconButton::new("selected_with_selected_icon_button", IconName::AudioOn)
                 .selected(true)
-                .selected_icon(Icon::AudioOff),
+                .selected_icon(IconName::AudioOff),
         )
         .description(
             "Displays an icon button that is selected and shows a different icon when selected.",
@@ -49,7 +49,7 @@ impl Render for IconButtonStory {
 
         let disabled_button = StoryItem::new(
             "Disabled",
-            IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
+            IconButton::new("disabled_icon_button", IconName::Hash).disabled(true),
         )
         .description("Displays an icon button that is disabled.")
         .usage(
@@ -60,7 +60,7 @@ impl Render for IconButtonStory {
 
         let with_on_click_button = StoryItem::new(
             "With `on_click`",
-            IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+            IconButton::new("with_on_click_button", IconName::Ai).on_click(|_event, _cx| {
                 println!("Clicked!");
             }),
         )
@@ -75,7 +75,7 @@ impl Render for IconButtonStory {
 
         let with_tooltip_button = StoryItem::new(
             "With `tooltip`",
-            IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+            IconButton::new("with_tooltip_button", IconName::MessageBubbles)
                 .tooltip(|cx| Tooltip::text("Open messages", cx)),
         )
         .description("Displays an icon button that has a tooltip when hovered.")
@@ -88,7 +88,7 @@ impl Render for IconButtonStory {
 
         let selected_with_tooltip_button = StoryItem::new(
             "Selected with `tooltip`",
-            IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+            IconButton::new("selected_with_tooltip_button", IconName::InlayHint)
                 .selected(true)
                 .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
         )

crates/ui/src/components/stories/list_header.rs 🔗

@@ -2,7 +2,7 @@ use gpui::Render;
 use story::Story;
 
 use crate::{prelude::*, IconButton};
-use crate::{Icon, ListHeader};
+use crate::{IconName, ListHeader};
 
 pub struct ListHeaderStory;
 
@@ -13,19 +13,19 @@ impl Render for ListHeaderStory {
             .child(Story::label("Default"))
             .child(ListHeader::new("Section 1"))
             .child(Story::label("With left icon"))
-            .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
+            .child(ListHeader::new("Section 2").start_slot(Icon::new(IconName::Bell)))
             .child(Story::label("With left icon and meta"))
             .child(
                 ListHeader::new("Section 3")
-                    .start_slot(IconElement::new(Icon::BellOff))
-                    .end_slot(IconButton::new("action_1", Icon::Bolt)),
+                    .start_slot(Icon::new(IconName::BellOff))
+                    .end_slot(IconButton::new("action_1", IconName::Bolt)),
             )
             .child(Story::label("With multiple meta"))
             .child(
                 ListHeader::new("Section 4")
-                    .end_slot(IconButton::new("action_1", Icon::Bolt))
-                    .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
-                    .end_slot(IconButton::new("action_3", Icon::Plus)),
+                    .end_slot(IconButton::new("action_1", IconName::Bolt))
+                    .end_slot(IconButton::new("action_2", IconName::ExclamationTriangle))
+                    .end_slot(IconButton::new("action_3", IconName::Plus)),
             )
     }
 }

crates/ui/src/components/stories/list_item.rs 🔗

@@ -1,8 +1,8 @@
-use gpui::Render;
+use gpui::{Render, SharedUrl};
 use story::Story;
 
 use crate::{prelude::*, Avatar};
-use crate::{Icon, ListItem};
+use crate::{IconName, ListItem};
 
 pub struct ListItemStory;
 
@@ -18,13 +18,13 @@ impl Render for ListItemStory {
                 ListItem::new("inset_list_item")
                     .inset(true)
                     .start_slot(
-                        IconElement::new(Icon::Bell)
+                        Icon::new(IconName::Bell)
                             .size(IconSize::Small)
                             .color(Color::Muted),
                     )
                     .child("Hello, world!")
                     .end_slot(
-                        IconElement::new(Icon::Bell)
+                        Icon::new(IconName::Bell)
                             .size(IconSize::Small)
                             .color(Color::Muted),
                     ),
@@ -34,7 +34,7 @@ impl Render for ListItemStory {
                 ListItem::new("with start slot_icon")
                     .child("Hello, world!")
                     .start_slot(
-                        IconElement::new(Icon::Bell)
+                        Icon::new(IconName::Bell)
                             .size(IconSize::Small)
                             .color(Color::Muted),
                     ),
@@ -43,7 +43,7 @@ impl Render for ListItemStory {
             .child(
                 ListItem::new("with_start slot avatar")
                     .child("Hello, world!")
-                    .start_slot(Avatar::new(SharedString::from(
+                    .start_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )
@@ -51,7 +51,7 @@ impl Render for ListItemStory {
             .child(
                 ListItem::new("with_left_avatar")
                     .child("Hello, world!")
-                    .end_slot(Avatar::new(SharedString::from(
+                    .end_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )
@@ -62,23 +62,23 @@ impl Render for ListItemStory {
                     .end_slot(
                         h_stack()
                             .gap_2()
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             ))),
                     )
-                    .end_hover_slot(Avatar::new(SharedString::from(
+                    .end_hover_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )

crates/ui/src/components/stories/tab.rs 🔗

@@ -27,7 +27,7 @@ impl Render for TabStory {
                 h_stack().child(
                     Tab::new("tab_1")
                         .end_slot(
-                            IconButton::new("close_button", Icon::Close)
+                            IconButton::new("close_button", IconName::Close)
                                 .icon_color(Color::Muted)
                                 .size(ButtonSize::None)
                                 .icon_size(IconSize::XSmall),

crates/ui/src/components/stories/tab_bar.rs 🔗

@@ -38,16 +38,19 @@ impl Render for TabBarStory {
                 h_stack().child(
                     TabBar::new("tab_bar_1")
                         .start_child(
-                            IconButton::new("navigate_backward", Icon::ArrowLeft)
+                            IconButton::new("navigate_backward", IconName::ArrowLeft)
                                 .icon_size(IconSize::Small),
                         )
                         .start_child(
-                            IconButton::new("navigate_forward", Icon::ArrowRight)
+                            IconButton::new("navigate_forward", IconName::ArrowRight)
                                 .icon_size(IconSize::Small),
                         )
-                        .end_child(IconButton::new("new", Icon::Plus).icon_size(IconSize::Small))
                         .end_child(
-                            IconButton::new("split_pane", Icon::Split).icon_size(IconSize::Small),
+                            IconButton::new("new", IconName::Plus).icon_size(IconSize::Small),
+                        )
+                        .end_child(
+                            IconButton::new("split_pane", IconName::Split)
+                                .icon_size(IconSize::Small),
                         )
                         .children(tabs),
                 ),

crates/ui/src/prelude.rs 🔗

@@ -14,6 +14,7 @@ pub use crate::visible_on_hover::*;
 pub use crate::{h_stack, v_stack};
 pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
 pub use crate::{ButtonCommon, Color, StyledExt};
-pub use crate::{Icon, IconElement, IconPosition, IconSize};
+pub use crate::{Headline, HeadlineSize};
+pub use crate::{Icon, IconName, IconPosition, IconSize};
 pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};
 pub use theme::ActiveTheme;

crates/ui/src/styles/typography.rs 🔗

@@ -1,4 +1,8 @@
-use gpui::{rems, Rems};
+use gpui::{
+    div, rems, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, WindowContext,
+};
+use settings::Settings;
+use theme::{ActiveTheme, ThemeSettings};
 
 #[derive(Debug, Default, Clone)]
 pub enum UiTextSize {
@@ -33,3 +37,69 @@ impl UiTextSize {
         }
     }
 }
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum HeadlineSize {
+    XSmall,
+    Small,
+    #[default]
+    Medium,
+    Large,
+    XLarge,
+}
+
+impl HeadlineSize {
+    pub fn size(self) -> Rems {
+        match self {
+            // Based on the Major Second scale
+            Self::XSmall => rems(0.88),
+            Self::Small => rems(1.0),
+            Self::Medium => rems(1.125),
+            Self::Large => rems(1.27),
+            Self::XLarge => rems(1.43),
+        }
+    }
+
+    pub fn line_height(self) -> Rems {
+        match self {
+            Self::XSmall => rems(1.6),
+            Self::Small => rems(1.6),
+            Self::Medium => rems(1.6),
+            Self::Large => rems(1.6),
+            Self::XLarge => rems(1.6),
+        }
+    }
+}
+
+#[derive(IntoElement)]
+pub struct Headline {
+    size: HeadlineSize,
+    text: SharedString,
+}
+
+impl RenderOnce for Headline {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+
+        div()
+            .font(ui_font)
+            .line_height(self.size.line_height())
+            .text_size(self.size.size())
+            .text_color(cx.theme().colors().text)
+            .child(self.text)
+    }
+}
+
+impl Headline {
+    pub fn new(text: impl Into<SharedString>) -> Self {
+        Self {
+            size: HeadlineSize::default(),
+            text: text.into(),
+        }
+    }
+
+    pub fn size(mut self, size: HeadlineSize) -> Self {
+        self.size = size;
+        self
+    }
+}

crates/vim/src/vim.rs 🔗

@@ -33,6 +33,9 @@ use workspace::{self, Workspace};
 
 use crate::state::ReplayableAction;
 
+/// Whether or not to enable Vim mode (work in progress).
+///
+/// Default: false
 pub struct VimModeSetting(pub bool);
 
 #[derive(Clone, Deserialize, PartialEq)]

crates/welcome/src/base_keymap_setting.rs 🔗

@@ -4,6 +4,9 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 
+/// Base key bindings scheme. Base keymaps can be overriden with user keymaps.
+///
+/// Default: VSCode
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
 pub enum BaseKeymap {
     #[default]

crates/workspace/src/dock.rs 🔗

@@ -28,7 +28,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
     fn size(&self, cx: &WindowContext) -> Pixels;
     fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>);
-    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
     fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
     fn toggle_action(&self) -> Box<dyn Action>;
     fn icon_label(&self, _: &WindowContext) -> Option<String> {
@@ -52,7 +52,7 @@ pub trait PanelHandle: Send + Sync {
     fn set_active(&self, active: bool, cx: &mut WindowContext);
     fn size(&self, cx: &WindowContext) -> Pixels;
     fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
-    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
     fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
     fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
@@ -104,7 +104,7 @@ where
         self.update(cx, |this, cx| this.set_size(size, cx))
     }
 
-    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
         self.read(cx).icon(cx)
     }
 
@@ -395,7 +395,6 @@ impl Dock {
                         })
                         .ok();
                 }
-                // todo!() we do not use this event in the production code (even in zed1), remove it
                 PanelEvent::Activate => {
                     if let Some(ix) = this
                         .panel_entries
@@ -775,7 +774,7 @@ pub mod test {
             self.size = size.unwrap_or(px(300.));
         }
 
-        fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
+        fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
             None
         }
 

crates/workspace/src/item.rs 🔗

@@ -60,7 +60,13 @@ impl ClosePosition {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ItemSettingsContent {
+    /// Whether to show the Git file status on a tab item.
+    ///
+    /// Default: true
     git_status: Option<bool>,
+    /// Position of the close button in a tab.
+    ///
+    /// Default: right
     close_position: Option<ClosePosition>,
 }
 

crates/workspace/src/notifications.rs 🔗

@@ -175,7 +175,7 @@ pub mod simple_message_notification {
     };
     use std::sync::Arc;
     use ui::prelude::*;
-    use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
+    use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt};
 
     pub struct MessageNotification {
         message: SharedString,
@@ -230,7 +230,7 @@ pub mod simple_message_notification {
                         .child(
                             div()
                                 .id("cancel")
-                                .child(IconElement::new(Icon::Close))
+                                .child(Icon::new(IconName::Close))
                                 .cursor_pointer()
                                 .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
                         ),
@@ -247,105 +247,6 @@ pub mod simple_message_notification {
                 }))
         }
     }
-    // todo!()
-    //     impl View for MessageNotification {
-    //         fn ui_name() -> &'static str {
-    //             "MessageNotification"
-    //         }
-
-    //         fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
-    //             let theme = theme::current(cx).clone();
-    //             let theme = &theme.simple_message_notification;
-
-    //             enum MessageNotificationTag {}
-
-    //             let click_message = self.click_message.clone();
-    //             let message = match &self.message {
-    //                 NotificationMessage::Text(text) => {
-    //                     Text::new(text.to_owned(), theme.message.text.clone()).into_any()
-    //                 }
-    //                 NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
-    //             };
-    //             let on_click = self.on_click.clone();
-    //             let has_click_action = on_click.is_some();
-
-    //             Flex::column()
-    //                 .with_child(
-    //                     Flex::row()
-    //                         .with_child(
-    //                             message
-    //                                 .contained()
-    //                                 .with_style(theme.message.container)
-    //                                 .aligned()
-    //                                 .top()
-    //                                 .left()
-    //                                 .flex(1., true),
-    //                         )
-    //                         .with_child(
-    //                             MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
-    //                                 let style = theme.dismiss_button.style_for(state);
-    //                                 Svg::new("icons/x.svg")
-    //                                     .with_color(style.color)
-    //                                     .constrained()
-    //                                     .with_width(style.icon_width)
-    //                                     .aligned()
-    //                                     .contained()
-    //                                     .with_style(style.container)
-    //                                     .constrained()
-    //                                     .with_width(style.button_width)
-    //                                     .with_height(style.button_width)
-    //                             })
-    //                             .with_padding(Padding::uniform(5.))
-    //                             .on_click(MouseButton::Left, move |_, this, cx| {
-    //                                 this.dismiss(&Default::default(), cx);
-    //                             })
-    //                             .with_cursor_style(CursorStyle::PointingHand)
-    //                             .aligned()
-    //                             .constrained()
-    //                             .with_height(cx.font_cache().line_height(theme.message.text.font_size))
-    //                             .aligned()
-    //                             .top()
-    //                             .flex_float(),
-    //                         ),
-    //                 )
-    //                 .with_children({
-    //                     click_message
-    //                         .map(|click_message| {
-    //                             MouseEventHandler::new::<MessageNotificationTag, _>(
-    //                                 0,
-    //                                 cx,
-    //                                 |state, _| {
-    //                                     let style = theme.action_message.style_for(state);
-
-    //                                     Flex::row()
-    //                                         .with_child(
-    //                                             Text::new(click_message, style.text.clone())
-    //                                                 .contained()
-    //                                                 .with_style(style.container),
-    //                                         )
-    //                                         .contained()
-    //                                 },
-    //                             )
-    //                             .on_click(MouseButton::Left, move |_, this, cx| {
-    //                                 if let Some(on_click) = on_click.as_ref() {
-    //                                     on_click(cx);
-    //                                     this.dismiss(&Default::default(), cx);
-    //                                 }
-    //                             })
-    //                             // Since we're not using a proper overlay, we have to capture these extra events
-    //                             .on_down(MouseButton::Left, |_, _, _| {})
-    //                             .on_up(MouseButton::Left, |_, _, _| {})
-    //                             .with_cursor_style(if has_click_action {
-    //                                 CursorStyle::PointingHand
-    //                             } else {
-    //                                 CursorStyle::Arrow
-    //                             })
-    //                         })
-    //                         .into_iter()
-    //                 })
-    //                 .into_any()
-    //         }
-    //     }
 }
 
 pub trait NotifyResultExt {

crates/workspace/src/pane.rs 🔗

@@ -31,8 +31,8 @@ use std::{
 use theme::ThemeSettings;
 
 use ui::{
-    prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
-    Tab, TabBar, TabPosition, Tooltip,
+    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconName, IconSize, Indicator,
+    Label, Tab, TabBar, TabPosition, Tooltip,
 };
 use ui::{v_stack, ContextMenu};
 use util::{maybe, truncate_and_remove_front, ResultExt};
@@ -384,7 +384,7 @@ impl Pane {
                 h_stack()
                     .gap_2()
                     .child(
-                        IconButton::new("plus", Icon::Plus)
+                        IconButton::new("plus", IconName::Plus)
                             .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
                             .on_click(cx.listener(|pane, _, cx| {
@@ -406,7 +406,7 @@ impl Pane {
                         el.child(Self::render_menu_overlay(new_item_menu))
                     })
                     .child(
-                        IconButton::new("split", Icon::Split)
+                        IconButton::new("split", IconName::Split)
                             .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
                             .on_click(cx.listener(|pane, _, cx| {
@@ -427,11 +427,11 @@ impl Pane {
                     )
                     .child({
                         let zoomed = pane.is_zoomed();
-                        IconButton::new("toggle_zoom", Icon::Maximize)
+                        IconButton::new("toggle_zoom", IconName::Maximize)
                             .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
                             .selected(zoomed)
-                            .selected_icon(Icon::Minimize)
+                            .selected_icon(IconName::Minimize)
                             .on_click(cx.listener(|pane, _, cx| {
                                 pane.toggle_zoom(&crate::ToggleZoom, cx);
                             }))
@@ -1570,7 +1570,7 @@ impl Pane {
             })
             .start_slot::<Indicator>(indicator)
             .end_slot(
-                IconButton::new("close tab", Icon::Close)
+                IconButton::new("close tab", IconName::Close)
                     .icon_color(Color::Muted)
                     .size(ButtonSize::None)
                     .icon_size(IconSize::XSmall)
@@ -1676,7 +1676,7 @@ impl Pane {
                     h_stack()
                         .gap_2()
                         .child(
-                            IconButton::new("navigate_backward", Icon::ArrowLeft)
+                            IconButton::new("navigate_backward", IconName::ArrowLeft)
                                 .icon_size(IconSize::Small)
                                 .on_click({
                                     let view = cx.view().clone();
@@ -1686,7 +1686,7 @@ impl Pane {
                                 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
                         )
                         .child(
-                            IconButton::new("navigate_forward", Icon::ArrowRight)
+                            IconButton::new("navigate_forward", IconName::ArrowRight)
                                 .icon_size(IconSize::Small)
                                 .on_click({
                                     let view = cx.view().clone();

crates/workspace/src/pane_group.rs 🔗

@@ -12,7 +12,7 @@ use serde::Deserialize;
 use std::sync::Arc;
 use ui::{prelude::*, Button};
 
-const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
+const HANDLE_HITBOX_SIZE: f32 = 4.0;
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
 const VERTICAL_MIN_SIZE: f32 = 100.;
 
@@ -579,12 +579,15 @@ mod element {
         Size, Style, WeakView, WindowContext,
     };
     use parking_lot::Mutex;
+    use settings::Settings;
     use smallvec::SmallVec;
     use ui::prelude::*;
     use util::ResultExt;
 
     use crate::Workspace;
 
+    use crate::WorkspaceSettings;
+
     use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
 
     const DIVIDER_SIZE: f32 = 1.0;
@@ -704,7 +707,6 @@ mod element {
                 proposed_current_pixel_change -= current_pixel_change;
             }
 
-            // todo!(schedule serialize)
             workspace
                 .update(cx, |this, cx| this.schedule_serialize(cx))
                 .log_err();
@@ -834,20 +836,39 @@ mod element {
             debug_assert!(flexes.len() == len);
             debug_assert!(flex_values_in_bounds(flexes.as_slice()));
 
+            let magnification_value = WorkspaceSettings::get(None, cx).active_pane_magnification;
+            let active_pane_magnification = if magnification_value == 1. {
+                None
+            } else {
+                Some(magnification_value)
+            };
+
+            let total_flex = if let Some(flex) = active_pane_magnification {
+                self.children.len() as f32 - 1. + flex
+            } else {
+                len as f32
+            };
+
             let mut origin = bounds.origin;
-            let space_per_flex = bounds.size.along(self.axis) / len as f32;
+            let space_per_flex = bounds.size.along(self.axis) / total_flex;
 
             let mut bounding_boxes = self.bounding_boxes.lock();
             bounding_boxes.clear();
 
             for (ix, child) in self.children.iter_mut().enumerate() {
-                //todo!(active_pane_magnification)
-                // If using active pane magnification, need to switch to using
-                // 1 for all non-active panes, and then the magnification for the
-                // active pane.
+                let child_flex = active_pane_magnification
+                    .map(|magnification| {
+                        if self.active_pane_ix == Some(ix) {
+                            magnification
+                        } else {
+                            1.
+                        }
+                    })
+                    .unwrap_or_else(|| flexes[ix]);
+
                 let child_size = bounds
                     .size
-                    .apply_along(self.axis, |_| space_per_flex * flexes[ix]);
+                    .apply_along(self.axis, |_| space_per_flex * child_flex);
 
                 let child_bounds = Bounds {
                     origin,
@@ -857,20 +878,23 @@ mod element {
                 cx.with_z_index(0, |cx| {
                     child.draw(origin, child_size.into(), cx);
                 });
-                cx.with_z_index(1, |cx| {
-                    if ix < len - 1 {
-                        Self::push_handle(
-                            self.flexes.clone(),
-                            state.clone(),
-                            self.axis,
-                            ix,
-                            child_bounds,
-                            bounds,
-                            self.workspace.clone(),
-                            cx,
-                        );
-                    }
-                });
+
+                if active_pane_magnification.is_none() {
+                    cx.with_z_index(1, |cx| {
+                        if ix < len - 1 {
+                            Self::push_handle(
+                                self.flexes.clone(),
+                                state.clone(),
+                                self.axis,
+                                ix,
+                                child_bounds,
+                                bounds,
+                                self.workspace.clone(),
+                                cx,
+                            );
+                        }
+                    });
+                }
 
                 origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
             }

crates/workspace/src/shared_screen.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
     WindowContext,
 };
 use std::sync::{Arc, Weak};
-use ui::{h_stack, prelude::*, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, Icon, IconName, Label};
 
 pub enum Event {
     Close,
@@ -100,7 +100,7 @@ impl Item for SharedScreen {
     ) -> gpui::AnyElement {
         h_stack()
             .gap_1()
-            .child(IconElement::new(Icon::Screen))
+            .child(Icon::new(IconName::Screen))
             .child(
                 Label::new(format!("{}'s screen", self.user.github_login)).color(if selected {
                     Color::Default

crates/workspace/src/toolbar.rs 🔗

@@ -133,82 +133,6 @@ impl Render for Toolbar {
     }
 }
 
-// todo!()
-// impl View for Toolbar {
-//     fn ui_name() -> &'static str {
-//         "Toolbar"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = &theme::current(cx).workspace.toolbar;
-
-//         let mut primary_left_items = Vec::new();
-//         let mut primary_right_items = Vec::new();
-//         let mut secondary_item = None;
-//         let spacing = theme.item_spacing;
-//         let mut primary_items_row_count = 1;
-
-//         for (item, position) in &self.items {
-//             match *position {
-//                 ToolbarItemLocation::Hidden => {}
-
-//                 ToolbarItemLocation::PrimaryLeft { flex } => {
-//                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-//                     let left_item = ChildView::new(item.as_any(), cx).aligned();
-//                     if let Some((flex, expanded)) = flex {
-//                         primary_left_items.push(left_item.flex(flex, expanded).into_any());
-//                     } else {
-//                         primary_left_items.push(left_item.into_any());
-//                     }
-//                 }
-
-//                 ToolbarItemLocation::PrimaryRight { flex } => {
-//                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-//                     let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
-//                     if let Some((flex, expanded)) = flex {
-//                         primary_right_items.push(right_item.flex(flex, expanded).into_any());
-//                     } else {
-//                         primary_right_items.push(right_item.into_any());
-//                     }
-//                 }
-
-//                 ToolbarItemLocation::Secondary => {
-//                     secondary_item = Some(
-//                         ChildView::new(item.as_any(), cx)
-//                             .constrained()
-//                             .with_height(theme.height * item.row_count(cx) as f32)
-//                             .into_any(),
-//                     );
-//                 }
-//             }
-//         }
-
-//         let container_style = theme.container;
-//         let height = theme.height * primary_items_row_count as f32;
-
-//         let mut primary_items = Flex::row().with_spacing(spacing);
-//         primary_items.extend(primary_left_items);
-//         primary_items.extend(primary_right_items);
-
-//         let mut toolbar = Flex::column();
-//         if !primary_items.is_empty() {
-//             toolbar.add_child(primary_items.constrained().with_height(height));
-//         }
-//         if let Some(secondary_item) = secondary_item {
-//             toolbar.add_child(secondary_item);
-//         }
-
-//         if toolbar.is_empty() {
-//             toolbar.into_any_named("toolbar")
-//         } else {
-//             toolbar
-//                 .contained()
-//                 .with_style(container_style)
-//                 .into_any_named("toolbar")
-//         }
-//     }
-// }
-
 impl Toolbar {
     pub fn new() -> Self {
         Self {
@@ -312,10 +236,3 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for View<T> {
         self.read(cx).row_count(cx)
     }
 }
-
-// todo!()
-// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
-//     fn from(val: &dyn ToolbarItemViewHandle) -> Self {
-//         val.as_any().clone()
-//     }
-// }

crates/workspace/src/workspace.rs 🔗

@@ -26,12 +26,12 @@ use futures::{
 };
 use gpui::{
     actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
-    AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
-    Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
-    ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
-    Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
-    WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context,
+    Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
+    GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model,
+    ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size,
+    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds,
+    WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -943,10 +943,8 @@ impl Workspace {
         cx: &mut ViewContext<Workspace>,
     ) -> Task<Result<()>> {
         let to_load = if let Some(pane) = pane.upgrade() {
-            // todo!("focus")
-            // cx.focus(&pane);
-
             pane.update(cx, |pane, cx| {
+                pane.focus(cx);
                 loop {
                     // Retrieve the weak item handle from the history.
                     let entry = pane.nav_history_mut().pop(mode, cx)?;
@@ -1631,8 +1629,7 @@ impl Workspace {
             });
         }
 
-        // todo!("focus")
-        // cx.focus_self();
+        cx.focus_self();
         cx.notify();
         self.serialize_workspace(cx);
     }
@@ -1713,6 +1710,7 @@ impl Workspace {
         cx.notify();
     }
 
+    // todo!()
     //     #[cfg(any(test, feature = "test-support"))]
     //     pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
     //         self.zoomed.and_then(|view| view.upgrade(cx))
@@ -2992,7 +2990,6 @@ impl Workspace {
         cx.notify();
     }
 
-    #[allow(unused)]
     fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
         self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
             cx.background_executor()
@@ -4034,34 +4031,34 @@ pub fn join_channel(
             return anyhow::Ok(());
         }
 
-        if requesting_window.is_some() {
-            return anyhow::Ok(());
-        }
-
         // find an existing workspace to focus and show call controls
-        let mut active_window = activate_any_workspace_window(&mut cx);
+        let mut active_window =
+            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
         if active_window.is_none() {
             // no open workspaces, make one to show the error in (blergh)
-            cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
+            let (window_handle, _) = cx
+                .update(|cx| {
+                    Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
+                })?
                 .await?;
-        }
 
-        active_window = activate_any_workspace_window(&mut cx);
-        let Some(active_window) = active_window else {
-            return anyhow::Ok(());
-        };
+            active_window = Some(window_handle);
+        }
 
         if let Err(err) = result {
-            active_window
-                .update(&mut cx, |_, cx| {
-                    cx.prompt(
-                        PromptLevel::Critical,
-                        &format!("Failed to join channel: {}", err),
-                        &["Ok"],
-                    )
-                })?
-                .await
-                .ok();
+            log::error!("failed to join channel: {}", err);
+            if let Some(active_window) = active_window {
+                active_window
+                    .update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Critical,
+                            &format!("Failed to join channel: {}", err),
+                            &["Ok"],
+                        )
+                    })?
+                    .await
+                    .ok();
+            }
         }
 
         // return ok, we showed the error to the user.
@@ -4079,19 +4076,17 @@ pub async fn get_any_active_workspace(
         cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
             .await?;
     }
-    activate_any_workspace_window(&mut cx)
-        .context("could not open zed")?
-        .downcast::<Workspace>()
-        .context("could not open zed workspace window")
+    activate_any_workspace_window(&mut cx).context("could not open zed")
 }
 
-fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
+fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
     cx.update(|cx| {
         for window in cx.windows() {
-            let is_workspace = window.downcast::<Workspace>().is_some();
-            if is_workspace {
-                window.update(cx, |_, cx| cx.activate_window()).ok();
-                return Some(window);
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
+                workspace_window
+                    .update(cx, |_, cx| cx.activate_window())
+                    .ok();
+                return Some(workspace_window);
             }
         }
         None

crates/workspace/src/workspace_settings.rs 🔗

@@ -12,35 +12,39 @@ pub struct WorkspaceSettings {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct WorkspaceSettingsContent {
+    /// Scale by which to zoom the active pane.
+    /// When set to 1.0, the active pane has the same size as others,
+    /// but when set to a larger value, the active pane takes up more space.
+    ///
+    /// Default: `1.0`
     pub active_pane_magnification: Option<f32>,
+    /// Whether or not to prompt the user to confirm before closing the application.
+    ///
+    /// Default: false
     pub confirm_quit: Option<bool>,
+    /// Whether or not to show the call status icon in the status bar.
+    ///
+    /// Default: true
     pub show_call_status_icon: Option<bool>,
+    /// When to automatically save edited buffers.
+    ///
+    /// Default: off
     pub autosave: Option<AutosaveSetting>,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AutosaveSetting {
+    /// Disable autosave.
     Off,
+    /// Save after inactivity period of `milliseconds`.
     AfterDelay { milliseconds: u64 },
+    /// Autosave when focus changes.
     OnFocusChange,
+    /// Autosave when the active window changes.
     OnWindowChange,
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct GitSettings {
-    pub git_gutter: Option<GitGutterSetting>,
-    pub gutter_debounce: Option<u64>,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutterSetting {
-    #[default]
-    TrackedFiles,
-    Hide,
-}
-
 impl Settings for WorkspaceSettings {
     const KEY: Option<&'static str> = None;
 

crates/zed/Cargo.toml 🔗

@@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" }
 client = { path = "../client" }
 # clock = { path = "../clock" }
 copilot = { path = "../copilot" }
-copilot_button = { path = "../copilot_button" }
+copilot_ui = { path = "../copilot_ui" }
 diagnostics = { path = "../diagnostics" }
 db = { path = "../db" }
 editor = { path = "../editor" }
@@ -74,6 +74,7 @@ vim = { path = "../vim" }
 workspace = { path = "../workspace" }
 welcome = { path = "../welcome" }
 zed_actions = {path = "../zed_actions"}
+assets = {path = "../assets"}
 anyhow.workspace = true
 async-compression.workspace = true
 async-tar = "0.4.2"

crates/zed/build.rs 🔗

@@ -22,7 +22,7 @@ fn main() {
     println!("cargo:rustc-link-arg=-Wl,-ObjC");
 
     // Populate git sha environment variable if git is available
-    println!("cargo:rerun-if-changed=.git/logs/HEAD");
+    println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
     if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
         if output.status.success() {
             let git_sha = String::from_utf8_lossy(&output.stdout);

crates/zed/src/main.rs 🔗

@@ -16,6 +16,7 @@ use isahc::{prelude::Configurable, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
 
+use assets::Assets;
 use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
@@ -49,8 +50,8 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
 use workspace::{AppState, WorkspaceStore};
 use zed::{
     app_menus, build_window_options, ensure_only_instance, handle_cli_connection,
-    handle_keymap_file_changes, initialize_workspace, languages, Assets, IsOnlyInstance,
-    OpenListener, OpenRequest,
+    handle_keymap_file_changes, initialize_workspace, languages, IsOnlyInstance, OpenListener,
+    OpenRequest,
 };
 
 fn main() {

crates/zed/src/zed.rs 🔗

@@ -1,11 +1,9 @@
 mod app_menus;
-mod assets;
 pub mod languages;
 mod only_instance;
 mod open_listener;
 
 pub use app_menus::*;
-pub use assets::*;
 use assistant::AssistantPanel;
 use breadcrumbs::Breadcrumbs;
 use collections::VecDeque;
@@ -18,6 +16,7 @@ pub use only_instance::*;
 pub use open_listener::*;
 
 use anyhow::{anyhow, Context as _};
+use assets::Assets;
 use futures::{channel::mpsc, select_biased, StreamExt};
 use project_panel::ProjectPanel;
 use quick_action_bar::QuickActionBar;
@@ -120,8 +119,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
         //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 
-        let copilot =
-            cx.new_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+        let copilot = cx.new_view(|cx| copilot_ui::CopilotButton::new(app_state.fs.clone(), cx));
         let diagnostic_summary =
             cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
         let activity_indicator =
@@ -766,7 +764,6 @@ fn open_bundled_file(
     .detach_and_log_err(cx);
 }
 
-// todo!()
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -877,6 +874,7 @@ mod tests {
         })
         .await
         .unwrap();
+        cx.background_executor.run_until_parked();
         assert_eq!(cx.read(|cx| cx.windows().len()), 2);
         let workspace_1 = cx
             .update(|cx| cx.windows()[0].downcast::<Workspace>())

script/lib/bump-version.sh 🔗

@@ -30,7 +30,7 @@ Locally committed and tagged ${package} version ${new_version}
 
 To push this:
 
-    git push origin ${tag_name} ${branch_name}
+    git push origin ${branch_name} ${tag_name}
 
 To undo this: