From 801125974ae1083223c7d97c2bb27e7e8dabaa3c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Dec 2023 15:32:32 +0100 Subject: [PATCH 01/94] Optimize inserting lots of primitives with the same StackingOrder --- crates/gpui2/src/scene.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index ca0a50546e..fd63b49a1a 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -17,6 +17,7 @@ pub type LayerId = u32; pub type DrawOrder = u32; pub(crate) struct SceneBuilder { + last_order: Option<(StackingOrder, LayerId)>, layers_by_order: BTreeMap, splitter: BspSplitter<(PrimitiveKind, usize)>, shadows: Vec, @@ -31,6 +32,7 @@ pub(crate) struct SceneBuilder { impl Default for SceneBuilder { fn default() -> Self { SceneBuilder { + last_order: None, layers_by_order: BTreeMap::new(), splitter: BspSplitter::new(), shadows: Vec::new(), @@ -156,14 +158,7 @@ impl SceneBuilder { return; } - let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { - *layer_id - } else { - let next_id = self.layers_by_order.len() as LayerId; - self.layers_by_order.insert(order.clone(), next_id); - next_id - }; - + let layer_id = self.layer_id_for_order(order); match primitive { Primitive::Shadow(mut shadow) => { shadow.order = layer_id; @@ -196,6 +191,24 @@ impl SceneBuilder { } } } + + fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 { + if let Some((last_order, last_layer_id)) = self.last_order.as_ref() { + if last_order == order { + return *last_layer_id; + } + }; + + let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { + *layer_id + } else { + let next_id = self.layers_by_order.len() as LayerId; + self.layers_by_order.insert(order.clone(), next_id); + next_id + }; + self.last_order = Some((order.clone(), layer_id)); + layer_id + } } pub struct Scene { From 5730d0ef2107f2a10fe01595ce34f14f97e289ad Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 13 Dec 2023 10:47:32 -0500 Subject: [PATCH 02/94] Bump livekit client --- crates/live_kit_client/LiveKitBridge/Package.resolved | 8 ++++---- crates/live_kit_client/LiveKitBridge/Package.swift | 2 +- crates/live_kit_client2/LiveKitBridge2/Package.resolved | 8 ++++---- crates/live_kit_client2/LiveKitBridge2/Package.swift | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index 85ae088565..b0ba567f2b 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "state": { "branch": null, - "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", - "version": "1.0.12" + "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50", + "version": "1.1.4" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "state": { "branch": null, - "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", - "version": "104.5112.17" + "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", + "version": "114.5735.8" } }, { diff --git a/crates/live_kit_client/LiveKitBridge/Package.swift b/crates/live_kit_client/LiveKitBridge/Package.swift index d7b5c271b9..abb38efca6 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.swift +++ b/crates/live_kit_client/LiveKitBridge/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["LiveKitBridge"]), ], dependencies: [ - .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/crates/live_kit_client2/LiveKitBridge2/Package.resolved b/crates/live_kit_client2/LiveKitBridge2/Package.resolved index b925bc8f0d..bf17ef24c5 100644 --- a/crates/live_kit_client2/LiveKitBridge2/Package.resolved +++ b/crates/live_kit_client2/LiveKitBridge2/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "state": { "branch": null, - "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", - "version": "1.0.12" + "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50", + "version": "1.1.4" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "state": { "branch": null, - "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", - "version": "104.5112.17" + "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", + "version": "114.5735.8" } }, { diff --git a/crates/live_kit_client2/LiveKitBridge2/Package.swift b/crates/live_kit_client2/LiveKitBridge2/Package.swift index 890eaa2f6d..29a5021e54 100644 --- a/crates/live_kit_client2/LiveKitBridge2/Package.swift +++ b/crates/live_kit_client2/LiveKitBridge2/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["LiveKitBridge2"]), ], dependencies: [ - .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. From 988fb91a61fa96abc5f8697c2a67a2e50027aa2c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 11:44:51 -0500 Subject: [PATCH 03/94] Fix tab bar drop target sizing (#3627) This PR fixes an issue where the tab bar drop target was not receiving any size. The styling isn't 100% correct yet, as the updated background color has a gap around it. Release Notes: - N/A --- crates/workspace2/src/pane.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2068245159..afd7e665c4 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1664,6 +1664,10 @@ impl Pane { ) .child( div() + .min_w_6() + // HACK: This empty child is currently necessary to force the drop traget to appear + // despite us setting a min width above. + .child("") .h_full() .flex_grow() .drag_over::(|bar| { From a71365a1d32c044f319e1cf9489347dd5997852b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:56:49 +0100 Subject: [PATCH 04/94] collab_ui: Wire up project picker Co-authored-by: Conrad --- crates/collab_ui2/src/collab_titlebar_item.rs | 107 ++++++++++-------- crates/picker2/src/picker2.rs | 23 ++-- .../recent_projects2/src/recent_projects.rs | 21 +++- 3 files changed, 93 insertions(+), 58 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2b931f7085..f48a78fb1d 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -2,11 +2,13 @@ use crate::face_pile::FacePile; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ - actions, canvas, div, point, px, rems, AppContext, Div, Element, Hsla, InteractiveElement, - IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled, - Subscription, ViewContext, VisualContext, WeakView, WindowBounds, + actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, + FocusableView, Hsla, InteractiveElement, IntoElement, Model, Overlay, ParentElement, Path, + Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; +use recent_projects::RecentProjects; use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ @@ -14,7 +16,7 @@ use ui::{ IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; -use workspace::{notifications::NotifyResultExt, Workspace}; +use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB}; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -49,7 +51,7 @@ pub struct CollabTitlebarItem { client: Arc, workspace: WeakView, //branch_popover: Option>, - //project_popover: Option>, + project_popover: Option, //user_menu: ViewHandle, _subscriptions: Vec, } @@ -328,7 +330,7 @@ impl CollabTitlebarItem { // menu // }), // branch_popover: None, - // project_popover: None, + project_popover: None, _subscriptions: subscriptions, } } @@ -366,11 +368,27 @@ impl CollabTitlebarItem { let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); - div().border().border_color(gpui::red()).child( - Button::new("project_name_trigger", name) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - ) + div() + .border() + .border_color(gpui::red()) + .child( + Button::new("project_name_trigger", name) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_project_menu(&ToggleProjectMenu, cx); + })), + ) + .children(self.project_popover.as_ref().map(|popover| { + overlay().child( + div() + .min_w_56() + .on_mouse_down_out(cx.listener_for(&popover.picker, |picker, _, cx| { + picker.cancel(&Default::default(), cx) + })) + .child(popover.picker.clone()), + ) + })) } pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { @@ -611,43 +629,40 @@ impl CollabTitlebarItem { // cx.notify(); // } - // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { - // let workspace = self.workspace.clone(); - // if self.project_popover.take().is_none() { - // cx.spawn(|this, mut cx| async move { - // let workspaces = WORKSPACE_DB - // .recent_workspaces_on_disk() - // .await - // .unwrap_or_default() - // .into_iter() - // .map(|(_, location)| location) - // .collect(); + pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + if self.project_popover.take().is_none() { + cx.spawn(|this, mut cx| async move { + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect(); - // let workspace = workspace.clone(); - // this.update(&mut cx, move |this, cx| { - // let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx)); + let workspace = workspace.clone(); + this.update(&mut cx, move |this, cx| { + let view = RecentProjects::open_popover(workspace, workspaces, cx); - // cx.subscribe(&view, |this, _, event, cx| { - // match event { - // PickerEvent::Dismiss => { - // this.project_popover = None; - // } - // } - - // cx.notify(); - // }) - // .detach(); - // cx.focus(&view); - // this.branch_popover.take(); - // this.project_popover = Some(view); - // cx.notify(); - // }) - // .log_err(); - // }) - // .detach(); - // } - // cx.notify(); - // } + cx.subscribe(&view.picker, |this, _, _: &DismissEvent, cx| { + this.project_popover = None; + cx.notify(); + }) + .detach(); + let focus_handle = view.focus_handle(cx); + cx.focus(&focus_handle); + // todo!() + //this.branch_popover.take(); + this.project_popover = Some(view); + cx.notify(); + }) + .log_err(); + }) + .detach(); + } + cx.notify(); + } // fn render_user_menu_button( // &self, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 98b6ce5ff0..db5eebff53 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,8 @@ use editor::Editor; use gpui::{ - div, prelude::*, rems, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView, - MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, - WindowContext, + div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter, + FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, + View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; @@ -113,8 +113,9 @@ impl Picker { cx.notify(); } - fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); + cx.emit(DismissEvent); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -146,9 +147,15 @@ impl Picker { event: &editor::EditorEvent, cx: &mut ViewContext, ) { - if let editor::EditorEvent::BufferEdited = event { - let query = self.editor.read(cx).text(cx); - self.update_matches(query, cx); + match event { + editor::EditorEvent::BufferEdited => { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + editor::EditorEvent::Blurred => { + self.cancel(&menu::Cancel, cx); + } + _ => {} } } @@ -189,6 +196,8 @@ impl Picker { } } +impl EventEmitter for Picker {} + impl Render for Picker { type Element = Div; diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs index e014783687..dff6aa12cc 100644 --- a/crates/recent_projects2/src/recent_projects.rs +++ b/crates/recent_projects2/src/recent_projects.rs @@ -23,14 +23,15 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); } +#[derive(Clone)] pub struct RecentProjects { - picker: View>, + pub picker: View>, } impl ModalView for RecentProjects {} impl RecentProjects { - fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext) -> Self { + fn new(delegate: RecentProjectsDelegate, cx: &mut WindowContext<'_>) -> Self { Self { picker: cx.build_view(|cx| Picker::new(delegate, cx)), } @@ -86,6 +87,16 @@ impl RecentProjects { Ok(()) })) } + pub fn open_popover( + workspace: WeakView, + workspaces: Vec, + cx: &mut WindowContext<'_>, + ) -> Self { + Self::new( + RecentProjectsDelegate::new(workspace, workspaces, false), + cx, + ) + } } impl EventEmitter for RecentProjects {} @@ -127,7 +138,7 @@ impl RecentProjectsDelegate { } } } - +impl EventEmitter for RecentProjectsDelegate {} impl PickerDelegate for RecentProjectsDelegate { type ListItem = ListItem; @@ -202,11 +213,11 @@ impl PickerDelegate for RecentProjectsDelegate { .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) }) .detach_and_log_err(cx); - self.dismissed(cx); + cx.emit(DismissEvent); } } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, _: &mut ViewContext>) {} fn render_match( &self, From 614753015693e8fbd91b184db82864f8a28463c2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:58:17 +0100 Subject: [PATCH 05/94] fixup! collab_ui: Wire up project picker --- crates/collab_ui2/src/collab_titlebar_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index f48a78fb1d..3d8fedd06b 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -3,8 +3,8 @@ use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, - FocusableView, Hsla, InteractiveElement, IntoElement, Model, Overlay, ParentElement, Path, - Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, + Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; From 599f81f3455eb079fdeba6f7059473c8dfe74761 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 12:11:33 -0500 Subject: [PATCH 06/94] Wire up tooltips on tab bar actions (#3629) This PR wires up the tooltips on the actions in the tab bar. Release Notes: - N/A --- crates/workspace2/src/pane.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index afd7e665c4..bcbadc4e53 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1597,7 +1597,8 @@ impl Pane { let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) }) - .disabled(!self.can_navigate_backward()), + .disabled(!self.can_navigate_backward()) + .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)), ) .start_child( IconButton::new("navigate_forward", Icon::ArrowRight) @@ -1606,7 +1607,8 @@ impl Pane { let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) }) - .disabled(!self.can_navigate_forward()), + .disabled(!self.can_navigate_forward()) + .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)), ) .end_child( div() @@ -1625,7 +1627,8 @@ impl Pane { }) .detach(); this.new_item_menu = Some(menu); - })), + })) + .tooltip(|cx| Tooltip::text("New...", cx)), ) .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { el.child(Self::render_menu_overlay(new_item_menu)) @@ -1649,7 +1652,8 @@ impl Pane { }) .detach(); this.split_item_menu = Some(menu); - })), + })) + .tooltip(|cx| Tooltip::text("Split Pane", cx)), ) .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { el.child(Self::render_menu_overlay(split_item_menu)) From 1055f59925b4df67ec058f328cc8fec80f382a7a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 12:24:10 -0500 Subject: [PATCH 07/94] v0.118.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 033ff8b69c..cc4393bffa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11882,7 +11882,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.117.0" +version = "0.118.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f665cc36db..0c115fb285 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.117.0" +version = "0.118.0" publish = false [lib] From 07a266d93fca26d5df3e415571ab9f317232cc89 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 12:39:29 -0500 Subject: [PATCH 08/94] collab 0.31.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc4393bffa..74ffb82f68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1753,7 +1753,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.30.1" +version = "0.31.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 674a324d97..fd5c183d2a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.30.1" +version = "0.31.0" publish = false [[bin]] From 61ab1834c799b1bd6c42dce1053ed0aca6bde643 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 13:06:53 -0500 Subject: [PATCH 09/94] Deploy the buffer search in a second row in the toolbar (#3630) This PR updates the buffer search to deploy to a second row in the toolbar, instead of trying to deploy into the initial row. This is the same way it works in Zed1. Release Notes: - N/A --- crates/search2/src/buffer_search.rs | 8 +++++++- crates/workspace2/src/toolbar.rs | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 2598049ce6..45495e5027 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -23,7 +23,7 @@ use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; #[derive(PartialEq, Clone, Deserialize)] @@ -456,6 +456,9 @@ impl BufferSearchBar { cx.focus(&handle); } cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Hidden, + )); cx.notify(); } @@ -488,6 +491,9 @@ impl BufferSearchBar { self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary, + )); true } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 1cc71e4d84..7971df40ff 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -74,12 +74,24 @@ impl Toolbar { } }) } + + fn secondary_items(&self) -> impl Iterator { + self.items.iter().filter_map(|(item, location)| { + if *location == ToolbarItemLocation::Secondary { + Some(item.as_ref()) + } else { + None + } + }) + } } impl Render for Toolbar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let secondary_item = self.secondary_items().next().map(|item| item.to_any()); + v_stack() .border_b() .border_color(cx.theme().colors().border_variant) @@ -87,8 +99,10 @@ impl Render for Toolbar { .child( h_stack() .justify_between() - .children(self.items.iter().map(|(child, _)| child.to_any())), + .child(h_stack().children(self.left_items().map(|item| item.to_any()))) + .child(h_stack().children(self.right_items().map(|item| item.to_any()))), ) + .children(secondary_item) } } From 9a7de98242b86688741445415f4eecaee3e63cf9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 14:15:03 -0500 Subject: [PATCH 10/94] Don't show empty documentation labels in completions menu (#3632) This PR fixes an issue where we would sometimes have extra blank lines in the completions menu. This was due to some items including documentation labels that were empty strings. Release Notes: - N/A --- crates/editor2/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 76085aeca8..0d19b53d29 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1250,6 +1250,7 @@ impl CompletionsMenu { let documentation_label = if let Some(Documentation::SingleLine(text)) = documentation { Some(SharedString::from(text.clone())) + .filter(|text| !text.trim().is_empty()) } else { None }; From 5c8257585aee12aae09986394a3dcce5de9c2c68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 14:50:15 -0500 Subject: [PATCH 11/94] Fix styling of project search tabs (#3633) This PR fixes the styling of the project search tabs. We now have spacing between the icon and the tab label, as well as use the correct color for the icon based on whether the tab is active or not. Release Notes: - N/A --- crates/search2/src/project_search.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index f1b0c16d57..21df6d9aa2 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -417,7 +417,12 @@ impl Item for ProjectSearchView { .filter(|query| !query.is_empty()) .unwrap_or_else(|| "Project search".into()); h_stack() - .child(IconElement::new(Icon::MagnifyingGlass)) + .gap_2() + .child(IconElement::new(Icon::MagnifyingGlass).color(if selected { + Color::Default + } else { + Color::Muted + })) .child(Label::new(tab_name).color(if selected { Color::Default } else { From 9ff73d3a0a9042a99ee856b22dd6ed8d2f2bf356 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Dec 2023 13:35:49 -0700 Subject: [PATCH 12/94] Port project_symbols --- Cargo.lock | 25 ++ Cargo.toml | 1 + crates/picker2/Cargo.toml | 1 + crates/picker2/src/picker2.rs | 16 +- crates/project_symbols2/Cargo.toml | 37 ++ .../project_symbols2/src/project_symbols.rs | 411 ++++++++++++++++++ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 8 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 crates/project_symbols2/Cargo.toml create mode 100644 crates/project_symbols2/src/project_symbols.rs diff --git a/Cargo.lock b/Cargo.lock index 033ff8b69c..b29c28c6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6510,6 +6510,7 @@ dependencies = [ "theme2", "ui2", "util", + "workspace2", ] [[package]] @@ -7019,6 +7020,29 @@ dependencies = [ "workspace", ] +[[package]] +name = "project_symbols2" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor2", + "futures 0.3.28", + "fuzzy2", + "gpui2", + "language2", + "lsp2", + "ordered-float 2.10.0", + "picker2", + "postage", + "project2", + "settings2", + "smol", + "text2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "prometheus" version = "0.13.3" @@ -12081,6 +12105,7 @@ dependencies = [ "postage", "project2", "project_panel2", + "project_symbols2", "quick_action_bar2", "rand 0.8.5", "recent_projects2", diff --git a/Cargo.toml b/Cargo.toml index 2190066df5..95cf2ae78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "crates/project_panel", "crates/project_panel2", "crates/project_symbols", + "crates/project_symbols2", "crates/quick_action_bar2", "crates/recent_projects", "crates/recent_projects2", diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 3c4d21ad50..e94702ff9c 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2"} parking_lot.workspace = true diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index db5eebff53..6543eb7213 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,11 +1,12 @@ use editor::Editor; use gpui::{ div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter, - FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, - View, ViewContext, WindowContext, + FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, + UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; +use workspace::ModalView; pub struct Picker { pub delegate: D, @@ -13,6 +14,7 @@ pub struct Picker { editor: View, pending_update_matches: Option>, confirm_on_update: Option, + width: Option, } pub trait PickerDelegate: Sized + 'static { @@ -55,11 +57,17 @@ impl Picker { scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, confirm_on_update: None, + width: None, }; this.update_matches("".to_string(), cx); this } + pub fn width(mut self, width: impl Into) -> Self { + self.width = Some(width.into()); + self + } + pub fn focus(&self, cx: &mut WindowContext) { self.editor.update(cx, |editor, cx| editor.focus(cx)); } @@ -197,6 +205,7 @@ impl Picker { } impl EventEmitter for Picker {} +impl ModalView for Picker {} impl Render for Picker { type Element = Div; @@ -221,6 +230,9 @@ impl Render for Picker { div() .key_context("picker") .size_full() + .when_some(self.width, |el, width| { + el.w(width) + }) .overflow_hidden() .elevation_3(cx) .on_action(cx.listener(Self::select_next)) diff --git a/crates/project_symbols2/Cargo.toml b/crates/project_symbols2/Cargo.toml new file mode 100644 index 0000000000..e11dd373a8 --- /dev/null +++ b/crates/project_symbols2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "project_symbols2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project_symbols.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = {package = "fuzzy2", path = "../fuzzy2" } +gpui = {package = "gpui2", path = "../gpui2" } +picker = {package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +text = {package = "text2", path = "../text2" } +settings = {package = "settings2", path = "../settings2" } +workspace = {package = "workspace2", path = "../workspace2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } + +anyhow.workspace = true +ordered-float.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +futures.workspace = true +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/project_symbols2/src/project_symbols.rs b/crates/project_symbols2/src/project_symbols.rs new file mode 100644 index 0000000000..da67fc888f --- /dev/null +++ b/crates/project_symbols2/src/project_symbols.rs @@ -0,0 +1,411 @@ +use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, + View, ViewContext, WeakView, +}; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use project::{Project, Symbol}; +use std::{borrow::Cow, cmp::Reverse, sync::Arc}; +use theme::ActiveTheme; +use util::ResultExt; +use workspace::{ + ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable}, + Workspace, +}; + +actions!(project_symbols, [Toggle]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace.register_action(|workspace, _: &Toggle, cx| { + let project = workspace.project().clone(); + let handle = cx.view().downgrade(); + workspace.toggle_modal(cx, move |cx| { + let delegate = ProjectSymbolsDelegate::new(handle, project); + Picker::new(delegate, cx).width(rems(34.)) + }) + }); + }, + ) + .detach(); +} + +pub type ProjectSymbols = View>; + +pub struct ProjectSymbolsDelegate { + workspace: WeakView, + project: Model, + selected_match_index: usize, + symbols: Vec, + visible_match_candidates: Vec, + external_match_candidates: Vec, + show_worktree_root_name: bool, + matches: Vec, +} + +impl ProjectSymbolsDelegate { + fn new(workspace: WeakView, project: Model) -> Self { + Self { + workspace, + project, + selected_match_index: 0, + symbols: Default::default(), + visible_match_candidates: Default::default(), + external_match_candidates: Default::default(), + matches: Default::default(), + show_worktree_root_name: false, + } + } + + fn filter(&mut self, query: &str, cx: &mut ViewContext>) { + const MAX_MATCHES: usize = 100; + let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( + &self.visible_match_candidates, + query, + false, + MAX_MATCHES, + &Default::default(), + cx.background_executor().clone(), + )); + let mut external_matches = cx.background_executor().block(fuzzy::match_strings( + &self.external_match_candidates, + query, + false, + MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), + &Default::default(), + cx.background_executor().clone(), + )); + let sort_key_for_match = |mat: &StringMatch| { + let symbol = &self.symbols[mat.candidate_id]; + ( + Reverse(OrderedFloat(mat.score)), + &symbol.label.text[symbol.label.filter_range.clone()], + ) + }; + + visible_matches.sort_unstable_by_key(sort_key_for_match); + external_matches.sort_unstable_by_key(sort_key_for_match); + let mut matches = visible_matches; + matches.append(&mut external_matches); + + for mat in &mut matches { + let symbol = &self.symbols[mat.candidate_id]; + let filter_start = symbol.label.filter_range.start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches; + self.set_selected_index(0, cx); + } +} + +impl PickerDelegate for ProjectSymbolsDelegate { + type ListItem = ListItem; + fn placeholder_text(&self) -> Arc { + "Search project symbols...".into() + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + if let Some(symbol) = self + .matches + .get(self.selected_match_index) + .map(|mat| self.symbols[mat.candidate_id].clone()) + { + let buffer = self.project.update(cx, |project, cx| { + project.open_buffer_for_symbol(&symbol, cx) + }); + let symbol = symbol.clone(); + let workspace = self.workspace.clone(); + cx.spawn(|_, mut cx| async move { + let buffer = buffer.await?; + workspace.update(&mut cx, |workspace, cx| { + let position = buffer + .read(cx) + .clip_point_utf16(symbol.range.start, Bias::Left); + + let editor = if secondary { + workspace.split_project_item::(buffer, cx) + } else { + workspace.open_project_item::(buffer, cx) + }; + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([position..position]) + }); + }); + })?; + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + cx.emit(DismissEvent); + } + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + self.selected_match_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + self.filter(&query, cx); + self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; + let symbols = self + .project + .update(cx, |project, cx| project.symbols(&query, cx)); + cx.spawn(|this, mut cx| async move { + let symbols = symbols.await.log_err(); + if let Some(symbols) = symbols { + this.update(&mut cx, |this, cx| { + let delegate = &mut this.delegate; + let project = delegate.project.read(cx); + let (visible_match_candidates, external_match_candidates) = symbols + .iter() + .enumerate() + .map(|(id, symbol)| { + StringMatchCandidate::new( + id, + symbol.label.text[symbol.label.filter_range.clone()].to_string(), + ) + }) + .partition(|candidate| { + project + .entry_for_path(&symbols[candidate.id].path, cx) + .map_or(false, |e| !e.is_ignored) + }); + + delegate.visible_match_candidates = visible_match_candidates; + delegate.external_match_candidates = external_match_candidates; + delegate.symbols = symbols; + delegate.filter(&query, cx); + }) + .log_err(); + } + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let string_match = &self.matches[ix]; + let symbol = &self.symbols[string_match.candidate_id]; + let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax()); + + let mut path = symbol.path.path.to_string_lossy(); + if self.show_worktree_root_name { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) { + path = Cow::Owned(format!( + "{}{}{}", + worktree.read(cx).root_name(), + std::path::MAIN_SEPARATOR, + path.as_ref() + )); + } + } + let label = symbol.label.text.clone(); + let path = path.to_string().clone(); + + let highlights = gpui::combine_highlights( + string_match + .positions + .iter() + .map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())), + syntax_runs.map(|(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) + }), + ); + + Some( + ListItem::new(ix).inset(true).selected(selected).child( + // todo!() combine_syntax_and_fuzzy_match_highlights() + v_stack() + .child( + LabelLike::new().child( + StyledText::new(label) + .with_highlights(&cx.text_style().clone(), highlights), + ), + ) + .child(Label::new(path).color(Color::Muted)), + ), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use gpui::{serde_json::json, TestAppContext, VisualContext}; + use language::{FakeLspAdapter, Language, LanguageConfig}; + use project::FakeFs; + use settings::SettingsStore; + use std::{path::Path, sync::Arc}; + + #[gpui::test] + async fn test_project_symbols(cx: &mut TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::::default()) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/test.rs", cx) + }) + .await + .unwrap(); + + // Set up fake language server to return fuzzy matches against + // a fixed set of symbol names. + let fake_symbols = [ + symbol("one", "/external"), + symbol("ton", "/dir/test.rs"), + symbol("uno", "/dir/test.rs"), + ]; + let fake_server = fake_servers.next().await.unwrap(); + fake_server.handle_request::( + move |params: lsp::WorkspaceSymbolParams, cx| { + let executor = cx.background_executor().clone(); + let fake_symbols = fake_symbols.clone(); + async move { + let candidates = fake_symbols + .iter() + .enumerate() + .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone())) + .collect::>(); + let matches = if params.query.is_empty() { + Vec::new() + } else { + fuzzy::match_strings( + &candidates, + ¶ms.query, + true, + 100, + &Default::default(), + executor.clone(), + ) + .await + }; + + Ok(Some(lsp::WorkspaceSymbolResponse::Flat( + matches + .into_iter() + .map(|mat| fake_symbols[mat.candidate_id].clone()) + .collect(), + ))) + } + }, + ); + + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // Create the project symbols view. + let symbols = cx.build_view(|cx| { + Picker::new( + ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), + cx, + ) + }); + + // Spawn multiples updates before the first update completes, + // such that in the end, there are no matches. Testing for regression: + // https://github.com/zed-industries/zed/issues/861 + symbols.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("on".to_string(), cx); + p.update_matches("onex".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + assert_eq!(symbols.delegate.matches.len(), 0); + }); + + // Spawn more updates such that in the end, there are matches. + symbols.update(cx, |p, cx| { + p.update_matches("one".to_string(), cx); + p.update_matches("on".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + let delegate = &symbols.delegate; + assert_eq!(delegate.matches.len(), 2); + assert_eq!(delegate.matches[0].string, "ton"); + assert_eq!(delegate.matches[1].string, "one"); + }); + + // Spawn more updates such that in the end, there are again no matches. + symbols.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + assert_eq!(symbols.delegate.matches.len(), 0); + }); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + }); + } + + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { + #[allow(deprecated)] + lsp::SymbolInformation { + name: name.to_string(), + kind: lsp::SymbolKind::FUNCTION, + tags: None, + deprecated: None, + container_name: None, + location: lsp::Location::new( + lsp::Url::from_file_path(path.as_ref()).unwrap(), + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + ), + } + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 859afee4f7..6646eb5ffc 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } -# project_symbols = { path = "../project_symbols" } +project_symbols = { package = "project_symbols2", path = "../project_symbols2" } quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } recent_projects = { package = "recent_projects2", path = "../recent_projects2" } rope = { package = "rope2", path = "../rope2"} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index bbb8382cb2..ca8cd7a2a2 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -205,7 +205,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); outline::init(cx); - // project_symbols::init(cx); + project_symbols::init(cx); project_panel::init(Assets, cx); channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); From 0e19da3107657fcdda07aa7145611a1f762dd126 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:53:53 +0100 Subject: [PATCH 13/94] Bring back semantic search UI --- crates/search2/src/project_search.rs | 340 ++++++++++----------------- 1 file changed, 128 insertions(+), 212 deletions(-) diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index 34cd2b9814..875d2fe095 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -1,7 +1,8 @@ use crate::{ - history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, - NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, + history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode, + ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, + ToggleWholeWord, }; use anyhow::{Context as _, Result}; use collections::HashMap; @@ -29,7 +30,7 @@ use std::{ mem, ops::{Not, Range}, path::PathBuf, - time::Duration, + time::{Duration, Instant}, }; use ui::{ @@ -283,196 +284,86 @@ impl Render for ProjectSearchView { let model = self.model.read(cx); let has_no_results = model.no_results.unwrap_or(false); let is_search_underway = model.pending_search.is_some(); - let major_text = if is_search_underway { + let mut major_text = if is_search_underway { Label::new("Searching...") } else if has_no_results { - Label::new("No results for a given query") + Label::new("No results") } else { Label::new(format!("{} search all files", self.current_mode.label())) }; + + let mut show_minor_text = true; + let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { + let status = semantic.index_status; + match status { + SemanticIndexStatus::NotAuthenticated => { + major_text = Label::new("Not Authenticated"); + show_minor_text = false; + Some( + "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()) + } + SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + if remaining_files == 0 { + Some("Indexing...".to_string()) + } else { + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = + rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) { + Some(format!( + "Remaining files to index (rate limit resets in {}s): {}", + remaining_seconds.as_secs(), + remaining_files + )) + } else { + Some(format!("Remaining files to index: {}", remaining_files)) + } + } else { + Some(format!("Remaining files to index: {}", remaining_files)) + } + } + } + SemanticIndexStatus::NotIndexed => None, + } + }); let major_text = div().justify_center().max_w_96().child(major_text); - let middle_text = div() - .items_center() - .max_w_96() - .child(Label::new(self.landing_text_minor()).size(LabelSize::Small)); + + let minor_text: Option = if let Some(no_results) = model.no_results { + if model.pending_search.is_none() && no_results { + Some("No results found in this project for the provided query".into()) + } else { + None + } + } else { + if let Some(mut semantic_status) = semantic_status { + semantic_status.extend(self.landing_text_minor().chars()); + Some(semantic_status.into()) + } else { + Some(self.landing_text_minor()) + } + }; + let minor_text = minor_text.map(|text| { + div() + .items_center() + .max_w_96() + .child(Label::new(text).size(LabelSize::Small)) + }); v_stack().flex_1().size_full().justify_center().child( h_stack() .size_full() .justify_center() .child(h_stack().flex_1()) - .child(v_stack().child(major_text).child(middle_text)) + .child(v_stack().child(major_text).children(minor_text)) .child(h_stack().flex_1()), ) } } } -// impl Entity for ProjectSearchView { -// type Event = ViewEvent; -// } - -// impl View for ProjectSearchView { -// fn ui_name() -> &'static str { -// "ProjectSearchView" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let model = &self.model.read(cx); -// if model.match_ranges.is_empty() { -// enum Status {} - -// let theme = theme::current(cx).clone(); - -// // If Search is Active -> Major: Searching..., Minor: None -// // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} -// // If Regex -> Major: "Search using Regex", Minor: {ex...} -// // If Text -> Major: "Text search all files and folders", Minor: {...} - -// let current_mode = self.current_mode; -// let mut major_text = if model.pending_search.is_some() { -// Cow::Borrowed("Searching...") -// } else if model.no_results.is_some_and(|v| v) { -// Cow::Borrowed("No Results") -// } else { -// match current_mode { -// SearchMode::Text => Cow::Borrowed("Text search all files and folders"), -// SearchMode::Semantic => { -// Cow::Borrowed("Search all code objects using Natural Language") -// } -// SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), -// } -// }; - -// let mut show_minor_text = true; -// let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { -// let status = semantic.index_status; -// match status { -// SemanticIndexStatus::NotAuthenticated => { -// major_text = Cow::Borrowed("Not Authenticated"); -// show_minor_text = false; -// Some(vec![ -// "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables." -// .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()]) -// } -// SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]), -// SemanticIndexStatus::Indexing { -// remaining_files, -// rate_limit_expiry, -// } => { -// if remaining_files == 0 { -// Some(vec![format!("Indexing...")]) -// } else { -// if let Some(rate_limit_expiry) = rate_limit_expiry { -// let remaining_seconds = -// rate_limit_expiry.duration_since(Instant::now()); -// if remaining_seconds > Duration::from_secs(0) { -// Some(vec![format!( -// "Remaining files to index (rate limit resets in {}s): {}", -// remaining_seconds.as_secs(), -// remaining_files -// )]) -// } else { -// Some(vec![format!("Remaining files to index: {}", remaining_files)]) -// } -// } else { -// Some(vec![format!("Remaining files to index: {}", remaining_files)]) -// } -// } -// } -// SemanticIndexStatus::NotIndexed => None, -// } -// }); - -// let minor_text = if let Some(no_results) = model.no_results { -// if model.pending_search.is_none() && no_results { -// vec!["No results found in this project for the provided query".to_owned()] -// } else { -// vec![] -// } -// } else { -// match current_mode { -// SearchMode::Semantic => { -// let mut minor_text: Vec = Vec::new(); -// minor_text.push("".into()); -// if let Some(semantic_status) = semantic_status { -// minor_text.extend(semantic_status); -// } -// if show_minor_text { -// minor_text -// .push("Simply explain the code you are looking to find.".into()); -// minor_text.push( -// "ex. 'prompt user for permissions to index their project'".into(), -// ); -// } -// minor_text -// } -// _ => vec![ -// "".to_owned(), -// "Include/exclude specific paths with the filter option.".to_owned(), -// "Matching exact word and/or casing is available too.".to_owned(), -// ], -// } -// }; - -// MouseEventHandler::new::(0, cx, |_, _| { -// Flex::column() -// .with_child(Flex::column().contained().flex(1., true)) -// .with_child( -// Flex::column() -// .align_children_center() -// .with_child(Label::new( -// major_text, -// theme.search.major_results_status.clone(), -// )) -// .with_children( -// minor_text.into_iter().map(|x| { -// Label::new(x, theme.search.minor_results_status.clone()) -// }), -// ) -// .aligned() -// .top() -// .contained() -// .flex(7., true), -// ) -// .contained() -// .with_background_color(theme.editor.background) -// }) -// .on_down(MouseButton::Left, |_, _, cx| { -// cx.focus_parent(); -// }) -// .into_any_named("project search view") -// } else { -// ChildView::new(&self.results_editor, cx) -// .flex(1., true) -// .into_any_named("project search view") -// } -// } - -// fn focus_in(&mut self, _: AnyView, cx: &mut ViewContext) { -// let handle = cx.weak_handle(); -// cx.update_global(|state: &mut ActiveSearches, cx| { -// state -// .0 -// .insert(self.model.read(cx).project.downgrade(), handle) -// }); - -// cx.update_global(|state: &mut ActiveSettings, cx| { -// state.0.insert( -// self.model.read(cx).project.downgrade(), -// self.current_settings(), -// ); -// }); - -// if cx.is_self_focused() { -// if self.query_editor_was_focused { -// cx.focus(&self.query_editor); -// } else { -// cx.focus(&self.results_editor); -// } -// } -// } -// } - impl FocusableView for ProjectSearchView { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { self.results_editor.focus_handle(cx) @@ -1256,7 +1147,7 @@ impl ProjectSearchView { fn landing_text_minor(&self) -> SharedString { match self.current_mode { SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(), - SearchMode::Semantic => ".Simply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into() + SearchMode::Semantic => "\nSimply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into() } } } @@ -1277,8 +1168,8 @@ impl ProjectSearchBar { fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext) { if let Some(view) = self.active_project_search.as_ref() { view.update(cx, |this, cx| { - // todo: po: 2nd argument of `next_mode` should be `SemanticIndex::enabled(cx))`, but we need to flesh out port of semantic_index first. - let new_mode = crate::mode::next_mode(&this.current_mode, false); + let new_mode = + crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); this.activate_search_mode(new_mode, cx); let editor_handle = this.query_editor.focus_handle(cx); cx.focus(&editor_handle); @@ -1548,7 +1439,7 @@ impl Render for ProjectSearchBar { }); } let search = search.read(cx); - + let semantic_is_available = SemanticIndex::enabled(cx); let query_column = v_stack() //.flex_1() .child( @@ -1578,42 +1469,51 @@ impl Render for ProjectSearchBar { .unwrap_or_default(), ), ) - .child( - IconButton::new( - "project-search-case-sensitive", - Icon::CaseSensitive, - ) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle case sensitive", - &ToggleCaseSensitive, - cx, + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + IconButton::new( + "project-search-case-sensitive", + Icon::CaseSensitive, ) - }) - .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) - .on_click(cx.listener( - |this, _, cx| { - this.toggle_search_option( - SearchOptions::CASE_SENSITIVE, - cx, - ); - }, - )), - ) - .child( - IconButton::new("project-search-whole-word", Icon::WholeWord) .tooltip(|cx| { Tooltip::for_action( - "Toggle whole word", - &ToggleWholeWord, + "Toggle case sensitive", + &ToggleCaseSensitive, cx, ) }) - .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) - .on_click(cx.listener(|this, _, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - })), - ), + .selected( + self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx), + ) + .on_click(cx.listener( + |this, _, cx| { + this.toggle_search_option( + SearchOptions::CASE_SENSITIVE, + cx, + ); + }, + )), + ) + .child( + IconButton::new("project-search-whole-word", Icon::WholeWord) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle whole word", + &ToggleWholeWord, + cx, + ) + }) + .selected( + self.is_option_enabled(SearchOptions::WHOLE_WORD, cx), + ) + .on_click(cx.listener(|this, _, cx| { + this.toggle_search_option( + SearchOptions::WHOLE_WORD, + cx, + ); + })), + ) + }), ) .border_2() .bg(white()) @@ -1668,7 +1568,23 @@ impl Render for ProjectSearchBar { cx, ) }), - ), + ) + .when(semantic_is_available, |this| { + this.child( + Button::new("project-search-semantic-button", "Semantic") + .selected(search.current_mode == SearchMode::Semantic) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Semantic, cx) + })) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle semantic search", + &ActivateSemanticMode, + cx, + ) + }), + ) + }), ) .child( IconButton::new("project-search-toggle-replace", Icon::Replace) From ce1489f5dcb6471216d7a66924f880333afdddf4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:22:29 +0100 Subject: [PATCH 14/94] Add inclusion of ignored files --- crates/search2/src/project_search.rs | 75 ++++++++++++++++++---------- crates/search2/src/search.rs | 5 ++ 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index 875d2fe095..f1b0c16d57 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -1,8 +1,8 @@ use crate::{ history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, - ToggleWholeWord, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, + ToggleReplace, ToggleWholeWord, }; use anyhow::{Context as _, Result}; use collections::HashMap; @@ -1530,7 +1530,22 @@ impl Render for ProjectSearchBar { .flex_1() .border_1() .mr_2() - .child(search.included_files_editor.clone()), + .child(search.included_files_editor.clone()) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + SearchOptions::INCLUDE_IGNORED.as_button( + search + .search_options + .contains(SearchOptions::INCLUDE_IGNORED), + cx.listener(|this, _, cx| { + this.toggle_search_option( + SearchOptions::INCLUDE_IGNORED, + cx, + ); + }), + ), + ) + }), ) .child( h_stack() @@ -1671,34 +1686,14 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, _: &ToggleFilters, cx| { this.toggle_filters(cx); })) - .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - })) - .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - })) - .on_action(cx.listener(|this, action, cx| { - this.toggle_replace(action, cx); - })) .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx) })) .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { this.activate_search_mode(SearchMode::Regex, cx) })) - .on_action(cx.listener(|this, action, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(action, cx); - }) - } - })) - .on_action(cx.listener(|this, action, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(action, cx); - }) - } + .on_action(cx.listener(|this, _: &ActivateSemanticMode, cx| { + this.activate_search_mode(SearchMode::Semantic, cx) })) .on_action(cx.listener(|this, action, cx| { this.tab(action, cx); @@ -1709,6 +1704,36 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, action, cx| { this.cycle_mode(action, cx); })) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.on_action(cx.listener(|this, action, cx| { + this.toggle_replace(action, cx); + })) + .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })) + .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(action, cx); + }) + } + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(action, cx); + }) + } + })) + .when(search.filters_enabled, |this| { + this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, cx| { + this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); + })) + }) + }) .child(query_column) .child(mode_column) .child(replace_column) diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 015c126aa1..18fcc258f4 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -28,6 +28,7 @@ actions!( CycleMode, ToggleWholeWord, ToggleCaseSensitive, + ToggleIncludeIgnored, ToggleReplace, SelectNextMatch, SelectPrevMatch, @@ -57,6 +58,7 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => "Match Whole Word", SearchOptions::CASE_SENSITIVE => "Match Case", + SearchOptions::INCLUDE_IGNORED => "Include ignored", _ => panic!("{:?} is not a named SearchOption", self), } } @@ -65,6 +67,7 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, + SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit, _ => panic!("{:?} is not a named SearchOption", self), } } @@ -73,6 +76,7 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), _ => panic!("{:?} is not a named SearchOption", self), } } @@ -85,6 +89,7 @@ impl SearchOptions { let mut options = SearchOptions::NONE; options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); + options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored()); options } From 85a1a8f777f4a8586d7adcd88863f6afeb086b2c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 11 Dec 2023 19:17:13 -0500 Subject: [PATCH 15/94] WIP --- docs/.gitignore | 2 + docs/book.toml | 6 + docs/src/CODE_OF_CONDUCT.md | 128 ++ docs/src/CONTRIBUTING.md | 3 + docs/src/SUMMARY.md | 18 + docs/src/configuring_zed.md | 1035 +++++++++++++++++ docs/src/configuring_zed__configuring_vim.md | 170 +++ docs/src/developing_zed__adding_languages.md | 83 ++ docs/src/developing_zed__building_zed.md | 107 ++ .../developing_zed__local_collaboration.md} | 0 docs/src/feedback.md | 29 + docs/src/getting_started.md | 15 + docs/src/system_requirements.md | 13 + docs/src/telemetry.md | 147 +++ {docs => docs_old}/backend-development.md | 0 {docs => docs_old}/building-zed.md | 0 {docs => docs_old}/company-and-vision.md | 0 {docs => docs_old}/design-tools.md | 0 {docs => docs_old}/index.md | 0 docs_old/local-collaboration.md | 22 + {docs => docs_old}/release-process.md | 0 .../theme/generating-theme-types.md | 0 {docs => docs_old}/tools.md | 0 {docs => docs_old}/zed/syntax-highlighting.md | 0 24 files changed, 1778 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/book.toml create mode 100644 docs/src/CODE_OF_CONDUCT.md create mode 100644 docs/src/CONTRIBUTING.md create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/configuring_zed.md create mode 100644 docs/src/configuring_zed__configuring_vim.md create mode 100644 docs/src/developing_zed__adding_languages.md create mode 100644 docs/src/developing_zed__building_zed.md rename docs/{local-collaboration.md => src/developing_zed__local_collaboration.md} (100%) create mode 100644 docs/src/feedback.md create mode 100644 docs/src/getting_started.md create mode 100644 docs/src/system_requirements.md create mode 100644 docs/src/telemetry.md rename {docs => docs_old}/backend-development.md (100%) rename {docs => docs_old}/building-zed.md (100%) rename {docs => docs_old}/company-and-vision.md (100%) rename {docs => docs_old}/design-tools.md (100%) rename {docs => docs_old}/index.md (100%) create mode 100644 docs_old/local-collaboration.md rename {docs => docs_old}/release-process.md (100%) rename {docs => docs_old}/theme/generating-theme-types.md (100%) rename {docs => docs_old}/tools.md (100%) rename {docs => docs_old}/zed/syntax-highlighting.md (100%) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..eed3a3e046 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +book +.vercel diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000000..8062a76a42 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Nate Butler"] +language = "en" +multilingual = false +src = "src" +title = "Zed App Docs" diff --git a/docs/src/CODE_OF_CONDUCT.md b/docs/src/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..bc1d5522a0 --- /dev/null +++ b/docs/src/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hi@zed.dev. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/docs/src/CONTRIBUTING.md b/docs/src/CONTRIBUTING.md new file mode 100644 index 0000000000..d48c26844f --- /dev/null +++ b/docs/src/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +TBD diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000000..4c6f5f9421 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,18 @@ +# Summary + +[Getting Started](./getting_started.md) +[Feedback](./feedback.md) + +# Configuring Zed +- [Settings](./configuring_zed.md) +- [Vim Mode](./configuring_zed__configuring_vim.md) + +# Developing Zed +- [Building from Source](./developing_zed__building_zed.md) +- [Local Collaboration](./developing_zed__local_collaboration.md) +- [Adding Languages](./developing_zed__adding_languages.md) + +# Other +- [Code of Conduct](./CODE_OF_CONDUCT.md) +- [Telemetry](./telemetry.md) +- [Contributing](./CONTRIBUTING.md) diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md new file mode 100644 index 0000000000..9b9205f70c --- /dev/null +++ b/docs/src/configuring_zed.md @@ -0,0 +1,1035 @@ +# Configuring Zed + +## Folder-specific settings + +Folder-specific settings are used to override Zed's global settings for files within a specific directory in the project panel. To get started, create a `.zed` subdirectory and add a `settings.json` within it. It should be noted that folder-specific settings don't need to live only a project's root, but can be defined at multiple levels in the project hierarchy. In setups like this, Zed will find the configuration nearest to the file you are working in and apply those settings to it. In most cases, this level of flexibility won't be needed and a single configuration for all files in a project is all that is required; the `Zed > Settings > Open Local Settings` menu action is built for this case. Running this action will look for a `.zed/settings.json` file at the root of the first top-level directory in your project panel. If it does not exist, it will create it. + +The following global settings can be overriden with a folder-specific configuration: + +- `copilot` +- `enable_language_server` +- `ensure_final_newline_on_save` +- `format_on_save` +- `formatter` +- `hard_tabs` +- `language_overrides` +- `preferred_line_length` +- `remove_trailing_whitespace_on_save` +- `soft_wrap` +- `tab_size` +- `show_copilot_suggestions` +- `show_whitespaces` + +*See the Global settings section for details about these settings* + +## Global settings + +To get started with editing Zed's global settings, open `~/.config/zed/settings.json` via `cmd-,`, the command palette (`zed: open settings`), or the `Zed > Settings > Open Settings` application menu item. + +Here are all the currently available settings. + +## Active Pane Magnification + +- Description: 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. +- Setting: `active_pane_magnification` +- Default: `1.0` + +**Options** + +`float` values + +## Autosave + +- Description: When to automatically save edited buffers. +- Setting: `autosave` +- Default: `off` + +**Options** + +1. To disable autosave, set it to `off` + +```json +{ + "autosave": "off" +} +``` + +2. To autosave when focus changes, use `on_focus_change`: + +```json +{ + "autosave": "on_focus_change" +} +``` + +3. To autosave when the active window changes, use `on_window_change`: + +```json +{ + "autosave": "on_window_change" +} +``` + +4. To autosave after an inactivity period, use `after_delay`: + +```json +{ + "autosave": { + "after_delay": { + "milliseconds": 1000 + } + } +} +``` + +## Auto Update + +- Description: Whether or not to automatically check for updates. +- Setting: `auto_update` +- Default: `true` + +**Options** + +`boolean` values + +## Buffer Font Family + +- Description: The name of a font to use for rendering text in the editor. +- Setting: `buffer_font_family` +- Default: `Zed Mono` + +**Options** + +The name of any font family installed on the user's system + +## Buffer Font Features + +- Description: The OpenType features to enable for text in the editor. +- Setting: `buffer_font_features` +- Default: `null` + +**Options** + +Zed supports a subset of OpenType features that can be enabled or disabled for a given buffer or terminal font. The following [OpenType features](https://en.wikipedia.org/wiki/List_of_typographic_features) can be enabled or disabled too: `calt`, `case`, `cpsp`, `frac`, `liga`, `onum`, `ordn`, `pnum`, `ss01`, `ss02`, `ss03`, `ss04`, `ss05`, `ss06`, `ss07`, `ss08`, `ss09`, `ss10`, `ss11`, `ss12`, `ss13`, `ss14`, `ss15`, `ss16`, `ss17`, `ss18`, `ss19`, `ss20`, `subs`, `sups`, `swsh`, `titl`, `tnum`, `zero`. + +For example, to disable ligatures for a given font you can add the following to your settings: + +```json +{ + "buffer_font_features": { + "calt": false + } +} +``` + +## Buffer Font Size + +- Description: The default font size for text in the editor. +- Setting: `buffer_font_size` +- Default: `15` + +**Options** + +`integer` values + +## Confirm Quit + +- Description: Whether or not to prompt the user to confirm before closing the application. +- Setting: `confirm_quit` +- Default: `false` + +**Options** + +`boolean` values + +## Copilot + +- Description: Copilot-specific settings. +- Setting: `copilot` +- Default: + +```json +"copilot": { + "disabled_globs": [ + ".env" + ] +} +``` + +**Options** + +### Disabled Globs + +- Description: The set of glob patterns for which Copilot should be disabled in any matching file. +- Setting: `disabled_globs` +- Default: [".env"] + +**Options** + +List of `string` values + +## Cursor Blink + +- Description: Whether or not the cursor blinks. +- Setting: `cursor_blink` +- Default: `true` + +**Options** + +`boolean` values + +## Default Dock Anchor + +- Description: The default anchor for new docks. +- Setting: `default_dock_anchor` +- Default: `bottom` + +**Options** + +1. Position the dock attached to the bottom of the workspace: `bottom` +2. Position the dock to the right of the workspace like a side panel: `right` +3. Position the dock full screen over the entire workspace: `expanded` + +## Enable Language Server + +- Description: Whether or not to use language servers to provide code intelligence. +- Setting: `enable_language_server` +- Default: `true` + +**Options** + +`boolean` values + +## Ensure Final Newline On Save + +- Description: Whether or not to ensure there's a single newline at the end of a buffer when saving it. +- Setting: `ensure_final_newline_on_save` +- Default: `true` + +**Options** + +`boolean` values + +## LSP + +- Description: Configuration for language servers. +- Setting: `lsp` +- Default: `null` + +**Options** + +The following settings can be overridden 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. Example: + +```json +"lsp": { + "rust-analyzer": { + "initialization_options": { + "checkOnSave": { + "command": "clippy" // rust-analyzer.checkOnSave.command + } + } + } +} +``` + +## Format On Save + +- Description: Whether or not to perform a buffer format before saving. +- Setting: `format_on_save` +- Default: `on` + +**Options** + +1. `on`, enables format on save obeying `formatter` setting: + +```json +{ + "format_on_save": "on" +} +``` + +2. `off`, disables format on save: + +```json +{ + "format_on_save": "off" +} +``` + +## Formatter + +- Description: How to perform a buffer format. +- Setting: `formatter` +- Default: `language_server` + +**Options** + +1. To use the current language server, use `"language_server"`: + +```json +{ + "formatter": "language_server" +} +``` + +2. Or to use an external command, use `"external"`. Specify the name of the formatting program to run, and an array of arguments to pass to the program. The buffer's text will be passed to the program on stdin, and the formatted output should be written to stdout. For example, the following command would strip trailing spaces using [`sed(1)`](https://linux.die.net/man/1/sed): + +```json +{ + "formatter": { + "external": { + "command": "sed", + "arguments": ["-e", "s/ *$//"] + } + } +} +``` + +## Git + +- Description: Configuration for git-related features. +- Setting: `git` +- Default: + +```json +"git": { + "git_gutter": "tracked_files" +}, +``` + +### Git Gutter + +- Description: Whether or not to show the git gutter. +- Setting: `git_gutter` +- Default: `tracked_files` + +**Options** + +1. Show git gutter in tracked files + +```json +{ + "git_gutter": "tracked_files" +} +``` + +2. Hide git gutter + +```json +{ + "git_gutter": "hide" +} +``` + +## Hard Tabs + +- Description: Whether to indent lines using tab characters or multiple spaces. +- Setting: `hard_tabs` +- Default: `false` + +**Options** + +`boolean` values + +## Hover Popover Enabled + +- Description: Whether or not to show the informational hover box when moving the mouse over symbols in the editor. +- Setting: `hover_popover_enabled` +- Default: `true` + +**Options** + +`boolean` values + +## Inlay hints + +- Description: Configuration for displaying extra text with hints in the editor. +- Setting: `inlay_hints` +- Default: + +```json +"inlay_hints": { + "enabled": false, + "show_type_hints": true, + "show_parameter_hints": true, + "show_other_hints": true +} +``` + +**Options** + +Inlay hints querying consists of two parts: editor (client) and LSP server. +With the inlay settings above are changed to enable the hints, editor will start to query certain types of hints and react on LSP hint refresh request from the server. +At this point, the server may or may not return hints depending on its implementation, further configuration might be needed, refer to the corresponding LSP server documentation. + +Use `lsp` section for the server configuration, below are some examples for well known servers: + +### Rust + +```json +"lsp": { + "rust-analyzer": { + "initialization_options": { + "inlayHints": { + "maxLength": null, + "lifetimeElisionHints": { + "useParameterNames": true, + "enable": "skip_trivial" + }, + "closureReturnTypeHints": { + "enable": "always" + } + } + } + } +} +``` + +### Typescript + +```json +"lsp": { + "typescript-language-server": { + "initialization_options": { + "preferences": { + "includeInlayParameterNameHints": "all", + "includeInlayParameterNameHintsWhenArgumentMatchesName": true, + "includeInlayFunctionParameterTypeHints": true, + "includeInlayVariableTypeHints": true, + "includeInlayVariableTypeHintsWhenTypeMatchesName": false, + "includeInlayPropertyDeclarationTypeHints": true, + "includeInlayFunctionLikeReturnTypeHints": true, + "includeInlayEnumMemberValueHints": true + } + } + } +} +``` + +### Go + +```json +"lsp": { + "gopls": { + "initialization_options": { + "hints": { + "assignVariableTypes": true, + "compositeLiteralFields": true, + "compositeLiteralTypes": true, + "constantValues": true, + "functionTypeParameters": true, + "parameterNames": true, + "rangeVariableTypes": true + } + } + } +} +``` + +### Svelte + +```json +{ + "lsp": { + "typescript-language-server": { + "initialization_options": { + "preferences": { + "includeInlayParameterNameHints": "all", + "includeInlayParameterNameHintsWhenArgumentMatchesName": true, + "includeInlayFunctionParameterTypeHints": true, + "includeInlayVariableTypeHints": true, + "includeInlayVariableTypeHintsWhenTypeMatchesName": false, + "includeInlayPropertyDeclarationTypeHints": true, + "includeInlayFunctionLikeReturnTypeHints": true, + "includeInlayEnumMemberValueHints": true, + "includeInlayEnumMemberDeclarationTypes": true + } + } + } + } +} +``` + +## Journal + +- Description: Configuration for the journal. +- Setting: `journal` +- Default: + +```json +"journal": { + "path": "~", + "hour_format": "hour12" +} +``` + +### Path + +- Description: The path of the directory where journal entries are stored. +- Setting: `path` +- Default: `~` + +**Options** + +`string` values + +### Hour Format + +- Description: The format to use for displaying hours in the journal. +- Setting: `hour_format` +- Default: `hour12` + +**Options** + +1. 12-hour format: + +```json +{ + "hour_format": "hour12" +} +``` + +2. 24-hour format: + +```json +{ + "hour_format": "hour24" +} +``` + +## Language Overrides + +- Description: Configuration overrides for specific languages. +- Setting: `language_overrides` +- Default: `null` + +**Options** + +To override settings for a language, add an entry for that languages name to the `language_overrides` value. Example: + +```json +"language_overrides": { + "C": { + "format_on_save": "off", + "preferred_line_length": 64, + "soft_wrap": "preferred_line_length" + }, + "JSON": { + "tab_size": 4 + } +} +``` + +The following settings can be overridden for each specific language: + +- `enable_language_server` +- `ensure_final_newline_on_save` +- `format_on_save` +- `formatter` +- `hard_tabs` +- `preferred_line_length` +- `remove_trailing_whitespace_on_save` +- `show_copilot_suggestions` +- `show_whitespaces` +- `soft_wrap` +- `tab_size` + +These values take in the same options as the root-level settings with the same name. + +## Preferred Line Length + +- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled. +- Setting: `preferred_line_length` +- Default: `80` + +**Options** + +`integer` values + +## Projects Online By Default + +- Description: Whether or not to show the online projects view by default. +- Setting: `projects_online_by_default` +- Default: `true` + +**Options** + +`boolean` values + +## Remove Trailing Whitespace On Save + +- Description: Whether or not to remove any trailing whitespace from lines of a buffer before saving it. +- Setting: `remove_trailing_whitespace_on_save` +- Default: `true` + +**Options** + +`boolean` values + +## Semantic Index + +- Description: Settings related to semantic index. +- Setting: `semantic_index` +- Default: + +```json +"semantic_index": { + "enabled": false +}, +``` + +### Enabled + +- Description: Whether or not to display the `Semantic` mode in project search. +- Setting: `enabled` +- Default: `true` + +**Options** + +`boolean` values + +## Show Call Status Icon + +- Description: Whether or not to show the call status icon in the status bar. +- Setting: `show_call_status_icon` +- Default: `true` + +**Options** + +`boolean` values + +## Show Completions On Input + +- Description: Whether or not to show completions as you type. +- Setting: `show_completions_on_input` +- Default: `true` + +**Options** + +`boolean` values + +## Show Completion Documentation + +- Description: Whether to display inline and alongside documentation for items in the completions menu. +- Setting: `show_completion_documentation` +- Default: `true` + +**Options** + +`boolean` values + +## Show Copilot Suggestions + +- Description: Whether or not to show Copilot suggestions as you type or wait for a `copilot::Toggle`. +- Setting: `show_copilot_suggestions` +- Default: `true` + +**Options** + +`boolean` values + +## Show Whitespaces + +- Description: Whether or not to show render whitespace characters in the editor. +- Setting: `show_whitespaces` +- Default: `selection` + +**Options** + +1. `all` +2. `selection` +3. `none` + +## Soft Wrap + +- Description: Whether or not to automatically wrap lines of text to fit editor / preferred width. +- Setting: `soft_wrap` +- Default: `none` + +**Options** + +1. `editor_width` +2. `preferred_line_length` +3. `none` + +## Tab Size + +- Description: The number of spaces to use for each tab character. +- Setting: `tab_size` +- Default: `4` + +**Options** + +`integer` values + +## Telemetry + +- Description: Control what info is collected by Zed. +- Setting: `telemetry` +- Default: + +```json +"telemetry": { + "diagnostics": true, + "metrics": true +}, +``` + +**Options** + +### Diagnostics + +- Description: Setting for sending debug-related data, such as crash reports. +- Setting: `diagnostics` +- Default: `true` + +**Options** + +`boolean` values + +### Metrics + +- Description: Setting for sending anonymized usage data, such what languages you're using Zed with. +- Setting: `metrics` +- Default: `true` + +**Options** + +`boolean` values + +## Terminal + +- Description: Configuration for the terminal. +- Setting: `terminal` +- Default: + +```json +"terminal": { + "alternate_scroll": "off", + "blinking": "terminal_controlled", + "copy_on_select": false, + "env": {}, + "font_family": null, + "font_features": null, + "font_size": null, + "option_as_meta": false, + "shell": {}, + "working_directory": "current_project_directory" +} +``` + +### Alternate Scroll + +- Description: Set whether Alternate Scroll mode (DECSET 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 with ANSI escape codes. +- Setting: `alternate_scroll` +- Default: `off` + +**Options** + +1. Default alternate scroll mode to on + +```json +{ + "alternate_scroll": "on" +} +``` + +2. Default alternate scroll mode to off + +```json +{ + "alternate_scroll": "off" +} +``` + +### Blinking + +- Description: Set the cursor blinking behavior in the terminal +- Setting: `blinking` +- Default: `terminal_controlled` + +**Options** + +1. Never blink the cursor, ignore the terminal mode + +```json +{ + "blinking": "off" +} +``` + +2. Default the cursor blink to off, but allow the terminal to turn blinking on + +```json +{ + "blinking": "terminal_controlled" +} +``` + +3. Always blink the cursor, ignore the terminal mode + +```json +"blinking": "on", +``` + +### Copy On Select + +- Description: Whether or not selecting text in the terminal will automatically copy to the system clipboard. +- Setting: `copy_on_select` +- Default: `false` + +**Options** + +`boolean` values + +### Env + +- Description: Any key-value pairs added to this object will be added to the terminal's environment. Keys must be unique, use `:` to separate multiple values in a single variable +- Setting: `env` +- Default: `{}` + +**Example** + +```json +"env": { + "ZED": "1", + "KEY": "value1:value2" +} +``` + +### Font Size + +- Description: What font size to use for the terminal. When not set defaults to matching the editor's font size +- Setting: `font_size` +- Default: `null` + +**Options** + +`integer` values + +### Font Family + +- Description: What font to use for the terminal. When not set, defaults to matching the editor's font. +- Setting: `font_family` +- Default: `null` + +**Options** + +The name of any font family installed on the user's system + +### Font Features + +- Description: What font features to use for the terminal. When not set, defaults to matching the editor's font features. +- Setting: `font_features` +- Default: `null` + +**Options** + +See Buffer Font Features + +### Option As Meta + +- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs. +- Setting: `option_as_meta` +- Default: `true` + +**Options** + +`boolean` values + +### Shell + +- Description: What shell to use when launching the terminal. +- Setting: `shell` +- Default: `system` + +**Options** + +1. Use the system's default terminal configuration (usually the `/etc/passwd` file). + +```json +{ + "shell": "system" +} +``` + +2. A program to launch: + +```json +"shell": { + "program": "sh" +} +``` + +3. A program with arguments: + +```json +"shell": { + "with_arguments": { + "program": "/bin/bash", + "args": ["--login"] + } +} +``` + +### Working Directory + +- Description: What working directory to use when launching the terminal. +- Setting: `working_directory` +- Default: `"current_project_directory"` + +**Options** + +1. Use the current file's project directory. Will Fallback to the first project directory strategy if unsuccessful + +```json +{ + "working_directory": "current_project_directory" +} +``` + +2. Use the first project in this workspace's directory. Will fallback to using this platform's home directory. + +```json +{ + "working_directory": "first_project_directory" +} +``` + +3. Always use this platform's home directory (if we can find it) + +```json +{ + "working_directory": "always_home" +} +``` + +4. Always 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. + +```json +"working_directory": { + "always": { + "directory": "~/zed/projects/" + } +} +``` + +## Theme + +- Description: The name of the Zed theme to use for the UI. +- Setting: `theme` +- Default: `One Dark` + +**Options** + +Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. + +## Vim + +- Description: Whether or not to enable vim mode (work in progress). +- Setting: `vim_mode` +- Default: `false` + +## Project Panel + +- Description: Customise project panel +- Setting: `project_panel` +- Default: + +```json +"project_panel": { + "dock": "left", + "git_status": true, + "default_width": "N/A - width in pixels" +}, +``` + +### Dock + +- Description: Control the position of the dock +- Setting: `dock` +- Default: `left` + +**Options** + +1. Default dock position to left + +```json +{ + "dock": "left" +} +``` + +2. Default dock position to right + +```json +{ + "dock": "right" +} +``` + +### Git Status + +- Description: Indicates newly created and updated files +- Setting: `git_status` +- Default: `true` + +1. Default enable git status + +```json +{ + "git_status": true +} +``` + +2. Default disable git status + +```json +{ + "git_status": false +} +``` + +### Default Width +- Description: Customise default width taken by project panel +- Setting: `default_width` +- Default: N/A width in pixels (eg: 420) + +**Options** + +`boolean` values + +## An example configuration: + +```json +// ~/.config/zed/settings.json +{ + "theme": "cave-light", + "tab_size": 2, + "preferred_line_length": 80, + "soft_wrap": "none", + + "buffer_font_size": 18, + "buffer_font_family": "Zed Mono", + + "autosave": "on_focus_change", + "format_on_save": "off", + "vim_mode": false, + "projects_online_by_default": true, + "terminal": { + "font_family": "FiraCode Nerd Font Mono", + "blinking": "off" + }, + "language_overrides": { + "C": { + "format_on_save": "language_server", + "preferred_line_length": 64, + "soft_wrap": "preferred_line_length" + } + } +} +``` diff --git a/docs/src/configuring_zed__configuring_vim.md b/docs/src/configuring_zed__configuring_vim.md new file mode 100644 index 0000000000..ddef07e3c2 --- /dev/null +++ b/docs/src/configuring_zed__configuring_vim.md @@ -0,0 +1,170 @@ +# Vim Mode + +Zed includes a vim emulation layer known as “vim mode”. This document aims to describe how it works, and how to make the most out of it. + +### Philosophy +Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. + +This means Zed will never be 100% vim compatible, but should be 100% vim familiar! We expect that our vim mode already copes with 90% of your workflow, and we'd like to keep improving it. If you find things that you can’t yet do in vim mode, but which you rely on in your current workflow, please leave feedback in the editor itself (`:feedback`), or [file an issue](https://github.com/zed-industries/community). + +### Zed-specific features +Zed is built on a modern foundation that (among other things) uses tree-sitter to understand the content of the file you're editing, and supports multiple cursors out of the box. + +Vim mode has several "core Zed" key bindings, that will help you make the most of Zed's specific feature set. +``` +# Normal mode +g d Go to definition +g D Go to type definition +c d Rename (change definition) +g A Go to All references to the current word + +g Open the current search excerpt in its own tab + +g s Find symbol in current file +g S Find symbol in entire project + +g n Add a visual selection for the next copy of the current word +g N The same, but backwards +g > Skip latest word selection, and add next. +g < The same, but backwards +g a Add a visual selection for every copy of the current word + +g h Show inline error (hover) + +# Insert mode +ctrl-x ctrl-o Open the completion menu +ctrl-x ctrl-c Request Github Copilot suggestion (if configured) +ctrl-x ctrl-a Open the inline AI assistant (if configured) +ctrl-x ctrl-l Open the LSP code actions +ctrl-x ctrl-z Hides all suggestions +``` + +Vim mode uses Zed to define concepts like "brackets" (for the `%` key) and "words" (for motions like `w` and `e`). This does lead to some differences, but they are mostly positive. For example `%` considers `|` to be a bracket in languages like Rust; and `w` considers `$` to be a word-character in languages like Javascript. + +Vim mode emulates visual block mode using Zed's multiple cursor support. This again leads to some differences, but is much more powerful. + +Finally, Vim mode's search and replace functionality is backed by Zed's. This means that the pattern syntax is slightly different, see the section on [Regex differences](#regex-differences) for details. + +### Custom key bindings +Zed does not yet have an equivalent to vim’s `map` command to convert one set of keystrokes into another, however you can bind any sequence of keys to fire any Action documented in the [Key bindings documentation](https://docs.zed.dev/configuration/key-bindings). + +You can edit your personal key bindings with `:keymap`. +For vim-specific shortcuts, you may find the following template a good place to start: + +```json +[ + { + "context": "Editor && VimControl && !VimWaiting && !menu", + "bindings": { + // put key-bindings here if you want them to work in normal & visual mode + } + }, + { + "context": "Editor && vim_mode == normal && !VimWaiting && !menu", + "bindings": { + // put key-bindings here if you want them to work only in normal mode + } + }, + { + "context": "Editor && vim_mode == visual && !VimWaiting && !menu", + "bindings": { + // visual, visual line & visual block modes + } + }, + { + "context": "Editor && vim_mode == insert && !menu", + "bindings": { + // put key-bindings here if you want them to work in insert mode + } + } +] +``` + +You can see the bindings that are enabled by default in vim mode [here](https://zed.dev/ref/vim.json). + +The details of the context are a little out of scope for this doc, but suffice to say that `menu` is true when a menu is open (e.g. the completions menu), `VimWaiting` is true after you type `f` or `t` when we’re waiting for a new key (and you probably don’t want bindings to happen). Please reach out on [Github](https://github.com/zed-industries/community) if you want help making a key bindings work. + +### Command palette + +Vim mode allows you to enable Zed’s command palette with `:`. This means that you can use vim's command palette to run any action that Zed supports. + +Additionally vim mode contains a number of aliases for popular vim commands to ensure that muscle memory works. For example `:w` will save the file. + +We do not (yet) emulate the full power of vim’s command line, in particular we special case specific patterns instead of using vim's range selection syntax, and we do not support arguments to commands yet. Please reach out on [Github](https://github.com/zed-industries/community) as you find things that are missing from the command palette. + +As mentioned above, one thing to be aware of is that the regex engine is slightly different from vim's in `:%s/a/b`. + +Currently supported vim-specific commands (as of Zed 0.106): +``` +# window management +:w[rite][!], :wq[!], :q[uit][!], :wa[ll][!], :wqa[ll][!], :qa[ll][!], :[e]x[it][!], :up[date] + to save/close tab(s) and pane(s) (no filename is supported yet) +:cq + to quit completely. +:vs[plit], :sp[lit] + to split vertically/horizontally (no filename is supported yet) +:new, :vne[w] + to create a new file in a new pane above or to the left +:tabedit, :tabnew + to create a new file in a new tab. +:tabn[ext], :tabp[rev] + to go to previous/next tabs +:tabc[lose] + to close the current tab + +# navigating diagnostics +:cn[ext], :cp[rev], :ln[ext], :lp[rev] + to go to the next/prev diagnostics +:cc, :ll + to open the errors page + +# jump to position +: + to jump to a line number +:$ + to jump to the end of the file +:/foo and :?foo + to jump to next/prev line matching foo + +# replacement +:%s/foo/bar/ + to replace instances of foo with bar (/g is always assumed, the range must always be %, and Zed uses different regex syntax to vim) + +# editing +:j[oin] + to join the current line (no range is yet supported) +:d[elete][l][p] + to delete the current line (no range is yet supported) +:s[ort] [i] + to sort the current selection (with i, case-insensitively) +``` + + +### Related settings +There are a few Zed settings that you may also enjoy if you use vim mode: +``` +{ + // disable cursor blink + "cursor_blink": false + // use relative line numbers + "relative_line_numbers": true, + // hide the scroll bar + "scrollbar": {"show": "never"}, +} +``` + +### Regex differences + +Zed uses a different regular expression engine from Vim. This means that you will have to use a different syntax for some things. + +Notably: +* Vim uses `\(` and `\)` to represent capture groups, in Zed these are `(` and `)`. +* On the flip side, `(` and `)` represent literal parentheses, but in Zed these must be escaped to `\(` and `\)`. +* When replacing, Vim uses `\0` to represent the entire match, in Zed this is `$0`, same for numbered capture groups `\1` -> `$1`. +* Vim uses `\<` and `\>` to represent word boundaries, in Zed these are both handled by `\b` +* Vim uses `/g` to indicate "all matches on one line", in Zed this is implied +* Vim uses `/i` to indicate "case-insensitive", in Zed you can either use `(?i)` at the start of the pattern or toggle case-sensitivity with `cmd-option-c`. + +To help with the transition, the command palette will fix parentheses and replace groups for you when you run `:%s//`. So `%s:/\(a\)(b)/\1/` will be converted into a search for "(a)\(b\)" and a replacement of "$1". + +For the full syntax supported by Zed's regex engine see the [regex crate documentation](https://docs.rs/regex/latest/regex/#syntax). diff --git a/docs/src/developing_zed__adding_languages.md b/docs/src/developing_zed__adding_languages.md new file mode 100644 index 0000000000..2917b08422 --- /dev/null +++ b/docs/src/developing_zed__adding_languages.md @@ -0,0 +1,83 @@ +# Adding New Languages to Zed + +## LSP + +Zed uses the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) to provide language support. This means, in theory, we can support any language that has an LSP server. + +## Syntax Highlighting + +### Defining syntax highlighting rules + +We use tree-sitter queries to match certian properties to highlight. + +#### Simple Example: + +```scheme +(property_identifier) @property +``` + +```ts +const font: FontFamily = { + weight: "normal", + underline: false, + italic: false, +} +``` + +Match a property identifier and highlight it using the identifier `@property`. In the above example, `weight`, `underline`, and `italic` would be highlighted. + +#### Complex example: + +```scheme +(_ + return_type: (type_annotation + [ + (type_identifier) @type.return + (generic_type + name: (type_identifier) @type.return) + ])) +``` + +```ts +function buildDefaultSyntax(colorScheme: Theme): Partial { + // ... +} +``` + +Match a function return type, and highlight the type using the identifier `@type.return`. In the above example, `Partial` would be highlighted. + +#### Example - Typescript + +Here is an example portion of our `highlights.scm` for TypeScript: + +```scheme +; crates/zed/src/languages/typescript/highlights.scm + +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +; ... +``` diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md new file mode 100644 index 0000000000..cb30051ffa --- /dev/null +++ b/docs/src/developing_zed__building_zed.md @@ -0,0 +1,107 @@ +# Building Zed + +🚧 TODO: +- [ ] Tidy up & update instructions +- [ ] Remove ZI-specific things +- [ ] Rework any steps that currently require a ZI-specific account + +How to build Zed from source for the first time. + +## Prerequisites + +- Be added to the GitHub organization +- Be added to the Vercel team + +## Process + +Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be. + +1. Install the [GitHub CLI](https://cli.github.com/): + - `brew install gh` +1. Clone the `zed` repo + - `gh repo clone zed-industries/zed` +1. Install Xcode from the macOS App Store +1. Install Xcode command line tools + - `xcode-select --install` + - If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` +1. Install [Postgres](https://postgresapp.com) +1. Install rust/rustup + - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` +1. Install the wasm toolchain + - `rustup target add wasm32-wasi` +1. Install Livekit & Foreman + - `brew install livekit` + - `brew install foreman` +1. Generate an GitHub API Key + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps +1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` +1. Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: + ``` + cd .. + git clone https://github.com/zed-industries/zed.dev + cd zed.dev && npm install + npm install -g vercel + ``` +1. Link your zed.dev project to Vercel + - `vercel link` + - Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. + - Select the `zed.dev` project +1. Run `vercel pull` to pull down the environment variables and project info from Vercel +1. Open Postgres.app +1. From `./path/to/zed/`: + - Run: + - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` + - Replace `{yourGithubAPIToken}` with the API token you generated above. + - You don't need to include the GITHUB_TOKEN if you exported it above. + - Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). + - If you get: + - ```bash + Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! + Please create a new installation in /opt/homebrew using one of the + "Alternative Installs" from: + https://docs.brew.sh/Installation + ``` + - In that case try: + - `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` + - If Homebrew is not in your PATH: + - Replace `{username}` with your home folder name (usually your login name) + - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` + - `eval "$(/opt/homebrew/bin/brew shellenv)"` +1. To run the Zed app: + - If you are working on zed: + - `cargo run` + - If you are just using the latest version, but not working on zed: + - `cargo run --release` + - If you need to run the collaboration server locally: + - `script/zed-local` + +## Troubleshooting + +### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)` + +- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` + +### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` + +### Seeding errors during `script/bootstrap` runs + +``` +seeding database... +thread 'main' panicked at 'failed to deserialize github user from 'https://api.github.com/orgs/zed-industries/teams/staff/members': reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }', crates/collab/src/bin/seed.rs:111:10 +``` + +Wrong permissions for `GITHUB_TOKEN` token used, the token needs to be able to read from private repos. +For Classic GitHub Tokens, that required OAuth scope `repo` (seacrh the scope name above for more details) + +Same command + +`sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` + +### If you experience errors that mention some dependency is using unstable features + +Try `cargo clean` and `cargo build` diff --git a/docs/local-collaboration.md b/docs/src/developing_zed__local_collaboration.md similarity index 100% rename from docs/local-collaboration.md rename to docs/src/developing_zed__local_collaboration.md diff --git a/docs/src/feedback.md b/docs/src/feedback.md new file mode 100644 index 0000000000..11ae444079 --- /dev/null +++ b/docs/src/feedback.md @@ -0,0 +1,29 @@ +# Giving feedback + +### Community repository + +We track our issues at [`zed-industries/community`](https://github.com/zed-industries/community/issues). + +#### Feature requests + +Try to focus on the things that are most critical to you rather than exhaustively listing all features another editor you have used has. + +Command palette: `request feature` + +#### Bug reports + +Try to add as much detail as possible, if it is not obvious to reproduce. Let us know how severe the problem is for you; is the issue more of a minor inconvenience or something that would prevent you from using Zed? + +Command palette: `file bug report` + +### In-app feedback + +Anonymous feedback can be submitted from within Zed via the feedback editor (command palette: `give feedback`). + +### Zed forum + +Use the [community forum](https://github.com/zed-industries/community/discussions) to ask questions and learn from one another. We will be present in the forum and answering questions as well. + +### Email + +If you prefer to write up your thoughts as an email, you can send them to [hi@zed.dev](mailto:hi@zed.dev). diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md new file mode 100644 index 0000000000..236249d00a --- /dev/null +++ b/docs/src/getting_started.md @@ -0,0 +1,15 @@ +# Getting Started + +Welcome to Zed! We are excited to have you. Here is a jumping-off point to getting started. + +### Download Zed + +You can obtain the release build via the [download page](https://zed.dev/download). After the first manual installation, Zed will periodically check for and install updates automatically for you. + +### Configure Zed + +Use `CMD + ,` to open your custom settings to set things like fonts, formatting settings, per-language settings and more. You can access the default configuration using the `Zed > Settings > Open Default Settings` menu item. See Configuring Zed for all available settings. + +### Set up your key bindings + +You can access the default key binding set using the `Zed > Settings > Open Default Key Bindings` menu item. Use `CMD + K`,`CMD + S` to open your custom keymap to add your own key bindings. See Key Bindings for more info., diff --git a/docs/src/system_requirements.md b/docs/src/system_requirements.md new file mode 100644 index 0000000000..debf7fa299 --- /dev/null +++ b/docs/src/system_requirements.md @@ -0,0 +1,13 @@ +# System Requirements + +## macOS + +Supported versions: Catalina (10.15) - Ventura (13.x). + +{% hint style="info" %} +The implementation of our screen sharing feature makes use of [LiveKit](https://livekit.io). The LiveKit SDK requires macOS Catalina (10.15); consequently, in v0.62.4, we dropped support for earlier macOS versions that we were initially supporting. +{% endhint %} + +## Linux, Windows, and Web + +_Not supported at this time. See our_ [_Platform Support_](https://github.com/zed-industries/community/issues/174) _issue._ diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md new file mode 100644 index 0000000000..834e02770a --- /dev/null +++ b/docs/src/telemetry.md @@ -0,0 +1,147 @@ +# Telemetry in Zed + +**Up to date with v0.112.0** + +Zed collects anonymous telemetry data to help the team understand how people are using the application and to see what sort of issues they are experiencing. + +## Dataflow + +Telemetry is sent from the application to zed.dev. Data is proxied through our servers to enable us to easily switch analytics services; we never store this data. The data is then sent off to various services: + +- [Datadog](https://www.datadoghq.com): Cloud-monitoring service - stores diagnostic events +- [Clickhouse](https://clickhouse.com): Business Intelligence platform - stores both diagnostic and metric events +- [Metabase](https://www.metabase.com): Dashboards - dashboards built around data pulled from Clickhouse + +## Types of Telemetry + +### Diagnostics + +Diagnostic events include debug information (stack traces) from crash reports. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. + +When a panic occurs, the following data is sent: + +#### PanicRequest + +- `panic`: The panic data +- `token`: An identifier that is used to authenticate the request on zed.dev + +#### Panic + +- `thread`: The name of the thread that panicked +- `payload`: The panic message +- `location_data`: The location of the panic + - `file` + - `line` +- `backtrace`: The backtrace of the panic +- `app_version`: Zed's app version +- `release_channel`: Zed's release channel + - `stable` + - `preview` + - `dev` +- `os_name`: The name of your operating system +- `os_version`: The version of your operating system +- `architecture`: The architecture of your CPU +- `panicked_on`: The time that the panic occurred +- `installation_id`: An identifier that is unique to each installation of Zed (this differs for stable, preview, and dev builds) +- `session_id`: An identifier that is unique to each Zed session (this differs for each time you open Zed) + +### Metrics + +Zed also collects metric information based on user actions. Metric events are reported over HTTPS, and requests are rate-limited to avoid using significant network bandwidth. All data remains anonymous, and can't be related to specific Zed users. + +The following data is sent: + +#### ClickhouseEventRequestBody + +- `token`: An identifier that is used to authenticate the request on zed.dev +- `installation_id`: An identifier that is unique to each installation of Zed (this differs for stable, preview, and dev builds) +- `session_id`: An identifier that is unique to each Zed session (this differs for each time you open Zed) +- `is_staff`: A boolean that indicates whether the user is a member of the Zed team or not +- `app_version`: Zed's app version +- `os_name`: The name of your operating system +- `os_version`: The version of your operating system +- `architecture`: The architecture of your CPU +- `release_channel`: Zed's release channel + - `stable` + - `preview` + - `dev` +- `events`: A vector of `ClickhouseEventWrapper`s + +#### ClickhouseEventWrapper + +- `signed_in`: A boolean that indicates whether the user is signed in or not +- `event`: An enum, where each variant can be one of the following `ClickhouseEvent` variants: + +#### ClickhouseEvent + +- `editor` + - `operation`: The editor operation that was performed + - `open` + - `save` + - `file_extension`: The extension of the file that was opened or saved + - `vim_mode`: A boolean that indicates whether the user is in vim mode or not + - `copilot_enabled`: A boolean that indicates whether the user has copilot enabled or not + - `copilot_enabled_for_language`: A boolean that indicates whether the user has copilot enabled for the language of the file that was opened or saved + - `milliseconds_since_first_event`: Duration of time between this event's timestamp and the timestamp of the first event in the current batch +- `copilot` + - `suggestion_id`: The ID of the suggestion + - `suggestion_accepted`: A boolean that indicates whether the suggestion was accepted or not + - `file_extension`: The file extension of the file that was opened or saved + - `milliseconds_since_first_event`: Same as above +- `call` + - `operation`: The call operation that was performed + - `accept incoming` + - `decline incoming` + - `disable microphone` + - `disable screen share` + - `enable microphone` + - `enable screen share` + - `hang up` + - `invite` + - `join channel` + - `open channel notes` + - `share project` + - `unshare project` + - `room_id`: The ID of the room + - `channel_id`: The ID of the channel + - `milliseconds_since_first_event`: Same as above +- `assistant` + - `conversation_id`: The ID of the conversation (for panel events only) + - `kind`: An enum with the following variants: + - `panel` + - `inline` + - `model`: The model that was used + - `milliseconds_since_first_event`: Same as above +- `cpu` + - `usage_as_percentage`: The CPU usage + - `core_count`: The number of cores on the CPU + - `milliseconds_since_first_event`: Same as above +- `memory` + - `memory_in_bytes`: The amount of memory used in bytes + - `virtual_memory_in_bytes`: The amount of virtual memory used in bytes + - `milliseconds_since_first_event`: Same as above +- `app` + - `operation`: The app operation that was performed + - `first open` + - `open` + - `close (only in GPUI2-powered Zed)` + - `milliseconds_since_first_event`: Same as above + +You can audit the metrics data that Zed has reported by running the command `zed: open telemetry log` from the command palette, or clicking `Help > View Telemetry Log` in the application menu. + +### Configuring Telemetry Settings + +You have full control over what data is sent out by Zed. To enable or disable some or all telemetry types, open your `settings.json` file via `zed: open settings` from the command palette. Insert and tweak the following: + +```json +"telemetry": { + "diagnostics": false, + "metrics": false +}, +``` + +The telemetry settings can also be configured via the `welcome` screen, which can be invoked via the `workspace: welcome` action in the command palette. + +### Concerns and Questions + +If you have concerns about telemetry, please feel free to open issues in our [community repository](https://github.com/zed-industries/community/issues/new/choose). diff --git a/docs/backend-development.md b/docs_old/backend-development.md similarity index 100% rename from docs/backend-development.md rename to docs_old/backend-development.md diff --git a/docs/building-zed.md b/docs_old/building-zed.md similarity index 100% rename from docs/building-zed.md rename to docs_old/building-zed.md diff --git a/docs/company-and-vision.md b/docs_old/company-and-vision.md similarity index 100% rename from docs/company-and-vision.md rename to docs_old/company-and-vision.md diff --git a/docs/design-tools.md b/docs_old/design-tools.md similarity index 100% rename from docs/design-tools.md rename to docs_old/design-tools.md diff --git a/docs/index.md b/docs_old/index.md similarity index 100% rename from docs/index.md rename to docs_old/index.md diff --git a/docs_old/local-collaboration.md b/docs_old/local-collaboration.md new file mode 100644 index 0000000000..4c059c0878 --- /dev/null +++ b/docs_old/local-collaboration.md @@ -0,0 +1,22 @@ +# Local Collaboration + +## Setting up the local collaboration server + +### Setting up for the first time? + +1. Make sure you have livekit installed (`brew install livekit`) +1. Install [Postgres](https://postgresapp.com) and run it. +1. Then, from the root of the repo, run `script/bootstrap`. + +### Have a db that is out of date? / Need to migrate? + +1. Make sure you have livekit installed (`brew install livekit`) +1. Try `cd crates/collab && cargo run -- migrate` from the root of the repo. +1. Run `script/seed-db` + +## Testing collab locally + +1. Run `foreman start` from the root of the repo. +1. In another terminal run `script/zed-local -2`. +1. Two copies of Zed will open. Add yourself as a contact in the one that is not you. +1. Start a collaboration session as normal with any open project. diff --git a/docs/release-process.md b/docs_old/release-process.md similarity index 100% rename from docs/release-process.md rename to docs_old/release-process.md diff --git a/docs/theme/generating-theme-types.md b/docs_old/theme/generating-theme-types.md similarity index 100% rename from docs/theme/generating-theme-types.md rename to docs_old/theme/generating-theme-types.md diff --git a/docs/tools.md b/docs_old/tools.md similarity index 100% rename from docs/tools.md rename to docs_old/tools.md diff --git a/docs/zed/syntax-highlighting.md b/docs_old/zed/syntax-highlighting.md similarity index 100% rename from docs/zed/syntax-highlighting.md rename to docs_old/zed/syntax-highlighting.md From 5b3b15e95cb97553377e7cc0661fec145f9dd5a3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 12 Dec 2023 09:10:12 -0500 Subject: [PATCH 16/94] Futher outline --- docs/src/CONTRIBUTING.md | 3 --- docs/src/SUMMARY.md | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 docs/src/CONTRIBUTING.md diff --git a/docs/src/CONTRIBUTING.md b/docs/src/CONTRIBUTING.md deleted file mode 100644 index d48c26844f..0000000000 --- a/docs/src/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -TBD diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4c6f5f9421..ad1cd6332c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,12 +7,19 @@ - [Settings](./configuring_zed.md) - [Vim Mode](./configuring_zed__configuring_vim.md) -# Developing Zed +# Using Zed +- [Workflows]() +- [Collaboration]() +- [Using AI]() + +# Contributing to Zed +- [How to Contribute]() - [Building from Source](./developing_zed__building_zed.md) - [Local Collaboration](./developing_zed__local_collaboration.md) - [Adding Languages](./developing_zed__adding_languages.md) +- [Adding UI]() -# Other -- [Code of Conduct](./CODE_OF_CONDUCT.md) -- [Telemetry](./telemetry.md) -- [Contributing](./CONTRIBUTING.md) +--- + +[Telemetry](./telemetry.md) +[Code of Conduct](./CODE_OF_CONDUCT.md) From f9e7c796720834ec7c7da0622b5b8a4da34d8afe Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 13 Dec 2023 10:35:03 -0500 Subject: [PATCH 17/94] Add deploy note --- docs/how-to-deploy.md | 8 ++++++++ {docs_old => docs/old}/backend-development.md | 0 {docs_old => docs/old}/building-zed.md | 0 {docs_old => docs/old}/company-and-vision.md | 0 {docs_old => docs/old}/design-tools.md | 0 {docs_old => docs/old}/index.md | 0 {docs_old => docs/old}/local-collaboration.md | 0 {docs_old => docs/old}/release-process.md | 0 {docs_old => docs/old}/theme/generating-theme-types.md | 0 {docs_old => docs/old}/tools.md | 0 {docs_old => docs/old}/zed/syntax-highlighting.md | 0 11 files changed, 8 insertions(+) create mode 100644 docs/how-to-deploy.md rename {docs_old => docs/old}/backend-development.md (100%) rename {docs_old => docs/old}/building-zed.md (100%) rename {docs_old => docs/old}/company-and-vision.md (100%) rename {docs_old => docs/old}/design-tools.md (100%) rename {docs_old => docs/old}/index.md (100%) rename {docs_old => docs/old}/local-collaboration.md (100%) rename {docs_old => docs/old}/release-process.md (100%) rename {docs_old => docs/old}/theme/generating-theme-types.md (100%) rename {docs_old => docs/old}/tools.md (100%) rename {docs_old => docs/old}/zed/syntax-highlighting.md (100%) diff --git a/docs/how-to-deploy.md b/docs/how-to-deploy.md new file mode 100644 index 0000000000..b1222aac5c --- /dev/null +++ b/docs/how-to-deploy.md @@ -0,0 +1,8 @@ +1. `cd docs` from repo root +1. Install the vercel cli if you haven't already + - `pnpm i -g vercel` +1. `vercel` to deploy if you already have the project linked +1. Otherwise, `vercel login` and `vercel` to link + - Choose Zed Industries as the team, then `zed-app-docs` as the project + +Someone can write a script for this when they have time. diff --git a/docs_old/backend-development.md b/docs/old/backend-development.md similarity index 100% rename from docs_old/backend-development.md rename to docs/old/backend-development.md diff --git a/docs_old/building-zed.md b/docs/old/building-zed.md similarity index 100% rename from docs_old/building-zed.md rename to docs/old/building-zed.md diff --git a/docs_old/company-and-vision.md b/docs/old/company-and-vision.md similarity index 100% rename from docs_old/company-and-vision.md rename to docs/old/company-and-vision.md diff --git a/docs_old/design-tools.md b/docs/old/design-tools.md similarity index 100% rename from docs_old/design-tools.md rename to docs/old/design-tools.md diff --git a/docs_old/index.md b/docs/old/index.md similarity index 100% rename from docs_old/index.md rename to docs/old/index.md diff --git a/docs_old/local-collaboration.md b/docs/old/local-collaboration.md similarity index 100% rename from docs_old/local-collaboration.md rename to docs/old/local-collaboration.md diff --git a/docs_old/release-process.md b/docs/old/release-process.md similarity index 100% rename from docs_old/release-process.md rename to docs/old/release-process.md diff --git a/docs_old/theme/generating-theme-types.md b/docs/old/theme/generating-theme-types.md similarity index 100% rename from docs_old/theme/generating-theme-types.md rename to docs/old/theme/generating-theme-types.md diff --git a/docs_old/tools.md b/docs/old/tools.md similarity index 100% rename from docs_old/tools.md rename to docs/old/tools.md diff --git a/docs_old/zed/syntax-highlighting.md b/docs/old/zed/syntax-highlighting.md similarity index 100% rename from docs_old/zed/syntax-highlighting.md rename to docs/old/zed/syntax-highlighting.md From 70c6660ae4f7d0c2a70218022509bc4c1d77e4f1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 13 Dec 2023 10:35:54 -0500 Subject: [PATCH 18/94] Add note --- docs/how-to-deploy.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/how-to-deploy.md b/docs/how-to-deploy.md index b1222aac5c..c32d3619a6 100644 --- a/docs/how-to-deploy.md +++ b/docs/how-to-deploy.md @@ -1,3 +1,5 @@ +These docs are intendended to replace both docs.zed.dev and introduce people to how to build Zed from source. + 1. `cd docs` from repo root 1. Install the vercel cli if you haven't already - `pnpm i -g vercel` From a874a96e76b8cced2bc0fe5e108f35b813814c7c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 11:44:51 -0500 Subject: [PATCH 19/94] Fix tab bar drop target sizing (#3627) This PR fixes an issue where the tab bar drop target was not receiving any size. The styling isn't 100% correct yet, as the updated background color has a gap around it. Release Notes: - N/A --- crates/workspace2/src/pane.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2068245159..afd7e665c4 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1664,6 +1664,10 @@ impl Pane { ) .child( div() + .min_w_6() + // HACK: This empty child is currently necessary to force the drop traget to appear + // despite us setting a min width above. + .child("") .h_full() .flex_grow() .drag_over::(|bar| { From ab8d0abbc142052779a9968c4d64c2c26d335cab Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 12:11:33 -0500 Subject: [PATCH 20/94] Wire up tooltips on tab bar actions (#3629) This PR wires up the tooltips on the actions in the tab bar. Release Notes: - N/A --- crates/workspace2/src/pane.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index afd7e665c4..bcbadc4e53 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1597,7 +1597,8 @@ impl Pane { let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) }) - .disabled(!self.can_navigate_backward()), + .disabled(!self.can_navigate_backward()) + .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)), ) .start_child( IconButton::new("navigate_forward", Icon::ArrowRight) @@ -1606,7 +1607,8 @@ impl Pane { let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) }) - .disabled(!self.can_navigate_forward()), + .disabled(!self.can_navigate_forward()) + .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)), ) .end_child( div() @@ -1625,7 +1627,8 @@ impl Pane { }) .detach(); this.new_item_menu = Some(menu); - })), + })) + .tooltip(|cx| Tooltip::text("New...", cx)), ) .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { el.child(Self::render_menu_overlay(new_item_menu)) @@ -1649,7 +1652,8 @@ impl Pane { }) .detach(); this.split_item_menu = Some(menu); - })), + })) + .tooltip(|cx| Tooltip::text("Split Pane", cx)), ) .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { el.child(Self::render_menu_overlay(split_item_menu)) From 48faa171b53e252a07e1f95c10603c9d6dd196e6 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 12:24:10 -0500 Subject: [PATCH 21/94] v0.118.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 033ff8b69c..cc4393bffa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11882,7 +11882,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.117.0" +version = "0.118.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f665cc36db..0c115fb285 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.117.0" +version = "0.118.0" publish = false [lib] From a91a42763f8abd5426059dfb1206e0239deba099 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:56:49 +0100 Subject: [PATCH 22/94] collab_ui: Wire up project picker Co-authored-by: Conrad --- crates/collab_ui2/src/collab_titlebar_item.rs | 107 ++++++++++-------- crates/picker2/src/picker2.rs | 23 ++-- .../recent_projects2/src/recent_projects.rs | 21 +++- 3 files changed, 93 insertions(+), 58 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2b931f7085..f48a78fb1d 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -2,11 +2,13 @@ use crate::face_pile::FacePile; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ - actions, canvas, div, point, px, rems, AppContext, Div, Element, Hsla, InteractiveElement, - IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled, - Subscription, ViewContext, VisualContext, WeakView, WindowBounds, + actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, + FocusableView, Hsla, InteractiveElement, IntoElement, Model, Overlay, ParentElement, Path, + Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; +use recent_projects::RecentProjects; use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ @@ -14,7 +16,7 @@ use ui::{ IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; -use workspace::{notifications::NotifyResultExt, Workspace}; +use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB}; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -49,7 +51,7 @@ pub struct CollabTitlebarItem { client: Arc, workspace: WeakView, //branch_popover: Option>, - //project_popover: Option>, + project_popover: Option, //user_menu: ViewHandle, _subscriptions: Vec, } @@ -328,7 +330,7 @@ impl CollabTitlebarItem { // menu // }), // branch_popover: None, - // project_popover: None, + project_popover: None, _subscriptions: subscriptions, } } @@ -366,11 +368,27 @@ impl CollabTitlebarItem { let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); - div().border().border_color(gpui::red()).child( - Button::new("project_name_trigger", name) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - ) + div() + .border() + .border_color(gpui::red()) + .child( + Button::new("project_name_trigger", name) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_project_menu(&ToggleProjectMenu, cx); + })), + ) + .children(self.project_popover.as_ref().map(|popover| { + overlay().child( + div() + .min_w_56() + .on_mouse_down_out(cx.listener_for(&popover.picker, |picker, _, cx| { + picker.cancel(&Default::default(), cx) + })) + .child(popover.picker.clone()), + ) + })) } pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { @@ -611,43 +629,40 @@ impl CollabTitlebarItem { // cx.notify(); // } - // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { - // let workspace = self.workspace.clone(); - // if self.project_popover.take().is_none() { - // cx.spawn(|this, mut cx| async move { - // let workspaces = WORKSPACE_DB - // .recent_workspaces_on_disk() - // .await - // .unwrap_or_default() - // .into_iter() - // .map(|(_, location)| location) - // .collect(); + pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + if self.project_popover.take().is_none() { + cx.spawn(|this, mut cx| async move { + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect(); - // let workspace = workspace.clone(); - // this.update(&mut cx, move |this, cx| { - // let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx)); + let workspace = workspace.clone(); + this.update(&mut cx, move |this, cx| { + let view = RecentProjects::open_popover(workspace, workspaces, cx); - // cx.subscribe(&view, |this, _, event, cx| { - // match event { - // PickerEvent::Dismiss => { - // this.project_popover = None; - // } - // } - - // cx.notify(); - // }) - // .detach(); - // cx.focus(&view); - // this.branch_popover.take(); - // this.project_popover = Some(view); - // cx.notify(); - // }) - // .log_err(); - // }) - // .detach(); - // } - // cx.notify(); - // } + cx.subscribe(&view.picker, |this, _, _: &DismissEvent, cx| { + this.project_popover = None; + cx.notify(); + }) + .detach(); + let focus_handle = view.focus_handle(cx); + cx.focus(&focus_handle); + // todo!() + //this.branch_popover.take(); + this.project_popover = Some(view); + cx.notify(); + }) + .log_err(); + }) + .detach(); + } + cx.notify(); + } // fn render_user_menu_button( // &self, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 98b6ce5ff0..db5eebff53 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,8 @@ use editor::Editor; use gpui::{ - div, prelude::*, rems, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView, - MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, - WindowContext, + div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter, + FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, + View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; @@ -113,8 +113,9 @@ impl Picker { cx.notify(); } - fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); + cx.emit(DismissEvent); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -146,9 +147,15 @@ impl Picker { event: &editor::EditorEvent, cx: &mut ViewContext, ) { - if let editor::EditorEvent::BufferEdited = event { - let query = self.editor.read(cx).text(cx); - self.update_matches(query, cx); + match event { + editor::EditorEvent::BufferEdited => { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + editor::EditorEvent::Blurred => { + self.cancel(&menu::Cancel, cx); + } + _ => {} } } @@ -189,6 +196,8 @@ impl Picker { } } +impl EventEmitter for Picker {} + impl Render for Picker { type Element = Div; diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs index e014783687..dff6aa12cc 100644 --- a/crates/recent_projects2/src/recent_projects.rs +++ b/crates/recent_projects2/src/recent_projects.rs @@ -23,14 +23,15 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); } +#[derive(Clone)] pub struct RecentProjects { - picker: View>, + pub picker: View>, } impl ModalView for RecentProjects {} impl RecentProjects { - fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext) -> Self { + fn new(delegate: RecentProjectsDelegate, cx: &mut WindowContext<'_>) -> Self { Self { picker: cx.build_view(|cx| Picker::new(delegate, cx)), } @@ -86,6 +87,16 @@ impl RecentProjects { Ok(()) })) } + pub fn open_popover( + workspace: WeakView, + workspaces: Vec, + cx: &mut WindowContext<'_>, + ) -> Self { + Self::new( + RecentProjectsDelegate::new(workspace, workspaces, false), + cx, + ) + } } impl EventEmitter for RecentProjects {} @@ -127,7 +138,7 @@ impl RecentProjectsDelegate { } } } - +impl EventEmitter for RecentProjectsDelegate {} impl PickerDelegate for RecentProjectsDelegate { type ListItem = ListItem; @@ -202,11 +213,11 @@ impl PickerDelegate for RecentProjectsDelegate { .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) }) .detach_and_log_err(cx); - self.dismissed(cx); + cx.emit(DismissEvent); } } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, _: &mut ViewContext>) {} fn render_match( &self, From 72eef116c965f49ead482b1cc765a580c50cd191 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:58:17 +0100 Subject: [PATCH 23/94] fixup! collab_ui: Wire up project picker --- crates/collab_ui2/src/collab_titlebar_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index f48a78fb1d..3d8fedd06b 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -3,8 +3,8 @@ use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, - FocusableView, Hsla, InteractiveElement, IntoElement, Model, Overlay, ParentElement, Path, - Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, + Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; From 06b9055e27d42fcbbd6181b1bba40ab5481c6cd6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Dec 2023 22:02:30 +0100 Subject: [PATCH 24/94] Clear last_order when building Scene --- crates/gpui2/src/scene.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index fd63b49a1a..68c068dfe9 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -54,6 +54,7 @@ impl SceneBuilder { layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32; } self.layers_by_order.clear(); + self.last_order = None; // Add all primitives to the BSP splitter to determine draw order self.splitter.reset(); From ee509e043d359e354b975c45c4f2873c133a40d6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 16:08:31 -0500 Subject: [PATCH 25/94] Rework `ListItem` and `ListHeader` to use slot-based APIs (#3635) This PR reworks the `ListItem` and `ListHeader` components to use slot-based APIs, making them less opinionated about their contents. Splitting this out of the collab UI styling PR so we can land it to avoid conflicts. Co-authored-by: Nate Release Notes: - N/A --- crates/collab_ui2/src/collab_panel.rs | 28 +-- crates/picker2/src/picker2.rs | 1 - crates/ui2/src/components/context_menu.rs | 5 +- crates/ui2/src/components/list/list_header.rs | 67 +++++--- crates/ui2/src/components/list/list_item.rs | 161 ++++++++++++------ .../ui2/src/components/list/list_separator.rs | 6 +- .../src/components/list/list_sub_header.rs | 8 +- .../ui2/src/components/stories/list_header.rs | 12 +- .../ui2/src/components/stories/list_item.rs | 72 +++++++- crates/ui2/src/styled_ext.rs | 16 +- 10 files changed, 267 insertions(+), 109 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index ac7457abe0..a34d574957 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1156,7 +1156,7 @@ impl CollabPanel { let tooltip = format!("Follow {}", user.github_login); ListItem::new(SharedString::from(user.github_login.clone())) - .left_child(Avatar::new(user.avatar_uri.clone())) + .start_slot(Avatar::new(user.avatar_uri.clone())) .child( h_stack() .w_full() @@ -1212,7 +1212,7 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .left_child(render_tree_branch(is_last, cx)) + .start_slot(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Folder)) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -1305,7 +1305,7 @@ impl CollabPanel { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) - .left_child(render_tree_branch(is_last, cx)) + .start_slot(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Screen)) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { @@ -1372,7 +1372,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) - .left_child(render_tree_branch(false, cx)) + .start_slot(render_tree_branch(false, cx)) .child(IconButton::new(0, Icon::File)) .child(Label::new("notes")) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) @@ -1387,7 +1387,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) - .left_child(render_tree_branch(true, cx)) + .start_slot(render_tree_branch(true, cx)) .child(IconButton::new(0, Icon::MessageBubbles)) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) @@ -2318,7 +2318,7 @@ impl CollabPanel { } else { el.child( ListHeader::new(text) - .when_some(button, |el, button| el.meta(button)) + .when_some(button, |el, button| el.end_slot(button)) .selected(is_selected), ) } @@ -2381,7 +2381,7 @@ impl CollabPanel { ) }), ) - .left_child( + .start_slot( // todo!() handle contacts with no avatar Avatar::new(contact.user.avatar_uri.clone()) .availability_indicator(if online { Some(!busy) } else { None }), @@ -2460,7 +2460,7 @@ impl CollabPanel { .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) - .left_avatar(user.avatar_uri.clone()) + .start_slot::(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar))) } fn render_contact_placeholder( @@ -2568,7 +2568,11 @@ impl CollabPanel { ListItem::new(channel_id as usize) .indent_level(depth) .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle - .left_icon(if is_public { Icon::Public } else { Icon::Hash }) + .start_slot( + IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) + .size(IconSize::Small) + .color(Color::Muted), + ) .selected(is_selected || is_active) .child( h_stack() @@ -2962,7 +2966,11 @@ impl CollabPanel { let item = ListItem::new("channel-editor") .inset(false) .indent_level(depth) - .left_icon(Icon::Hash); + .start_slot( + IconElement::new(Icon::Hash) + .size(IconSize::Small) + .color(Color::Muted), + ); if let Some(pending_name) = self .channel_editing_state diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index db5eebff53..8d80f4b36c 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -271,7 +271,6 @@ impl Render for Picker { }, ) .track_scroll(self.scroll_handle.clone()) - .p_1() ) .max_h_72() .overflow_hidden(), diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 3e54298514..250272b198 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -255,6 +255,9 @@ impl Render for ContextMenu { }; ListItem::new(label.clone()) + .inset(true) + .selected(Some(ix) == self.selected_index) + .on_click(move |_, cx| handler(cx)) .child( h_stack() .w_full() @@ -265,8 +268,6 @@ impl Render for ContextMenu { .map(|binding| div().ml_1().child(binding)) })), ) - .selected(Some(ix) == self.selected_index) - .on_click(move |_, cx| handler(cx)) .into_any_element() } }, diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 933a1a95d7..6c497752ae 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -1,12 +1,18 @@ -use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label}; +use crate::{h_stack, prelude::*, Disclosure, Label}; use gpui::{AnyElement, ClickEvent, Div}; -use smallvec::SmallVec; #[derive(IntoElement)] pub struct ListHeader { + /// The label of the header. label: SharedString, - left_icon: Option, - meta: SmallVec<[AnyElement; 2]>, + /// A slot for content that appears before the label, like an icon or avatar. + start_slot: Option, + /// A slot for content that appears after the label, usually on the other side of the header. + /// This might be a button, a disclosure arrow, a face pile, etc. + end_slot: Option, + /// A slot for content that appears on hover after the label + /// It will obscure the `end_slot` when visible. + end_hover_slot: Option, toggle: Option, on_toggle: Option>, inset: bool, @@ -17,8 +23,9 @@ impl ListHeader { pub fn new(label: impl Into) -> Self { Self { label: label.into(), - left_icon: None, - meta: SmallVec::new(), + start_slot: None, + end_slot: None, + end_hover_slot: None, inset: false, toggle: None, on_toggle: None, @@ -39,13 +46,23 @@ impl ListHeader { self } - pub fn left_icon(mut self, left_icon: impl Into>) -> Self { - self.left_icon = left_icon.into(); + pub fn start_slot(mut self, start_slot: impl Into>) -> Self { + self.start_slot = start_slot.into().map(IntoElement::into_any_element); self } - pub fn meta(mut self, meta: impl IntoElement) -> Self { - self.meta.push(meta.into_any_element()); + pub fn end_slot(mut self, end_slot: impl Into>) -> Self { + self.end_slot = end_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { + self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn inset(mut self, inset: bool) -> Self { + self.inset = inset; self } } @@ -61,9 +78,9 @@ impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - h_stack().w_full().relative().child( + h_stack().w_full().relative().group("list_header").child( div() - .h_5() + .h_7() .when(self.inset, |this| this.px_2()) .when(self.selected, |this| { this.bg(cx.theme().colors().ghost_element_selected) @@ -77,24 +94,30 @@ impl RenderOnce for ListHeader { .child( h_stack() .gap_1() + .children( + self.toggle + .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), + ) .child( div() .flex() .gap_1() .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(Color::Muted) - .size(IconSize::Small) - })) + .children(self.start_slot) .child(Label::new(self.label.clone()).color(Color::Muted)), - ) - .children( - self.toggle - .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ), ) - .child(h_stack().gap_2().items_center().children(self.meta)), + .child(h_stack().children(self.end_slot)) + .when_some(self.end_hover_slot, |this, end_hover_slot| { + this.child( + div() + .invisible() + .group_hover("list_header", |this| this.visible()) + .absolute() + .right_0() + .child(end_hover_slot), + ) + }), ) } } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 28a8b8cecb..df6e542816 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,7 +1,6 @@ -use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize}; +use crate::{prelude::*, Disclosure}; use gpui::{ - px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, - Stateful, + px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful, }; use smallvec::SmallVec; @@ -9,11 +8,16 @@ use smallvec::SmallVec; pub struct ListItem { id: ElementId, selected: bool, - // TODO: Reintroduce this - // disclosure_control_style: DisclosureControlVisibility, indent_level: usize, indent_step_size: Pixels, - left_slot: Option, + /// A slot for content that appears before the children, like an icon or avatar. + start_slot: Option, + /// A slot for content that appears after the children, usually on the other side of the header. + /// This might be a button, a disclosure arrow, a face pile, etc. + end_slot: Option, + /// A slot for content that appears on hover after the children + /// It will obscure the `end_slot` when visible. + end_hover_slot: Option, toggle: Option, inset: bool, on_click: Option>, @@ -30,7 +34,9 @@ impl ListItem { selected: false, indent_level: 0, indent_step_size: px(12.), - left_slot: None, + start_slot: None, + end_slot: None, + end_hover_slot: None, toggle: None, inset: false, on_click: None, @@ -87,23 +93,18 @@ impl ListItem { self } - pub fn left_child(mut self, left_content: impl IntoElement) -> Self { - self.left_slot = Some(left_content.into_any_element()); + pub fn start_slot(mut self, start_slot: impl Into>) -> Self { + self.start_slot = start_slot.into().map(IntoElement::into_any_element); self } - pub fn left_icon(mut self, left_icon: Icon) -> Self { - self.left_slot = Some( - IconElement::new(left_icon) - .size(IconSize::Small) - .color(Color::Muted) - .into_any_element(), - ); + pub fn end_slot(mut self, end_slot: impl Into>) -> Self { + self.end_slot = end_slot.into().map(IntoElement::into_any_element); self } - pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { - self.left_slot = Some(Avatar::new(left_avatar).into_any_element()); + pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { + self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); self } } @@ -125,49 +126,105 @@ impl RenderOnce for ListItem { type Rendered = Stateful
; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .id(self.id) + h_stack() + .id("item_container") + .w_full() .relative() - // TODO: Add focus state - // .when(self.state == InteractionState::Focused, |this| { - // this.border() - // .border_color(cx.theme().colors().border_focused) - // }) - .when(self.inset, |this| this.rounded_md()) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) + // When an item is inset draw the indent spacing outside of the item + .when(self.inset, |this| { + this.ml(self.indent_level as f32 * self.indent_step_size) + .px_1() }) - .when_some(self.on_click, |this, on_click| { - this.cursor_pointer().on_click(move |event, cx| { - // HACK: GPUI currently fires `on_click` with any mouse button, - // but we only care about the left button. - if event.down.button == MouseButton::Left { - (on_click)(event, cx) - } - }) + .when(!self.inset, |this| { + this + // TODO: Add focus state + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(cx.theme().colors().border_focused) + // }) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) }) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, cx| { - (on_mouse_down)(event, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) .child( - div() - .when(self.inset, |this| this.px_2()) - .ml(self.indent_level as f32 * self.indent_step_size) - .flex() - .gap_1() - .items_center() + h_stack() + .id(self.id) + .w_full() .relative() + .gap_1() + .px_2() + .group("list_item") + .when(self.inset, |this| { + this + // TODO: Add focus state + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(cx.theme().colors().border_focused) + // }) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) + }) + .when_some(self.on_click, |this, on_click| { + this.cursor_pointer().on_click(move |event, cx| { + // HACK: GPUI currently fires `on_click` with any mouse button, + // but we only care about the left button. + if event.down.button == MouseButton::Left { + (on_click)(event, cx) + } + }) + }) + .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { + this.on_mouse_down(MouseButton::Right, move |event, cx| { + (on_mouse_down)(event, cx) + }) + }) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) + .map(|this| { + if self.inset { + this.rounded_md() + } else { + // When an item is not inset draw the indent spacing inside of the item + this.ml(self.indent_level as f32 * self.indent_step_size) + } + }) .children( self.toggle .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ) - .children(self.left_slot) - .children(self.children), + .child( + h_stack() + .flex_1() + .gap_1() + .children(self.start_slot) + .children(self.children), + ) + .when_some(self.end_slot, |this, end_slot| { + this.justify_between().child( + h_stack() + .when(self.end_hover_slot.is_some(), |this| { + this.visible() + .group_hover("list_item", |this| this.invisible()) + }) + .child(end_slot), + ) + }) + .when_some(self.end_hover_slot, |this, end_hover_slot| { + this.child( + h_stack() + .h_full() + .absolute() + .right_2() + .top_0() + .invisible() + .group_hover("list_item", |this| this.visible()) + .child(end_hover_slot), + ) + }), ) } } diff --git a/crates/ui2/src/components/list/list_separator.rs b/crates/ui2/src/components/list/list_separator.rs index 0398a110e9..346b13ddaa 100644 --- a/crates/ui2/src/components/list/list_separator.rs +++ b/crates/ui2/src/components/list/list_separator.rs @@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div().h_px().w_full().bg(cx.theme().colors().border_variant) + div() + .h_px() + .w_full() + .my_1() + .bg(cx.theme().colors().border_variant) } } diff --git a/crates/ui2/src/components/list/list_sub_header.rs b/crates/ui2/src/components/list/list_sub_header.rs index 17f07b7b0b..07a99dabe5 100644 --- a/crates/ui2/src/components/list/list_sub_header.rs +++ b/crates/ui2/src/components/list/list_sub_header.rs @@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label}; #[derive(IntoElement)] pub struct ListSubHeader { label: SharedString, - left_icon: Option, + start_slot: Option, inset: bool, } @@ -14,13 +14,13 @@ impl ListSubHeader { pub fn new(label: impl Into) -> Self { Self { label: label.into(), - left_icon: None, + start_slot: None, inset: false, } } pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; + self.start_slot = left_icon; self } } @@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader { .flex() .gap_1() .items_center() - .children(self.left_icon.map(|i| { + .children(self.start_slot.map(|i| { IconElement::new(i) .color(Color::Muted) .size(IconSize::Small) diff --git a/crates/ui2/src/components/stories/list_header.rs b/crates/ui2/src/components/stories/list_header.rs index 056eaa2762..3c80afdde3 100644 --- a/crates/ui2/src/components/stories/list_header.rs +++ b/crates/ui2/src/components/stories/list_header.rs @@ -15,19 +15,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").left_icon(Icon::Bell)) + .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell))) .child(Story::label("With left icon and meta")) .child( ListHeader::new("Section 3") - .left_icon(Icon::BellOff) - .meta(IconButton::new("action_1", Icon::Bolt)), + .start_slot(IconElement::new(Icon::BellOff)) + .end_slot(IconButton::new("action_1", Icon::Bolt)), ) .child(Story::label("With multiple meta")) .child( ListHeader::new("Section 4") - .meta(IconButton::new("action_1", Icon::Bolt)) - .meta(IconButton::new("action_2", Icon::ExclamationTriangle)) - .meta(IconButton::new("action_3", Icon::Plus)), + .end_slot(IconButton::new("action_1", Icon::Bolt)) + .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle)) + .end_slot(IconButton::new("action_3", Icon::Plus)), ) } } diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index 91e95348fd..fbcea44b57 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -1,7 +1,7 @@ use gpui::{Div, Render}; use story::Story; -use crate::prelude::*; +use crate::{prelude::*, Avatar}; use crate::{Icon, ListItem}; pub struct ListItemStory; @@ -9,24 +9,80 @@ pub struct ListItemStory; impl Render for ListItemStory { type Element = Div; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container() + .bg(cx.theme().colors().background) .child(Story::title_for::()) .child(Story::label("Default")) .child(ListItem::new("hello_world").child("Hello, world!")) - .child(Story::label("With left icon")) + .child(Story::label("Inset")) .child( - ListItem::new("with_left_icon") + ListItem::new("hello_world") + .inset(true) + .start_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ) .child("Hello, world!") - .left_icon(Icon::Bell), + .end_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ), ) - .child(Story::label("With left avatar")) + .child(Story::label("With start slot icon")) + .child( + ListItem::new("with start slot_icon") + .child("Hello, world!") + .start_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child(Story::label("With start slot avatar")) + .child( + ListItem::new("with_start slot avatar") + .child("Hello, world!") + .start_slot(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ))), + ) + .child(Story::label("With end slot")) .child( ListItem::new("with_left_avatar") .child("Hello, world!") - .left_avatar(SharedString::from( + .end_slot(Avatar::new(SharedString::from( "https://avatars.githubusercontent.com/u/1714999?v=4", - )), + ))), + ) + .child(Story::label("With end hover slot")) + .child( + ListItem::new("with_left_avatar") + .child("Hello, world!") + .end_slot( + h_stack() + .gap_2() + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))), + ) + .end_hover_slot(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ))), ) .child(Story::label("With `on_click`")) .child( diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index ed81c2cd0a..3358968c72 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized { elevated(self, cx, ElevationIndex::ModalSurface) } + /// The theme's primary border color. + fn border_primary(self, cx: &mut WindowContext) -> Self { + self.border_color(cx.theme().colors().border) + } + + /// The theme's secondary or muted border color. + fn border_muted(self, cx: &mut WindowContext) -> Self { + self.border_color(cx.theme().colors().border_variant) + } + fn debug_bg_red(self) -> Self { - self.bg(gpui::red()) + self.bg(hsla(0. / 360., 1., 0.5, 1.)) } fn debug_bg_green(self) -> Self { - self.bg(gpui::green()) + self.bg(hsla(120. / 360., 1., 0.5, 1.)) } fn debug_bg_blue(self) -> Self { - self.bg(gpui::blue()) + self.bg(hsla(240. / 360., 1., 0.5, 1.)) } fn debug_bg_yellow(self) -> Self { From 3094cb749ed1691a230b4bc037bd5e9676164c18 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Dec 2023 14:12:59 -0700 Subject: [PATCH 26/94] Implement user menu --- Cargo.lock | 2 + crates/collab_ui2/Cargo.toml | 4 +- crates/collab_ui2/src/collab_titlebar_item.rs | 60 ++++++++++++++----- crates/zed2/src/zed2.rs | 3 +- crates/zed_actions2/src/lib.rs | 2 +- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba868ebbae..a6cc38ef6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1957,6 +1957,7 @@ dependencies = [ "db2", "editor2", "feature_flags2", + "feedback2", "futures 0.3.28", "fuzzy2", "gpui2", @@ -1978,6 +1979,7 @@ dependencies = [ "settings2", "smallvec", "theme2", + "theme_selector2", "time", "tree-sitter-markdown", "ui2", diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 65aced8e7e..88c27bc165 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -32,7 +32,7 @@ collections = { path = "../collections" } # context_menu = { path = "../context_menu" } # drag_and_drop = { path = "../drag_and_drop" } editor = { package="editor2", path = "../editor2" } -#feedback = { path = "../feedback" } +feedback = { package = "feedback2", path = "../feedback2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } @@ -46,7 +46,7 @@ rpc = { package ="rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2"} theme = { package = "theme2", path = "../theme2" } -# theme_selector = { path = "../theme_selector" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } # vcs_menu = { path = "../vcs_menu" } ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 3d8fedd06b..d6de5a1565 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -1,8 +1,8 @@ use crate::face_pile::FacePile; use call::{ActiveCall, ParticipantLocation, Room}; -use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; +use client::{proto::PeerId, Client, ParticipantIndex, SignOut, User, UserStore}; use gpui::{ - actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, + actions, canvas, div, overlay, point, px, rems, Action, AppContext, DismissEvent, Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, @@ -16,7 +16,7 @@ use ui::{ IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; -use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB}; +use workspace::{notifications::NotifyResultExt, Feedback, Workspace, WORKSPACE_DB}; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -239,7 +239,19 @@ impl Render for CollabTitlebarItem { this.child( popover_menu("user-menu") .menu(|cx| { - ContextMenu::build(cx, |menu, _| menu.header("ADADA")) + ContextMenu::build(cx, |menu, _| { + menu.action( + "Settings", + zed_actions::OpenSettings.boxed_clone(), + ) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action( + "Share Feedback", + feedback::GiveFeedback.boxed_clone(), + ) + .action("Sign Out", client::SignOut.boxed_clone()) + }) }) .trigger( ButtonLike::new("user-menu") @@ -259,16 +271,6 @@ impl Render for CollabTitlebarItem { ) .anchor(gpui::AnchorCorner::TopRight), ) - // this.child( - // ButtonLike::new("user-menu") - // .child( - // h_stack().gap_0p5().child(Avatar::data(avatar)).child( - // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // ), - // ) - // .style(ButtonStyle::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), - // ) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { let client = client.clone(); @@ -280,6 +282,36 @@ impl Render for CollabTitlebarItem { }) .detach(); })) + .child( + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| { + menu.action( + "Settings", + zed_actions::OpenSettings.boxed_clone(), + ) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action( + "Share Feedback", + feedback::GiveFeedback.boxed_clone(), + ) + }) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| { + Tooltip::text("Toggle User Menu", cx) + }), + ), + ) } })), ) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 611cf64552..45ef08fdcc 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -41,7 +41,7 @@ use workspace::{ notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; -use zed_actions::{OpenBrowser, OpenZedURL, Quit}; +use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit}; actions!( zed, @@ -59,7 +59,6 @@ actions!( OpenLicenses, OpenLocalSettings, OpenLog, - OpenSettings, OpenTelemetryLog, ResetBufferFontSize, ResetDatabase, diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index fa1a4a5ea9..badf76a6e7 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -22,4 +22,4 @@ pub struct OpenZedURL { impl_actions!(zed, [OpenBrowser, OpenZedURL]); -actions!(zed, [Quit]); +actions!(zed, [OpenSettings, Quit]); From bfbbec0b019c26d73ab03b4cb363c3d44aa23e63 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 13 Dec 2023 13:21:48 -0800 Subject: [PATCH 27/94] Add fluent quad API --- crates/collab_ui2/src/collab_panel.rs | 26 ++-- crates/editor2/src/element.rs | 104 +++++---------- crates/gpui2/src/geometry.rs | 33 +++++ crates/gpui2/src/style.rs | 10 +- crates/gpui2/src/text_system/line.rs | 26 ++-- crates/gpui2/src/window.rs | 124 ++++++++++++++---- crates/terminal_view2/src/terminal_element.rs | 24 +--- 7 files changed, 196 insertions(+), 151 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index ac7457abe0..cd7b2b9e9d 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -175,12 +175,12 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json, - size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, - EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, - Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, - RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems, + serde_json, size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, + Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, + IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, + Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -2994,7 +2994,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement let right = bounds.right(); let top = bounds.top(); - cx.paint_quad( + cx.paint_quad(fill( Bounds::from_corners( point(start_x, top), point( @@ -3002,18 +3002,12 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement if is_last { start_y } else { bounds.bottom() }, ), ), - Default::default(), color, - Default::default(), - Hsla::transparent_black(), - ); - cx.paint_quad( + )); + cx.paint_quad(fill( Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), - Default::default(), color, - Default::default(), - Hsla::transparent_black(), - ); + )); }) .w(width) .h(line_height) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 307a95b70a..2b6db125da 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -23,13 +23,14 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use git::diff::DiffHunkStatus; use gpui::{ - div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement, - AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle, - DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla, - InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, - ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, + div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, + AnchorCorner, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, + ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementId, + ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, + IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, + Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, + ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::{language_settings::ShowWhitespaceSetting, Language}; @@ -620,20 +621,8 @@ impl EditorElement { let scroll_top = layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; let gutter_bg = cx.theme().colors().editor_gutter_background; - cx.paint_quad( - gutter_bounds, - Corners::default(), - gutter_bg, - Edges::default(), - transparent_black(), - ); - cx.paint_quad( - text_bounds, - Corners::default(), - self.style.background, - Edges::default(), - transparent_black(), - ); + cx.paint_quad(fill(gutter_bounds, gutter_bg)); + cx.paint_quad(fill(text_bounds, self.style.background)); if let EditorMode::Full = layout.mode { let mut active_rows = layout.active_rows.iter().peekable(); @@ -657,13 +646,7 @@ impl EditorElement { layout.position_map.line_height * (end_row - start_row + 1) as f32, ); let active_line_bg = cx.theme().colors().editor_active_line_background; - cx.paint_quad( - Bounds { origin, size }, - Corners::default(), - active_line_bg, - Edges::default(), - transparent_black(), - ); + cx.paint_quad(fill(Bounds { origin, size }, active_line_bg)); } } @@ -679,13 +662,7 @@ impl EditorElement { layout.position_map.line_height * highlighted_rows.len() as f32, ); let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background; - cx.paint_quad( - Bounds { origin, size }, - Corners::default(), - highlighted_line_bg, - Edges::default(), - transparent_black(), - ); + cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg)); } let scroll_left = @@ -706,16 +683,13 @@ impl EditorElement { } else { cx.theme().colors().editor_wrap_guide }; - cx.paint_quad( + cx.paint_quad(fill( Bounds { origin: point(x, text_bounds.origin.y), size: size(px(1.), text_bounds.size.height), }, - Corners::default(), color, - Edges::default(), - transparent_black(), - ); + )); } } } @@ -812,13 +786,13 @@ impl EditorElement { let highlight_origin = bounds.origin + point(-width, start_y); let highlight_size = size(width * 2., end_y - start_y); let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad( + cx.paint_quad(quad( highlight_bounds, Corners::all(1. * line_height), gpui::yellow(), // todo!("use the right color") Edges::default(), transparent_black(), - ); + )); continue; } @@ -845,13 +819,13 @@ impl EditorElement { let highlight_origin = bounds.origin + point(-width, start_y); let highlight_size = size(width * 2., end_y - start_y); let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad( + cx.paint_quad(quad( highlight_bounds, Corners::all(1. * line_height), cx.theme().status().deleted, Edges::default(), transparent_black(), - ); + )); continue; } @@ -867,13 +841,13 @@ impl EditorElement { let highlight_origin = bounds.origin + point(-width, start_y); let highlight_size = size(width * 2., end_y - start_y); let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad( + cx.paint_quad(quad( highlight_bounds, Corners::all(0.05 * line_height), color, // todo!("use the right color") Edges::default(), transparent_black(), - ); + )); } } @@ -1278,7 +1252,7 @@ impl EditorElement { let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom)); if layout.show_scrollbars { - cx.paint_quad( + cx.paint_quad(quad( track_bounds, Corners::default(), cx.theme().colors().scrollbar_track_background, @@ -1289,7 +1263,7 @@ impl EditorElement { left: px(1.), }, cx.theme().colors().scrollbar_track_border, - ); + )); let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; if layout.is_singleton && scrollbar_settings.selections { let start_anchor = Anchor::min(); @@ -1309,7 +1283,7 @@ impl EditorElement { end_y = start_y + px(1.); } let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - cx.paint_quad( + cx.paint_quad(quad( bounds, Corners::default(), cx.theme().status().info, @@ -1320,7 +1294,7 @@ impl EditorElement { left: px(1.), }, cx.theme().colors().scrollbar_thumb_border, - ); + )); } } @@ -1352,7 +1326,7 @@ impl EditorElement { DiffHunkStatus::Modified => cx.theme().status().modified, DiffHunkStatus::Removed => cx.theme().status().deleted, }; - cx.paint_quad( + cx.paint_quad(quad( bounds, Corners::default(), color, @@ -1363,11 +1337,11 @@ impl EditorElement { left: px(1.), }, cx.theme().colors().scrollbar_thumb_border, - ); + )); } } - cx.paint_quad( + cx.paint_quad(quad( thumb_bounds, Corners::default(), cx.theme().colors().scrollbar_thumb_background, @@ -1378,7 +1352,7 @@ impl EditorElement { left: px(1.), }, cx.theme().colors().scrollbar_thumb_border, - ); + )); } let mouse_position = cx.mouse_position(); @@ -3085,23 +3059,13 @@ impl Cursor { }; //Draw background or border quad - if matches!(self.shape, CursorShape::Hollow) { - cx.paint_quad( - bounds, - Corners::default(), - transparent_black(), - Edges::all(px(1.)), - self.color, - ); + let cursor = if matches!(self.shape, CursorShape::Hollow) { + outline(bounds, self.color) } else { - cx.paint_quad( - bounds, - Corners::default(), - self.color, - Edges::default(), - transparent_black(), - ); - } + fill(bounds, self.color) + }; + + cx.paint_quad(cursor); if let Some(block_text) = &self.block_text { block_text.paint(self.origin + origin, self.line_height, cx); diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index ee2f42d2a2..f58435d7b9 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -1592,6 +1592,17 @@ impl Edges { } } +impl Into> for f32 { + fn into(self) -> Edges { + Edges { + top: self.into(), + right: self.into(), + bottom: self.into(), + left: self.into(), + } + } +} + /// Represents the corners of a box in a 2D space, such as border radius. /// /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. @@ -1808,6 +1819,28 @@ where impl Copy for Corners where T: Copy + Clone + Default + Debug {} +impl Into> for f32 { + fn into(self) -> Corners { + Corners { + top_left: self.into(), + top_right: self.into(), + bottom_right: self.into(), + bottom_left: self.into(), + } + } +} + +impl Into> for Pixels { + fn into(self) -> Corners { + Corners { + top_left: self, + top_right: self, + bottom_right: self, + bottom_left: self, + } + } +} + /// Represents a length in pixels, the base unit of measurement in the UI framework. /// /// `Pixels` is a value type that represents an absolute length in pixels, which is used diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 04f247d076..4031a35f75 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,9 +1,9 @@ use std::{iter, mem, ops::Range}; use crate::{ - black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, - Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, - FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, + black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, + ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, + Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; use collections::HashSet; @@ -348,13 +348,13 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.is_some() || self.is_border_visible() { cx.with_z_index(1, |cx| { - cx.paint_quad( + cx.paint_quad(quad( bounds, self.corner_radii.to_pixels(bounds.size, rem_size), background_color.unwrap_or_default(), self.border_widths.to_pixels(rem_size), self.border_color.unwrap_or_default(), - ); + )); }); } } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index d62bee69c0..a58d77f585 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -1,7 +1,6 @@ use crate::{ - black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, - LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, - WrappedLineLayout, + black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, + SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -109,16 +108,13 @@ fn paint_line( if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { wraps.next(); if let Some((background_origin, background_color)) = current_background.as_mut() { - cx.paint_quad( + cx.paint_quad(fill( Bounds { origin: *background_origin, size: size(glyph_origin.x - background_origin.x, line_height), }, - Corners::default(), *background_color, - Edges::default(), - transparent_black(), - ); + )); background_origin.x = origin.x; background_origin.y += line_height; } @@ -180,16 +176,13 @@ fn paint_line( } if let Some((background_origin, background_color)) = finished_background { - cx.paint_quad( + cx.paint_quad(fill( Bounds { origin: background_origin, size: size(glyph_origin.x - background_origin.x, line_height), }, - Corners::default(), background_color, - Edges::default(), - transparent_black(), - ); + )); } if let Some((underline_origin, underline_style)) = finished_underline { @@ -235,16 +228,13 @@ fn paint_line( } if let Some((background_origin, background_color)) = current_background.take() { - cx.paint_quad( + cx.paint_quad(fill( Bounds { origin: background_origin, size: size(last_line_end_x - background_origin.x, line_height), }, - Corners::default(), background_color, - Edges::default(), - transparent_black(), - ); + )); } if let Some((underline_start, underline_style)) = current_underline.take() { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 77eb4e27be..554a6d6e6b 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,15 @@ use crate::{ - key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext, - AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, - EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, - ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId, - Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, - Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style, - SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + key_dispatch::DispatchActionListener, px, size, transparent_black, Action, AnyDrag, AnyView, + AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, + CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, + EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, + Hsla, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, + LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, + PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, + RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, + Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -963,14 +963,8 @@ impl<'a> WindowContext<'a> { /// Paint one or more quads into the scene for the next frame at the current stacking context. /// Quads are colored rectangular regions with an optional background, border, and corner radius. - pub fn paint_quad( - &mut self, - bounds: Bounds, - corner_radii: Corners, - background: impl Into, - border_widths: Edges, - border_color: impl Into, - ) { + /// see [`fill`], [`outline`], and [`quad`] to construct this type. + pub fn paint_quad(&mut self, quad: PaintQuad) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); @@ -979,12 +973,12 @@ impl<'a> WindowContext<'a> { &window.next_frame.z_index_stack, Quad { order: 0, - bounds: bounds.scale(scale_factor), + bounds: quad.bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), - background: background.into(), - border_color: border_color.into(), - corner_radii: corner_radii.scale(scale_factor), - border_widths: border_widths.scale(scale_factor), + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), }, ); } @@ -2962,3 +2956,85 @@ impl From<(&'static str, u64)> for ElementId { ElementId::NamedInteger(name.into(), id as usize) } } + +/// A rectangle, to be rendered on the screen by GPUI at the given position and size. +pub struct PaintQuad { + bounds: Bounds, + corner_radii: Corners, + background: Hsla, + border_widths: Edges, + border_color: Hsla, +} + +impl PaintQuad { + /// Set the corner radii of the quad. + pub fn corner_radii(self, corner_radii: impl Into>) -> Self { + PaintQuad { + corner_radii: corner_radii.into(), + ..self + } + } + + /// Set the border widths of the quad. + pub fn border_widths(self, border_widths: impl Into>) -> Self { + PaintQuad { + border_widths: border_widths.into(), + ..self + } + } + + /// Set the border color of the quad. + pub fn border_color(self, border_color: impl Into) -> Self { + PaintQuad { + border_color: border_color.into(), + ..self + } + } + + /// Set the background color of the quad. + pub fn background(self, background: impl Into) -> Self { + PaintQuad { + background: background.into(), + ..self + } + } +} + +/// Create a quad with the given parameters. +pub fn quad( + bounds: Bounds, + corner_radii: impl Into>, + background: impl Into, + border_widths: impl Into>, + border_color: impl Into, +) -> PaintQuad { + PaintQuad { + bounds, + corner_radii: corner_radii.into(), + background: background.into(), + border_widths: border_widths.into(), + border_color: border_color.into(), + } +} + +/// Create a filled quad with the given bounds and background color. +pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: background.into(), + border_widths: (0.).into(), + border_color: transparent_black(), + } +} + +/// Create a rectangle outline with the given bounds, border color, and a 1px border width +pub fn outline(bounds: impl Into>, border_color: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: transparent_black(), + border_widths: (1.).into(), + border_color: border_color.into(), + } +} diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 56b5920aec..7358f2e1d7 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,9 +1,9 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext, - AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, - FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, - IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, + black, div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, + Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, FontStyle, + FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement, + LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext, }; @@ -133,13 +133,7 @@ impl LayoutRect { ) .into(); - cx.paint_quad( - Bounds::new(position, size), - Default::default(), - self.color, - Default::default(), - transparent_black(), - ); + cx.paint_quad(fill(Bounds::new(position, size), self.color)); } } @@ -775,13 +769,7 @@ impl Element for TerminalElement { let theme = cx.theme(); - cx.paint_quad( - bounds, - Default::default(), - layout.background_color, - Default::default(), - Hsla::default(), - ); + cx.paint_quad(fill(bounds, layout.background_color)); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); let terminal_input_handler = TerminalInputHandler { From 26a31b41b96d56815c581b587d381d632e9cf85b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Dec 2023 13:02:19 -0800 Subject: [PATCH 28/94] frame time --- crates/gpui2/src/window.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 77eb4e27be..a0db7d6d9e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1236,6 +1236,8 @@ impl<'a> WindowContext<'a> { /// Draw pixels to the display for this window based on the contents of its scene. pub(crate) fn draw(&mut self) -> Scene { + let t0 = std::time::Instant::now(); + let window_was_focused = self .window .focus @@ -1326,6 +1328,7 @@ impl<'a> WindowContext<'a> { } self.window.dirty = false; + eprintln!("frame: {:?}", t0.elapsed()); scene } From a807e798ecb640b53a85a589c3ca344ae028f2bd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 13 Dec 2023 13:40:19 -0800 Subject: [PATCH 29/94] Add new drag API --- crates/gpui2/src/app.rs | 4 ++ crates/gpui2/src/element.rs | 63 +++++++++--------------- crates/gpui2/src/elements/canvas.rs | 6 +-- crates/gpui2/src/elements/div.rs | 37 +++++++++++++- crates/gpui2_macros/src/derive_render.rs | 23 +++++++++ crates/gpui2_macros/src/gpui2_macros.rs | 6 +++ crates/workspace2/src/dock.rs | 25 ++-------- crates/workspace2/src/workspace2.rs | 51 ++++++++----------- 8 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 crates/gpui2_macros/src/derive_render.rs diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 62ce6305ea..18f688f179 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1138,6 +1138,10 @@ impl AppContext { pub fn has_active_drag(&self) -> bool { self.active_drag.is_some() } + + pub fn active_drag(&self) -> Option { + self.active_drag.as_ref().map(|drag| drag.view.clone()) + } } impl Context for AppContext { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 226a477012..e5ecd195ba 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -482,48 +482,31 @@ impl IntoElement for AnyElement { } } -// impl Element for Option -// where -// V: 'static, -// E: Element, -// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static, -// { -// type State = Option; +/// The empty element, which renders nothing. +pub type Empty = (); -// fn element_id(&self) -> Option { -// None -// } +impl IntoElement for () { + type Element = Self; -// fn layout( -// &mut self, -// _: Option, -// cx: &mut WindowContext, -// ) -> (LayoutId, Self::State) { -// let render = self.take().unwrap(); -// let mut element = (render)(view_state, cx).into_any(); -// let layout_id = element.layout(view_state, cx); -// (layout_id, Some(element)) -// } + fn element_id(&self) -> Option { + None + } -// fn paint( -// self, -// _bounds: Bounds, -// rendered_element: &mut Self::State, -// cx: &mut WindowContext, -// ) { -// rendered_element.take().unwrap().paint(view_state, cx); -// } -// } + fn into_element(self) -> Self::Element { + self + } +} -// impl RenderOnce for Option -// where -// V: 'static, -// E: Element, -// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static, -// { -// type Element = Self; +impl Element for () { + type State = (); -// fn render(self) -> Self::Element { -// self -// } -// } + fn layout( + &mut self, + _state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + (cx.request_layout(&crate::Style::default(), None), ()) + } + + fn paint(self, _bounds: Bounds, _state: &mut Self::State, _cx: &mut WindowContext) {} +} diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs index 287a3b4b5a..b3afd335d4 100644 --- a/crates/gpui2/src/elements/canvas.rs +++ b/crates/gpui2/src/elements/canvas.rs @@ -2,7 +2,7 @@ use refineable::Refineable as _; use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; -pub fn canvas(callback: impl 'static + FnOnce(Bounds, &mut WindowContext)) -> Canvas { +pub fn canvas(callback: impl 'static + FnOnce(&Bounds, &mut WindowContext)) -> Canvas { Canvas { paint_callback: Box::new(callback), style: StyleRefinement::default(), @@ -10,7 +10,7 @@ pub fn canvas(callback: impl 'static + FnOnce(Bounds, &mut WindowContext } pub struct Canvas { - paint_callback: Box, &mut WindowContext)>, + paint_callback: Box, &mut WindowContext)>, style: StyleRefinement, } @@ -41,7 +41,7 @@ impl Element for Canvas { } fn paint(self, bounds: Bounds, _: &mut (), cx: &mut WindowContext) { - (self.paint_callback)(bounds, cx) + (self.paint_callback)(&bounds, cx) } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a102c71a6f..7dfc9bae6a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -29,6 +29,11 @@ pub struct GroupStyle { pub style: Box, } +pub struct DragMoveEvent { + pub event: MouseMoveEvent, + pub drag: View, +} + pub trait InteractiveElement: Sized { fn interactivity(&mut self) -> &mut Interactivity; @@ -192,6 +197,34 @@ pub trait InteractiveElement: Sized { self } + fn on_drag_move( + mut self, + listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, + ) -> Self + where + W: Render, + { + self.interactivity().mouse_move_listeners.push(Box::new( + move |event, bounds, phase, cx| { + if phase == DispatchPhase::Capture + && bounds.drag_target_contains(&event.position, cx) + { + if let Some(view) = cx.active_drag().and_then(|view| view.downcast::().ok()) + { + (listener)( + &DragMoveEvent { + event: event.clone(), + drag: view, + }, + cx, + ); + } + } + }, + )); + self + } + fn on_scroll_wheel( mut self, listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, @@ -403,7 +436,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } - fn on_drag(mut self, listener: impl Fn(&mut WindowContext) -> View + 'static) -> Self + fn on_drag(mut self, constructor: impl Fn(&mut WindowContext) -> View + 'static) -> Self where Self: Sized, W: 'static + Render, @@ -413,7 +446,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { "calling on_drag more than once on the same element is not supported" ); self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag { - view: listener(cx).into(), + view: constructor(cx).into(), cursor_offset, })); self diff --git a/crates/gpui2_macros/src/derive_render.rs b/crates/gpui2_macros/src/derive_render.rs new file mode 100644 index 0000000000..3983a572f0 --- /dev/null +++ b/crates/gpui2_macros/src/derive_render.rs @@ -0,0 +1,23 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn derive_render(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::Render for #type_name #type_generics + #where_clause + { + type Element = (); + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + () + } + } + }; + + gen.into() +} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index a56d80b86d..f0cd59908d 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,4 +1,5 @@ mod derive_into_element; +mod derive_render; mod register_action; mod style_helpers; mod test; @@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream { derive_into_element::derive_into_element(input) } +#[proc_macro_derive(Render)] +pub fn derive_render(input: TokenStream) -> TokenStream { + derive_render::derive_render(input) +} + #[proc_macro] pub fn style_helpers(input: TokenStream) -> TokenStream { style_helpers::style_helpers(input) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index b656a79a8d..a71b8b4d6f 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,5 +1,5 @@ +use crate::DraggedDock; use crate::{status_bar::StatusItemView, Workspace}; -use crate::{DockClickReset, DockDragState}; use gpui::{ div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render, @@ -493,27 +493,10 @@ impl Render for Dock { let handler = div() .id("resize-handle") .bg(cx.theme().colors().border) - .on_mouse_down(gpui::MouseButton::Left, move |_, cx| { - cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position)) - }) + .on_drag(move |cx| cx.build_view(|_| DraggedDock(position))) .on_click(cx.listener(|v, e: &ClickEvent, cx| { - if e.down.button == MouseButton::Left { - cx.update_global(|state: &mut DockClickReset, cx| { - if state.0.is_some() { - state.0 = None; - v.resize_active_panel(None, cx) - } else { - let double_click = cx.double_click_interval(); - let timer = cx.background_executor().timer(double_click); - state.0 = Some(cx.spawn(|_, mut cx| async move { - timer.await; - cx.update_global(|state: &mut DockClickReset, cx| { - state.0 = None; - }) - .ok(); - })); - } - }) + if e.down.button == MouseButton::Left && e.down.click_count == 2 { + v.resize_active_panel(None, cx) } })); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a7dc76f41d..fb465b2637 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -30,11 +30,11 @@ use futures::{ }; use gpui::{ actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView, - AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, - EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, - KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions, - Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, + DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, + InteractiveElement, KeyContext, 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; @@ -227,9 +227,6 @@ pub fn init_settings(cx: &mut AppContext) { } pub fn init(app_state: Arc, cx: &mut AppContext) { - cx.default_global::(); - cx.default_global::(); - init_settings(cx); notifications::init(cx); @@ -466,6 +463,7 @@ pub struct Workspace { _observe_current_user: Task>, _schedule_serialize: Option>, pane_history_timestamp: Arc, + bounds: Bounds, } impl EventEmitter for Workspace {} @@ -708,6 +706,8 @@ impl Workspace { subscriptions, pane_history_timestamp, workspace_actions: Default::default(), + // This data will be incorrect, but it will be overwritten by the time it needs to be used. + bounds: Default::default(), } } @@ -3580,13 +3580,8 @@ impl FocusableView for Workspace { struct WorkspaceBounds(Bounds); -//todo!("remove this when better drag APIs are in GPUI2") -#[derive(Default)] -struct DockDragState(Option); - -//todo!("remove this when better double APIs are in GPUI2") -#[derive(Default)] -struct DockClickReset(Option>); +#[derive(Render)] +struct DraggedDock(DockPosition); impl Render for Workspace { type Element = Div; @@ -3632,37 +3627,33 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) - .on_mouse_up(gpui::MouseButton::Left, |_, cx| { - cx.update_global(|drag: &mut DockDragState, cx| { - drag.0 = None; - }) - }) - .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| { - if let Some(types) = &cx.global::().0 { - let workspace_bounds = cx.global::().0; - match types { + .child(canvas( + cx.listener(|workspace, bounds, cx| workspace.bounds = *bounds), + )) + .on_drag_move( + cx.listener(|workspace, e: &DragMoveEvent, cx| { + match e.drag.read(cx).0 { DockPosition::Left => { - let size = e.position.x; + let size = e.event.position.x; workspace.left_dock.update(cx, |left_dock, cx| { left_dock.resize_active_panel(Some(size.0), cx); }); } DockPosition::Right => { - let size = workspace_bounds.size.width - e.position.x; + let size = workspace.bounds.size.width - e.event.position.x; workspace.right_dock.update(cx, |right_dock, cx| { right_dock.resize_active_panel(Some(size.0), cx); }); } DockPosition::Bottom => { - let size = workspace_bounds.size.height - e.position.y; + let size = workspace.bounds.size.height - e.event.position.y; workspace.bottom_dock.update(cx, |bottom_dock, cx| { bottom_dock.resize_active_panel(Some(size.0), cx); }); } } - } - })) - .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds)))) + }), + ) .child(self.modal_layer.clone()) .child( div() From 985d4c7429986f5e6d06c7e26d89656716390c89 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 17:09:26 -0500 Subject: [PATCH 30/94] Remove TODO Thanks @ConradIrwin --- crates/feedback2/src/feedback_modal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index e8715034c2..92ecd8d930 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -309,7 +309,6 @@ impl FeedbackModal { Ok(()) } - // TODO: Escape button calls dismiss fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { cx.emit(DismissEvent) } From aa55e55c7a3f886df7fab25b8d8ef1348a34b9f9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 17:25:07 -0500 Subject: [PATCH 31/94] Add config files for running Postgres inside Docker Compose (#3637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds config files for running the Postgres instance for local Zed development in a Docker Compose instance. For those of us who don't like to have a Postgres install always present on the host system 😄 Usage: ``` docker compose up -d ``` Release Notes: - N/A --- docker-compose.sql | 1 + docker-compose.yml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docker-compose.sql create mode 100644 docker-compose.yml diff --git a/docker-compose.sql b/docker-compose.sql new file mode 100644 index 0000000000..9cbd0bf0d1 --- /dev/null +++ b/docker-compose.sql @@ -0,0 +1 @@ +create database zed; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..78faf21a60 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.7" + +services: + postgres: + image: postgres:15 + container_name: zed_postgres + ports: + - 5432:5432 + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + postgres_data: From 1ad1cc114871061fc5a2aa01829e53c1efccaef2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 17:31:51 -0500 Subject: [PATCH 32/94] Fix variable name --- crates/feedback2/src/feedback_modal.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 92ecd8d930..e5c1ccdc9d 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -53,7 +53,7 @@ pub struct FeedbackModal { email_address_editor: View, awaiting_submission: bool, user_submitted: bool, - discarded: bool, + user_discarded: bool, character_count: i32, } @@ -71,7 +71,7 @@ impl ModalView for FeedbackModal { return true; } - if self.discarded { + if self.user_discarded { return true; } @@ -85,7 +85,7 @@ impl ModalView for FeedbackModal { cx.spawn(move |this, mut cx| async move { if answer.await.ok() == Some(0) { this.update(&mut cx, |this, cx| { - this.discarded = true; + this.user_discarded = true; cx.emit(DismissEvent) }) .log_err(); @@ -184,7 +184,7 @@ impl FeedbackModal { email_address_editor, awaiting_submission: false, user_submitted: false, - discarded: false, + user_discarded: false, character_count: 0, } } From 7899833367ea3971dd87b014aec4e962d3ffce23 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Dec 2023 15:44:27 -0700 Subject: [PATCH 33/94] Don't hang the app when signing in offline --- Cargo.lock | 1 + crates/auto_update2/src/auto_update.rs | 4 +- crates/client2/src/client2.rs | 171 ++++---- crates/collab_ui2/Cargo.toml | 2 +- crates/collab_ui2/src/collab_titlebar_item.rs | 388 ++++++------------ crates/ui2/src/components/icon.rs | 2 + 6 files changed, 225 insertions(+), 343 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6cc38ef6a..3b0a8e57fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1949,6 +1949,7 @@ name = "collab_ui2" version = "0.1.0" dependencies = [ "anyhow", + "auto_update2", "call2", "channel2", "client2", diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index 31e474242a..e1e1de0da4 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -6,7 +6,7 @@ use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; use gpui::{ actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, - ViewContext, VisualContext, + ViewContext, VisualContext, WindowContext, }; use isahc::AsyncBody; use serde::Deserialize; @@ -125,7 +125,7 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo } } -pub fn check(_: &Check, cx: &mut ViewContext) { +pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); } else { diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index bf8d5dda77..2c97872989 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{ http::{Request, StatusCode}, }; use futures::{ - future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, - TryStreamExt, + channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, + TryFutureExt as _, TryStreamExt, }; use gpui::{ actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, @@ -1020,91 +1020,116 @@ impl Client { ) -> Task> { let http = self.http.clone(); cx.spawn(|cx| async move { - // Generate a pair of asymmetric encryption keys. The public key will be used by the - // zed server to encrypt the user's access token, so that it can'be intercepted by - // any other app running on the user's device. - let (public_key, private_key) = - rpc::auth::keypair().expect("failed to generate keypair for auth"); - let public_key_string = - String::try_from(public_key).expect("failed to serialize public key for auth"); + let background = cx.background_executor().clone(); - if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - return Self::authenticate_as_admin(http, login.clone(), token.clone()).await; - } + let (open_url_tx, open_url_rx) = oneshot::channel::(); + cx.update(|cx| { + cx.spawn(move |cx| async move { + let url = open_url_rx.await?; + cx.update(|cx| cx.open_url(&url)) + }) + .detach_and_log_err(cx); + }) + .log_err(); - // Start an HTTP server to receive the redirect from Zed's sign-in page. - let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); - let port = server.server_addr().port(); + let credentials = background + .clone() + .spawn(async move { + // Generate a pair of asymmetric encryption keys. The public key will be used by the + // zed server to encrypt the user's access token, so that it can'be intercepted by + // any other app running on the user's device. + let (public_key, private_key) = + rpc::auth::keypair().expect("failed to generate keypair for auth"); + let public_key_string = String::try_from(public_key) + .expect("failed to serialize public key for auth"); - // Open the Zed sign-in page in the user's browser, with query parameters that indicate - // that the user is signing in from a Zed app running on the same device. - let mut url = format!( - "{}/native_app_signin?native_app_port={}&native_app_public_key={}", - *ZED_SERVER_URL, port, public_key_string - ); + if let Some((login, token)) = + IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) + { + return Self::authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } - if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { - log::info!("impersonating user @{}", impersonate_login); - write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); - } + // Start an HTTP server to receive the redirect from Zed's sign-in page. + let server = + tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); + let port = server.server_addr().port(); - cx.update(|cx| cx.open_url(&url))?; + // Open the Zed sign-in page in the user's browser, with query parameters that indicate + // that the user is signing in from a Zed app running on the same device. + let mut url = format!( + "{}/native_app_signin?native_app_port={}&native_app_public_key={}", + *ZED_SERVER_URL, port, public_key_string + ); - // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted - // access token from the query params. - // - // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a - // custom URL scheme instead of this local HTTP server. - let (user_id, access_token) = cx - .spawn(|_| async move { - for _ in 0..100 { - if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); + if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { + log::info!("impersonating user @{}", impersonate_login); + write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); + } + + open_url_tx.send(url).log_err(); + + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted + // access token from the query params. + // + // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a + // custom URL scheme instead of this local HTTP server. + let (user_id, access_token) = background + .spawn(async move { + for _ in 0..100 { + if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } + } + + let post_auth_url = + format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); + req.respond( + tiny_http::Response::empty(302).with_header( + tiny_http::Header::from_bytes( + &b"Location"[..], + post_auth_url.as_bytes(), + ) + .unwrap(), + ), + ) + .context("failed to respond to login http request")?; + return Ok(( + user_id + .ok_or_else(|| anyhow!("missing user_id parameter"))?, + access_token.ok_or_else(|| { + anyhow!("missing access_token parameter") + })?, + )); } } - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - return Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )); - } - } + Err(anyhow!("didn't receive login redirect")) + }) + .await?; - Err(anyhow!("didn't receive login redirect")) + let access_token = private_key + .decrypt_string(&access_token) + .context("failed to decrypt access token")?; + + Ok(Credentials { + user_id: user_id.parse()?, + access_token, + }) }) .await?; - let access_token = private_key - .decrypt_string(&access_token) - .context("failed to decrypt access token")?; cx.update(|cx| cx.activate(true))?; - - Ok(Credentials { - user_id: user_id.parse()?, - access_token, - }) + Ok(credentials) }) } diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 88c27bc165..6249e9fdaf 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -22,7 +22,7 @@ test-support = [ ] [dependencies] -# auto_update = { path = "../auto_update" } +auto_update = { package = "auto_update2", path = "../auto_update2" } db = { package = "db2", path = "../db2" } call = { package = "call2", path = "../call2" } client = { package = "client2", path = "../client2" } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index d6de5a1565..276455151e 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -1,10 +1,11 @@ use crate::face_pile::FacePile; +use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; -use client::{proto::PeerId, Client, ParticipantIndex, SignOut, User, UserStore}; +use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ - actions, canvas, div, overlay, point, px, rems, Action, AppContext, DismissEvent, Div, Element, - FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, - Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent, + Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, + Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; @@ -16,7 +17,7 @@ use ui::{ IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; -use workspace::{notifications::NotifyResultExt, Feedback, Workspace, WORKSPACE_DB}; +use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB}; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -52,7 +53,6 @@ pub struct CollabTitlebarItem { workspace: WeakView, //branch_popover: Option>, project_popover: Option, - //user_menu: ViewHandle, _subscriptions: Vec, } @@ -232,88 +232,17 @@ impl Render for CollabTitlebarItem { }), ) }) - .child(h_stack().px_1p5().map(|this| { - if let Some(user) = current_user { - // TODO: Finish implementing user menu popover - // - this.child( - popover_menu("user-menu") - .menu(|cx| { - ContextMenu::build(cx, |menu, _| { - menu.action( - "Settings", - zed_actions::OpenSettings.boxed_clone(), - ) - .action("Theme", theme_selector::Toggle.boxed_clone()) - .separator() - .action( - "Share Feedback", - feedback::GiveFeedback.boxed_clone(), - ) - .action("Sign Out", client::SignOut.boxed_clone()) - }) - }) - .trigger( - ButtonLike::new("user-menu") - .child( - h_stack() - .gap_0p5() - .child(Avatar::new(user.avatar_uri.clone())) - .child( - IconElement::new(Icon::ChevronDown) - .color(Color::Muted), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| { - Tooltip::text("Toggle User Menu", cx) - }), - ) - .anchor(gpui::AnchorCorner::TopRight), - ) + .map(|el| { + let status = self.client.status(); + let status = &*status.borrow(); + if matches!(status, client::Status::Connected { .. }) { + el.child(self.render_user_menu_button(cx)) } else { - this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { - let client = client.clone(); - cx.spawn(move |mut cx| async move { - client - .authenticate_and_connect(true, &cx) - .await - .notify_async_err(&mut cx); - }) - .detach(); - })) - .child( - popover_menu("user-menu") - .menu(|cx| { - ContextMenu::build(cx, |menu, _| { - menu.action( - "Settings", - zed_actions::OpenSettings.boxed_clone(), - ) - .action("Theme", theme_selector::Toggle.boxed_clone()) - .separator() - .action( - "Share Feedback", - feedback::GiveFeedback.boxed_clone(), - ) - }) - }) - .trigger( - ButtonLike::new("user-menu") - .child( - h_stack().gap_0p5().child( - IconElement::new(Icon::ChevronDown) - .color(Color::Muted), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| { - Tooltip::text("Toggle User Menu", cx) - }), - ), - ) + el.children(self.render_connection_status(status, cx)) + .child(self.render_sign_in_button(cx)) + .child(self.render_user_menu_button(cx)) } - })), + }), ) } } @@ -355,12 +284,6 @@ impl CollabTitlebarItem { project, user_store, client, - // user_menu: cx.add_view(|cx| { - // let view_id = cx.view_id(); - // let mut menu = ContextMenu::new(view_id, cx); - // menu.set_position_mode(OverlayPositionMode::Local); - // menu - // }), // branch_popover: None, project_popover: None, _subscriptions: subscriptions, @@ -535,34 +458,6 @@ impl CollabTitlebarItem { .log_err(); } - // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { - // self.user_menu.update(cx, |user_menu, cx| { - // let items = if let Some(_) = self.user_store.read(cx).current_user() { - // vec![ - // ContextMenuItem::action("Settings", zed_actions::OpenSettings), - // ContextMenuItem::action("Theme", theme_selector::Toggle), - // ContextMenuItem::separator(), - // ContextMenuItem::action( - // "Share Feedback", - // feedback::feedback_editor::GiveFeedback, - // ), - // ContextMenuItem::action("Sign Out", SignOut), - // ] - // } else { - // vec![ - // ContextMenuItem::action("Settings", zed_actions::OpenSettings), - // ContextMenuItem::action("Theme", theme_selector::Toggle), - // ContextMenuItem::separator(), - // ContextMenuItem::action( - // "Share Feedback", - // feedback::feedback_editor::GiveFeedback, - // ), - // ] - // }; - // user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx); - // }); - // } - // fn render_branches_popover_host<'a>( // &'a self, // _theme: &'a theme::Titlebar, @@ -696,154 +591,113 @@ impl CollabTitlebarItem { cx.notify(); } - // fn render_user_menu_button( - // &self, - // theme: &Theme, - // avatar: Option>, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let tooltip = theme.tooltip.clone(); - // let user_menu_button_style = if avatar.is_some() { - // &theme.titlebar.user_menu.user_menu_button_online - // } else { - // &theme.titlebar.user_menu.user_menu_button_offline - // }; + fn render_connection_status( + &self, + status: &client::Status, + cx: &mut ViewContext, + ) -> Option { + match status { + client::Status::ConnectionError + | client::Status::ConnectionLost + | client::Status::Reauthenticating { .. } + | client::Status::Reconnecting { .. } + | client::Status::ReconnectionError { .. } => Some( + div() + .id("disconnected") + .bg(gpui::red()) // todo!() @nate + .child(IconElement::new(Icon::Disconnected)) + .tooltip(|cx| Tooltip::text("Disconnected", cx)) + .into_any_element(), + ), + client::Status::UpgradeRequired => { + let auto_updater = auto_update::AutoUpdater::get(cx); + let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { + Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", + Some(AutoUpdateStatus::Installing) + | Some(AutoUpdateStatus::Downloading) + | Some(AutoUpdateStatus::Checking) => "Updating...", + Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { + "Please update Zed to Collaborate" + } + }; - // let avatar_style = &user_menu_button_style.avatar; - // Stack::new() - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = user_menu_button_style - // .user_menu - // .inactive_state() - // .style_for(state); + Some( + div() + .bg(gpui::red()) // todo!() @nate + .child(Button::new("connection-status", label).on_click(|_, cx| { + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { + if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { + workspace::restart(&Default::default(), cx); + return; + } + } + auto_update::check(&Default::default(), cx); + })) + .into_any_element(), + ) + } + _ => None, + } + } - // let mut dropdown = Flex::row().align_children_center(); + pub fn render_sign_in_button(&mut self, _: &mut ViewContext) -> Button { + let client = self.client.clone(); + Button::new("sign_in", "Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach(); + }) + } - // if let Some(avatar_img) = avatar { - // dropdown = dropdown.with_child(Self::render_face( - // avatar_img, - // *avatar_style, - // Color::transparent_black(), - // None, - // )); - // }; - - // dropdown - // .with_child( - // Svg::new("icons/caret_down.svg") - // .with_color(user_menu_button_style.icon.color) - // .constrained() - // .with_width(user_menu_button_style.icon.width) - // .contained() - // .into_any(), - // ) - // .aligned() - // .constrained() - // .with_height(style.width) - // .contained() - // .with_style(style.container) - // .into_any() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.user_menu.update(cx, |menu, _| menu.delay_cancel()); - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.toggle_user_menu(&Default::default(), cx) - // }) - // .with_tooltip::( - // 0, - // "Toggle User Menu".to_owned(), - // Some(Box::new(ToggleUserMenu)), - // tooltip, - // cx, - // ) - // .contained(), - // ) - // .with_child( - // ChildView::new(&self.user_menu, cx) - // .aligned() - // .bottom() - // .right(), - // ) - // .into_any() - // } - - // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar.sign_in_button.inactive_state().style_for(state); - // Label::new("Sign In", style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // let client = this.client.clone(); - // cx.app_context() - // .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await }) - // .detach_and_log_err(cx); - // }) - // .into_any() - // } - - // fn render_connection_status( - // &self, - // status: &client::Status, - // cx: &mut ViewContext, - // ) -> Option> { - // enum ConnectionStatusButton {} - - // let theme = &theme::current(cx).clone(); - // match status { - // client::Status::ConnectionError - // | client::Status::ConnectionLost - // | client::Status::Reauthenticating { .. } - // | client::Status::Reconnecting { .. } - // | client::Status::ReconnectionError { .. } => Some( - // Svg::new("icons/disconnected.svg") - // .with_color(theme.titlebar.offline_icon.color) - // .constrained() - // .with_width(theme.titlebar.offline_icon.width) - // .aligned() - // .contained() - // .with_style(theme.titlebar.offline_icon.container) - // .into_any(), - // ), - // client::Status::UpgradeRequired => { - // let auto_updater = auto_update::AutoUpdater::get(cx); - // let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { - // Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", - // Some(AutoUpdateStatus::Installing) - // | Some(AutoUpdateStatus::Downloading) - // | Some(AutoUpdateStatus::Checking) => "Updating...", - // Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { - // "Please update Zed to Collaborate" - // } - // }; - - // Some( - // MouseEventHandler::new::(0, cx, |_, _| { - // Label::new(label, theme.titlebar.outdated_warning.text.clone()) - // .contained() - // .with_style(theme.titlebar.outdated_warning.container) - // .aligned() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { - // if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { - // workspace::restart(&Default::default(), cx); - // return; - // } - // } - // auto_update::check(&Default::default(), cx); - // }) - // .into_any(), - // ) - // } - // _ => None, - // } - // } + pub fn render_user_menu_button(&mut self, cx: &mut ViewContext) -> impl Element { + if let Some(user) = self.user_store.read(cx).current_user() { + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| { + menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) + .action("Sign Out", client::SignOut.boxed_clone()) + }) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(Avatar::new(user.avatar_uri.clone())) + .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight) + } else { + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| { + menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) + }) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + } + } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index b68aa579c0..6876630100 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -50,6 +50,7 @@ pub enum Icon { CopilotError, CopilotDisabled, Dash, + Disconnected, Envelope, ExternalLink, ExclamationTriangle, @@ -129,6 +130,7 @@ impl Icon { Icon::CopilotError => "icons/copilot_error.svg", Icon::CopilotDisabled => "icons/copilot_disabled.svg", Icon::Dash => "icons/dash.svg", + Icon::Disconnected => "icons/disconnected.svg", Icon::Envelope => "icons/feedback.svg", Icon::ExclamationTriangle => "icons/warning.svg", Icon::ExternalLink => "icons/external_link.svg", From 1e4a7e6ef18e765fc30303e2ec7176a61a1d8b72 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 13 Dec 2023 16:01:49 -0700 Subject: [PATCH 34/94] Don't notify when drawing --- crates/gpui2/src/window.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index a0db7d6d9e..d0056617ed 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -237,6 +237,7 @@ pub struct Window { bounds_observers: SubscriberSet<(), AnyObserver>, active: bool, pub(crate) dirty: bool, + pub(crate) drawing: bool, activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) last_blur: Option>, pub(crate) focus: Option, @@ -371,6 +372,7 @@ impl Window { bounds_observers: SubscriberSet::new(), active: false, dirty: false, + drawing: false, activation_observers: SubscriberSet::new(), last_blur: None, focus: None, @@ -422,7 +424,9 @@ impl<'a> WindowContext<'a> { /// Mark the window as dirty, scheduling it to be redrawn on the next frame. pub fn notify(&mut self) { - self.window.dirty = true; + if !self.window.drawing { + self.window.dirty = true; + } } /// Close this window. @@ -1237,6 +1241,8 @@ impl<'a> WindowContext<'a> { /// Draw pixels to the display for this window based on the contents of its scene. pub(crate) fn draw(&mut self) -> Scene { let t0 = std::time::Instant::now(); + self.window.dirty = false; + self.window.drawing = true; let window_was_focused = self .window @@ -1327,7 +1333,7 @@ impl<'a> WindowContext<'a> { self.platform.set_cursor_style(cursor_style); } - self.window.dirty = false; + self.window.drawing = false; eprintln!("frame: {:?}", t0.elapsed()); scene @@ -2346,10 +2352,12 @@ impl<'a, V: 'static> ViewContext<'a, V> { } pub fn notify(&mut self) { - self.window_cx.notify(); - self.window_cx.app.push_effect(Effect::Notify { - emitter: self.view.model.entity_id, - }); + if !self.window.drawing { + self.window_cx.notify(); + self.window_cx.app.push_effect(Effect::Notify { + emitter: self.view.model.entity_id, + }); + } } pub fn observe_window_bounds( From e2bfd464555bfc3e9a9403c42ce5c348e49145ca Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 13 Dec 2023 15:14:51 -0800 Subject: [PATCH 35/94] Fix dock split resizin --- crates/workspace2/src/dock.rs | 35 ++++++++++++++++++++--------- crates/workspace2/src/workspace2.rs | 25 +++++++++++++-------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index a71b8b4d6f..495fa01423 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,9 +1,10 @@ use crate::DraggedDock; use crate::{status_bar::StatusItemView, Workspace}; use gpui::{ - div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId, - EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render, - SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, + div, px, red, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, + EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, + Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -492,7 +493,8 @@ impl Render for Dock { let position = self.position; let handler = div() .id("resize-handle") - .bg(cx.theme().colors().border) + // .bg(cx.theme().colors().border) + .bg(red()) .on_drag(move |cx| cx.build_view(|_| DraggedDock(position))) .on_click(cx.listener(|v, e: &ClickEvent, cx| { if e.down.button == MouseButton::Left && e.down.click_count == 2 { @@ -500,23 +502,29 @@ impl Render for Dock { } })); + const HANDLE_SIZE: Pixels = Pixels(4. * 3.); + match self.position() { DockPosition::Left => { - post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize()) + post_resize_handle = + Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize()) } DockPosition::Bottom => { - pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize()) + pre_resize_handle = + Some(handler.w_full().min_h(HANDLE_SIZE).cursor_row_resize()) } DockPosition::Right => { - pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize()) + pre_resize_handle = + Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize()) } } div() + .flex() .border_color(cx.theme().colors().border) .map(|this| match self.position().axis() { - Axis::Horizontal => this.w(px(size)).h_full(), - Axis::Vertical => this.h(px(size)).w_full(), + Axis::Horizontal => this.w(px(size)).h_full().flex_row(), + Axis::Vertical => this.h(px(size)).w_full().flex_col(), }) .map(|this| match self.position() { DockPosition::Left => this.border_r(), @@ -524,7 +532,14 @@ impl Render for Dock { DockPosition::Bottom => this.border_t(), }) .children(pre_resize_handle) - .child(entry.panel.to_any()) + .child( + div() + .map(|this| match self.position().axis() { + Axis::Horizontal => this.min_w(px(size) - HANDLE_SIZE).h_full(), + Axis::Vertical => this.min_h(px(size) - HANDLE_SIZE).w_full(), + }) + .child(entry.panel.to_any()), + ) .children(post_resize_handle) } else { div() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index fb465b2637..c070bdfad2 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,9 +29,9 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView, - AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, - DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, + actions, canvas, div, impl_actions, outline, point, size, Action, AnyModel, AnyView, + AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, + Div, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, @@ -3627,26 +3627,33 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) - .child(canvas( - cx.listener(|workspace, bounds, cx| workspace.bounds = *bounds), - )) + .child( + canvas(cx.listener(|workspace, bounds, cx| { + cx.with_z_index(100, |cx| { + cx.paint_quad(outline(*bounds, gpui::green())); + }); + workspace.bounds = *bounds; + })) + .absolute() + .size_full(), + ) .on_drag_move( cx.listener(|workspace, e: &DragMoveEvent, cx| { match e.drag.read(cx).0 { DockPosition::Left => { - let size = e.event.position.x; + let size = workspace.bounds.left() + e.event.position.x; workspace.left_dock.update(cx, |left_dock, cx| { left_dock.resize_active_panel(Some(size.0), cx); }); } DockPosition::Right => { - let size = workspace.bounds.size.width - e.event.position.x; + let size = workspace.bounds.right() - e.event.position.x; workspace.right_dock.update(cx, |right_dock, cx| { right_dock.resize_active_panel(Some(size.0), cx); }); } DockPosition::Bottom => { - let size = workspace.bounds.size.height - e.event.position.y; + let size = workspace.bounds.bottom() - e.event.position.y; workspace.bottom_dock.update(cx, |bottom_dock, cx| { bottom_dock.resize_active_panel(Some(size.0), cx); }); From d59de96921a4e0768c7590010c39f4cf391b6190 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 18:20:04 -0500 Subject: [PATCH 36/94] Style collab panel (#3638) This PR styles the collab panel. Release Notes: - N/A --------- Co-authored-by: Nate Butler Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/collab_ui2/src/collab_panel.rs | 269 ++++++++++---------- crates/ui2/src/components/disclosure.rs | 3 +- crates/ui2/src/components/list/list_item.rs | 16 +- script/storybook | 15 ++ 4 files changed, 167 insertions(+), 136 deletions(-) create mode 100755 script/storybook diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index a34d574957..0ad10f58de 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -176,11 +176,11 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json, - size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, - EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, - Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, - RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, + Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, + IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, + Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -402,7 +402,7 @@ impl CollabPanel { let filter_editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Filter channels, contacts", cx); + editor.set_placeholder_text("Filter...", cx); editor }); @@ -1157,24 +1157,20 @@ impl CollabPanel { ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) - .child( - h_stack() - .w_full() - .justify_between() - .child(Label::new(user.github_login.clone())) - .child(if is_pending { - Label::new("Calling").color(Color::Muted).into_any_element() - } else if is_current_user { - IconButton::new("leave-call", Icon::ArrowRight) - .on_click(cx.listener(move |this, _, cx| { - Self::leave_call(cx); - })) - .tooltip(|cx| Tooltip::text("Leave Call", cx)) - .into_any_element() - } else { - div().into_any_element() - }), - ) + .child(Label::new(user.github_login.clone())) + .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) + .style(ButtonStyle::Subtle) + .on_click(cx.listener(move |this, _, cx| { + Self::leave_call(cx); + })) + .tooltip(|cx| Tooltip::text("Leave Call", cx)) + .into_any_element() + } else { + div().into_any_element() + }) .when_some(peer_id, |this, peer_id| { this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { @@ -1212,8 +1208,12 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .start_slot(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Folder)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Folder)), + ) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -1305,8 +1305,12 @@ impl CollabPanel { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) - .start_slot(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Screen)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Screen)), + ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { this.on_click(cx.listener(move |this, _, cx| { @@ -1372,9 +1376,13 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) - .start_slot(render_tree_branch(false, cx)) - .child(IconButton::new(0, Icon::File)) - .child(Label::new("notes")) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::File)), + ) + .child(div().h_7().w_full().child(Label::new("notes"))) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } @@ -1387,8 +1395,12 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) - .start_slot(render_tree_branch(true, cx)) - .child(IconButton::new(0, Icon::MessageBubbles)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::MessageBubbles)), + ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } @@ -2149,11 +2161,6 @@ impl CollabPanel { fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { v_stack() .size_full() - .child( - div() - .p_2() - .child(div().rounded(px(2.0)).child(self.filter_editor.clone())), - ) .child( v_stack() .size_full() @@ -2223,6 +2230,14 @@ impl CollabPanel { } })), ) + .child( + div().p_2().child( + div() + .border_primary(cx) + .border_t() + .child(self.filter_editor.clone()), + ), + ) } fn render_header( @@ -2274,22 +2289,32 @@ 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) - .on_click(move |_, cx| { - let item = ClipboardItem::new(channel_link_copy.clone()); - cx.write_to_clipboard(item) - }) - .tooltip(|cx| Tooltip::text("Copy channel link", cx)) + div() + .invisible() + .group_hover("section-header", |this| this.visible()) + .child( + IconButton::new("channel-link", Icon::Copy) + .icon_size(IconSize::Small) + .size(ButtonSize::None) + .on_click(move |_, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) + }) + .tooltip(|cx| Tooltip::text("Copy channel link", cx)), + ) + .into_any_element() }), Section::Contacts => Some( IconButton::new("add-contact", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) - .tooltip(|cx| Tooltip::text("Search for new contact", cx)), + .tooltip(|cx| Tooltip::text("Search for new contact", cx)) + .into_any_element(), ), Section::Channels => Some( IconButton::new("add-channel", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) - .tooltip(|cx| Tooltip::text("Create a channel", cx)), + .tooltip(|cx| Tooltip::text("Create a channel", cx)) + .into_any_element(), ), _ => None, }; @@ -2304,25 +2329,18 @@ impl CollabPanel { h_stack() .w_full() - .map(|el| { - if can_collapse { - el.child( - ListItem::new(text.clone()) - .child(div().w_full().child(Label::new(text))) - .selected(is_selected) - .toggle(Some(!is_collapsed)) - .on_click(cx.listener(move |this, _, cx| { - this.toggle_section_expanded(section, cx) - })), - ) - } else { - el.child( - ListHeader::new(text) - .when_some(button, |el, button| el.end_slot(button)) - .selected(is_selected), - ) - } - }) + .group("section-header") + .child( + ListHeader::new(text) + .toggle(if can_collapse { + Some(!is_collapsed) + } else { + None + }) + .inset(true) + .end_slot::(button) + .selected(is_selected), + ) .when(section == Section::Channels, |el| { el.drag_over::(|style| { style.bg(cx.theme().colors().ghost_element_hover) @@ -2460,7 +2478,7 @@ impl CollabPanel { .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) - .start_slot::(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar))) + .start_slot(Avatar::new(user.avatar_uri.clone())) } fn render_contact_placeholder( @@ -2541,6 +2559,8 @@ impl CollabPanel { div() .id(channel_id as usize) .group("") + .flex() + .w_full() .on_drag({ let channel = channel.clone(); move |cx| { @@ -2566,71 +2586,10 @@ impl CollabPanel { ) .child( ListItem::new(channel_id as usize) - .indent_level(depth) + // Offset the indent depth by one to give us room to show the disclosure. + .indent_level(depth + 1) .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle - .start_slot( - IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) - .size(IconSize::Small) - .color(Color::Muted), - ) .selected(is_selected || is_active) - .child( - h_stack() - .w_full() - .justify_between() - .child( - h_stack() - .id(channel_id as usize) - .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render(cx))), - ) - .child( - h_stack() - .child( - div() - .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new( - "channel_chat", - Icon::MessageBubbles, - ) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx) - })) - .tooltip(|cx| { - Tooltip::text("Open channel chat", cx) - }), - ), - ) - .child( - div() - .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_notes", Icon::File) - .icon_color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx) - })) - .tooltip(|cx| { - Tooltip::text("Open channel notes", cx) - }), - ), - ), - ), - ) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { @@ -2650,7 +2609,57 @@ impl CollabPanel { move |this, event: &MouseDownEvent, cx| { this.deploy_channel_context_menu(event.position, channel_id, ix, cx) }, - )), + )) + .start_slot( + IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child( + h_stack() + .id(channel_id as usize) + .child(Label::new(channel.name.clone())) + .children(face_pile.map(|face_pile| face_pile.render(cx))), + ) + .end_slot( + h_stack() + .child( + div() + .id("channel_chat") + .when(!has_messages_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_chat", Icon::MessageBubbles) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), + ), + ) + .child( + div() + .id("channel_notes") + .when(!has_notes_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_notes", Icon::File) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel notes", cx)), + ), + ), + ), ) .tooltip(|cx| Tooltip::text("Join channel", cx)) diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index 7d9a69bb3a..7d0f911d96 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/crates/ui2/src/components/disclosure.rs @@ -1,6 +1,7 @@ -use crate::{prelude::*, Color, Icon, IconButton, IconSize}; use gpui::ClickEvent; +use crate::{prelude::*, Color, Icon, IconButton, IconSize}; + #[derive(IntoElement)] pub struct Disclosure { is_open: bool, diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index df6e542816..403d3e7605 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,9 +1,10 @@ -use crate::{prelude::*, Disclosure}; use gpui::{ px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful, }; use smallvec::SmallVec; +use crate::{prelude::*, Disclosure}; + #[derive(IntoElement)] pub struct ListItem { id: ElementId, @@ -192,10 +193,15 @@ impl RenderOnce for ListItem { this.ml(self.indent_level as f32 * self.indent_step_size) } }) - .children( - self.toggle - .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), - ) + .children(self.toggle.map(|is_open| { + div() + .flex() + .absolute() + .left(rems(-1.)) + .invisible() + .group_hover("", |style| style.visible()) + .child(Disclosure::new(is_open).on_toggle(self.on_toggle)) + })) .child( h_stack() .flex_1() diff --git a/script/storybook b/script/storybook new file mode 100755 index 0000000000..bcabdef0af --- /dev/null +++ b/script/storybook @@ -0,0 +1,15 @@ +#!/bin/bash + +# This script takes a single text input and replaces 'list_item' with the input in a cargo run command + +# Check if an argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign the argument to a variable +COMPONENT_NAME="$1" + +# Run the cargo command with the provided component name +cargo run -p storybook2 -- components/"$COMPONENT_NAME" From 137e4e9251937ce5268ab0f0b3391f1ef641d6e4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 19:12:20 -0500 Subject: [PATCH 37/94] Add `.visible_on_hover` helper method (#3639) This PR adds a `.visible_on_hover` helper method that can be used to make an element only visible on hover. I noticed we were repeating this similar stanza in a bunch of different spots: ```rs some_element .invisible() .group_hover("", |style| style.visible()) ``` so it seemed like a nice thing to factor out into a reusable utility. Release Notes: - N/A --- crates/collab_ui2/src/chat_panel.rs | 8 ++--- crates/collab_ui2/src/collab_panel.rs | 35 ++++++++----------- crates/editor2/src/editor.rs | 3 +- crates/ui2/src/components/list/list_header.rs | 3 +- crates/ui2/src/components/list/list_item.rs | 6 ++-- crates/ui2/src/components/tab.rs | 3 +- crates/ui2/src/prelude.rs | 1 + crates/ui2/src/ui2.rs | 2 ++ crates/ui2/src/visible_on_hover.rs | 13 +++++++ 9 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 crates/ui2/src/visible_on_hover.rs diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 587efbe95f..9096770166 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore}; use std::sync::Arc; use theme::ActiveTheme as _; use time::{OffsetDateTime, UtcOffset}; -use ui::{ - h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon, - IconButton, Label, Tooltip, -}; +use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -382,12 +379,11 @@ impl ChatPanel { .child(text.element("body".into(), cx)) .child( div() - .invisible() .absolute() .top_1() .right_2() .w_8() - .group_hover("", |this| this.visible()) + .visible_on_hover("") .child(render_remove(message_id_to_remove, cx)), ) .into_any() diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 0ad10f58de..cf1ac5205a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2290,8 +2290,7 @@ impl CollabPanel { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); div() - .invisible() - .group_hover("section-header", |this| this.visible()) + .visible_on_hover("section-header") .child( IconButton::new("channel-link", Icon::Copy) .icon_size(IconSize::Small) @@ -2381,21 +2380,17 @@ impl CollabPanel { }) .when(!calling, |el| { el.child( - div() - .id("remove_contact") - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("remove_contact", Icon::Close) - .icon_color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener({ - let github_login = github_login.clone(); - move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - } - })), - ), + div().visible_on_hover("").child( + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), ) }), ) @@ -2626,8 +2621,7 @@ impl CollabPanel { .child( div() .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) + .when(!has_messages_notification, |el| el.visible_on_hover("")) .child( IconButton::new("channel_chat", Icon::MessageBubbles) .icon_color(if has_messages_notification { @@ -2644,8 +2638,7 @@ impl CollabPanel { .child( div() .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) + .when(!has_notes_notification, |el| el.visible_on_hover("")) .child( IconButton::new("channel_notes", Icon::File) .icon_color(if has_notes_notification { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0d19b53d29..aba8dbd4d4 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9766,8 +9766,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend div() .border() .border_color(gpui::red()) - .invisible() - .group_hover(group_id, |style| style.visible()) + .visible_on_hover(group_id) .child( IconButton::new(copy_id.clone(), Icon::Copy) .icon_color(Color::Muted) diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 6c497752ae..d082574a92 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -111,10 +111,9 @@ impl RenderOnce for ListHeader { .when_some(self.end_hover_slot, |this, end_hover_slot| { this.child( div() - .invisible() - .group_hover("list_header", |this| this.visible()) .absolute() .right_0() + .visible_on_hover("list_header") .child(end_hover_slot), ) }), diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 403d3e7605..44b7f33c38 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -198,8 +198,7 @@ impl RenderOnce for ListItem { .flex() .absolute() .left(rems(-1.)) - .invisible() - .group_hover("", |style| style.visible()) + .visible_on_hover("") .child(Disclosure::new(is_open).on_toggle(self.on_toggle)) })) .child( @@ -226,8 +225,7 @@ impl RenderOnce for ListItem { .absolute() .right_2() .top_0() - .invisible() - .group_hover("list_item", |this| this.visible()) + .visible_on_hover("list_item") .child(end_hover_slot), ) }), diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index be1ce8dd12..8114a322e3 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -158,7 +158,6 @@ impl RenderOnce for Tab { ) .child( h_stack() - .invisible() .w_3() .h_3() .justify_center() @@ -167,7 +166,7 @@ impl RenderOnce for Tab { TabCloseSide::Start => this.left_1(), TabCloseSide::End => this.right_1(), }) - .group_hover("", |style| style.visible()) + .visible_on_hover("") .children(self.end_slot), ) .children(self.children), diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 076d34644c..a0a0adeb1d 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -9,6 +9,7 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::visible_on_hover::*; pub use crate::{h_stack, v_stack}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton}; pub use crate::{ButtonCommon, Color, StyledExt}; diff --git a/crates/ui2/src/ui2.rs b/crates/ui2/src/ui2.rs index 6c5669741b..5c79199100 100644 --- a/crates/ui2/src/ui2.rs +++ b/crates/ui2/src/ui2.rs @@ -21,6 +21,7 @@ mod selectable; mod styled_ext; mod styles; pub mod utils; +mod visible_on_hover; pub use clickable::*; pub use components::*; @@ -30,3 +31,4 @@ pub use prelude::*; pub use selectable::*; pub use styled_ext::*; pub use styles::*; +pub use visible_on_hover::*; diff --git a/crates/ui2/src/visible_on_hover.rs b/crates/ui2/src/visible_on_hover.rs new file mode 100644 index 0000000000..dfab5ab3e6 --- /dev/null +++ b/crates/ui2/src/visible_on_hover.rs @@ -0,0 +1,13 @@ +use gpui::{InteractiveElement, SharedString, Styled}; + +pub trait VisibleOnHover: InteractiveElement + Styled + Sized { + /// Sets the element to only be visible when the specified group is hovered. + /// + /// Pass `""` as the `group_name` to use the global group. + fn visible_on_hover(self, group_name: impl Into) -> Self { + self.invisible() + .group_hover(group_name, |style| style.visible()) + } +} + +impl VisibleOnHover for E {} From 416bb455318f97d3c07a8bb37875c1af309cf848 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 13 Dec 2023 16:35:25 -0800 Subject: [PATCH 38/94] Tidy up workspace resizing --- crates/workspace2/src/dock.rs | 15 +++++++-------- crates/workspace2/src/workspace2.rs | 9 +++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 495fa01423..f9e294763b 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,10 +1,9 @@ use crate::DraggedDock; use crate::{status_bar::StatusItemView, Workspace}; use gpui::{ - div, px, red, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, - EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, - Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, - WindowContext, + div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId, + EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render, + SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -493,16 +492,16 @@ impl Render for Dock { let position = self.position; let handler = div() .id("resize-handle") - // .bg(cx.theme().colors().border) - .bg(red()) + .bg(cx.theme().colors().border) .on_drag(move |cx| cx.build_view(|_| DraggedDock(position))) .on_click(cx.listener(|v, e: &ClickEvent, cx| { if e.down.button == MouseButton::Left && e.down.click_count == 2 { v.resize_active_panel(None, cx) } - })); + })) + .z_index(1); - const HANDLE_SIZE: Pixels = Pixels(4. * 3.); + const HANDLE_SIZE: Pixels = Pixels(6.); match self.position() { DockPosition::Left => { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c070bdfad2..629e151372 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,9 +29,9 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, canvas, div, impl_actions, outline, point, size, Action, AnyModel, AnyView, - AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, - Div, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, + actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView, + AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, + DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, @@ -3629,9 +3629,6 @@ impl Render for Workspace { .border_color(cx.theme().colors().border) .child( canvas(cx.listener(|workspace, bounds, cx| { - cx.with_z_index(100, |cx| { - cx.paint_quad(outline(*bounds, gpui::green())); - }); workspace.bounds = *bounds; })) .absolute() From 04389939d3e09d82839333b7964873dee57e2326 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 13 Dec 2023 16:36:12 -0800 Subject: [PATCH 39/94] Add debug and debug below methods for observing the bounds of divs --- crates/gpui2/src/elements/div.rs | 7 +-- crates/gpui2/src/elements/uniform_list.rs | 64 +++++++++++------------ crates/gpui2/src/style.rs | 39 +++++++++++++- crates/gpui2/src/styled.rs | 12 +++++ 4 files changed, 84 insertions(+), 38 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 7dfc9bae6a..85b172687a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -640,10 +640,7 @@ impl Element for Div { let z_index = style.z_index.unwrap_or(0); cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - }); - cx.with_z_index(1, |cx| { + style.paint(bounds, cx, |cx| { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { @@ -653,7 +650,7 @@ impl Element for Div { }) }) }) - }) + }); }) }, ); diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index d27b3bdb77..bddfc8c9a1 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -197,41 +197,41 @@ impl Element for UniformList { ); cx.with_z_index(style.z_index.unwrap_or(0), |cx| { - style.paint(bounds, cx); + style.paint(bounds, cx, |cx| { + if self.item_count > 0 { + if let Some(scroll_handle) = self.scroll_handle.clone() { + scroll_handle.0.borrow_mut().replace(ScrollHandleState { + item_height, + list_height: padded_bounds.size.height, + scroll_offset: shared_scroll_offset, + }); + } - if self.item_count > 0 { - if let Some(scroll_handle) = self.scroll_handle.clone() { - scroll_handle.0.borrow_mut().replace(ScrollHandleState { - item_height, - list_height: padded_bounds.size.height, - scroll_offset: shared_scroll_offset, + let first_visible_element_ix = + (-scroll_offset.y / item_height).floor() as usize; + let last_visible_element_ix = + ((-scroll_offset.y + padded_bounds.size.height) / item_height) + .ceil() as usize; + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.item_count); + + let items = (self.render_items)(visible_range.clone(), cx); + cx.with_z_index(1, |cx| { + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (item, ix) in items.into_iter().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, cx); + } + }); }); } - - let first_visible_element_ix = - (-scroll_offset.y / item_height).floor() as usize; - let last_visible_element_ix = - ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil() - as usize; - let visible_range = first_visible_element_ix - ..cmp::min(last_visible_element_ix, self.item_count); - - let items = (self.render_items)(visible_range.clone(), cx); - cx.with_z_index(1, |cx| { - let content_mask = ContentMask { bounds }; - cx.with_content_mask(Some(content_mask), |cx| { - for (item, ix) in items.into_iter().zip(visible_range) { - let item_origin = padded_bounds.origin - + point(px(0.), item_height * ix + scroll_offset.y); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, cx); - } - }); - }); - } + }); }) }, ); diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 4031a35f75..115db99784 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -14,6 +14,9 @@ pub use taffy::style::{ Overflow, Position, }; +#[cfg(debug_assertions)] +pub struct DebugBelow; + pub type StyleCascade = Cascade