From 379ba620b215e552e927bb606e1d168aae7e86e0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 15:59:40 -0700 Subject: [PATCH 01/78] Move workspace bindings to workspace context Without this, hitting cmd-n on the context menu in the project browser invokes the workspace::NewFile action instead of the project::NewFile action. We're considering changing the behavior so that bindings with no context can only invoke global actions. Co-Authored-By: Max --- assets/keymaps/default.json | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2a8d19f8829039d759c92e79b6acebe79e55b143..25fafa755e842359fd882b416f5e33e06c42a202 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -17,18 +17,8 @@ "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "cmd-{": "pane::ActivatePrevItem", - "cmd-}": "pane::ActivateNextItem", - "alt-cmd-left": "pane::ActivatePrevItem", - "alt-cmd-right": "pane::ActivateNextItem", - "cmd-w": "pane::CloseActiveItem", - "alt-cmd-t": "pane::CloseInactiveItems", - "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", - "cmd-k u": "pane::CloseCleanItems", - "cmd-k cmd-w": "pane::CloseAllItems", "cmd-shift-w": "workspace::CloseWindow", - "cmd-s": "workspace::Save", - "cmd-shift-s": "workspace::SaveAs", + "cmd-o": "workspace::Open", "cmd-=": "zed::IncreaseBufferFontSize", "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", @@ -38,15 +28,7 @@ "cmd-h": "zed::Hide", "alt-cmd-h": "zed::HideOthers", "cmd-m": "zed::Minimize", - "ctrl-cmd-f": "zed::ToggleFullScreen", - "cmd-n": "workspace::NewFile", - "cmd-shift-n": "workspace::NewWindow", - "cmd-o": "workspace::Open", - "alt-cmd-o": "projects::OpenRecent", - "alt-cmd-b": "branches::OpenRecent", - "ctrl-~": "workspace::NewTerminal", - "ctrl-`": "terminal_panel::ToggleFocus", - "shift-escape": "workspace::ToggleZoom" + "ctrl-cmd-f": "zed::ToggleFullScreen" } }, { @@ -284,6 +266,15 @@ { "context": "Pane", "bindings": { + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", + "cmd-w": "pane::CloseActiveItem", + "alt-cmd-t": "pane::CloseInactiveItems", + "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", + "cmd-k u": "pane::CloseCleanItems", + "cmd-k cmd-w": "pane::CloseAllItems", "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", @@ -389,6 +380,15 @@ { "context": "Workspace", "bindings": { + "alt-cmd-o": "projects::OpenRecent", + "alt-cmd-b": "branches::OpenRecent", + "ctrl-~": "workspace::NewTerminal", + "cmd-s": "workspace::Save", + "cmd-shift-s": "workspace::SaveAs", + "cmd-n": "workspace::NewFile", + "cmd-shift-n": "workspace::NewWindow", + "ctrl-`": "terminal_panel::ToggleFocus", + "shift-escape": "workspace::ToggleZoom", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], From 6f7995c150a1f3c722d700e47813899a6c7ccb45 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 16:08:57 -0700 Subject: [PATCH 02/78] Enable workspace::Open global action --- crates/workspace2/src/workspace2.rs | 42 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5dcec2cabd5392a5d65695602a1ae6c05aabe63c..40b222d3896b44f5ff674249f56c893b270ea144 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -209,27 +209,29 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &Open, cx: &mut AppContext| { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: true, - // directories: true, - // multiple: true, - // }); - // if let Some(app_state) = app_state.upgrade() { - // cx.spawn(move |mut cx| async move { - // if let Some(paths) = paths.recv().await.flatten() { - // cx.update(|cx| { - // open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) - // }); - // } - // }) - // .detach(); - // } - // } - // }); + cx.on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &Open, cx: &mut AppContext| { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + + if let Some(app_state) = app_state.upgrade() { + cx.spawn(move |mut cx| async move { + if let Some(paths) = paths.await.log_err().flatten() { + cx.update(|cx| { + open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) + }) + .ok(); + } + }) + .detach(); + } + } + }); } type ProjectItemBuilders = From 0edd89a92f4f15bb89fad34e8c054aab9ff07a85 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 16:17:48 -0700 Subject: [PATCH 03/78] Bind CloseWindow and Open actions on workspace Co-Authored-By: Max --- crates/workspace2/src/workspace2.rs | 171 ++++++++++++++-------------- crates/zed2/src/zed2.rs | 4 +- 2 files changed, 86 insertions(+), 89 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 40b222d3896b44f5ff674249f56c893b270ea144..c063a02174b10ce47fa80cfe09e39b7e5757490b 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -210,6 +210,9 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); + cx.on_action(Workspace::close_global); + cx.on_action(restart); + cx.on_action({ let app_state = Arc::downgrade(&app_state); move |_: &Open, cx: &mut AppContext| { @@ -1178,7 +1181,6 @@ impl Workspace { } } - // todo!(Non-window-actions) pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { cx.windows().iter().find(|window| { window @@ -1196,21 +1198,18 @@ impl Workspace { }); } - pub fn close( - &mut self, - _: &CloseWindow, - cx: &mut ViewContext, - ) -> Option>> { + pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext) { let window = cx.window_handle(); let prepare = self.prepare_to_close(false, cx); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_, mut cx| async move { if prepare.await? { window.update(&mut cx, |_, cx| { cx.remove_window(); })?; } - Ok(()) - })) + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } pub fn prepare_to_close( @@ -2427,90 +2426,92 @@ impl Workspace { // })) // } - // pub fn follow_next_collaborator( - // &mut self, - // _: &FollowNextCollaborator, - // cx: &mut ViewContext, - // ) -> Option>> { - // let collaborators = self.project.read(cx).collaborators(); - // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { - // let mut collaborators = collaborators.keys().copied(); - // for peer_id in collaborators.by_ref() { - // if peer_id == leader_id { - // break; - // } - // } - // collaborators.next() - // } else if let Some(last_leader_id) = - // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) - // { - // if collaborators.contains_key(last_leader_id) { - // Some(*last_leader_id) - // } else { - // None + // pub fn follow_next_collaborator( + // &mut self, + // _: &FollowNextCollaborator, + // cx: &mut ViewContext, + // ) { + // let collaborators = self.project.read(cx).collaborators(); + // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + // let mut collaborators = collaborators.keys().copied(); + // for peer_id in collaborators.by_ref() { + // if peer_id == leader_id { + // break; // } + // } + // collaborators.next() + // } else if let Some(last_leader_id) = + // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + // { + // if collaborators.contains_key(last_leader_id) { + // Some(*last_leader_id) // } else { // None - // }; - - // let pane = self.active_pane.clone(); - // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) - // else { - // return None; - // }; - // if Some(leader_id) == self.unfollow(&pane, cx) { - // return None; // } - // self.follow(leader_id, cx) + // } else { + // None + // }; + + // let pane = self.active_pane.clone(); + // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) + // else { + // return; + // }; + // if Some(leader_id) == self.unfollow(&pane, cx) { + // return; // } + // if let Some(task) = self.follow(leader_id, cx) { + // task.detach(); + // } + // } - // pub fn follow( - // &mut self, - // leader_id: PeerId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - // let project = self.project.read(cx); - - // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - // return None; - // }; - - // let other_project_id = match remote_participant.location { - // call::ParticipantLocation::External => None, - // call::ParticipantLocation::UnsharedProject => None, - // call::ParticipantLocation::SharedProject { project_id } => { - // if Some(project_id) == project.remote_id() { - // None - // } else { - // Some(project_id) - // } + // pub fn follow( + // &mut self, + // leader_id: PeerId, + // cx: &mut ViewContext, + // ) -> Option>> { + // let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + // let project = self.project.read(cx); + + // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { + // return None; + // }; + + // let other_project_id = match remote_participant.location { + // call::ParticipantLocation::External => None, + // call::ParticipantLocation::UnsharedProject => None, + // call::ParticipantLocation::SharedProject { project_id } => { + // if Some(project_id) == project.remote_id() { + // None + // } else { + // Some(project_id) // } - // }; - - // // if they are active in another project, follow there. - // if let Some(project_id) = other_project_id { - // let app_state = self.app_state.clone(); - // return Some(crate::join_remote_project( - // project_id, - // remote_participant.user.id, - // app_state, - // cx, - // )); // } + // }; - // // if you're already following, find the right pane and focus it. - // for (pane, state) in &self.follower_states { - // if leader_id == state.leader_id { - // cx.focus(pane); - // return None; - // } - // } + // // if they are active in another project, follow there. + // if let Some(project_id) = other_project_id { + // let app_state = self.app_state.clone(); + // return Some(crate::join_remote_project( + // project_id, + // remote_participant.user.id, + // app_state, + // cx, + // )); + // } - // // Otherwise, follow. - // self.start_following(leader_id, cx) + // // if you're already following, find the right pane and focus it. + // for (pane, state) in &self.follower_states { + // if leader_id == state.leader_id { + // cx.focus(pane); + // return None; + // } // } + // // Otherwise, follow. + // self.start_following(leader_id, cx) + // } + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { let follower_states = &mut self.follower_states; let state = follower_states.remove(pane)?; @@ -3287,13 +3288,8 @@ impl Workspace { fn actions(&self, div: Div, cx: &mut ViewContext) -> Div { self.add_workspace_actions_listeners(div, cx) - // cx.add_async_action(Workspace::open); - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); .on_action(cx.listener(Self::close_inactive_items_and_panes)) .on_action(cx.listener(Self::close_all_items_and_panes)) - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); .on_action(cx.listener(Self::save_all)) .on_action(cx.listener(Self::add_folder_to_project)) .on_action(cx.listener(|workspace, _: &Unfollow, cx| { @@ -3342,6 +3338,9 @@ impl Workspace { workspace.close_all_docks(cx); }), ) + .on_action(cx.listener(Workspace::open)) + .on_action(cx.listener(Workspace::close_window)) + // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index b8976874898cc53f59a98fffeb6113e69afe7df5..8ff0d2a61976995c4ce8faad215ddf401f02c28e 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -168,9 +168,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.on_window_should_close(move |cx| { handle .update(cx, |workspace, cx| { - if let Some(task) = workspace.close(&Default::default(), cx) { - task.detach_and_log_err(cx); - } + workspace.close_window(&Default::default(), cx); false }) .unwrap_or(true) From 5bdaf0e074f2c034ff418ae713cfaa20be811eab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 17:54:37 -0500 Subject: [PATCH 04/78] Work on light theme, update tab --- crates/theme2/src/default_colors.rs | 24 +++--- crates/theme2/src/default_theme.rs | 78 +++++++++---------- crates/theme2/src/registry.rs | 4 +- crates/theme2/src/styles/syntax.rs | 16 ++-- crates/ui2/src/components/icon.rs | 2 +- crates/workspace2/src/pane.rs | 111 ++++++++++++++++------------ 6 files changed, 125 insertions(+), 110 deletions(-) diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 4a47bc05366c5c8a9063e1b4f8fd4be560b11195..b61e4792a4242199585e3a17bac493bfce7797e3 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -5,7 +5,7 @@ use crate::ColorScale; use crate::{SystemColors, ThemeColors}; pub(crate) fn neutral() -> ColorScaleSet { - slate() + sand() } impl ThemeColors { @@ -29,12 +29,12 @@ impl ThemeColors { element_disabled: neutral().light_alpha().step_3(), drop_target_background: blue().light_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().light_alpha().step_4(), - ghost_element_active: neutral().light_alpha().step_5(), + ghost_element_hover: neutral().light_alpha().step_3(), + ghost_element_active: neutral().light_alpha().step_4(), ghost_element_selected: neutral().light_alpha().step_5(), ghost_element_disabled: neutral().light_alpha().step_3(), - text: yellow().light().step_9(), - text_muted: neutral().light().step_11(), + text: neutral().light().step_12(), + text_muted: neutral().light().step_10(), text_placeholder: neutral().light().step_10(), text_disabled: neutral().light().step_9(), text_accent: blue().light().step_11(), @@ -53,13 +53,13 @@ impl ThemeColors { editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors") editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), - editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") + editor_line_number: neutral().light().step_10(), + editor_active_line_number: neutral().light().step_11(), + editor_highlighted_line_background: neutral().light_alpha().step_3(), + editor_invisible: neutral().light().step_10(), + editor_wrap_guide: neutral().light_alpha().step_7(), + editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors") + editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors") editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") terminal_background: neutral().light().step_1(), terminal_ansi_black: black().light().step_12(), diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 8502f433f4a919d7d661f00e55d0dd353ff46fc5..269414b36a0747e5e1bfed677bfb69378ec2ab03 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,47 +1,49 @@ +use std::sync::Arc; + use crate::{ one_themes::{one_dark, one_family}, - Theme, ThemeFamily, + Theme, ThemeFamily, Appearance, ThemeStyles, SystemColors, ThemeColors, StatusColors, PlayerColors, SyntaxTheme, default_color_scales, }; -// fn zed_pro_daylight() -> Theme { -// Theme { -// id: "zed_pro_daylight".to_string(), -// name: "Zed Pro Daylight".into(), -// appearance: Appearance::Light, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::light(), -// status: StatusColors::light(), -// player: PlayerColors::light(), -// syntax: Arc::new(SyntaxTheme::light()), -// }, -// } -// } +fn zed_pro_daylight() -> Theme { + Theme { + id: "zed_pro_daylight".to_string(), + name: "Zed Pro Daylight".into(), + appearance: Appearance::Light, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::light(), + status: StatusColors::light(), + player: PlayerColors::light(), + syntax: Arc::new(SyntaxTheme::light()), + }, + } +} -// pub(crate) fn zed_pro_moonlight() -> Theme { -// Theme { -// id: "zed_pro_moonlight".to_string(), -// name: "Zed Pro Moonlight".into(), -// appearance: Appearance::Dark, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::dark(), -// status: StatusColors::dark(), -// player: PlayerColors::dark(), -// syntax: Arc::new(SyntaxTheme::dark()), -// }, -// } -// } +pub(crate) fn zed_pro_moonlight() -> Theme { + Theme { + id: "zed_pro_moonlight".to_string(), + name: "Zed Pro Moonlight".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::dark(), + status: StatusColors::dark(), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), + }, + } +} -// pub fn zed_pro_family() -> ThemeFamily { -// ThemeFamily { -// id: "zed_pro".to_string(), -// name: "Zed Pro".into(), -// author: "Zed Team".into(), -// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], -// scales: default_color_scales(), -// } -// } +pub fn zed_pro_family() -> ThemeFamily { + ThemeFamily { + id: "zed_pro".to_string(), + name: "Zed Pro".into(), + author: "Zed Team".into(), + themes: vec![zed_pro_daylight(), zed_pro_moonlight()], + scales: default_color_scales(), + } +} impl Default for ThemeFamily { fn default() -> Self { diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index b50eb831dda51b8357ad2b8c8ff9a7b6a86cfe81..8e2a4d401fd201515baa5bfd42d4d2a506798b93 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -7,7 +7,7 @@ use refineable::Refineable; use crate::{ one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, - Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, zed_pro_family, }; pub struct ThemeRegistry { @@ -117,7 +117,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([one_family()]); + this.insert_theme_families([zed_pro_family(), one_family()]); this } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index 8675d30e3a00a94d3ea05efa018dfd7775dabace..cc73caa6dfca3c920cf79af89eb7d1993d670688 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -22,8 +22,8 @@ impl SyntaxTheme { highlights: vec![ ("attribute".into(), cyan().light().step_11().into()), ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), + ("comment".into(), neutral().light().step_10().into()), + ("comment.doc".into(), iris().light().step_11().into()), ("constant".into(), red().light().step_9().into()), ("constructor".into(), red().light().step_9().into()), ("embedded".into(), red().light().step_9().into()), @@ -32,11 +32,11 @@ impl SyntaxTheme { ("enum".into(), red().light().step_9().into()), ("function".into(), red().light().step_9().into()), ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), + ("keyword".into(), orange().light().step_9().into()), ("label".into(), red().light().step_9().into()), ("link_text".into(), red().light().step_9().into()), ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), + ("number".into(), purple().light().step_10().into()), ("operator".into(), red().light().step_9().into()), ("predictive".into(), red().light().step_9().into()), ("preproc".into(), red().light().step_9().into()), @@ -49,16 +49,16 @@ impl SyntaxTheme { ), ( "punctuation.delimiter".into(), - neutral().light().step_11().into(), + neutral().light().step_10().into(), ), ( "punctuation.list_marker".into(), blue().light().step_11().into(), ), ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), + ("string".into(), jade().light().step_9().into()), ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), + ("string.regex".into(), tomato().light().step_9().into()), ("string.special".into(), red().light().step_9().into()), ( "string.special.symbol".into(), @@ -67,7 +67,7 @@ impl SyntaxTheme { ("tag".into(), red().light().step_9().into()), ("text.literal".into(), red().light().step_9().into()), ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), + ("type".into(), cyan().light().step_9().into()), ("variable".into(), red().light().step_9().into()), ("variable.special".into(), red().light().step_9().into()), ("variant".into(), red().light().step_9().into()), diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..3f2cb725f9b4d53b01e2180dbdf50d477cab0e3b 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -171,7 +171,7 @@ impl RenderOnce for IconElement { fn render(self, cx: &mut WindowContext) -> Self::Rendered { let svg_size = match self.size { - IconSize::Small => rems(14. / 16.), + IconSize::Small => rems(12. / 16.), IconSize::Medium => rems(16. / 16.), }; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 438ad396936e740fe0c1cc3518e7bdca9f02e941..4f77becbadc638b809c3e23492874dd499263575 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,7 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip, + h_stack, prelude::*, right_click_menu, ButtonLike, Color, Icon, IconButton, IconElement, + IconSize, Label, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1415,20 +1416,38 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let label = item.tab_content(Some(detail), cx); + let close_right = ItemSettings::get_global(cx).close_position.right(); + let close_icon = || { let id = item.item_id(); div() .id(ix) + .w_3p5() + .h_3p5() + .rounded_sm() + .border() + .border_color(cx.theme().colors().border_variant) + .absolute() + .map(|this| { + if close_right { + this.right_1() + } else { + this.left_1() + } + }) .invisible() .group_hover("", |style| style.visible()) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .on_click(cx.listener(move |pane, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + })) .child( - IconButton::new("close_tab", Icon::Close).on_click(cx.listener( - move |pane, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - }, - )), + IconElement::new(Icon::Close) + .color(Color::Muted) + .size(IconSize::Small), ) }; @@ -1447,12 +1466,12 @@ impl Pane { ), }; - let close_right = ItemSettings::get_global(cx).close_position.right(); let is_active = ix == self.active_item_index; - let tab = div() + let tab = h_stack() .group("") .id(ix) + .relative() .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) @@ -1466,15 +1485,15 @@ impl Pane { .flex() .items_center() .justify_center() - // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") - .map(|this| { - if close_right { - this.pl_3().pr_1() - } else { - this.pr_1().pr_3() - } - }) - .py_1() + .px_5() + // .map(|this| { + // if close_right { + // this.pl_3().pr_1() + // } else { + // this.pr_1().pr_3() + // } + // }) + .h(rems(1.875)) .bg(tab_bg) .border_color(cx.theme().colors().border) .text_color(if is_active { @@ -1485,46 +1504,40 @@ impl Pane { .map(|this| { let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { - cmp::Ordering::Less => this.border_l().mr_px(), + cmp::Ordering::Less => this.border_l().mr_px().border_b(), cmp::Ordering::Greater => { if is_last_item { - this.mr_px().ml_px() + this.mr_px().ml_px().border_b() } else { - this.border_r().ml_px() + this.border_r().ml_px().border_b() } } - cmp::Ordering::Equal => this.border_l().border_r(), + cmp::Ordering::Equal => this.border_l().border_r().mb_px(), } }) // .hover(|h| h.bg(tab_hover_bg)) // .active(|a| a.bg(tab_active_bg)) - .child( - div() - .flex() - .items_center() - .gap_1() - .text_color(text_color) - .children( - item.has_conflict(cx) - .then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Warning), - ) - }) - .or(item.is_dirty(cx).then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Info), - ) - })), - ) - .children((!close_right).then(|| close_icon())) - .child(label) - .children(close_right.then(|| close_icon())), - ); + .gap_1() + .text_color(text_color) + .children( + item.has_conflict(cx) + .then(|| { + div().border().border_color(gpui::red()).child( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(Color::Warning), + ) + }) + .or(item.is_dirty(cx).then(|| { + div().border().border_color(gpui::red()).child( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(Color::Info), + ) + })), + ) + .child(label) + .child(close_icon()); right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { From fc16e4509a85ff69d76b648f9191ba4e50e27138 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 18:13:28 -0500 Subject: [PATCH 05/78] Fix double border --- crates/workspace2/src/pane.rs | 193 ++++++++++++++-------------------- 1 file changed, 80 insertions(+), 113 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4f77becbadc638b809c3e23492874dd499263575..ad771bf84c76c39fdcc8ee88a68bc83e251b6946 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,8 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, ButtonLike, Color, Icon, IconButton, IconElement, - IconSize, Label, Tooltip, + h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, IconSize, Label, + Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1486,13 +1486,6 @@ impl Pane { .items_center() .justify_center() .px_5() - // .map(|this| { - // if close_right { - // this.pl_3().pr_1() - // } else { - // this.pr_1().pr_3() - // } - // }) .h(rems(1.875)) .bg(tab_bg) .border_color(cx.theme().colors().border) @@ -1502,9 +1495,16 @@ impl Pane { cx.theme().colors().text_muted }) .map(|this| { + let is_first_item = ix == 0; let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { - cmp::Ordering::Less => this.border_l().mr_px().border_b(), + cmp::Ordering::Less => { + if is_first_item { + this.ml_px().mr_px().border_b() + } else { + this.border_l().mr_px().border_b() + } + } cmp::Ordering::Greater => { if is_last_item { this.mr_px().ml_px().border_b() @@ -1542,24 +1542,18 @@ impl Pane { right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { menu.action( - "Close Active Item", + "Close", CloseActiveItem { save_intent: None }.boxed_clone(), cx, ) - .action("Close Inactive Items", CloseInactiveItems.boxed_clone(), cx) - .action("Close Clean Items", CloseCleanItems.boxed_clone(), cx) - .action( - "Close Items To The Left", - CloseItemsToTheLeft.boxed_clone(), - cx, - ) - .action( - "Close Items To The Right", - CloseItemsToTheRight.boxed_clone(), - cx, - ) + .action("Close Others", CloseInactiveItems.boxed_clone(), cx) + .separator() + .action("Close Left", CloseItemsToTheLeft.boxed_clone(), cx) + .action("Close Right", CloseItemsToTheRight.boxed_clone(), cx) + .separator() + .action("Close Clean", CloseCleanItems.boxed_clone(), cx) .action( - "Close All Items", + "Close All", CloseAllItems { save_intent: None }.boxed_clone(), cx, ) @@ -1582,30 +1576,29 @@ impl Pane { // Left Side .child( h_stack() - .px_2() .flex() .flex_none() .gap_1() + .px_1() + .border_b() + .border_r() + .border_color(cx.theme().colors().border) // Nav Buttons .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_backward", Icon::ArrowLeft) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_backward()), - ), + IconButton::new("navigate_backward", Icon::ArrowLeft) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_backward()), ) .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_forward", Icon::ArrowRight) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_forward()), - ), + IconButton::new("navigate_forward", Icon::ArrowRight) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_forward()), ), ) .child( @@ -1621,86 +1614,60 @@ impl Pane { ) // Right Side .child( - div() - .px_1() + h_stack() .flex() .flex_none() - .gap_2() - // Nav Buttons + .gap_1() + .px_1() + .border_b() + .border_l() + .border_color(cx.theme().colors().border) .child( div() .flex() .items_center() .gap_px() - .child( - div() - .bg(gpui::blue()) - .border() - .border_color(gpui::red()) - .child(IconButton::new("plus", Icon::Plus).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone(), cx) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - cx, - ) - .action( - "New Search", - NewSearch.boxed_clone(), - cx, - ) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }, - ) - .detach(); - this.new_item_menu = Some(menu); - }), - )) - .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { - el.child(Self::render_menu_overlay(new_item_menu)) - }), - ) - .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("split", Icon::Split).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action( - "Split Right", - SplitRight.boxed_clone(), - cx, - ) - .action("Split Left", SplitLeft.boxed_clone(), cx) - .action("Split Up", SplitUp.boxed_clone(), cx) - .action("Split Down", SplitDown.boxed_clone(), cx) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }, + .child(IconButton::new("plus", Icon::Plus).on_click(cx.listener( + |this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone(), cx) + .action( + "New Terminal", + NewCenterTerminal.boxed_clone(), + cx, ) - .detach(); - this.split_item_menu = Some(menu); - }), - )) - .when_some( - self.split_item_menu.as_ref(), - |el, split_item_menu| { - el.child(Self::render_menu_overlay(split_item_menu)) - }, - ), - ), + .action("New Search", NewSearch.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + }, + ))) + .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Self::render_menu_overlay(new_item_menu)) + }) + .child(IconButton::new("split", Icon::Split).on_click(cx.listener( + |this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone(), cx) + .action("Split Left", SplitLeft.boxed_clone(), cx) + .action("Split Up", SplitUp.boxed_clone(), cx) + .action("Split Down", SplitDown.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + }, + ))) + .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { + el.child(Self::render_menu_overlay(split_item_menu)) + }), ), ) } From 7db0a9e105a3791a2a8fc32bd70989c69f3d3b73 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 18:21:50 -0500 Subject: [PATCH 06/78] Draw border under tabs --- crates/workspace2/src/pane.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index ad771bf84c76c39fdcc8ee88a68bc83e251b6946..69c255ea81f16b59925861794950537bd3bb198e 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1602,15 +1602,32 @@ impl Pane { ), ) .child( - div().flex_1().h_full().child( - div().id("tabs").flex().overflow_x_scroll().children( - self.items - .iter() - .enumerate() - .zip(self.tab_details(cx)) - .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + div() + .relative() + .flex_1() + .h_full() + .child( + div() + .absolute() + .top_0() + .left_0() + .z_index(1) + .size_full() + .border_b() + .border_color(cx.theme().colors().border), + ) + .child( + div() + .id("tabs") + .z_index(2) + .flex() + .overflow_x_scroll() + .children( + self.items.iter().enumerate().zip(self.tab_details(cx)).map( + |((ix, item), detail)| self.render_tab(ix, item, detail, cx), + ), + ), ), - ), ) // Right Side .child( From 591dc9d82a4cd6b0835e2da9e5da3e782602d4cf Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 20:13:52 -0500 Subject: [PATCH 07/78] Remove double first item border in tabs --- crates/workspace2/src/pane.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 69c255ea81f16b59925861794950537bd3bb198e..18a5de1bc19db09a8e24471a8814470f84669a62 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1512,7 +1512,13 @@ impl Pane { this.border_r().ml_px().border_b() } } - cmp::Ordering::Equal => this.border_l().border_r().mb_px(), + cmp::Ordering::Equal => { + if is_first_item { + this.ml_px().border_r().mb_px() + } else { + this.border_l().border_r().mb_px() + } + } } }) // .hover(|h| h.bg(tab_hover_bg)) From b73ccc8180ca4f46027b874ec205ef4461045547 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:53:01 +0100 Subject: [PATCH 08/78] Start out Copilot2; Add hidden_action_types to CommandPaletteFilter. WindowContext.available_actions now returns global actions as well. Co-authored-by: Antonio --- crates/collections/src/collections.rs | 4 +- crates/command_palette/src/command_palette.rs | 4 +- .../command_palette2/src/command_palette.rs | 7 +- crates/copilot/src/copilot.rs | 12 +- crates/copilot2/src/copilot2.rs | 119 ++-- crates/copilot2/src/sign_in.rs | 587 +++++++++--------- crates/gpui2/src/window.rs | 12 +- crates/vim/src/vim.rs | 6 +- 8 files changed, 391 insertions(+), 360 deletions(-) diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index eb4e4d8462720c773d7e48b92f413cad2ca1970a..bffa5c877a2e33788da4c5bb4d3f7d2918dbe2ea 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -23,11 +23,13 @@ pub type HashMap = std::collections::HashMap; #[cfg(not(feature = "test-support"))] pub type HashSet = std::collections::HashSet; +use std::any::TypeId; pub use std::collections::*; // NEW TYPES #[derive(Default)] pub struct CommandPaletteFilter { - pub filtered_namespaces: HashSet<&'static str>, + pub hidden_namespaces: HashSet<&'static str>, + pub hidden_action_types: HashSet, } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index ce762876a41238ab4e0f51ddcd29155b39274840..356300052e4c420ed872ac21f3c7f3da541c5ad8 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate { let filtered = cx.read(|cx| { if cx.has_global::() { let filter = cx.global::(); - filter.filtered_namespaces.contains(action.namespace()) + filter.hidden_namespaces.contains(action.namespace()) } else { false } @@ -430,7 +430,7 @@ mod tests { // Add namespace filter, and redeploy the palette cx.update(|cx| { cx.update_default_global::(|filter, _| { - filter.filtered_namespaces.insert("editor"); + filter.hidden_namespaces.insert("editor"); }) }); diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 04688b05492c8c298c33d32423cdb5e7ce1fe393..f94b5e77caa5cb3676073147555753471db13c98 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -49,7 +49,10 @@ impl CommandPalette { .filter_map(|action| { let name = gpui::remove_the_2(action.name()); let namespace = name.split("::").next().unwrap_or("malformed action name"); - if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + if filter.is_some_and(|f| { + f.hidden_namespaces.contains(namespace) + || f.hidden_action_types.contains(&action.type_id()) + }) { return None; } @@ -429,7 +432,7 @@ mod tests { cx.update(|cx| { cx.set_global(CommandPaletteFilter::default()); cx.update_global::(|filter, _| { - filter.filtered_namespaces.insert("editor"); + filter.hidden_namespaces.insert("editor"); }) }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 92d430e3fb06699ff86fba1f7c4e6de8eb222182..0c6f7e3907000cdcc8a9de5317a307de4421757d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -58,16 +58,16 @@ pub fn init( cx.update_default_global::(move |filter, _cx| { match status { Status::Disabled => { - filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE); } Status::Authorized => { - filter.filtered_namespaces.remove(COPILOT_NAMESPACE); - filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); } _ => { - filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); } } }); diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index b2454728644b212c8736b49876b213123cf039e8..d23d25119b152be1c232304d2992af81af3ab9e4 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -22,6 +22,7 @@ use request::StatusNotification; use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ + any::TypeId, ffi::OsString, mem, ops::Range, @@ -32,13 +33,14 @@ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -// todo!() -// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; -actions!(SignIn, SignOut); - -// todo!() -// const COPILOT_NAMESPACE: &'static str = "copilot"; -actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall); +actions!( + Suggest, + NextSuggestion, + PreviousSuggestion, + Reinstall, + SignIn, + SignOut +); pub fn init( new_server_id: LanguageServerId, @@ -51,52 +53,63 @@ pub fn init( move |cx| Copilot::start(new_server_id, http, node_runtime, cx) }); cx.set_global(copilot.clone()); - - // TODO - // cx.observe(&copilot, |handle, cx| { - // let status = handle.read(cx).status(); - // cx.update_default_global::(move |filter, _cx| { - // match status { - // Status::Disabled => { - // filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - // filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); - // } - // Status::Authorized => { - // filter.filtered_namespaces.remove(COPILOT_NAMESPACE); - // filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); - // } - // _ => { - // filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - // filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); - // } - // } - // }); - // }) - // .detach(); - - // sign_in::init(cx); - // cx.add_global_action(|_: &SignIn, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.sign_in(cx)) - // .detach_and_log_err(cx); - // } - // }); - // cx.add_global_action(|_: &SignOut, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.sign_out(cx)) - // .detach_and_log_err(cx); - // } - // }); - - // cx.add_global_action(|_: &Reinstall, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.reinstall(cx)) - // .detach(); - // } - // }); + cx.observe(&copilot, |handle, cx| { + let copilot_action_types = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + let copilot_auth_action_types = [TypeId::of::(), TypeId::of::()]; + + let status = handle.read(cx).status(); + let filter = cx.default_global::(); + + match status { + Status::Disabled => { + filter.hidden_action_types.extend(copilot_action_types); + filter.hidden_action_types.extend(copilot_auth_action_types); + } + Status::Authorized => { + for type_id in copilot_action_types + .iter() + .chain(&copilot_auth_action_types) + { + filter.hidden_action_types.remove(type_id); + } + } + _ => { + filter.hidden_action_types.extend(copilot_action_types); + for type_id in &copilot_auth_action_types { + filter.hidden_action_types.remove(type_id); + } + } + } + }) + .detach(); + + sign_in::init(cx); + cx.on_action(|_: &SignIn, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }); + cx.on_action(|_: &SignOut, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + } + }); + cx.on_action(|_: &Reinstall, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.reinstall(cx)) + .detach(); + } + }); } enum CopilotServer { diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 57f248aa52486d8c04672eeef8d33e5acda2a52c..7973d935af1be1c455bb2b90a0ed8dcf2c1b3566 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -9,314 +9,319 @@ // }; // use theme::ui::modal; -// #[derive(PartialEq, Eq, Debug, Clone)] -// struct CopyUserCode; +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; -// #[derive(PartialEq, Eq, Debug, Clone)] -// struct OpenGithub; +use crate::{Copilot, Status}; +use gpui::{ + px, size, AppContext, Bounds, Div, GlobalPixels, Point, Render, ViewContext, VisualContext, + WindowBounds, WindowHandle, WindowKind, WindowOptions, +}; -// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; +pub fn init(cx: &mut AppContext) { + if let Some(copilot) = Copilot::global(cx) { + let mut verification_window: Option> = None; + cx.observe(&copilot, move |copilot, cx| { + let status = copilot.read(cx).status(); -// pub fn init(cx: &mut AppContext) { -// if let Some(copilot) = Copilot::global(cx) { -// let mut verification_window: Option> = None; -// cx.observe(&copilot, move |copilot, cx| { -// let status = copilot.read(cx).status(); + match &status { + crate::Status::SigningIn { prompt } => { + if let Some(window) = verification_window.as_mut() { + let updated = window + .update(cx, |verification, cx| { + verification.set_status(status.clone(), cx); + cx.activate_window(); + }) + .is_ok(); + if !updated { + verification_window = Some(create_copilot_auth_window(cx, &status)); + } + } else if let Some(_prompt) = prompt { + verification_window = Some(create_copilot_auth_window(cx, &status)); + } + } + Status::Authorized | Status::Unauthorized => { + if let Some(window) = verification_window.as_ref() { + window + .update(cx, |verification, cx| { + verification.set_status(status, cx); + cx.activate(true); + cx.activate_window(); + }) + .ok(); + } + } + _ => { + if let Some(code_verification) = verification_window.take() { + code_verification.update(cx, |_, cx| cx.remove_window()); + } + } + } + }) + .detach(); + } +} -// match &status { -// crate::Status::SigningIn { prompt } => { -// if let Some(window) = verification_window.as_mut() { -// let updated = window -// .root(cx) -// .map(|root| { -// root.update(cx, |verification, cx| { -// verification.set_status(status.clone(), cx); -// cx.activate_window(); -// }) -// }) -// .is_some(); -// if !updated { -// verification_window = Some(create_copilot_auth_window(cx, &status)); -// } -// } else if let Some(_prompt) = prompt { -// verification_window = Some(create_copilot_auth_window(cx, &status)); -// } -// } -// Status::Authorized | Status::Unauthorized => { -// if let Some(window) = verification_window.as_ref() { -// if let Some(verification) = window.root(cx) { -// verification.update(cx, |verification, cx| { -// verification.set_status(status, cx); -// cx.platform().activate(true); -// cx.activate_window(); -// }); -// } -// } -// } -// _ => { -// if let Some(code_verification) = verification_window.take() { -// code_verification.update(cx, |cx| cx.remove_window()); -// } -// } -// } -// }) -// .detach(); -// } -// } +fn create_copilot_auth_window( + cx: &mut AppContext, + status: &Status, +) -> WindowHandle { + let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.)); + let window_options = WindowOptions { + bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)), + titlebar: None, + center: true, + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + display_id: None, + }; + cx.open_window(window_options, |cx| { + cx.build_view(|_| CopilotCodeVerification::new(status.clone())) + }) +} -// fn create_copilot_auth_window( -// cx: &mut AppContext, -// status: &Status, -// ) -> WindowHandle { -// let window_size = theme::current(cx).copilot.modal.dimensions(); -// let window_options = WindowOptions { -// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), -// titlebar: None, -// center: true, -// focus: true, -// show: true, -// kind: WindowKind::Normal, -// is_movable: true, -// screen: None, -// }; -// cx.add_window(window_options, |_cx| { -// CopilotCodeVerification::new(status.clone()) -// }) -// } +pub struct CopilotCodeVerification { + status: Status, + connect_clicked: bool, +} -// pub struct CopilotCodeVerification { -// status: Status, -// connect_clicked: bool, -// } +impl CopilotCodeVerification { + pub fn new(status: Status) -> Self { + Self { + status, + connect_clicked: false, + } + } -// impl CopilotCodeVerification { -// pub fn new(status: Status) -> Self { -// Self { -// status, -// connect_clicked: false, -// } -// } + pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + self.status = status; + cx.notify(); + } -// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { -// self.status = status; -// cx.notify(); -// } + // fn render_device_code( + // data: &PromptUserDeviceFlow, + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> impl IntoAnyElement { + // let copied = cx + // .read_from_clipboard() + // .map(|item| item.text() == &data.user_code) + // .unwrap_or(false); -// fn render_device_code( -// data: &PromptUserDeviceFlow, -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> impl IntoAnyElement { -// let copied = cx -// .read_from_clipboard() -// .map(|item| item.text() == &data.user_code) -// .unwrap_or(false); + // let device_code_style = &style.auth.prompting.device_code; -// let device_code_style = &style.auth.prompting.device_code; + // MouseEventHandler::new::(0, cx, |state, _cx| { + // Flex::row() + // .with_child( + // Label::new(data.user_code.clone(), device_code_style.text.clone()) + // .aligned() + // .contained() + // .with_style(device_code_style.left_container) + // .constrained() + // .with_width(device_code_style.left), + // ) + // .with_child( + // Label::new( + // if copied { "Copied!" } else { "Copy" }, + // device_code_style.cta.style_for(state).text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(*device_code_style.right_container.style_for(state)) + // .constrained() + // .with_width(device_code_style.right), + // ) + // .contained() + // .with_style(device_code_style.cta.style_for(state).container) + // }) + // .on_click(gpui::platform::MouseButton::Left, { + // let user_code = data.user_code.clone(); + // move |_, _, cx| { + // cx.platform() + // .write_to_clipboard(ClipboardItem::new(user_code.clone())); + // cx.notify(); + // } + // }) + // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) + // } -// MouseEventHandler::new::(0, cx, |state, _cx| { -// Flex::row() -// .with_child( -// Label::new(data.user_code.clone(), device_code_style.text.clone()) -// .aligned() -// .contained() -// .with_style(device_code_style.left_container) -// .constrained() -// .with_width(device_code_style.left), -// ) -// .with_child( -// Label::new( -// if copied { "Copied!" } else { "Copy" }, -// device_code_style.cta.style_for(state).text.clone(), -// ) -// .aligned() -// .contained() -// .with_style(*device_code_style.right_container.style_for(state)) -// .constrained() -// .with_width(device_code_style.right), -// ) -// .contained() -// .with_style(device_code_style.cta.style_for(state).container) -// }) -// .on_click(gpui::platform::MouseButton::Left, { -// let user_code = data.user_code.clone(); -// move |_, _, cx| { -// cx.platform() -// .write_to_clipboard(ClipboardItem::new(user_code.clone())); -// cx.notify(); -// } -// }) -// .with_cursor_style(gpui::platform::CursorStyle::PointingHand) -// } + // fn render_prompting_modal( + // connect_clicked: bool, + // data: &PromptUserDeviceFlow, + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ConnectButton {} -// fn render_prompting_modal( -// connect_clicked: bool, -// data: &PromptUserDeviceFlow, -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum ConnectButton {} + // Flex::column() + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Enable Copilot by connecting", + // style.auth.prompting.subheading.text.clone(), + // ) + // .aligned(), + // Label::new( + // "your existing license.", + // style.auth.prompting.subheading.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.subheading.container), + // ) + // .with_child(Self::render_device_code(data, &style, cx)) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Paste this code into GitHub after", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "clicking the button below.", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.hint.container.clone()), + // ) + // .with_child(theme::ui::cta_button::( + // if connect_clicked { + // "Waiting for connection..." + // } else { + // "Connect to GitHub" + // }, + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // { + // let verification_uri = data.verification_uri.clone(); + // move |_, verification, cx| { + // cx.platform().open_url(&verification_uri); + // verification.connect_clicked = true; + // } + // }, + // )) + // .align_children_center() + // .into_any() + // } -// Flex::column() -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Enable Copilot by connecting", -// style.auth.prompting.subheading.text.clone(), -// ) -// .aligned(), -// Label::new( -// "your existing license.", -// style.auth.prompting.subheading.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(style.auth.prompting.subheading.container), -// ) -// .with_child(Self::render_device_code(data, &style, cx)) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Paste this code into GitHub after", -// style.auth.prompting.hint.text.clone(), -// ) -// .aligned(), -// Label::new( -// "clicking the button below.", -// style.auth.prompting.hint.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(style.auth.prompting.hint.container.clone()), -// ) -// .with_child(theme::ui::cta_button::( -// if connect_clicked { -// "Waiting for connection..." -// } else { -// "Connect to GitHub" -// }, -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// { -// let verification_uri = data.verification_uri.clone(); -// move |_, verification, cx| { -// cx.platform().open_url(&verification_uri); -// verification.connect_clicked = true; -// } -// }, -// )) -// .align_children_center() -// .into_any() -// } + // fn render_enabled_modal( + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum DoneButton {} -// fn render_enabled_modal( -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum DoneButton {} + // let enabled_style = &style.auth.authorized; + // Flex::column() + // .with_child( + // Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) + // .contained() + // .with_style(enabled_style.subheading.container) + // .aligned(), + // ) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "You can update your settings or", + // enabled_style.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "sign out from the Copilot menu in", + // enabled_style.hint.text.clone(), + // ) + // .aligned(), + // Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(enabled_style.hint.container), + // ) + // .with_child(theme::ui::cta_button::( + // "Done", + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // |_, _, cx| cx.remove_window(), + // )) + // .align_children_center() + // .into_any() + // } -// let enabled_style = &style.auth.authorized; -// Flex::column() -// .with_child( -// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) -// .contained() -// .with_style(enabled_style.subheading.container) -// .aligned(), -// ) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "You can update your settings or", -// enabled_style.hint.text.clone(), -// ) -// .aligned(), -// Label::new( -// "sign out from the Copilot menu in", -// enabled_style.hint.text.clone(), -// ) -// .aligned(), -// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(enabled_style.hint.container), -// ) -// .with_child(theme::ui::cta_button::( -// "Done", -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// |_, _, cx| cx.remove_window(), -// )) -// .align_children_center() -// .into_any() -// } + // fn render_unauthorized_modal( + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // let unauthorized_style = &style.auth.not_authorized; -// fn render_unauthorized_modal( -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// let unauthorized_style = &style.auth.not_authorized; + // Flex::column() + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Enable Copilot by connecting", + // unauthorized_style.subheading.text.clone(), + // ) + // .aligned(), + // Label::new( + // "your existing license.", + // unauthorized_style.subheading.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(unauthorized_style.subheading.container), + // ) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "You must have an active copilot", + // unauthorized_style.warning.text.clone(), + // ) + // .aligned(), + // Label::new( + // "license to use it in Zed.", + // unauthorized_style.warning.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(unauthorized_style.warning.container), + // ) + // .with_child(theme::ui::cta_button::( + // "Subscribe on GitHub", + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // |_, _, cx| { + // cx.remove_window(); + // cx.platform().open_url(COPILOT_SIGN_UP_URL) + // }, + // )) + // .align_children_center() + // .into_any() + // } +} -// Flex::column() -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Enable Copilot by connecting", -// unauthorized_style.subheading.text.clone(), -// ) -// .aligned(), -// Label::new( -// "your existing license.", -// unauthorized_style.subheading.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(unauthorized_style.subheading.container), -// ) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "You must have an active copilot", -// unauthorized_style.warning.text.clone(), -// ) -// .aligned(), -// Label::new( -// "license to use it in Zed.", -// unauthorized_style.warning.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(unauthorized_style.warning.container), -// ) -// .with_child(theme::ui::cta_button::( -// "Subscribe on GitHub", -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// |_, _, cx| { -// cx.remove_window(); -// cx.platform().open_url(COPILOT_SIGN_UP_URL) -// }, -// )) -// .align_children_center() -// .into_any() -// } -// } +impl Render for CopilotCodeVerification { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } +} // impl Entity for CopilotCodeVerification { // type Event = (); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 40594a71875f8bf826ef3b47df656db5bd8a7ff7..5ca35844d88ff36b6d230f84a51eb9f4ecc5ff65 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1486,10 +1486,18 @@ impl<'a> WindowContext<'a> { pub fn available_actions(&self) -> Vec> { if let Some(focus_id) = self.window.focus { - self.window + let mut actions = self + .window .current_frame .dispatch_tree - .available_actions(focus_id) + .available_actions(focus_id); + actions.extend( + self.app + .global_action_listeners + .keys() + .filter_map(|type_id| self.app.actions.build_action_type(type_id).ok()), + ); + actions } else { Vec::new() } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8eee654331a0e0307e9e0cc6e53c043fd77d24fd..7bfec95317307ebbe42969d79298bda21562f9fe 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -101,7 +101,7 @@ pub fn init(cx: &mut AppContext) { // will be initialized as disabled by default, so we filter its commands // out when starting up. cx.update_default_global::(|filter, _| { - filter.filtered_namespaces.insert("vim"); + filter.hidden_namespaces.insert("vim"); }); cx.update_global(|vim: &mut Vim, cx: &mut AppContext| { vim.set_enabled(settings::get::(cx).0, cx) @@ -477,9 +477,9 @@ impl Vim { cx.update_default_global::(|filter, _| { if self.enabled { - filter.filtered_namespaces.remove("vim"); + filter.hidden_namespaces.remove("vim"); } else { - filter.filtered_namespaces.insert("vim"); + filter.hidden_namespaces.insert("vim"); } }); From af72772a72f46948566887ae550a0b5cf81394e8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 09:02:21 -0500 Subject: [PATCH 09/78] Expand toolbar tools --- crates/workspace2/src/toolbar.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 8c554dcd6744d94fb7c3b0dfb30e40fad5afeadc..d80452ac8b63232e180c63bb186ab144adc2437c 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -3,8 +3,8 @@ use gpui::{ div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext, WindowContext, }; -use ui::prelude::*; use ui::{h_stack, v_stack, Icon, IconButton}; +use ui::{prelude::*, Tooltip}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), @@ -93,17 +93,24 @@ impl Render for Toolbar { .child( h_stack() .p_1() + .gap_2() .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)), + IconButton::new("toggle-inlay-hints", Icon::InlayHint) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), ) .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("inline-assist", Icon::MagicWand)), + IconButton::new("buffer-search", Icon::MagnifyingGlass) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Search in File", cx)), + ) + .child( + IconButton::new("inline-assist", Icon::MagicWand) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), ), ), ) From f9efaebddf335ebc24b591c3759a153b1da25a12 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 09:10:16 -0500 Subject: [PATCH 10/78] Update icon size --- crates/ui2/src/components/icon.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 3f2cb725f9b4d53b01e2180dbdf50d477cab0e3b..599eb0e9f8723d9c685c1c556c3420bdbee6680c 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,15 +1,26 @@ -use gpui::{rems, svg, IntoElement, Svg}; +use gpui::{rems, svg, IntoElement, Rems, Svg}; use strum::EnumIter; use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { + XSmall, Small, #[default] Medium, } +impl IconSize { + pub fn rems(self) -> Rems { + match self { + IconSize::XSmall => rems(12. / 16.), + IconSize::Small => rems(14. / 16.), + IconSize::Medium => rems(16. / 16.), + } + } +} + #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -170,13 +181,8 @@ impl RenderOnce for IconElement { type Rendered = Svg; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let svg_size = match self.size { - IconSize::Small => rems(12. / 16.), - IconSize::Medium => rems(16. / 16.), - }; - svg() - .size(svg_size) + .size(self.size.rems()) .flex_none() .path(self.path) .text_color(self.color.color(cx)) From 7c5df51d2eda06b16ec3f1326c7ab4126416e3ab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 10:11:18 -0500 Subject: [PATCH 11/78] Update button sizes --- crates/workspace2/src/pane.rs | 102 +++++++++++++++++-------------- crates/workspace2/src/toolbar.rs | 3 + 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 18a5de1bc19db09a8e24471a8814470f84669a62..2433edee0efe25cf7125559ef9f6e364c69b4681 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1447,7 +1447,7 @@ impl Pane { .child( IconElement::new(Icon::Close) .color(Color::Muted) - .size(IconSize::Small), + .size(IconSize::XSmall), ) }; @@ -1589,9 +1589,11 @@ impl Pane { .border_b() .border_r() .border_color(cx.theme().colors().border) + .bg(gpui::red()) // Nav Buttons .child( IconButton::new("navigate_backward", Icon::ArrowLeft) + .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) @@ -1600,6 +1602,7 @@ impl Pane { ) .child( IconButton::new("navigate_forward", Icon::ArrowRight) + .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) @@ -1612,6 +1615,8 @@ impl Pane { .relative() .flex_1() .h_full() + .overflow_hidden_x() + .bg(gpui::green()) .child( div() .absolute() @@ -1623,21 +1628,19 @@ impl Pane { .border_color(cx.theme().colors().border), ) .child( - div() - .id("tabs") - .z_index(2) - .flex() - .overflow_x_scroll() - .children( - self.items.iter().enumerate().zip(self.tab_details(cx)).map( - |((ix, item), detail)| self.render_tab(ix, item, detail, cx), - ), - ), + h_stack().id("tabs").z_index(2).children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + ), ), ) // Right Side .child( h_stack() + .bg(gpui::blue()) .flex() .flex_none() .gap_1() @@ -1650,44 +1653,48 @@ impl Pane { .flex() .items_center() .gap_px() - .child(IconButton::new("plus", Icon::Plus).on_click(cx.listener( - |this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone(), cx) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - cx, - ) - .action("New Search", NewSearch.boxed_clone(), cx) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }) - .detach(); - this.new_item_menu = Some(menu); - }, - ))) + .child( + IconButton::new("plus", Icon::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone(), cx) + .action( + "New Terminal", + NewCenterTerminal.boxed_clone(), + cx, + ) + .action("New Search", NewSearch.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + })), + ) .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { el.child(Self::render_menu_overlay(new_item_menu)) }) - .child(IconButton::new("split", Icon::Split).on_click(cx.listener( - |this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("Split Right", SplitRight.boxed_clone(), cx) - .action("Split Left", SplitLeft.boxed_clone(), cx) - .action("Split Up", SplitUp.boxed_clone(), cx) - .action("Split Down", SplitDown.boxed_clone(), cx) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }) - .detach(); - this.split_item_menu = Some(menu); - }, - ))) + .child( + IconButton::new("split", Icon::Split) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone(), cx) + .action("Split Left", SplitLeft.boxed_clone(), cx) + .action("Split Up", SplitUp.boxed_clone(), cx) + .action("Split Down", SplitDown.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + })), + ) .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { el.child(Self::render_menu_overlay(split_item_menu)) }), @@ -2108,6 +2115,8 @@ impl Render for Pane { v_stack() .key_context("Pane") .track_focus(&self.focus_handle) + .size_full() + .overflow_hidden() .on_focus_in({ let this = this.clone(); move |event, cx| { @@ -2175,7 +2184,6 @@ impl Render for Pane { pane.close_all_items(action, cx) .map(|task| task.detach_and_log_err(cx)); })) - .size_full() .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d80452ac8b63232e180c63bb186ab144adc2437c..d7cb741791789b2276b64892930352e777ef14e1 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -97,18 +97,21 @@ impl Render for Toolbar { .child( IconButton::new("toggle-inlay-hints", Icon::InlayHint) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), ) .child( IconButton::new("buffer-search", Icon::MagnifyingGlass) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Search in File", cx)), ) .child( IconButton::new("inline-assist", Icon::MagicWand) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), ), From c9b50c8bab78740f12035ab7ba2dbf90695eabbe Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:48:17 +0100 Subject: [PATCH 12/78] Add v_stack and h_stack to ui::prelude --- crates/ui2/src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 6fd0262c674ac6d13d9728b78f23a8db1f6e6ebd..38065b62754b5facb5ed9440ad2b74535f40d445 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -8,5 +8,6 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::{h_stack, v_stack}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use theme::ActiveTheme; From ede86d91874a1846082d2d923e126d93fc1a8770 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 16:49:36 +0100 Subject: [PATCH 13/78] WIP --- Cargo.lock | 41 + Cargo.toml | 1 + crates/assistant2/Cargo.toml | 54 + crates/assistant2/README.zmd | 63 + crates/assistant2/features.zmd | 3 + crates/assistant2/src/assistant.rs | 113 + crates/assistant2/src/assistant_panel.rs | 3660 +++++++++++++++++++ crates/assistant2/src/assistant_settings.rs | 80 + crates/assistant2/src/codegen.rs | 695 ++++ crates/assistant2/src/prompts.rs | 388 ++ crates/assistant2/src/streaming_diff.rs | 293 ++ crates/collab_ui2/src/collab_panel.rs | 4 - crates/project_panel2/src/project_panel.rs | 14 +- crates/terminal_view2/src/terminal_panel.rs | 4 - crates/workspace2/src/dock.rs | 23 +- crates/workspace2/src/workspace2.rs | 10 +- 16 files changed, 5405 insertions(+), 41 deletions(-) create mode 100644 crates/assistant2/Cargo.toml create mode 100644 crates/assistant2/README.zmd create mode 100644 crates/assistant2/features.zmd create mode 100644 crates/assistant2/src/assistant.rs create mode 100644 crates/assistant2/src/assistant_panel.rs create mode 100644 crates/assistant2/src/assistant_settings.rs create mode 100644 crates/assistant2/src/codegen.rs create mode 100644 crates/assistant2/src/prompts.rs create mode 100644 crates/assistant2/src/streaming_diff.rs diff --git a/Cargo.lock b/Cargo.lock index 39683c9fc11b41700a5b5123287444979ae87bc9..3808d17f4f908eaabea911d39513261723ff8343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,47 @@ dependencies = [ "workspace", ] +[[package]] +name = "assistant2" +version = "0.1.0" +dependencies = [ + "ai2", + "anyhow", + "chrono", + "client2", + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fs2", + "futures 0.3.28", + "gpui2", + "indoc", + "isahc", + "language2", + "log", + "menu2", + "multi_buffer2", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "project2", + "rand 0.8.5", + "regex", + "schemars", + "search2", + "semantic_index2", + "serde", + "serde_json", + "settings2", + "smol", + "theme2", + "tiktoken-rs", + "ui2", + "util", + "uuid 1.4.1", + "workspace2", +] + [[package]] name = "async-broadcast" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 610a4dc11e03cc24c86db033d7b5b95c25ab64ae..017a1f42e92e26a7f356c22c360bc57fc2b454b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/activity_indicator2", "crates/ai", "crates/assistant", + "crates/assistant2", "crates/audio", "crates/audio2", "crates/auto_update", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2fbc8850be4361671bc8eb852001b91ff2e56106 --- /dev/null +++ b/crates/assistant2/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "assistant2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +ai = { package = "ai2", path = "../ai2" } +client = { package = "client2", path = "../client2" } +collections = { path = "../collections"} +editor = { package = "editor2", path = "../editor2" } +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } +project = { package = "project2", path = "../project2" } +search = { package = "search2", path = "../search2" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } + +uuid.workspace = true +log.workspace = true +anyhow.workspace = true +chrono = { version = "0.4", features = ["serde"] } +futures.workspace = true +indoc.workspace = true +isahc.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +regex.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +tiktoken-rs.workspace = true + +[dev-dependencies] +ai = { package = "ai2", path = "../ai2", features = ["test-support"]} +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +log.workspace = true +rand.workspace = true diff --git a/crates/assistant2/README.zmd b/crates/assistant2/README.zmd new file mode 100644 index 0000000000000000000000000000000000000000..af8702b60949826645ad9a72585feedf23adea66 --- /dev/null +++ b/crates/assistant2/README.zmd @@ -0,0 +1,63 @@ +> Hello, I'm Nathan Sobo. +I'm currently your sole creator and the leader of Zed Industries. +Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation. + +< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development. + +As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world. + +> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish. + +We've decided that our next milestone will be open-sourcing Zed on the Zed platform. + +< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further. + +Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation. + +Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future! + +@nathan> #zed, can you describe the conversation UI as you currently understand it. + +< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary. + +Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered. + +@nathan> What details around the conversation view are worth exploring? + +< @nathan, there are several aspects of the conversation view that are worth exploring and refining: + +1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context. +2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history. +3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials. +4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants. +5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view. + +Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed. + +@nathan> @nate, which of these would you like to explore? + +@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5. + +#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots). + +@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try... + +#zed describe a hypothetical conversation UI that meets the description. + +< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements: + +1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space. + +2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions. + +3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view. + +4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly. + +5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow. + +6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view. + +7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context. + +This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs. diff --git a/crates/assistant2/features.zmd b/crates/assistant2/features.zmd new file mode 100644 index 0000000000000000000000000000000000000000..f3183d68bc66a66a6a8fa936bdc376856e6698b3 --- /dev/null +++ b/crates/assistant2/features.zmd @@ -0,0 +1,3 @@ +Push content to a deeper layer. +A context can have multiple sublayers. +You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document. diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs new file mode 100644 index 0000000000000000000000000000000000000000..91d61a19f98b4cf4a61257a68d8f7212c6a33586 --- /dev/null +++ b/crates/assistant2/src/assistant.rs @@ -0,0 +1,113 @@ +pub mod assistant_panel; +mod assistant_settings; +mod codegen; +mod prompts; +mod streaming_diff; + +use ai::providers::open_ai::Role; +use anyhow::Result; +pub use assistant_panel::AssistantPanel; +use assistant_settings::OpenAIModel; +use chrono::{DateTime, Local}; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt; +use gpui::AppContext; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; +use util::paths::CONVERSATIONS_DIR; + +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +struct MessageId(usize); + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum MessageStatus { + Pending, + Done, + Error(Arc), +} + +#[derive(Serialize, Deserialize)] +struct SavedMessage { + id: MessageId, + start: usize, +} + +#[derive(Serialize, Deserialize)] +struct SavedConversation { + id: Option, + zed: String, + version: String, + text: String, + messages: Vec, + message_metadata: HashMap, + summary: String, + model: OpenAIModel, +} + +impl SavedConversation { + const VERSION: &'static str = "0.1.0"; +} + +struct SavedConversationMetadata { + title: String, + path: PathBuf, + mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::::new(); + while let Some(path) = paths.next().await { + let path = path?; + if path.extension() != Some(OsStr::new("json")) { + continue; + } + + let pattern = r" - \d+.zed.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } +} + +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3bd06328d3e42760b0cc70cc0543f1c3cbfcdaa --- /dev/null +++ b/crates/assistant2/src/assistant_panel.rs @@ -0,0 +1,3660 @@ +use crate::{ + assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, + codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, + MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, +}; + +use ai::{ + auth::ProviderCredential, + completion::{CompletionProvider, CompletionRequest}, + providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage}, +}; + +use ai::prompts::repository_context::PromptCodeSnippet; +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Local}; +use client::{telemetry::AssistantKind, TelemetrySettings}; +use collections::{hash_map, HashMap, HashSet, VecDeque}; +use editor::{ + display_map::{ + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, + }, + scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, + Anchor, Editor, EditorEvent, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use fs::Fs; +use futures::StreamExt; +use gpui::{ + actions, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, + Div, Element, Entity, EventEmitter, FocusHandle, FocusableView, HighlightStyle, + InteractiveElement, IntoElement, Model, ModelContext, Render, Styled, Subscription, Task, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, +}; +use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use project::Project; +use search::BufferSearchBar; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; +use settings::{Settings, SettingsStore}; +use std::{ + cell::Cell, + cmp, + fmt::Write, + iter, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::{Duration, Instant}, +}; +use ui::{h_stack, v_stack, ButtonCommon, ButtonLike, Clickable, IconButton, Label}; +use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; +use uuid::Uuid; +use workspace::{ + dock::{DockPosition, Panel, PanelEvent}, + searchable::Direction, + Save, Toast, ToggleZoom, Toolbar, Workspace, +}; + +actions!( + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, +); + +pub fn init(cx: &mut AppContext) { + AssistantSettings::register(cx); + cx.add_action( + |this: &mut AssistantPanel, + _: &workspace::NewFile, + cx: &mut ViewContext| { + this.new_conversation(cx); + }, + ); + cx.add_action(ConversationEditor::assist); + cx.capture_action(ConversationEditor::cancel_last_assist); + cx.capture_action(ConversationEditor::save); + cx.add_action(ConversationEditor::quote_selection); + cx.capture_action(ConversationEditor::copy); + cx.add_action(ConversationEditor::split); + cx.capture_action(ConversationEditor::cycle_message_role); + cx.add_action(AssistantPanel::save_credentials); + cx.add_action(AssistantPanel::reset_credentials); + cx.add_action(AssistantPanel::toggle_zoom); + cx.add_action(AssistantPanel::deploy); + cx.add_action(AssistantPanel::select_next_match); + cx.add_action(AssistantPanel::select_prev_match); + cx.add_action(AssistantPanel::handle_editor_cancel); + cx.add_action( + |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); + cx.add_action(AssistantPanel::inline_assist); + cx.add_action(AssistantPanel::cancel_last_inline_assist); + cx.add_action(InlineAssistant::confirm); + cx.add_action(InlineAssistant::cancel); + cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::toggle_retrieve_context); + cx.add_action(InlineAssistant::move_up); + cx.add_action(InlineAssistant::move_down); +} + +#[derive(Debug)] +pub enum AssistantPanelEvent { + ZoomIn, + ZoomOut, + Focus, + Close, + DockPositionChanged, +} + +pub struct AssistantPanel { + workspace: WeakView, + width: Option, + height: Option, + active_editor_index: Option, + prev_active_editor_index: Option, + editors: Vec>, + saved_conversations: Vec, + saved_conversations_scroll_handle: UniformListScrollHandle, + zoomed: bool, + // todo!("remove has_focus field") + focus_handle: FocusHandle, + toolbar: View, + completion_provider: Arc, + api_key_editor: Option>, + languages: Arc, + fs: Arc, + subscriptions: Vec, + next_inline_assist_id: usize, + pending_inline_assists: HashMap, + pending_inline_assist_ids_by_editor: HashMap, Vec>, + include_conversation_in_next_inline_assist: bool, + inline_prompt_history: VecDeque, + _watch_saved_conversations: Task>, + semantic_index: Option>, + retrieve_context_in_next_inline_assist: bool, +} + +impl AssistantPanel { + const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; + + pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { + cx.spawn(|mut cx| async move { + let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + + // TODO: deserialize state. + let workspace_handle = workspace.clone(); + workspace.update(&mut cx, |workspace, cx| { + cx.add_view::(|cx| { + const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); + let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { + let mut events = fs + .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION) + .await; + while events.next().await.is_some() { + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.saved_conversations = saved_conversations; + cx.notify(); + }) + .ok(); + } + + anyhow::Ok(()) + }); + + let toolbar = cx.add_view(|cx| { + let mut toolbar = Toolbar::new(); + toolbar.set_can_navigate(false, cx); + toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar + }); + + let semantic_index = SemanticIndex::global(cx); + // Defaulting currently to GPT4, allow for this to be set via config. + let completion_provider = Arc::new(OpenAICompletionProvider::new( + "gpt-4", + cx.background_executor().clone(), + )); + + let focus_handle = cx.focus_handle(); + cx.on_focus_in(Self::focus_in).detach(); + cx.on_focus_out(Self::focus_out).detach(); + + let mut this = Self { + workspace: workspace_handle, + active_editor_index: Default::default(), + prev_active_editor_index: Default::default(), + editors: Default::default(), + saved_conversations, + saved_conversations_scroll_handle: Default::default(), + zoomed: false, + focus_handle, + toolbar, + completion_provider, + api_key_editor: None, + languages: workspace.app_state().languages.clone(), + fs: workspace.app_state().fs.clone(), + width: None, + height: None, + subscriptions: Default::default(), + next_inline_assist_id: 0, + pending_inline_assists: Default::default(), + pending_inline_assist_ids_by_editor: Default::default(), + include_conversation_in_next_inline_assist: false, + inline_prompt_history: Default::default(), + _watch_saved_conversations, + semantic_index, + retrieve_context_in_next_inline_assist: false, + }; + + let mut old_dock_position = this.position(cx); + this.subscriptions = + vec![cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(AssistantPanelEvent::DockPositionChanged); + } + cx.notify(); + })]; + + this + }) + }) + }) + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); + cx.notify(); + if self.focus_handle.is_focused(cx) { + if let Some(editor) = self.active_editor() { + cx.focus_view(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus_view(api_key_editor); + } + } + } + + fn focus_out(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); + cx.notify(); + } + + pub fn inline_assist( + workspace: &mut Workspace, + _: &InlineAssist, + cx: &mut ViewContext, + ) { + let this = if let Some(this) = workspace.panel::(cx) { + if this.update(cx, |assistant, cx| { + if !assistant.has_credentials() { + assistant.load_credentials(cx); + }; + + assistant.has_credentials() + }) { + this + } else { + workspace.focus_panel::(cx); + return; + } + } else { + return; + }; + + let active_editor = if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor + } else { + return; + }; + + let project = workspace.project(); + + this.update(cx, |assistant, cx| { + assistant.new_inline_assist(&active_editor, cx, project) + }); + } + + fn new_inline_assist( + &mut self, + editor: &View, + cx: &mut ViewContext, + project: &Model, + ) { + let selection = editor.read(cx).selections.newest_anchor().clone(); + if selection.start.excerpt_id != selection.end.excerpt_id { + return; + } + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + + // Extend the selection to the start and the end of the line. + let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); + if point_selection.end > point_selection.start { + point_selection.start.column = 0; + // If the selection ends at the start of the line, we don't want to include it. + if point_selection.end.column == 0 { + point_selection.end.row -= 1; + } + point_selection.end.column = snapshot.line_len(point_selection.end.row); + } + + let codegen_kind = if point_selection.start == point_selection.end { + CodegenKind::Generate { + position: snapshot.anchor_after(point_selection.start), + } + } else { + CodegenKind::Transform { + range: snapshot.anchor_before(point_selection.start) + ..snapshot.anchor_after(point_selection.end), + } + }; + + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); + let provider = self.completion_provider.clone(); + + // Retrieve Credentials Authenticates the Provider + provider.retrieve_credentials(cx); + + let codegen = cx.add_model(|cx| { + Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) + }); + + if let Some(semantic_index) = self.semantic_index.clone() { + let project = project.clone(); + cx.spawn(|_, mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + }) + .await + .unwrap_or(false); + if previously_indexed { + let _ = semantic_index + .update(&mut cx, |index, cx| { + index.index_project(project.clone(), cx) + }) + .await; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); + let inline_assistant = cx.add_view(|cx| { + let assistant = InlineAssistant::new( + inline_assist_id, + measurements.clone(), + self.include_conversation_in_next_inline_assist, + self.inline_prompt_history.clone(), + codegen.clone(), + self.workspace.clone(), + cx, + self.retrieve_context_in_next_inline_assist, + self.semantic_index.clone(), + project.clone(), + ); + cx.focus_self(); + assistant + }); + let block_id = editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_anchor_ranges([selection.head()..selection.head()]) + }); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: snapshot.anchor_before(point_selection.head()), + height: 2, + render: Arc::new({ + let inline_assistant = inline_assistant.clone(); + move |cx: &mut BlockContext| { + measurements.set(BlockMeasurements { + anchor_x: cx.anchor_x, + gutter_width: cx.gutter_width, + }); + inline_assistant.clone().into_any_element() + } + }), + disposition: if selection.reversed { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0] + }); + + self.pending_inline_assists.insert( + inline_assist_id, + PendingInlineAssist { + editor: editor.downgrade(), + inline_assistant: Some((block_id, inline_assistant.clone())), + codegen: codegen.clone(), + project: project.downgrade(), + _subscriptions: vec![ + cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), + cx.subscribe(editor, { + let inline_assistant = inline_assistant.downgrade(); + move |_, editor, event, cx| { + if let Some(inline_assistant) = inline_assistant.upgrade() { + if let EditorEvent::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } + } + } + } + }), + cx.observe(&codegen, { + let editor = editor.downgrade(); + move |this, _, cx| { + if let Some(editor) = editor.upgrade() { + this.update_highlights_for_editor(&editor, cx); + } + } + }), + cx.subscribe(&codegen, move |this, codegen, event, cx| match event { + codegen::Event::Undone => { + this.finish_inline_assist(inline_assist_id, false, cx) + } + codegen::Event::Finished => { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + let error = codegen + .read(cx) + .error() + .map(|error| format!("Inline assistant error: {}", error)); + if let Some(error) = error { + if pending_assist.inline_assistant.is_none() { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(inline_assist_id, error), + cx, + ); + }) + } + + this.finish_inline_assist(inline_assist_id, false, cx); + } + } else { + this.finish_inline_assist(inline_assist_id, false, cx); + } + } + }), + ], + }, + ); + self.pending_inline_assist_ids_by_editor + .entry(editor.downgrade()) + .or_default() + .push(inline_assist_id); + self.update_highlights_for_editor(&editor, cx); + } + + fn handle_inline_assistant_event( + &mut self, + inline_assistant: View, + event: &InlineAssistantEvent, + cx: &mut ViewContext, + ) { + let assist_id = inline_assistant.read(cx).id; + match event { + InlineAssistantEvent::Confirmed { + prompt, + include_conversation, + retrieve_context, + } => { + self.confirm_inline_assist( + assist_id, + prompt, + *include_conversation, + cx, + *retrieve_context, + ); + } + InlineAssistantEvent::Canceled => { + self.finish_inline_assist(assist_id, true, cx); + } + InlineAssistantEvent::Dismissed => { + self.hide_inline_assist(assist_id, cx); + } + InlineAssistantEvent::IncludeConversationToggled { + include_conversation, + } => { + self.include_conversation_in_next_inline_assist = *include_conversation; + } + InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { + self.retrieve_context_in_next_inline_assist = *retrieve_context + } + } + } + + fn cancel_last_inline_assist( + workspace: &mut Workspace, + _: &editor::Cancel, + cx: &mut ViewContext, + ) { + if let Some(panel) = workspace.panel::(cx) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.finish_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + if handled { + return; + } + } + } + + cx.propagate_action(); + } + + fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { + self.hide_inline_assist(assist_id, cx); + + if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { + if let hash_map::Entry::Occupied(mut entry) = self + .pending_inline_assist_ids_by_editor + .entry(pending_assist.editor) + { + entry.get_mut().retain(|id| *id != assist_id); + if entry.get().is_empty() { + entry.remove(); + } + } + + if let Some(editor) = pending_assist.editor.upgrade() { + self.update_highlights_for_editor(&editor, cx); + + if undo { + pending_assist + .codegen + .update(cx, |codegen, cx| codegen.undo(cx)); + } + } + } + } + + fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { + if let Some(editor) = pending_assist.editor.upgrade() { + if let Some((block_id, _)) = pending_assist.inline_assistant.take() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }); + } + } + } + } + + fn confirm_inline_assist( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + include_conversation: bool, + cx: &mut ViewContext, + retrieve_context: bool, + ) { + let conversation = if include_conversation { + self.active_editor() + .map(|editor| editor.read(cx).conversation.clone()) + } else { + None + }; + + let pending_assist = + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { + pending_assist + } else { + return; + }; + + let editor = if let Some(editor) = pending_assist.editor.upgrade() { + editor + } else { + return; + }; + + let project = pending_assist.project.clone(); + + let project_name = if let Some(project) = project.upgrade() { + Some( + project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"), + ) + } else { + None + }; + + self.inline_prompt_history + .retain(|prompt| prompt != user_prompt); + self.inline_prompt_history.push_back(user_prompt.into()); + if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { + self.inline_prompt_history.pop_front(); + } + + let codegen = pending_assist.codegen.clone(); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let range = codegen.read(cx).range(); + let start = snapshot.point_to_buffer_offset(range.start); + let end = snapshot.point_to_buffer_offset(range.end); + let (buffer, range) = if let Some((start, end)) = start.zip(end) { + let (start_buffer, start_buffer_offset) = start; + let (end_buffer, end_buffer_offset) = end; + if start_buffer.remote_id() == end_buffer.remote_id() { + (start_buffer.clone(), start_buffer_offset..end_buffer_offset) + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + } + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + }; + + let language = buffer.language_at(range.start); + let language_name = if let Some(language) = language.as_ref() { + if Arc::ptr_eq(language, &language::PLAIN_TEXT) { + None + } else { + Some(language.name()) + } + } else { + None + }; + + // Higher Temperature increases the randomness of model outputs. + // If Markdown or No Language is Known, increase the randomness for more creative output + // If Code, decrease temperature to get more deterministic outputs + let temperature = if let Some(language) = language_name.clone() { + if language.to_string() != "Markdown".to_string() { + 0.5 + } else { + 1.0 + } + } else { + 1.0 + }; + + let user_prompt = user_prompt.to_string(); + + let snippets = if retrieve_context { + let Some(project) = project.upgrade() else { + return; + }; + + let search_results = if let Some(semantic_index) = self.semantic_index.clone() { + let search_results = semantic_index.update(cx, |this, cx| { + this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + }); + + cx.background_executor() + .spawn(async move { search_results.await.unwrap_or_default() }) + } else { + Task::ready(Vec::new()) + }; + + let snippets = cx.spawn(|_, mut cx| async move { + let mut snippets = Vec::new(); + for result in search_results.await { + snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx)); + } + snippets + }); + snippets + } else { + Task::ready(Vec::new()) + }; + + let mut model = AssistantSettings::get_global(cx) + .default_open_ai_model + .clone(); + let model_name = model.full_name(); + + let prompt = cx.background_executor().spawn(async move { + let snippets = snippets.await; + + let language_name = language_name.as_deref(); + generate_content_prompt( + user_prompt, + language_name, + buffer, + range, + snippets, + model_name, + project_name, + ) + }); + + let mut messages = Vec::new(); + if let Some(conversation) = conversation { + let conversation = conversation.read(cx); + let buffer = conversation.buffer.read(cx); + messages.extend( + conversation + .messages(cx) + .map(|message| message.to_open_ai_message(buffer)), + ); + model = conversation.model.clone(); + } + + cx.spawn(|_, mut cx| async move { + // I Don't know if we want to return a ? here. + let prompt = prompt.await?; + + messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); + + let request = Box::new(OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + stop: vec!["|END|>".to_string()], + temperature, + }); + + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + anyhow::Ok(()) + }) + .detach(); + } + + fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { + let mut background_ranges = Vec::new(); + let mut foreground_ranges = Vec::new(); + let empty_inline_assist_ids = Vec::new(); + let inline_assist_ids = self + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .unwrap_or(&empty_inline_assist_ids); + + for inline_assist_id in inline_assist_ids { + if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { + let codegen = pending_assist.codegen.read(cx); + background_ranges.push(codegen.range()); + foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); + } + } + + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + merge_ranges(&mut background_ranges, &snapshot); + merge_ranges(&mut foreground_ranges, &snapshot); + editor.update(cx, |editor, cx| { + if background_ranges.is_empty() { + editor.clear_background_highlights::(cx); + } else { + editor.highlight_background::( + background_ranges, + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + } + + if foreground_ranges.is_empty() { + editor.clear_highlights::(cx); + } else { + editor.highlight_text::( + foreground_ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + } + }); + } + + fn new_conversation(&mut self, cx: &mut ViewContext) -> View { + let editor = cx.add_view(|cx| { + ConversationEditor::new( + self.completion_provider.clone(), + self.languages.clone(), + self.fs.clone(), + self.workspace.clone(), + cx, + ) + }); + self.add_conversation(editor.clone(), cx); + editor + } + + fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); + + let conversation = editor.read(cx).conversation.clone(); + self.subscriptions + .push(cx.observe(&conversation, |_, _, cx| cx.notify())); + + let index = self.editors.len(); + self.editors.push(editor); + self.set_active_editor_index(Some(index), cx); + } + + fn set_active_editor_index(&mut self, index: Option, cx: &mut ViewContext) { + self.prev_active_editor_index = self.active_editor_index; + self.active_editor_index = index; + if let Some(editor) = self.active_editor() { + let editor = editor.read(cx).editor.clone(); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(Some(&editor), cx); + }); + if self.has_focus(cx) { + cx.focus(&editor); + } + } else { + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(None, cx); + }); + } + + cx.notify(); + } + + fn handle_conversation_editor_event( + &mut self, + _: View, + event: &ConversationEditorEvent, + cx: &mut ViewContext, + ) { + match event { + ConversationEditorEvent::TabContentChanged => cx.notify(), + } + } + + fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(api_key) = self + .api_key_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + { + if !api_key.is_empty() { + let credential = ProviderCredential::Credentials { + api_key: api_key.clone(), + }; + + self.completion_provider.save_credentials(cx, credential); + + self.api_key_editor.take(); + cx.focus_self(); + cx.notify(); + } + } else { + cx.propagate_action(); + } + } + + fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { + self.completion_provider.delete_credentials(cx); + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.focus_self(); + cx.notify(); + } + + fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { + if self.zoomed { + cx.emit(AssistantPanelEvent::ZoomOut) + } else { + cx.emit(AssistantPanelEvent::ZoomIn) + } + } + + fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false + } + }); + } + if propagate_action { + cx.propagate_action(); + } + } + + fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + if !search_bar.read(cx).is_dismissed() { + search_bar.update(cx, |search_bar, cx| { + search_bar.dismiss(&Default::default(), cx) + }); + return; + } + } + cx.propagate_action(); + } + + fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); + } + } + + fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); + } + } + + fn active_editor(&self) -> Option<&View> { + self.editors.get(self.active_editor_index?) + } + + fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("hamburger_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if this.active_editor().is_some() { + this.set_active_editor_index(None, cx); + } else { + this.set_active_editor_index(this.prev_active_editor_index, cx); + } + })) + .tooltip(|cx| ui::Tooltip::text("History", cx)) + } + + fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { + if self.active_editor().is_some() { + vec![ + Self::render_split_button(cx).into_any(), + Self::render_quote_button(cx).into_any(), + Self::render_assist_button(cx).into_any(), + ] + } else { + Default::default() + } + } + + fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("split_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Split Message", &Split, cx)) + } + + fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("assist_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Assist", &Assist, cx)) + } + + fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("quote_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(workspace) = this.workspace.upgrade() { + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + ConversationEditor::quote_selection(workspace, &Default::default(), cx) + }); + }); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) + } + + fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("plus_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + this.new_conversation(cx); + })) + .tooltip(|cx| ui::Tooltip::for_action("New Conversation", &NewConversation, cx)) + } + + fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("zoom_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + this.toggle_zoom(&ToggleZoom, cx); + })) + .tooltip(|cx| { + ui::Tooltip::for_action( + if self.zoomed { "Zoom Out" } else { "Zoom In" }, + &ToggleZoom, + cx, + ) + }) + } + + fn render_saved_conversation( + &mut self, + index: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let conversation = &self.saved_conversations[index]; + let path = conversation.path.clone(); + + ButtonLike::new(index) + .on_click(cx.listener(move |this, _, cx| { + this.open_conversation(path.clone(), cx) + .detach_and_log_err(cx) + })) + .child(Label::new( + conversation.mtime.format("%F %I:%M%p").to_string(), + )) + .child(Label::new(conversation.title.clone())) + } + + fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { + if let Some(ix) = self.editor_index_for_path(&path, cx) { + self.set_active_editor_index(Some(ix), cx); + return Task::ready(Ok(())); + } + + let fs = self.fs.clone(); + let workspace = self.workspace.clone(); + let languages = self.languages.clone(); + cx.spawn(|this, mut cx| async move { + let saved_conversation = fs.load(&path).await?; + let saved_conversation = serde_json::from_str(&saved_conversation)?; + let conversation = cx.add_model(|cx| { + Conversation::deserialize(saved_conversation, path.clone(), languages, cx) + }); + this.update(&mut cx, |this, cx| { + // If, by the time we've loaded the conversation, the user has already opened + // the same conversation, we don't want to open it again. + if let Some(ix) = this.editor_index_for_path(&path, cx) { + this.set_active_editor_index(Some(ix), cx); + } else { + let editor = cx.add_view(|cx| { + ConversationEditor::for_conversation(conversation, fs, workspace, cx) + }); + this.add_conversation(editor, cx); + } + })?; + Ok(()) + }) + } + + fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option { + self.editors + .iter() + .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) + } + + fn has_credentials(&mut self) -> bool { + self.completion_provider.has_credentials() + } + + fn load_credentials(&mut self, cx: &mut ViewContext) { + self.completion_provider.retrieve_credentials(cx); + } +} + +fn build_api_key_editor(cx: &mut ViewContext) -> View { + cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); + editor + }) +} + +impl Render for AssistantPanel { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(api_key_editor) = self.api_key_editor.clone() { + v_stack() + .track_focus(&self.focus_handle) + .child(Label::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + )) + .child(Label::new( + " - Having a subscription for another service like GitHub Copilot won't work." + )) + .child(Label::new( + " - You can create a api key at: platform.openai.com/api-keys" + )) + .child(Label::new( + " " + )) + .child(Label::new( + "Paste your OpenAI API key and press Enter to use the assistant" + )) + .child(api_key_editor) + .child(Label::new( + "Click on the Z button in the status bar to close this panel." + )) + .border() + .border_color(gpui::red()) + } else { + let title = self + .active_editor() + .map(|editor| Label::new(editor.read(cx).title(cx))); + + let mut header = h_stack() + .child(Self::render_hamburger_button(cx)) + .children(title); + + if self.focus_handle.contains_focused(cx) { + header = header + .children(self.render_editor_tools(cx)) + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)); + } + + v_stack() + .track_focus(&self.focus_handle) + .child(header) + .children(if self.toolbar.read(cx).hidden() { + None + } else { + Some(self.toolbar.clone()) + }) + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + uniform_list( + cx.view().clone(), + "saved_conversations", + self.saved_conversations.len(), + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx).into_any()) + .collect() + }, + ) + .track_scroll(self.saved_conversations_scroll_handle.clone()) + .into_any_element() + }) + .border() + .border_color(gpui::red()) + } + } +} + +impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel" + } + + fn position(&self, cx: &WindowContext) -> DockPosition { + match AssistantSettings::get_global(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { + let dock = match position { + DockPosition::Left => AssistantDockPosition::Left, + DockPosition::Bottom => AssistantDockPosition::Bottom, + DockPosition::Right => AssistantDockPosition::Right, + }; + settings.dock = Some(dock); + }); + } + + fn size(&self, cx: &WindowContext) -> f32 { + let settings = AssistantSettings::get_global(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or_else(|| settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), + } + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => self.height = size, + } + cx.notify(); + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + if active { + self.load_credentials(cx); + + if self.editors.is_empty() { + self.new_conversation(cx); + } + } + } + + fn icon(&self, cx: &WindowContext) -> Option { + Some(ui::Icon::Ai) + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +impl EventEmitter for AssistantPanel {} + +impl FocusableView for AssistantPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +enum ConversationEvent { + MessagesEdited, + SummaryChanged, + StreamedCompletion, +} + +#[derive(Default)] +struct Summary { + text: String, + done: bool, +} + +struct Conversation { + id: Option, + buffer: Model, + message_anchors: Vec, + messages_metadata: HashMap, + next_message_id: MessageId, + summary: Option, + pending_summary: Task>, + completion_count: usize, + pending_completions: Vec, + model: OpenAIModel, + token_count: Option, + max_token_count: usize, + pending_token_count: Task>, + pending_save: Task>, + path: Option, + _subscriptions: Vec, + completion_provider: Arc, +} + +impl EventEmitter for Conversation {} + +impl Conversation { + fn new( + language_registry: Arc, + cx: &mut ModelContext, + completion_provider: Arc, + ) -> Self { + let markdown = language_registry.language_for_name("Markdown"); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + buffer.set_language_registry(language_registry); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer + }); + + let settings = AssistantSettings::get_global(cx); + let model = settings.default_open_ai_model.clone(); + + let mut this = Self { + id: Some(Uuid::new_v4().to_string()), + message_anchors: Default::default(), + messages_metadata: Default::default(), + next_message_id: Default::default(), + summary: None, + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), + pending_token_count: Task::ready(None), + model: model.clone(), + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: None, + buffer, + completion_provider, + }; + let message = MessageAnchor { + id: MessageId(post_inc(&mut this.next_message_id.0)), + start: language::Anchor::MIN, + }; + this.message_anchors.push(message.clone()); + this.messages_metadata.insert( + message.id, + MessageMetadata { + role: Role::User, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + + this.count_remaining_tokens(cx); + this + } + + fn serialize(&self, cx: &AppContext) -> SavedConversation { + SavedConversation { + id: self.id.clone(), + zed: "conversation".into(), + version: SavedConversation::VERSION.into(), + text: self.buffer.read(cx).text(), + message_metadata: self.messages_metadata.clone(), + messages: self + .messages(cx) + .map(|message| SavedMessage { + id: message.id, + start: message.offset_range.start, + }) + .collect(), + summary: self + .summary + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_default(), + model: self.model.clone(), + } + } + + fn deserialize( + saved_conversation: SavedConversation, + path: PathBuf, + language_registry: Arc, + cx: &mut ModelContext, + ) -> Self { + let id = match saved_conversation.id { + Some(id) => Some(id), + None => Some(Uuid::new_v4().to_string()), + }; + let model = saved_conversation.model; + let completion_provider: Arc = Arc::new( + OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), + ); + completion_provider.retrieve_credentials(cx); + let markdown = language_registry.language_for_name("Markdown"); + let mut message_anchors = Vec::new(); + let mut next_message_id = MessageId(0); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + for message in saved_conversation.messages { + message_anchors.push(MessageAnchor { + id: message.id, + start: buffer.anchor_before(message.start), + }); + next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); + } + buffer.set_language_registry(language_registry); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer + }); + + let mut this = Self { + id, + message_anchors, + messages_metadata: saved_conversation.message_metadata, + next_message_id, + summary: Some(Summary { + text: saved_conversation.summary, + done: true, + }), + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), + pending_token_count: Task::ready(None), + model, + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: Some(path), + buffer, + completion_provider, + }; + this.count_remaining_tokens(cx); + this + } + + fn handle_buffer_event( + &mut self, + _: Model, + event: &language::Event, + cx: &mut ModelContext, + ) { + match event { + language::Event::Edited => { + self.count_remaining_tokens(cx); + cx.emit(ConversationEvent::MessagesEdited); + } + _ => {} + } + } + + fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + let messages = self + .messages(cx) + .into_iter() + .filter_map(|message| { + Some(tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some( + self.buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), + ), + name: None, + function_call: None, + }) + }) + .collect::>(); + let model = self.model.clone(); + self.pending_token_count = cx.spawn_weak(|this, mut cx| { + async move { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + let token_count = cx + .background() + .spawn(async move { + tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) + }) + .await?; + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + }); + anyhow::Ok(()) + } + .log_err() + }); + } + + fn remaining_tokens(&self) -> Option { + Some(self.max_token_count as isize - self.token_count? as isize) + } + + fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { + self.model = model; + self.count_remaining_tokens(cx); + cx.notify(); + } + + fn assist( + &mut self, + selected_messages: HashSet, + cx: &mut ModelContext, + ) -> Vec { + let mut user_messages = Vec::new(); + + let last_message_id = if let Some(last_message_id) = + self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }) { + last_message_id + } else { + return Default::default(); + }; + + let mut should_assist = false; + for selected_message_id in selected_messages { + let selected_message_role = + if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { + metadata.role + } else { + continue; + }; + + if selected_message_role == Role::Assistant { + if let Some(user_message) = self.insert_message_after( + selected_message_id, + Role::User, + MessageStatus::Done, + cx, + ) { + user_messages.push(user_message); + } + } else { + should_assist = true; + } + } + + if should_assist { + if !self.completion_provider.has_credentials() { + return Default::default(); + } + + let request: Box = Box::new(OpenAIRequest { + model: self.model.full_name().to_string(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .collect(), + stream: true, + stop: vec![], + temperature: 1.0, + }); + + let stream = self.completion_provider.complete(request); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); + + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); + user_messages.push(user_message); + + let task = cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let text = message?; + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); + } + } + cx.notify(); + } + }); + } + } + }); + + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _task: task, + }); + } + + user_messages + } + + fn cancel_last_assist(&mut self) -> bool { + self.pending_completions.pop().is_some() + } + + fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { + for id in ids { + if let Some(metadata) = self.messages_metadata.get_mut(&id) { + metadata.role.cycle(); + cx.emit(ConversationEvent::MessagesEdited); + cx.notify(); + } + } + } + + fn insert_message_after( + &mut self, + message_id: MessageId, + role: Role, + status: MessageStatus, + cx: &mut ModelContext, + ) -> Option { + if let Some(prev_message_ix) = self + .message_anchors + .iter() + .position(|message| message.id == message_id) + { + // Find the next valid message after the one we were given. + let mut next_message_ix = prev_message_ix + 1; + while let Some(next_message) = self.message_anchors.get(next_message_ix) { + if next_message.start.is_valid(self.buffer.read(cx)) { + break; + } + next_message_ix += 1; + } + + let start = self.buffer.update(cx, |buffer, cx| { + let offset = self + .message_anchors + .get(next_message_ix) + .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1); + buffer.edit([(offset..offset, "\n")], None, cx); + buffer.anchor_before(offset + 1) + }); + let message = MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start, + }; + self.message_anchors + .insert(next_message_ix, message.clone()); + self.messages_metadata.insert( + message.id, + MessageMetadata { + role, + sent_at: Local::now(), + status, + }, + ); + cx.emit(ConversationEvent::MessagesEdited); + Some(message) + } else { + None + } + } + + fn split_message( + &mut self, + range: Range, + cx: &mut ModelContext, + ) -> (Option, Option) { + let start_message = self.message_for_offset(range.start, cx); + let end_message = self.message_for_offset(range.end, cx); + if let Some((start_message, end_message)) = start_message.zip(end_message) { + // Prevent splitting when range spans multiple messages. + if start_message.id != end_message.id { + return (None, None); + } + + let message = start_message; + let role = message.role; + let mut edited_buffer = false; + + let mut suffix_start = None; + if range.start > message.offset_range.start && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end); + } + } + + let suffix = if let Some(suffix_start) = suffix_start { + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(suffix_start), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.end..range.end, "\n")], None, cx); + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, suffix.clone()); + self.messages_metadata.insert( + suffix.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + + let new_messages = + if range.start == range.end || range.start == message.offset_range.start { + (None, Some(suffix)) + } else { + let mut prefix_end = None; + if range.start > message.offset_range.start + && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') { + prefix_end = Some(range.start + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.start).next() + == Some('\n') + { + prefix_end = Some(range.start); + } + } + + let selection = if let Some(prefix_end) = prefix_end { + cx.emit(ConversationEvent::MessagesEdited); + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(prefix_end), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.start..range.start, "\n")], None, cx) + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, selection.clone()); + self.messages_metadata.insert( + selection.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + (Some(selection), Some(suffix)) + }; + + if !edited_buffer { + cx.emit(ConversationEvent::MessagesEdited); + } + new_messages + } else { + (None, None) + } + } + + fn summarize(&mut self, cx: &mut ModelContext) { + if self.message_anchors.len() >= 2 && self.summary.is_none() { + if !self.completion_provider.has_credentials() { + return; + } + + let messages = self + .messages(cx) + .take(2) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .chain(Some(RequestMessage { + role: Role::User, + content: "Summarize the conversation into a short title without punctuation" + .into(), + })); + let request: Box = Box::new(OpenAIRequest { + model: self.model.full_name().to_string(), + messages: messages.collect(), + stream: true, + stop: vec![], + temperature: 1.0, + }); + + let stream = self.completion_provider.complete(request); + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let text = message?; + this.update(&mut cx, |this, cx| { + this.summary + .get_or_insert(Default::default()) + .text + .push_str(&text); + cx.emit(ConversationEvent::SummaryChanged); + }); + } + + this.update(&mut cx, |this, cx| { + if let Some(summary) = this.summary.as_mut() { + summary.done = true; + cx.emit(ConversationEvent::SummaryChanged); + } + }); + + anyhow::Ok(()) + } + .log_err() + }); + } + } + + fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { + self.messages_for_offsets([offset], cx).pop() + } + + fn messages_for_offsets( + &self, + offsets: impl IntoIterator, + cx: &AppContext, + ) -> Vec { + let mut result = Vec::new(); + + let mut messages = self.messages(cx).peekable(); + let mut offsets = offsets.into_iter().peekable(); + let mut current_message = messages.next(); + while let Some(offset) = offsets.next() { + // Locate the message that contains the offset. + while current_message.as_ref().map_or(false, |message| { + !message.offset_range.contains(&offset) && messages.peek().is_some() + }) { + current_message = messages.next(); + } + let Some(message) = current_message.as_ref() else { + break; + }; + + // Skip offsets that are in the same message. + while offsets.peek().map_or(false, |offset| { + message.offset_range.contains(offset) || messages.peek().is_none() + }) { + offsets.next(); + } + + result.push(message.clone()); + } + result + } + + fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { + let buffer = self.buffer.read(cx); + let mut message_anchors = self.message_anchors.iter().enumerate().peekable(); + iter::from_fn(move || { + while let Some((start_ix, message_anchor)) = message_anchors.next() { + let metadata = self.messages_metadata.get(&message_anchor.id)?; + let message_start = message_anchor.start.to_offset(buffer); + let mut message_end = None; + let mut end_ix = start_ix; + while let Some((_, next_message)) = message_anchors.peek() { + if next_message.start.is_valid(buffer) { + message_end = Some(next_message.start); + break; + } else { + end_ix += 1; + message_anchors.next(); + } + } + let message_end = message_end + .unwrap_or(language::Anchor::MAX) + .to_offset(buffer); + return Some(Message { + index_range: start_ix..end_ix, + offset_range: message_start..message_end, + id: message_anchor.id, + anchor: message_anchor.start, + role: metadata.role, + sent_at: metadata.sent_at, + status: metadata.status.clone(), + }); + } + None + }) + } + + fn save( + &mut self, + debounce: Option, + fs: Arc, + cx: &mut ModelContext, + ) { + self.pending_save = cx.spawn(|this, mut cx| async move { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + + let (old_path, summary) = this.read_with(&cx, |this, _| { + let path = this.path.clone(); + let summary = if let Some(summary) = this.summary.as_ref() { + if summary.done { + Some(summary.text.clone()) + } else { + None + } + } else { + None + }; + (path, summary) + }); + + if let Some(summary) = summary { + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let path = if let Some(old_path) = old_path { + old_path + } else { + let mut discriminant = 1; + let mut new_path; + loop { + new_path = CONVERSATIONS_DIR.join(&format!( + "{} - {}.zed.json", + summary.trim(), + discriminant + )); + if fs.is_file(&new_path).await { + discriminant += 1; + } else { + break; + } + } + new_path + }; + + fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; + fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) + .await?; + this.update(&mut cx, |this, _| this.path = Some(path)); + } + + Ok(()) + }); + } +} + +struct PendingCompletion { + id: usize, + _task: Task<()>, +} + +enum ConversationEditorEvent { + TabContentChanged, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +struct ScrollPosition { + offset_before_cursor: gpui::Point, + cursor: Anchor, +} + +struct ConversationEditor { + conversation: Model, + fs: Arc, + workspace: WeakView, + editor: View, + blocks: HashSet, + scroll_position: Option, + _subscriptions: Vec, +} + +impl ConversationEditor { + fn new( + completion_provider: Arc, + language_registry: Arc, + fs: Arc, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let conversation = + cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + Self::for_conversation(conversation, fs, workspace, cx) + } + + fn for_conversation( + conversation: Model, + fs: Arc, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_show_gutter(false, cx); + editor.set_show_wrap_guides(false, cx); + editor + }); + + let _subscriptions = vec![ + cx.observe(&conversation, |_, _, cx| cx.notify()), + cx.subscribe(&conversation, Self::handle_conversation_event), + cx.subscribe(&editor, Self::handle_editor_event), + ]; + + let mut this = Self { + conversation, + editor, + blocks: Default::default(), + scroll_position: None, + fs, + workspace, + _subscriptions, + }; + this.update_message_headers(cx); + this + } + + fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + report_assistant_event( + self.workspace.clone(), + self.conversation.read(cx).id.clone(), + AssistantKind::Panel, + cx, + ); + + let cursors = self.cursors(cx); + + let user_messages = self.conversation.update(cx, |conversation, cx| { + let selected_messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.assist(selected_messages, cx) + }); + let new_selections = user_messages + .iter() + .map(|message| { + let cursor = message + .start + .to_offset(self.conversation.read(cx).buffer.read(cx)); + cursor..cursor + }) + .collect::>(); + if !new_selections.is_empty() { + self.editor.update(cx, |editor, cx| { + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + cx, + |selections| selections.select_ranges(new_selections), + ); + }); + // Avoid scrolling to the new cursor position so the assistant's output is stable. + cx.defer(|this, _| this.scroll_position = None); + } + } + + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if !self + .conversation + .update(cx, |conversation, _| conversation.cancel_last_assist()) + { + cx.propagate_action(); + } + } + + fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { + let cursors = self.cursors(cx); + self.conversation.update(cx, |conversation, cx| { + let messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.cycle_message_roles(messages, cx) + }); + } + + fn cursors(&self, cx: &AppContext) -> Vec { + let selections = self.editor.read(cx).selections.all::(cx); + selections + .into_iter() + .map(|selection| selection.head()) + .collect() + } + + fn handle_conversation_event( + &mut self, + _: Model, + event: &ConversationEvent, + cx: &mut ViewContext, + ) { + match event { + ConversationEvent::MessagesEdited => { + self.update_message_headers(cx); + self.conversation.update(cx, |conversation, cx| { + conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + }); + } + ConversationEvent::SummaryChanged => { + cx.emit(ConversationEditorEvent::TabContentChanged); + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx); + }); + } + ConversationEvent::StreamedCompletion => { + self.editor.update(cx, |editor, cx| { + if let Some(scroll_position) = self.scroll_position { + let snapshot = editor.snapshot(cx); + let cursor_point = scroll_position.cursor.to_display_point(&snapshot); + let scroll_top = + cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; + editor.set_scroll_position( + point(scroll_position.offset_before_cursor.x, scroll_top), + cx, + ); + } + }); + } + } + } + + fn handle_editor_event( + &mut self, + _: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + match event { + EditorEvent::ScrollPositionChanged { autoscroll, .. } => { + let cursor_scroll_position = self.cursor_scroll_position(cx); + if *autoscroll { + self.scroll_position = cursor_scroll_position; + } else if self.scroll_position != cursor_scroll_position { + self.scroll_position = None; + } + } + EditorEvent::SelectionsChanged { .. } => { + self.scroll_position = self.cursor_scroll_position(cx); + } + _ => {} + } + } + + fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let cursor = editor.selections.newest_anchor().head(); + let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32; + let scroll_position = editor + .scroll_manager + .anchor() + .scroll_position(&snapshot.display_snapshot); + + let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + Some(ScrollPosition { + cursor, + offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), + }) + } else { + None + } + }) + } + + fn update_message_headers(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let excerpt_id = *buffer.as_singleton().unwrap().0; + let old_blocks = std::mem::take(&mut self.blocks); + let new_blocks = self + .conversation + .read(cx) + .messages(cx) + .map(|message| BlockProperties { + position: buffer.anchor_in_excerpt(excerpt_id, message.anchor), + height: 2, + style: BlockStyle::Sticky, + render: Arc::new({ + let conversation = self.conversation.clone(); + // let metadata = message.metadata.clone(); + // let message = message.clone(); + move |cx| { + enum Sender {} + enum ErrorTooltip {} + + let message_id = message.id; + let sender = MouseEventHandler::new::( + message_id.0, + cx, + |state, _| match message.role { + Role::User => { + let style = style.user_sender.style_for(state); + Label::new("You", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::Assistant => { + let style = style.assistant_sender.style_for(state); + Label::new("Assistant", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::System => { + let style = style.system_sender.style_for(state); + Label::new("System", style.text.clone()) + .contained() + .with_style(style.container) + } + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, { + let conversation = conversation.clone(); + move |_, _, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) + } + }); + + Flex::row() + .with_child(sender.aligned()) + .with_child( + Label::new( + message.sent_at.format("%I:%M%P").to_string(), + style.sent_at.text.clone(), + ) + .contained() + .with_style(style.sent_at.container) + .aligned(), + ) + .with_children( + if let MessageStatus::Error(error) = &message.status { + Some( + Svg::new("icons/error.svg") + .with_color(style.error_icon.color) + .constrained() + .with_width(style.error_icon.width) + .contained() + .with_style(style.error_icon.container) + .with_tooltip::( + message_id.0, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }, + ) + .aligned() + .left() + .contained() + .with_style(style.message_header) + .into_any() + } + }), + disposition: BlockDisposition::Above, + }) + .collect::>(); + + editor.remove_blocks(old_blocks, None, cx); + let ids = editor.insert_blocks(new_blocks, None, cx); + self.blocks = HashSet::from_iter(ids); + }); + } + + fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { + return; + }; + + let text = editor.read_with(cx, |editor, cx| { + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + + let selected_text = buffer.text_for_range(range).collect::(); + if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + format!("```{language_name}\n{selected_text}\n```") + }) + } + }); + + // Activate the panel + if !panel.read(cx).has_focus(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(text) = text { + panel.update(cx, |panel, cx| { + let conversation = panel + .active_editor() + .cloned() + .unwrap_or_else(|| panel.new_conversation(cx)); + conversation.update(cx, |conversation, cx| { + conversation + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + }); + } + } + + fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { + let editor = self.editor.read(cx); + let conversation = self.conversation.read(cx); + if editor.selections.count() == 1 { + let selection = editor.selections.newest::(cx); + let mut copied_text = String::new(); + let mut spanned_messages = 0; + for message in conversation.messages(cx) { + if message.offset_range.start >= selection.range().end { + break; + } else if message.offset_range.end >= selection.range().start { + let range = cmp::max(message.offset_range.start, selection.range().start) + ..cmp::min(message.offset_range.end, selection.range().end); + if !range.is_empty() { + spanned_messages += 1; + write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); + for chunk in conversation.buffer.read(cx).text_for_range(range) { + copied_text.push_str(&chunk); + } + copied_text.push('\n'); + } + } + } + + if spanned_messages > 1 { + cx.platform() + .write_to_clipboard(ClipboardItem::new(copied_text)); + return; + } + } + + cx.propagate_action(); + } + + fn split(&mut self, _: &Split, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + let selections = self.editor.read(cx).selections.disjoint_anchors(); + for selection in selections.into_iter() { + let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let range = selection + .map(|endpoint| endpoint.to_offset(&buffer)) + .range(); + conversation.split_message(range, cx); + } + }); + } + + fn save(&mut self, _: &Save, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx) + }); + } + + fn cycle_model(&mut self, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + let new_model = conversation.model.cycle(); + conversation.set_model(new_model, cx); + }); + } + + fn title(&self, cx: &AppContext) -> String { + self.conversation + .read(cx) + .summary + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_else(|| "New Conversation".into()) + } + + fn render_current_model( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> impl Element { + enum Model {} + + MouseEventHandler::new::(0, cx, |state, cx| { + let style = style.model.style_for(state); + let model_display_name = self.conversation.read(cx).model.short_name(); + Label::new(model_display_name, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + } + + fn render_remaining_tokens( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> Option> { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_style = if remaining_tokens <= 0 { + &style.no_remaining_tokens + } else if remaining_tokens <= 500 { + &style.low_remaining_tokens + } else { + &style.remaining_tokens + }; + Some( + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container), + ) + } +} + +impl EventEmitter for ConversationEditor {} + +impl View for ConversationEditor { + fn ui_name() -> &'static str { + "ConversationEditor" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).assistant; + Stack::new() + .with_child( + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container), + ) + .with_child( + Flex::row() + .with_child(self.render_current_model(theme, cx)) + .with_children(self.render_remaining_tokens(theme, cx)) + .aligned() + .top() + .right(), + ) + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } +} + +#[derive(Clone, Debug)] +struct MessageAnchor { + id: MessageId, + start: language::Anchor, +} + +#[derive(Clone, Debug)] +pub struct Message { + offset_range: Range, + index_range: Range, + id: MessageId, + anchor: language::Anchor, + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +impl Message { + fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { + let content = buffer + .text_for_range(self.offset_range.clone()) + .collect::(); + RequestMessage { + role: self.role, + content: content.trim_end().into(), + } + } +} + +enum InlineAssistantEvent { + Confirmed { + prompt: String, + include_conversation: bool, + retrieve_context: bool, + }, + Canceled, + Dismissed, + IncludeConversationToggled { + include_conversation: bool, + }, + RetrieveContextToggled { + retrieve_context: bool, + }, +} + +struct InlineAssistant { + id: usize, + prompt_editor: View, + workspace: WeakView, + confirmed: bool, + has_focus: bool, + include_conversation: bool, + measurements: Rc>, + prompt_history: VecDeque, + prompt_history_ix: Option, + pending_prompt: String, + codegen: Model, + _subscriptions: Vec, + retrieve_context: bool, + semantic_index: Option>, + semantic_permissioned: Option, + project: WeakModel, + maintain_rate_limit: Option>, +} + +impl Entity for InlineAssistant { + type Event = InlineAssistantEvent; +} + +impl View for InlineAssistant { + fn ui_name() -> &'static str { + "InlineAssistant" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum ErrorIcon {} + let theme = theme::current(cx); + + Flex::row() + .with_children([Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if SemanticIndex::enabled(cx) { + Some( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) + .element() + .aligned(), + ) + } else { + None + }) + .with_children(if let Some(error) = self.codegen.read(cx).error() { + Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), + } + } + })]) + .with_child(Empty::new().constrained().dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.min.y(), + ), + max: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.max.y(), + ), + } + } + })) + .with_child( + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .with_children(if self.retrieve_context { + Some( + Flex::row() + .with_children(self.retrieve_context_status(cx)) + .flex(1., true) + .aligned(), + ) + } else { + None + }) + .contained() + .with_style(theme.assistant.inline.container) + .into_any() + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.focus(&self.prompt_editor); + self.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl InlineAssistant { + fn new( + id: usize, + measurements: Rc>, + include_conversation: bool, + prompt_history: VecDeque, + codegen: Model, + workspace: WeakView, + cx: &mut ViewContext, + retrieve_context: bool, + semantic_index: Option>, + project: Model, + ) -> Self { + let prompt_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + let placeholder = match codegen.read(cx).kind() { + CodegenKind::Transform { .. } => "Enter transformation prompt…", + CodegenKind::Generate { .. } => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor + }); + let mut subscriptions = vec![ + cx.observe(&codegen, Self::handle_codegen_changed), + cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + ]; + + if let Some(semantic_index) = semantic_index.clone() { + subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); + } + + let assistant = Self { + id, + prompt_editor, + workspace, + confirmed: false, + has_focus: false, + include_conversation, + measurements, + prompt_history, + prompt_history_ix: None, + pending_prompt: String::new(), + codegen, + _subscriptions: subscriptions, + retrieve_context, + semantic_permissioned: None, + semantic_index, + project: project.downgrade(), + maintain_rate_limit: None, + }; + + assistant.index_project(cx).log_err(); + + assistant + } + + fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + let Some(project) = self.project.upgrade() else { + return Task::ready(Err(anyhow!("project was dropped"))); + }; + + self.semantic_index + .as_ref() + .map(|semantic| { + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } + + fn handle_prompt_editor_events( + &mut self, + _: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + if let EditorEvent::Edited = event { + self.pending_prompt = self.prompt_editor.read(cx).text(cx); + cx.notify(); + } + } + + fn semantic_index_changed( + &mut self, + semantic_index: Model, + cx: &mut ViewContext, + ) { + let Some(project) = self.project.upgrade() else { + return; + }; + + let status = semantic_index.read(cx).status(&project); + match status { + SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } => { + if self.maintain_rate_limit.is_none() { + self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background_executor().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + } + return; + } + _ => { + self.maintain_rate_limit = None; + } + } + } + + fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { + let is_read_only = !self.codegen.read(cx).idle(); + self.prompt_editor.update(cx, |editor, cx| { + let was_read_only = editor.read_only(); + if was_read_only != is_read_only { + if is_read_only { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + } else { + self.confirmed = false; + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + } + } + }); + cx.notify(); + } + + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(InlineAssistantEvent::Canceled); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if self.confirmed { + cx.emit(InlineAssistantEvent::Dismissed); + } else { + report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); + + let prompt = self.prompt_editor.read(cx).text(cx); + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + }); + cx.emit(InlineAssistantEvent::Confirmed { + prompt, + include_conversation: self.include_conversation, + retrieve_context: self.retrieve_context, + }); + self.confirmed = true; + cx.notify(); + } + } + + fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { + let semantic_permissioned = self.semantic_permissioned(cx); + + let Some(project) = self.project.upgrade() else { + return; + }; + + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + + cx.spawn(|this, mut cx| async move { + // If Necessary prompt user + if !semantic_permissioned.await.unwrap_or(false) { + let mut answer = this.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + return anyhow::Ok(()); + } + } + + // If permissioned, update context appropriately + this.update(&mut cx, |this, cx| { + this.retrieve_context = !this.retrieve_context; + + cx.emit(InlineAssistantEvent::RetrieveContextToggled { + retrieve_context: this.retrieve_context, + }); + + if this.retrieve_context { + this.index_project(cx).log_err(); + } + + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { + let Some(project) = self.project.upgrade() else { + return Err(anyhow!("project was dropped!")); + }; + + let semantic_permissioned = self.semantic_permissioned(cx); + if let Some(semantic_index) = SemanticIndex::global(cx) { + cx.spawn(|_, mut cx| async move { + // This has to be updated to accomodate for semantic_permissions + if semantic_permissioned.await.unwrap_or(false) { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx)) + .await + } else { + Err(anyhow!("project is not permissioned for semantic indexing")) + } + }) + .detach_and_log_err(cx); + } + + anyhow::Ok(()) + } + + fn retrieve_context_status( + &self, + cx: &mut ViewContext, + ) -> Option> { + enum ContextStatusIcon {} + + let Some(project) = self.project.upgrade() else { + return None; + }; + + if let Some(semantic_index) = SemanticIndex::global(cx) { + let status = semantic_index.update(cx, |index, _| index.status(&project)); + let theme = theme::current(cx); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::NotIndexed {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.inline.context_status.error_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.error_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.error_icon.container) + .with_tooltip::( + self.id, + "Not Indexed", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + 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) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); + } + } + Some( + Svg::new("icons/update.svg") + .with_color(theme.assistant.inline.context_status.in_progress_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.in_progress_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.in_progress_icon.container) + .with_tooltip::( + self.id, + status_text, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ) + } + SemanticIndexStatus::Indexed {} => Some( + Svg::new("icons/check.svg") + .with_color(theme.assistant.inline.context_status.complete_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.complete_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.complete_icon.container) + .with_tooltip::( + self.id, + "Index up to date", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + } + } else { + None + } + } + + // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + // let project = self.project.clone(); + // if let Some(semantic_index) = self.semantic_index.clone() { + // let status = semantic_index.update(cx, |index, cx| index.status(&project)); + // return match status { + // // This theoretically shouldnt be a valid code path + // // As the inline assistant cant be launched without an API key + // // We keep it here for safety + // semantic_index::SemanticIndexStatus::NotAuthenticated => { + // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexed => { + // "Indexing Complete!".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + + // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); + + // 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) { + // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); + // } + // } + // status + // } + // semantic_index::SemanticIndexStatus::NotIndexed => { + // "Not Indexed for Context Retrieval".to_string() + // } + // }; + // } + + // "".to_string() + // } + + fn toggle_include_conversation( + &mut self, + _: &ToggleIncludeConversation, + cx: &mut ViewContext, + ) { + self.include_conversation = !self.include_conversation; + cx.emit(InlineAssistantEvent::IncludeConversationToggled { + include_conversation: self.include_conversation, + }); + cx.notify(); + } + + fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix > 0 { + self.prompt_history_ix = Some(ix - 1); + let prompt = self.prompt_history[ix - 1].clone(); + self.set_prompt(&prompt, cx); + } + } else if !self.prompt_history.is_empty() { + self.prompt_history_ix = Some(self.prompt_history.len() - 1); + let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); + self.set_prompt(&prompt, cx); + } + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix < self.prompt_history.len() - 1 { + self.prompt_history_ix = Some(ix + 1); + let prompt = self.prompt_history[ix + 1].clone(); + self.set_prompt(&prompt, cx); + } else { + self.prompt_history_ix = None; + let pending_prompt = self.pending_prompt.clone(); + self.set_prompt(&pending_prompt, cx); + } + } + } + + fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { + self.prompt_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let len = buffer.len(cx); + buffer.edit([(0..len, prompt)], None, cx); + }); + }); + } +} + +// This wouldn't need to exist if we could pass parameters when rendering child views. +#[derive(Copy, Clone, Default)] +struct BlockMeasurements { + anchor_x: f32, + gutter_width: f32, +} + +struct PendingInlineAssist { + editor: WeakView, + inline_assistant: Option<(BlockId, View)>, + codegen: Model, + _subscriptions: Vec, + project: WeakModel, +} + +fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { + ranges.sort_unstable_by(|a, b| { + a.start + .cmp(&b.start, buffer) + .then_with(|| b.end.cmp(&a.end, buffer)) + }); + + let mut ix = 0; + while ix + 1 < ranges.len() { + let b = ranges[ix + 1].clone(); + let a = &mut ranges[ix]; + if a.end.cmp(&b.start, buffer).is_gt() { + if a.end.cmp(&b.end, buffer).is_lt() { + a.end = b.end; + } + ranges.remove(ix + 1); + } else { + ix += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MessageId; + use ai::test::FakeCompletionProvider; + use gpui::AppContext; + + #[gpui::test] + fn test_inserting_and_removing_messages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..1), + (message_2.id, Role::Assistant, 1..1) + ] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..3) + ] + ); + + let message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_3.id, Role::User, 4..4) + ] + ); + + let message_4 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..5), + (message_3.id, Role::User, 5..5), + ] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..6), + (message_3.id, Role::User, 6..7), + ] + ); + + // Deleting across message boundaries merges the messages. + buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_3.id, Role::User, 3..4), + ] + ); + + // Undoing the deletion should also undo the merge. + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..6), + (message_3.id, Role::User, 6..7), + ] + ); + + // Redoing the deletion should also redo the merge. + buffer.update(cx, |buffer, cx| buffer.redo(cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_3.id, Role::User, 3..4), + ] + ); + + // Ensure we can still insert after a merged message. + let message_5 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_5.id, Role::System, 3..4), + (message_3.id, Role::User, 4..5) + ] + ); + } + + #[gpui::test] + fn test_message_splitting(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) + }); + + let (_, message_2) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_2 = message_2.unwrap(); + + // We recycle newlines in the middle of a split message + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..16), + ] + ); + + let (_, message_3) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_3 = message_3.unwrap(); + + // We don't recycle newlines at the end of a split message + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..17), + ] + ); + + let (_, message_4) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_4 = message_4.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..17), + ] + ); + + let (_, message_5) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_5 = message_5.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..18), + ] + ); + + let (message_6, message_7) = conversation.update(cx, |conversation, cx| { + conversation.split_message(14..16, cx) + }); + let message_6 = message_6.unwrap(); + let message_7 = message_7.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..14), + (message_6.id, Role::User, 14..17), + (message_7.id, Role::User, 17..19), + ] + ); + } + + #[gpui::test] + fn test_messages_for_offsets(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); + let message_2 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); + + let message_3 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); + + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..11) + ] + ); + + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 9], cx), + [message_1.id, message_2.id, message_3.id] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 1, 11], cx), + [message_1.id, message_3.id] + ); + + let message_4 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..12), + (message_4.id, Role::User, 12..12) + ] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx), + [message_1.id, message_2.id, message_3.id, message_4.id] + ); + + fn message_ids_for_offsets( + conversation: &Model, + offsets: &[usize], + cx: &AppContext, + ) -> Vec { + conversation + .read(cx) + .messages_for_offsets(offsets.iter().copied(), cx) + .into_iter() + .map(|message| message.id) + .collect() + } + } + + #[gpui::test] + fn test_serialization(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = + cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + let message_0 = conversation.read(cx).message_anchors[0].id; + let message_1 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) + .unwrap() + }); + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); + buffer.finalize_last_transaction(); + }); + let _message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + + let deserialized_conversation = cx.add_model(|cx| { + Conversation::deserialize( + conversation.read(cx).serialize(cx), + Default::default(), + registry.clone(), + cx, + ) + }); + let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone(); + assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&deserialized_conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + } + + fn messages( + conversation: &Model, + cx: &AppContext, + ) -> Vec<(MessageId, Role, Range)> { + conversation + .read(cx) + .messages(cx) + .map(|message| (message.id, message.role, message.offset_range)) + .collect() + } +} + +fn report_assistant_event( + workspace: WeakView, + conversation_id: Option, + assistant_kind: AssistantKind, + cx: &AppContext, +) { + let Some(workspace) = workspace.upgrade() else { + return; + }; + + let client = workspace.read(cx).project().read(cx).client(); + let telemetry = client.telemetry(); + + let model = AssistantSettings::get_global(cx) + .default_open_ai_model + .clone(); + + let telemetry_settings = TelemetrySettings::get_global(cx); + + telemetry.report_assistant_event( + telemetry_settings, + conversation_id, + assistant_kind, + model.full_name(), + ) +} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a727df621bd911ee56b4ba2b3d8b13567b35b63 --- /dev/null +++ b/crates/assistant2/src/assistant_settings.rs @@ -0,0 +1,80 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub enum OpenAIModel { + #[serde(rename = "gpt-3.5-turbo-0613")] + ThreePointFiveTurbo, + #[serde(rename = "gpt-4-0613")] + Four, + #[serde(rename = "gpt-4-1106-preview")] + FourTurbo, +} + +impl OpenAIModel { + pub fn full_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", + OpenAIModel::Four => "gpt-4-0613", + OpenAIModel::FourTurbo => "gpt-4-1106-preview", + } + } + + pub fn short_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", + OpenAIModel::Four => "gpt-4", + OpenAIModel::FourTurbo => "gpt-4-turbo", + } + } + + pub fn cycle(&self) -> Self { + match self { + OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, + OpenAIModel::Four => OpenAIModel::FourTurbo, + OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AssistantDockPosition { + Left, + Right, + Bottom, +} + +#[derive(Deserialize, Debug)] +pub struct AssistantSettings { + pub button: bool, + pub dock: AssistantDockPosition, + pub default_width: f32, + pub default_height: f32, + pub default_open_ai_model: OpenAIModel, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AssistantSettingsContent { + pub button: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub default_open_ai_model: Option, +} + +impl Settings for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs new file mode 100644 index 0000000000000000000000000000000000000000..9696c629ac910010d193057adcdb1db3f2070935 --- /dev/null +++ b/crates/assistant2/src/codegen.rs @@ -0,0 +1,695 @@ +use crate::streaming_diff::{Hunk, StreamingDiff}; +use ai::completion::{CompletionProvider, CompletionRequest}; +use anyhow::Result; +use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use gpui::{EventEmitter, Model, ModelContext, Task}; +use language::{Rope, TransactionId}; +use multi_buffer; +use std::{cmp, future, ops::Range, sync::Arc}; + +pub enum Event { + Finished, + Undone, +} + +#[derive(Clone)] +pub enum CodegenKind { + Transform { range: Range }, + Generate { position: Anchor }, +} + +pub struct Codegen { + provider: Arc, + buffer: Model, + snapshot: MultiBufferSnapshot, + kind: CodegenKind, + last_equal_ranges: Vec>, + transaction_id: Option, + error: Option, + generation: Task<()>, + idle: bool, + _subscription: gpui::Subscription, +} + +impl EventEmitter for Codegen {} + +impl Codegen { + pub fn new( + buffer: Model, + kind: CodegenKind, + provider: Arc, + cx: &mut ModelContext, + ) -> Self { + let snapshot = buffer.read(cx).snapshot(cx); + Self { + provider, + buffer: buffer.clone(), + snapshot, + kind, + last_equal_ranges: Default::default(), + transaction_id: Default::default(), + error: Default::default(), + idle: true, + generation: Task::ready(()), + _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), + } + } + + fn handle_buffer_event( + &mut self, + _buffer: Model, + event: &multi_buffer::Event, + cx: &mut ModelContext, + ) { + if let multi_buffer::Event::TransactionUndone { transaction_id } = event { + if self.transaction_id == Some(*transaction_id) { + self.transaction_id = None; + self.generation = Task::ready(()); + cx.emit(Event::Undone); + } + } + } + + pub fn range(&self) -> Range { + match &self.kind { + CodegenKind::Transform { range } => range.clone(), + CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, + } + } + + pub fn kind(&self) -> &CodegenKind { + &self.kind + } + + pub fn last_equal_ranges(&self) -> &[Range] { + &self.last_equal_ranges + } + + pub fn idle(&self) -> bool { + self.idle + } + + pub fn error(&self) -> Option<&anyhow::Error> { + self.error.as_ref() + } + + pub fn start(&mut self, prompt: Box, cx: &mut ModelContext) { + let range = self.range(); + let snapshot = self.snapshot.clone(); + let selected_text = snapshot + .text_for_range(range.start..range.end) + .collect::(); + + let selection_start = range.start.to_point(&snapshot); + let suggested_line_indent = snapshot + .suggested_indents(selection_start.row..selection_start.row + 1, cx) + .into_values() + .next() + .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); + + let response = self.provider.complete(prompt); + self.generation = cx.spawn_weak(|this, mut cx| { + async move { + let generate = async { + let mut edit_start = range.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let chunks = strip_invalid_spans_from_codeblock(response.await?); + futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let mut new_text = String::new(); + let mut base_indent = None; + let mut line_indent = None; + let mut first_line = true; + + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + + let mut lines = chunk.split('\n').peekable(); + while let Some(line) = lines.next() { + new_text.push_str(line); + if line_indent.is_none() { + if let Some(non_whitespace_ch_ix) = + new_text.find(|ch: char| !ch.is_whitespace()) + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); + } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); + } + } + + if line_indent.is_some() { + hunks_tx.send(diff.push_new(&new_text)).await?; + new_text.clear(); + } + + if lines.peek().is_some() { + hunks_tx.send(diff.push_new("\n")).await?; + line_indent = None; + first_line = false; + } + } + } + hunks_tx.send(diff.push_new(&new_text)).await?; + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + + let transaction = this.buffer.update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + this.last_equal_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = this.transaction_id { + // Group all assistant edits into the first transaction. + this.buffer.update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + this.transaction_id = Some(transaction); + this.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + + cx.notify(); + }); + } + + diff.await?; + anyhow::Ok(()) + }; + + let result = generate.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }); + } + } + }); + self.error.take(); + self.idle = false; + cx.notify(); + } + + pub fn undo(&mut self, cx: &mut ModelContext) { + if let Some(transaction_id) = self.transaction_id { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } + } +} + +fn strip_invalid_spans_from_codeblock( + stream: impl Stream>, +) -> impl Stream> { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_markdown_codeblock = false; + let mut includes_start_or_end_span = false; + stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; + buffer.push_str(&chunk); + + if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") { + includes_start_or_end_span = true; + + buffer = buffer + .strip_prefix("<|S|>") + .or_else(|| buffer.strip_prefix("<|S|")) + .unwrap_or(&buffer) + .to_string(); + } else if buffer.ends_with("|E|>") { + includes_start_or_end_span = true; + } else if buffer.starts_with("<|") + || buffer.starts_with("<|S") + || buffer.starts_with("<|S|") + || buffer.ends_with("|") + || buffer.ends_with("|E") + || buffer.ends_with("|E|") + { + return future::ready(None); + } + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return future::ready(None); + } else if buffer.starts_with("```") { + starts_with_markdown_codeblock = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return future::ready(None); + } + } + } + + let mut text = buffer.to_string(); + if starts_with_markdown_codeblock { + text = text + .strip_suffix("\n```\n") + .or_else(|| text.strip_suffix("\n```")) + .or_else(|| text.strip_suffix("\n``")) + .or_else(|| text.strip_suffix("\n`")) + .or_else(|| text.strip_suffix('\n')) + .unwrap_or(&text) + .to_string(); + } + + if includes_start_or_end_span { + text = text + .strip_suffix("|E|>") + .or_else(|| text.strip_suffix("E|>")) + .or_else(|| text.strip_prefix("|>")) + .or_else(|| text.strip_prefix(">")) + .unwrap_or(&text) + .to_string(); + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(Ok(buffer.clone())) + }; + + buffer = remainder; + future::ready(result) + }) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use ai::test::FakeCompletionProvider; + use futures::stream::{self}; + use gpui::TestAppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use rand::prelude::*; + use serde::Serialize; + use settings::SettingsStore; + + #[derive(Serialize)] + pub struct DummyCompletionRequest { + pub name: String, + } + + impl CompletionRequest for DummyCompletionRequest { + fn data(&self) -> serde_json::Result { + serde_json::to_string(self) + } + } + + #[gpui::test(iterations = 10)] + async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + let x = 0; + for _ in 0..10 { + x += 1; + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let range = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Transform { range }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + " let mut x = 0;\n", + " while x < 10 {\n", + " x += 1;\n", + " }", + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + println!("CHUNK: {:?}", &chunk); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_past_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + le + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 6)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + "t mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_before_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = concat!( + "fn main() {\n", + " \n", + "}\n" // + ); + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 2)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + "let mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + println!("{:?}", &chunk); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test] + async fn test_strip_invalid_spans_from_codeblock() { + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks( + "```html\n```js\nLorem ipsum dolor\n```\n```", + 2 + )) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + fn chunks(text: &str, size: usize) -> impl Stream> { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| Ok(chunk.iter().collect::())) + .collect::>(), + ) + } + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + } +} diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs new file mode 100644 index 0000000000000000000000000000000000000000..b678c6fe3b402129692b02f0094841cafe92ffca --- /dev/null +++ b/crates/assistant2/src/prompts.rs @@ -0,0 +1,388 @@ +use ai::models::LanguageModel; +use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate}; +use ai::prompts::file_context::FileContext; +use ai::prompts::generate::GenerateInlineContent; +use ai::prompts::preamble::EngineerPreamble; +use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext}; +use ai::providers::open_ai::OpenAILanguageModel; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp::{self, Reverse}; +use std::ops::Range; +use std::sync::Arc; + +#[allow(dead_code)] +fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { + #[derive(Debug)] + struct Match { + collapse: Range, + keep: Vec>, + } + + let selected_range = selected_range.to_offset(buffer); + let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { + Some(&grammar.embedding_config.as_ref()?.query) + }); + let configs = ts_matches + .grammars() + .iter() + .map(|g| g.embedding_config.as_ref().unwrap()) + .collect::>(); + let mut matches = Vec::new(); + while let Some(mat) = ts_matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) + } else { + None + } + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); + } else { + continue; + } + } + ts_matches.advance(); + matches.push(Match { collapse, keep }); + } else { + ts_matches.advance(); + } + } + matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); + let mut matches = matches.into_iter().peekable(); + + let mut summary = String::new(); + let mut offset = 0; + let mut flushed_selection = false; + while let Some(mat) = matches.next() { + // Keep extending the collapsed range if the next match surrounds + // the current one. + while let Some(next_mat) = matches.peek() { + if mat.collapse.start <= next_mat.collapse.start + && mat.collapse.end >= next_mat.collapse.end + { + matches.next().unwrap(); + } else { + break; + } + } + + if offset > mat.collapse.start { + // Skip collapsed nodes that have already been summarized. + offset = cmp::max(offset, mat.collapse.end); + continue; + } + + if offset <= selected_range.start && selected_range.start <= mat.collapse.end { + if !flushed_selection { + // The collapsed node ends after the selection starts, so we'll flush the selection first. + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|S|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|E|>"); + } + offset = selected_range.end; + flushed_selection = true; + } + + // If the selection intersects the collapsed node, we won't collapse it. + if selected_range.end >= mat.collapse.start { + continue; + } + } + + summary.extend(buffer.text_for_range(offset..mat.collapse.start)); + for keep in mat.keep { + summary.extend(buffer.text_for_range(keep)); + } + offset = mat.collapse.end; + } + + // Flush selection if we haven't already done so. + if !flushed_selection && offset <= selected_range.start { + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|S|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|E|>"); + } + offset = selected_range.end; + } + + summary.extend(buffer.text_for_range(offset..buffer.len())); + summary +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: BufferSnapshot, + range: Range, + search_results: Vec, + model: &str, + project_name: Option, +) -> anyhow::Result { + // Using new Prompt Templates + let openai_model: Arc = Arc::new(OpenAILanguageModel::load(model)); + let lang_name = if let Some(language_name) = language_name { + Some(language_name.to_string()) + } else { + None + }; + + let args = PromptArguments { + model: openai_model, + language_name: lang_name.clone(), + project_name, + snippets: search_results.clone(), + reserved_tokens: 1000, + buffer: Some(buffer), + selected_range: Some(range), + user_prompt: Some(user_prompt.clone()), + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::Mandatory, Box::new(EngineerPreamble {})), + ( + PromptPriority::Ordered { order: 1 }, + Box::new(RepositoryContext {}), + ), + ( + PromptPriority::Ordered { order: 0 }, + Box::new(FileContext {}), + ), + ( + PromptPriority::Mandatory, + Box::new(GenerateInlineContent {}), + ), + ]; + let chain = PromptChain::new(args, templates); + let (prompt, _) = chain.generate(true)?; + + anyhow::Ok(prompt) +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_embedding_query( + r#" + ( + [(line_comment) (attribute_item)]* @context + . + [ + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + assert_eq!( + summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), + indoc! {" + struct X { + <|S|>a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let <|S|a |E|>= 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + <|S|> + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + <|S|>"} + ); + + // Ensure nested functions get collapsed properly. + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + let a = 30; + fn nested() -> usize { + 3 + } + self.a + nested() + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + assert_eq!( + summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), + indoc! {" + <|S|>struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + } +} diff --git a/crates/assistant2/src/streaming_diff.rs b/crates/assistant2/src/streaming_diff.rs new file mode 100644 index 0000000000000000000000000000000000000000..7399a7b4faf2629310bbf9e7ec573a651e52feaf --- /dev/null +++ b/crates/assistant2/src/streaming_diff.rs @@ -0,0 +1,293 @@ +use collections::HashMap; +use ordered_float::OrderedFloat; +use std::{ + cmp, + fmt::{self, Debug}, + ops::Range, +}; + +struct Matrix { + cells: Vec, + rows: usize, + cols: usize, +} + +impl Matrix { + fn new() -> Self { + Self { + cells: Vec::new(), + rows: 0, + cols: 0, + } + } + + fn resize(&mut self, rows: usize, cols: usize) { + self.cells.resize(rows * cols, 0.); + self.rows = rows; + self.cols = cols; + } + + fn get(&self, row: usize, col: usize) -> f64 { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + self.cells[col * self.rows + row] + } + + fn set(&mut self, row: usize, col: usize, value: f64) { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + + self.cells[col * self.rows + row] = value; + } +} + +impl Debug for Matrix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + for i in 0..self.rows { + for j in 0..self.cols { + write!(f, "{:5}", self.get(i, j))?; + } + writeln!(f)?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum Hunk { + Insert { text: String }, + Remove { len: usize }, + Keep { len: usize }, +} + +pub struct StreamingDiff { + old: Vec, + new: Vec, + scores: Matrix, + old_text_ix: usize, + new_text_ix: usize, + equal_runs: HashMap<(usize, usize), u32>, +} + +impl StreamingDiff { + const INSERTION_SCORE: f64 = -1.; + const DELETION_SCORE: f64 = -20.; + const EQUALITY_BASE: f64 = 1.8; + const MAX_EQUALITY_EXPONENT: i32 = 16; + + pub fn new(old: String) -> Self { + let old = old.chars().collect::>(); + let mut scores = Matrix::new(); + scores.resize(old.len() + 1, 1); + for i in 0..=old.len() { + scores.set(i, 0, i as f64 * Self::DELETION_SCORE); + } + Self { + old, + new: Vec::new(), + scores, + old_text_ix: 0, + new_text_ix: 0, + equal_runs: Default::default(), + } + } + + pub fn push_new(&mut self, text: &str) -> Vec { + self.new.extend(text.chars()); + self.scores.resize(self.old.len() + 1, self.new.len() + 1); + + for j in self.new_text_ix + 1..=self.new.len() { + self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE); + for i in 1..=self.old.len() { + let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE; + let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE; + let equality_score = if self.old[i - 1] == self.new[j - 1] { + let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); + equal_run += 1; + self.equal_runs.insert((i, j), equal_run); + + let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) + } else { + f64::NEG_INFINITY + }; + + let score = insertion_score.max(deletion_score).max(equality_score); + self.scores.set(i, j, score); + } + } + + let mut max_score = f64::NEG_INFINITY; + let mut next_old_text_ix = self.old_text_ix; + let next_new_text_ix = self.new.len(); + for i in self.old_text_ix..=self.old.len() { + let score = self.scores.get(i, next_new_text_ix); + if score > max_score { + max_score = score; + next_old_text_ix = i; + } + } + + let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); + self.old_text_ix = next_old_text_ix; + self.new_text_ix = next_new_text_ix; + hunks + } + + fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { + let mut pending_insert: Option> = None; + let mut hunks = Vec::new(); + let mut i = old_text_ix; + let mut j = new_text_ix; + while (i, j) != (self.old_text_ix, self.new_text_ix) { + let insertion_score = if j > self.new_text_ix { + Some((i, j - 1)) + } else { + None + }; + let deletion_score = if i > self.old_text_ix { + Some((i - 1, j)) + } else { + None + }; + let equality_score = if i > self.old_text_ix && j > self.new_text_ix { + if self.old[i - 1] == self.new[j - 1] { + Some((i - 1, j - 1)) + } else { + None + } + } else { + None + }; + + let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] + .iter() + .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) + .unwrap() + .unwrap(); + + if prev_i == i && prev_j == j - 1 { + if let Some(pending_insert) = pending_insert.as_mut() { + pending_insert.start = prev_j; + } else { + pending_insert = Some(prev_j..j); + } + } else { + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + let char_len = self.old[i - 1].len_utf8(); + if prev_i == i - 1 && prev_j == j { + if let Some(Hunk::Remove { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Remove { len: char_len }) + } + } else { + if let Some(Hunk::Keep { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Keep { len: char_len }) + } + } + } + + i = prev_i; + j = prev_j; + } + + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + hunks.reverse(); + hunks + } + + pub fn finish(self) -> Vec { + self.backtrack(self.old.len(), self.new.len()) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + use rand::prelude::*; + + #[gpui::test(iterations = 100)] + fn test_random_diffs(mut rng: StdRng) { + let old_text_len = env::var("OLD_TEXT_LEN") + .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) + .unwrap_or(10); + let new_text_len = env::var("NEW_TEXT_LEN") + .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) + .unwrap_or(10); + + let old = util::RandomCharIter::new(&mut rng) + .take(old_text_len) + .collect::(); + log::info!("old text: {:?}", old); + + let mut diff = StreamingDiff::new(old.clone()); + let mut hunks = Vec::new(); + let mut new_len = 0; + let mut new = String::new(); + while new_len < new_text_len { + let new_chunk_len = rng.gen_range(1..=new_text_len - new_len); + let new_chunk = util::RandomCharIter::new(&mut rng) + .take(new_len) + .collect::(); + log::info!("new chunk: {:?}", new_chunk); + new_len += new_chunk_len; + new.push_str(&new_chunk); + let new_hunks = diff.push_new(&new_chunk); + log::info!("hunks: {:?}", new_hunks); + hunks.extend(new_hunks); + } + let final_hunks = diff.finish(); + log::info!("final hunks: {:?}", final_hunks); + hunks.extend(final_hunks); + + log::info!("new text: {:?}", new); + let mut old_ix = 0; + let mut new_ix = 0; + let mut patched = String::new(); + for hunk in hunks { + match hunk { + Hunk::Keep { len } => { + assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]); + patched.push_str(&old[old_ix..old_ix + len]); + old_ix += len; + new_ix += len; + } + Hunk::Remove { len } => { + old_ix += len; + } + Hunk::Insert { text } => { + assert_eq!(text, &new[new_ix..new_ix + text.len()]); + patched.push_str(&text); + new_ix += text.len(); + } + } + } + assert_eq!(patched, new); + } +} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c55bfa8cf5a7beec1c516598f242fcfa2fefe018..89b83a7001c5a3e2c649c8e58f30a98a5bd5fafe 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -3344,10 +3344,6 @@ impl Panel for CollabPanel { Box::new(ToggleFocus) } - fn has_focus(&self, cx: &gpui::WindowContext) -> bool { - self.focus_handle.contains_focused(cx) - } - fn persistent_name() -> &'static str { "CollabPanel" } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index ce039071cf830d3282dcc860fc6ce083576aafc1..cb6515f5297a3de20ff16b80a74a50700693438c 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -55,7 +55,6 @@ pub struct ProjectPanel { clipboard_entry: Option, _dragged_entry_destination: Option>, _workspace: WeakView, - has_focus: bool, width: Option, pending_serialization: Task>, } @@ -172,7 +171,6 @@ impl ProjectPanel { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::focus_in).detach(); - cx.on_blur(&focus_handle, Self::focus_out).detach(); cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { @@ -238,7 +236,6 @@ impl ProjectPanel { // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), _dragged_entry_destination: None, _workspace: workspace.weak_handle(), - has_focus: false, width: None, pending_serialization: Task::ready(None), }; @@ -356,16 +353,11 @@ impl ProjectPanel { } fn focus_in(&mut self, cx: &mut ViewContext) { - if !self.has_focus { - self.has_focus = true; + if !self.focus_handle.contains_focused(cx) { cx.emit(Event::Focus); } } - fn focus_out(&mut self, _: &mut ViewContext) { - self.has_focus = false; - } - fn deploy_context_menu( &mut self, position: Point, @@ -1554,10 +1546,6 @@ impl Panel for ProjectPanel { Box::new(ToggleFocus) } - fn has_focus(&self, _: &WindowContext) -> bool { - self.has_focus - } - fn persistent_name() -> &'static str { "Project Panel" } diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index b6582b07b194331f0e33f7e5b6b70557bfa50cbc..caf339a8c65b7f93460bad67c7be5f51a3eeec4f 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -415,10 +415,6 @@ impl Panel for TerminalPanel { } } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus(cx) - } - fn persistent_name() -> &'static str { "TerminalPanel" } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 437e7c01926a460bed320f17b6e943966d714d48..abcf5c49bc929c5954c3e40557e419e4ecaca702 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -26,6 +26,7 @@ pub trait Panel: FocusableView + EventEmitter { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&mut self, size: Option, cx: &mut ViewContext); + // todo!("We should have a icon tooltip method, rather than using persistant_name") fn icon(&self, cx: &WindowContext) -> Option; fn toggle_action(&self) -> Box; fn icon_label(&self, _: &WindowContext) -> Option { @@ -36,7 +37,6 @@ pub trait Panel: FocusableView + EventEmitter { } fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} - fn has_focus(&self, cx: &WindowContext) -> bool; } pub trait PanelHandle: Send + Sync { @@ -53,7 +53,6 @@ pub trait PanelHandle: Send + Sync { fn icon(&self, cx: &WindowContext) -> Option; fn toggle_action(&self, cx: &WindowContext) -> Box; fn icon_label(&self, cx: &WindowContext) -> Option; - fn has_focus(&self, cx: &WindowContext) -> bool; fn focus_handle(&self, cx: &AppContext) -> FocusHandle; fn to_any(&self) -> AnyView; } @@ -114,10 +113,6 @@ where self.read(cx).icon_label(cx) } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.read(cx).has_focus(cx) - } - fn to_any(&self) -> AnyView { self.clone().into() } @@ -319,7 +314,7 @@ impl Dock { } PanelEvent::ZoomIn => { this.set_panel_zoomed(&panel.to_any(), true, cx); - if !panel.has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { cx.focus_view(&panel); } workspace @@ -760,7 +755,7 @@ pub mod test { pub position: DockPosition, pub zoomed: bool, pub active: bool, - pub has_focus: bool, + pub focus_handle: FocusHandle, pub size: f32, } actions!(ToggleTestPanel); @@ -768,12 +763,12 @@ pub mod test { impl EventEmitter for TestPanel {} impl TestPanel { - pub fn new(position: DockPosition) -> Self { + pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self { Self { position, zoomed: false, active: false, - has_focus: false, + focus_handle: cx.focus_handle(), size: 300., } } @@ -832,15 +827,11 @@ pub mod test { fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { self.active = active; } - - fn has_focus(&self, _cx: &WindowContext) -> bool { - self.has_focus - } } impl FocusableView for TestPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - unimplemented!() + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5741fa4a94cf79202f7f9b4a5694c1058be59dd5..a916ec733cd864167f5b0451133d21d879c377e4 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -65,7 +65,7 @@ use std::{ time::Duration, }; use theme::{ActiveTheme, ThemeSettings}; -pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; +pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use util::ResultExt; use uuid::Uuid; @@ -1542,7 +1542,7 @@ impl Workspace { if let Some(active_panel) = dock.active_panel() { if was_visible { - if active_panel.has_focus(cx) { + if active_panel.focus_handle(cx).contains_focused(cx) { focus_center = true; } } else { @@ -1589,7 +1589,9 @@ impl Workspace { /// Focus the panel of the given type if it isn't already focused. If it is /// already focused, then transfer focus back to the workspace center. pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { - self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); + self.focus_or_unfocus_panel::(cx, |panel, cx| { + !panel.focus_handle(cx).contains_focused(cx) + }); } /// Focus or unfocus the given panel type, depending on the given callback. @@ -1681,7 +1683,7 @@ impl Workspace { if Some(dock.position()) != dock_to_reveal { if let Some(panel) = dock.active_panel() { if panel.is_zoomed(cx) { - focus_center |= panel.has_focus(cx); + focus_center |= panel.focus_handle(cx).contains_focused(cx); dock.set_open(false, cx); } } From dffe0ea058a59cbcd0dd99dfb41bbaf6f4511d4f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 09:23:24 -0700 Subject: [PATCH 14/78] Reintroduce menu-related platform callbacks --- crates/gpui2/src/platform.rs | 6 +- crates/gpui2/src/platform/mac/platform.rs | 114 ++++++++++----------- crates/gpui2/src/platform/test/platform.rs | 12 +++ 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 651392c9c80eef548fb15a3c2d88d33488740661..40c555301bdf00cbad7d2ef36b2c99511daa6d90 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -5,7 +5,7 @@ mod mac; mod test; use crate::{ - point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, TaskLabel, @@ -90,6 +90,10 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); + fn on_menu_command(&self, callback: Box); + fn on_will_open_menu(&self, callback: Box); + fn on_validate_menu_command(&self, callback: Box bool>); + fn os_name(&self) -> &'static str; fn os_version(&self) -> Result; fn app_version(&self) -> Result; diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 314f055811c57cde9c654294e2d39ed7f1cc3806..9d02c8fb938e9b6f4cd1014e5ebce4f037911bcd 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,9 +1,9 @@ use super::BoolExt; use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, - InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, - PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, - SemanticVersion, VideoTimestamp, WindowOptions, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, + MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, + Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -155,12 +155,12 @@ pub struct MacPlatformState { reopen: Option>, quit: Option>, event: Option bool>>, - // menu_command: Option>, - // validate_menu_command: Option bool>>, + menu_command: Option>, + validate_menu_command: Option bool>>, will_open_menu: Option>, + menu_actions: Vec>, open_urls: Option)>>, finish_launching: Option>, - // menu_actions: Vec>, } impl MacPlatform { @@ -179,12 +179,12 @@ impl MacPlatform { reopen: None, quit: None, event: None, + menu_command: None, + validate_menu_command: None, will_open_menu: None, + menu_actions: Default::default(), open_urls: None, finish_launching: None, - // menu_command: None, - // validate_menu_command: None, - // menu_actions: Default::default(), })) } @@ -681,17 +681,17 @@ impl Platform for MacPlatform { } } - // fn on_menu_command(&self, callback: Box) { - // self.0.lock().menu_command = Some(callback); - // } + fn on_menu_command(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } - // fn on_will_open_menu(&self, callback: Box) { - // self.0.lock().will_open_menu = Some(callback); - // } + fn on_will_open_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } - // fn on_validate_menu_command(&self, callback: Box bool>) { - // self.0.lock().validate_menu_command = Some(callback); - // } + fn on_validate_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { // unsafe { @@ -956,7 +956,7 @@ unsafe fn path_from_objc(path: id) -> PathBuf { PathBuf::from(path) } -unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform { +unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); assert!(!platform_ptr.is_null()); &*(platform_ptr as *const MacPlatform) @@ -965,7 +965,7 @@ unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform { extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { unsafe { if let Some(event) = InputEvent::from_native(native_event, None) { - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); if let Some(callback) = platform.0.lock().event.as_mut() { if !callback(event) { return; @@ -981,7 +981,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { let app: id = msg_send![APP_CLASS, sharedApplication]; app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); let callback = platform.0.lock().finish_launching.take(); if let Some(callback) = callback { callback(); @@ -991,7 +991,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { if !has_open_windows { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().reopen.as_mut() { callback(); } @@ -999,21 +999,21 @@ extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_wi } extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().become_active.as_mut() { callback(); } } extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().resign_active.as_mut() { callback(); } } extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().quit.as_mut() { callback(); } @@ -1035,49 +1035,47 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { }) .collect::>() }; - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().open_urls.as_mut() { callback(urls); } } -extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) { - todo!() - // unsafe { - // let platform = get_foreground_platform(this); - // let mut platform = platform.0.lock(); - // if let Some(mut callback) = platform.menu_command.take() { - // let tag: NSInteger = msg_send![item, tag]; - // let index = tag as usize; - // if let Some(action) = platform.menu_actions.get(index) { - // callback(action.as_ref()); - // } - // platform.menu_command = Some(callback); - // } - // } +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let platform = get_mac_platform(this); + let mut platform = platform.0.lock(); + if let Some(mut callback) = platform.menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = platform.menu_actions.get(index) { + callback(action.as_ref()); + } + platform.menu_command = Some(callback); + } + } } -extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool { - todo!() - // unsafe { - // let mut result = false; - // let platform = get_foreground_platform(this); - // let mut platform = platform.0.lock(); - // if let Some(mut callback) = platform.validate_menu_command.take() { - // let tag: NSInteger = msg_send![item, tag]; - // let index = tag as usize; - // if let Some(action) = platform.menu_actions.get(index) { - // result = callback(action.as_ref()); - // } - // platform.validate_menu_command = Some(callback); - // } - // result - // } +extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { + unsafe { + let mut result = false; + let platform = get_mac_platform(this); + let mut platform = platform.0.lock(); + if let Some(mut callback) = platform.validate_menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = platform.menu_actions.get(index) { + result = callback(action.as_ref()); + } + platform.validate_menu_command = Some(callback); + } + result + } } extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { unsafe { - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); let mut platform = platform.0.lock(); if let Some(mut callback) = platform.will_open_menu.take() { callback(); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index fa4b6e18c587521d88d8d4a4fd4041952c584f4a..6fa706f617119bfad728078a378309c16ab19df4 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -205,6 +205,18 @@ impl Platform for TestPlatform { unimplemented!() } + fn on_menu_command(&self, _callback: Box) { + unimplemented!() + } + + fn on_will_open_menu(&self, _callback: Box) { + unimplemented!() + } + + fn on_validate_menu_command(&self, _callback: Box bool>) { + unimplemented!() + } + fn os_name(&self) -> &'static str { "test" } From 412c6157b108a250cf1c2e85eafd3f731f71f4c2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:33:35 +0100 Subject: [PATCH 15/78] Port quick_action_bar to zed2 Co-authored-by: Nate --- Cargo.lock | 12 + Cargo.toml | 1 + crates/quick_action_bar2/Cargo.toml | 22 ++ .../quick_action_bar2/src/quick_action_bar.rs | 285 ++++++++++++++++++ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/zed2.rs | 11 +- 6 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 crates/quick_action_bar2/Cargo.toml create mode 100644 crates/quick_action_bar2/src/quick_action_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 66125d770390222874f86b419bf3741147edea5c..1388b3f0536a6218445d0041afc9b3ec29362eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7074,6 +7074,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "quick_action_bar2" +version = "0.1.0" +dependencies = [ + "editor2", + "gpui2", + "search2", + "ui2", + "workspace2", +] + [[package]] name = "quote" version = "1.0.33" @@ -11843,6 +11854,7 @@ dependencies = [ "postage", "project2", "project_panel2", + "quick_action_bar2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 3658ffad297f2c9d4fb3bf5eb6b03ede591d37e6..6477e2216c775c7e1a70a26d4169931e653b5da7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ members = [ "crates/project_panel", "crates/project_panel2", "crates/project_symbols", + "crates/quick_action_bar2", "crates/recent_projects", "crates/rope", "crates/rpc", diff --git a/crates/quick_action_bar2/Cargo.toml b/crates/quick_action_bar2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..32f440d202648b5c0dba8071d7aa5e49d4da18db --- /dev/null +++ b/crates/quick_action_bar2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "quick_action_bar2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/quick_action_bar.rs" +doctest = false + +[dependencies] +#assistant = { path = "../assistant" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +search = { package = "search2", path = "../search2" } +workspace = { package = "workspace2", path = "../workspace2" } +ui = { package = "ui2", path = "../ui2" } + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..6b8f15d4c95f361ab2e881ab32174b474f16713a --- /dev/null +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -0,0 +1,285 @@ +// use assistant::{assistant_panel::InlineAssist, AssistantPanel}; +use editor::Editor; + +use gpui::{ + Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, + Styled, Subscription, View, ViewContext, WeakView, +}; +use search::{buffer_search, BufferSearchBar}; +use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; +use workspace::{ + item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +pub struct QuickActionBar { + buffer_search_bar: View, + active_item: Option>, + _inlay_hints_enabled_subscription: Option, + workspace: WeakView, +} + +impl QuickActionBar { + pub fn new(buffer_search_bar: View, workspace: &Workspace) -> Self { + Self { + buffer_search_bar, + active_item: None, + _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), + } + } + + fn active_editor(&self) -> Option> { + self.active_item + .as_ref() + .and_then(|item| item.downcast::()) + } +} + +impl Render for QuickActionBar { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let search_button = QuickActionBarButton::new( + "toggle buffer search", + Icon::MagnifyingGlass, + !self.buffer_search_bar.read(cx).is_dismissed(), + Box::new(search::buffer_search::Deploy { focus: false }), + "Buffer Search", + ); + let assistant_button = QuickActionBarButton::new( + "toggle inline assitant", + Icon::MagicWand, + false, + Box::new(gpui::NoAction), + "Inline assistant", + ); + h_stack() + .id("quick action bar") + .p_1() + .gap_2() + .child(search_button) + .child( + div() + .border() + .border_color(gpui::red()) + .child(assistant_button), + ) + } +} + +impl EventEmitter for QuickActionBar {} + +// impl View for QuickActionBar { +// fn ui_name() -> &'static str { +// "QuickActionsBar" +// } + +// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { +// let Some(editor) = self.active_editor() else { +// return div(); +// }; + +// let mut bar = Flex::row(); +// if editor.read(cx).supports_inlay_hints(cx) { +// bar = bar.with_child(render_quick_action_bar_button( +// 0, +// "icons/inlay_hint.svg", +// editor.read(cx).inlay_hints_enabled(), +// ( +// "Toggle Inlay Hints".to_string(), +// Some(Box::new(editor::ToggleInlayHints)), +// ), +// cx, +// |this, cx| { +// if let Some(editor) = this.active_editor() { +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); +// }); +// } +// }, +// )); +// } + +// if editor.read(cx).buffer().read(cx).is_singleton() { +// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); +// let search_action = buffer_search::Deploy { focus: true }; + +// bar = bar.with_child(render_quick_action_bar_button( +// 1, +// "icons/magnifying_glass.svg", +// search_bar_shown, +// ( +// "Buffer Search".to_string(), +// Some(Box::new(search_action.clone())), +// ), +// cx, +// move |this, cx| { +// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { +// if search_bar_shown { +// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); +// } else { +// buffer_search_bar.deploy(&search_action, cx); +// } +// }); +// }, +// )); +// } + +// bar.add_child(render_quick_action_bar_button( +// 2, +// "icons/magic-wand.svg", +// false, +// ("Inline Assist".into(), Some(Box::new(InlineAssist))), +// cx, +// move |this, cx| { +// if let Some(workspace) = this.workspace.upgrade(cx) { +// workspace.update(cx, |workspace, cx| { +// AssistantPanel::inline_assist(workspace, &Default::default(), cx); +// }); +// } +// }, +// )); + +// bar.into_any() +// } +// } + +#[derive(IntoElement)] +struct QuickActionBarButton { + id: ElementId, + icon: Icon, + toggled: bool, + action: Box, + tooltip: SharedString, + tooltip_meta: Option, +} + +impl QuickActionBarButton { + fn new( + id: impl Into, + icon: Icon, + toggled: bool, + action: Box, + tooltip: impl Into, + ) -> Self { + Self { + id: id.into(), + icon, + toggled, + action, + tooltip: tooltip.into(), + tooltip_meta: None, + } + } + + pub fn meta(mut self, meta: Option>) -> Self { + self.tooltip_meta = meta.map(|meta| meta.into()); + self + } +} + +impl RenderOnce for QuickActionBarButton { + type Rendered = IconButton; + + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let tooltip = self.tooltip.clone(); + let action = self.action.boxed_clone(); + let tooltip_meta = self.tooltip_meta.clone(); + + IconButton::new(self.id.clone(), self.icon) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggled) + .tooltip(move |cx| { + if let Some(meta) = &tooltip_meta { + Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx) + } else { + Tooltip::for_action(tooltip.clone(), &*action, cx) + } + }) + .on_click({ + let action = self.action.boxed_clone(); + move |_, cx| cx.dispatch_action(action.boxed_clone()) + }) + } +} + +// fn render_quick_action_bar_button< +// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext), +// >( +// index: usize, +// icon: &'static str, +// toggled: bool, +// tooltip: (String, Option>), +// cx: &mut ViewContext, +// on_click: F, +// ) -> AnyElement { +// enum QuickActionBarButton {} + +// let theme = theme::current(cx); +// let (tooltip_text, action) = tooltip; + +// MouseEventHandler::new::(index, cx, |mouse_state, _| { +// let style = theme +// .workspace +// .toolbar +// .toggleable_tool +// .in_state(toggled) +// .style_for(mouse_state); +// Svg::new(icon) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .constrained() +// .with_width(style.button_width) +// .with_height(style.button_width) +// .contained() +// .with_style(style.container) +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) +// .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) +// .into_any_named("quick action bar button") +// } + +impl ToolbarItemView for QuickActionBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + match active_pane_item { + Some(active_item) => { + self.active_item = Some(active_item.boxed_clone()); + self._inlay_hints_enabled_subscription.take(); + + if let Some(editor) = active_item.downcast::() { + let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); + self._inlay_hints_enabled_subscription = + Some(cx.observe(&editor, move |_, editor, cx| { + let editor = editor.read(cx); + let new_inlay_hints_enabled = editor.inlay_hints_enabled(); + let new_supports_inlay_hints = editor.supports_inlay_hints(cx); + let should_notify = inlay_hints_enabled != new_inlay_hints_enabled + || supports_inlay_hints != new_supports_inlay_hints; + inlay_hints_enabled = new_inlay_hints_enabled; + supports_inlay_hints = new_supports_inlay_hints; + if should_notify { + cx.notify() + } + })); + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } + None => { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ee9416e234188400164f2ebd1f713bc145364862..cc6150676438fef760aa70913651695968380e2d 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } -# quick_action_bar = { path = "../quick_action_bar" } +quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } # recent_projects = { path = "../recent_projects" } rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 1b9f1cc719bc889377ba113a78b09a72d91656a1..abd6b16e3d376c1c690b0851a1db722dbd345e19 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -19,6 +19,7 @@ pub use open_listener::*; use anyhow::{anyhow, Context as _}; use project_panel::ProjectPanel; +use quick_action_bar::QuickActionBar; use settings::{initial_local_settings_content, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; @@ -100,11 +101,11 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - // todo!() - // let quick_action_bar = cx.add_view(|_| { - // QuickActionBar::new(buffer_search_bar, workspace) - // }); - // toolbar.add_item(quick_action_bar, cx); + + let quick_action_bar = cx.build_view(|_| { + QuickActionBar::new(buffer_search_bar, workspace) + }); + toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); From eed5a698cff8d19614e9bc07c5e656e34703ec26 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 11:59:23 -0500 Subject: [PATCH 16/78] Update tab close button --- crates/workspace2/src/pane.rs | 11 +++++------ crates/workspace2/src/toolbar.rs | 30 +----------------------------- 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2433edee0efe25cf7125559ef9f6e364c69b4681..599c1d88c78d48dcb31a5784ab44afa3d74015c9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1421,13 +1421,12 @@ impl Pane { let close_icon = || { let id = item.item_id(); - div() + h_stack() .id(ix) - .w_3p5() - .h_3p5() - .rounded_sm() - .border() - .border_color(cx.theme().colors().border_variant) + .justify_center() + .w_4() + .h_4() + .rounded_md() .absolute() .map(|this| { if close_right { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d7cb741791789b2276b64892930352e777ef14e1..d47e99cb2027bfea51775e2060a4ea948342d0e7 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -87,35 +87,7 @@ impl Render for Toolbar { .child( h_stack() .justify_between() - // Toolbar left side - .children(self.items.iter().map(|(child, _)| child.to_any())) - // Toolbar right side - .child( - h_stack() - .p_1() - .gap_2() - .child( - IconButton::new("toggle-inlay-hints", Icon::InlayHint) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), - ) - .child( - IconButton::new("buffer-search", Icon::MagnifyingGlass) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Search in File", cx)), - ) - .child( - IconButton::new("inline-assist", Icon::MagicWand) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), - ), - ), + .children(self.items.iter().map(|(child, _)| child.to_any())), ) } } From e534c5fdcd18c7003502dc8a31e8ff4555fec5c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 18:14:24 +0100 Subject: [PATCH 17/78] WIP --- crates/assistant2/src/assistant_panel.rs | 503 ++++++++--------------- crates/ui2/src/components/icon.rs | 2 + 2 files changed, 167 insertions(+), 338 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index f3bd06328d3e42760b0cc70cc0543f1c3cbfcdaa..a6a04421b0bd15cae36b5a96d426e50e78ffd5d7 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -27,9 +27,10 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, - Div, Element, Entity, EventEmitter, FocusHandle, FocusableView, HighlightStyle, - InteractiveElement, IntoElement, Model, ModelContext, Render, Styled, Subscription, Task, + actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, + ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView, + HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, + PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; @@ -48,7 +49,10 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use ui::{h_stack, v_stack, ButtonCommon, ButtonLike, Clickable, IconButton, Label}; +use ui::{ + h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, + IconElement, Label, Selectable, StyledExt, Tooltip, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ @@ -958,7 +962,7 @@ impl AssistantPanel { } fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("hamburger_button", ui::Icon::Menu) + IconButton::new("hamburger_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if this.active_editor().is_some() { this.set_active_editor_index(None, cx); @@ -966,7 +970,7 @@ impl AssistantPanel { this.set_active_editor_index(this.prev_active_editor_index, cx); } })) - .tooltip(|cx| ui::Tooltip::text("History", cx)) + .tooltip(|cx| Tooltip::text("History", cx)) } fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { @@ -982,27 +986,27 @@ impl AssistantPanel { } fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", ui::Icon::Menu) + IconButton::new("split_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); } })) - .tooltip(|cx| ui::Tooltip::for_action("Split Message", &Split, cx)) + .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) } fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("assist_button", ui::Icon::Menu) + IconButton::new("assist_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); } })) - .tooltip(|cx| ui::Tooltip::for_action("Assist", &Assist, cx)) + .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) } fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("quote_button", ui::Icon::Menu) + IconButton::new("quote_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(workspace) = this.workspace.upgrade() { cx.window_context().defer(move |cx| { @@ -1012,24 +1016,24 @@ impl AssistantPanel { }); } })) - .tooltip(|cx| ui::Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) + .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) } fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("plus_button", ui::Icon::Menu) + IconButton::new("plus_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.new_conversation(cx); })) - .tooltip(|cx| ui::Tooltip::for_action("New Conversation", &NewConversation, cx)) + .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) } fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("zoom_button", ui::Icon::Menu) + IconButton::new("zoom_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) .tooltip(|cx| { - ui::Tooltip::for_action( + Tooltip::for_action( if self.zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx, @@ -1111,9 +1115,9 @@ fn build_api_key_editor(cx: &mut ViewContext) -> View { } impl Render for AssistantPanel { - type Element = Div; + type Element = Focusable
; - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { if let Some(api_key_editor) = self.api_key_editor.clone() { v_stack() .track_focus(&self.focus_handle) @@ -1249,8 +1253,8 @@ impl Panel for AssistantPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { - Some(ui::Icon::Ai) + fn icon(&self, cx: &WindowContext) -> Option { + Some(Icon::Ai) } fn toggle_action(&self) -> Box { @@ -2052,6 +2056,7 @@ struct ConversationEditor { editor: View, blocks: HashSet, scroll_position: Option, + focus_handle: FocusHandle, _subscriptions: Vec, } @@ -2082,10 +2087,13 @@ impl ConversationEditor { editor }); + let focus_handle = cx.focus_handle(); + let _subscriptions = vec![ cx.observe(&conversation, |_, _, cx| cx.notify()), cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), + cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)), ]; let mut this = Self { @@ -2095,6 +2103,7 @@ impl ConversationEditor { scroll_position: None, fs, workspace, + focus_handle, _subscriptions, }; this.update_message_headers(cx); @@ -2265,88 +2274,47 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - // let metadata = message.metadata.clone(); - // let message = message.clone(); move |cx| { - enum Sender {} - enum ErrorTooltip {} - let message_id = message.id; - let sender = MouseEventHandler::new::( - message_id.0, - cx, - |state, _| match message.role { - Role::User => { - let style = style.user_sender.style_for(state); - Label::new("You", style.text.clone()) - .contained() - .with_style(style.container) - } + let sender = ButtonLike::new("role") + .child(match message.role { + Role::User => Label::new("You").color(Color::Default), Role::Assistant => { - let style = style.assistant_sender.style_for(state); - Label::new("Assistant", style.text.clone()) - .contained() - .with_style(style.container) + Label::new("Assistant").color(Color::Modified) } - Role::System => { - let style = style.system_sender.style_for(state); - Label::new("System", style.text.clone()) - .contained() - .with_style(style.container) + Role::System => Label::new("System").color(Color::Warning), + }) + .on_click({ + let conversation = conversation.clone(); + move |_, _, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) } - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, { - let conversation = conversation.clone(); - move |_, _, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); + }); - Flex::row() - .with_child(sender.aligned()) - .with_child( - Label::new( - message.sent_at.format("%I:%M%P").to_string(), - style.sent_at.text.clone(), - ) - .contained() - .with_style(style.sent_at.container) - .aligned(), - ) + h_stack() + .id(("message_header", message_id.0)) + .border() + .border_color(gpui::red()) + .child(sender) + .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) .with_children( if let MessageStatus::Error(error) = &message.status { Some( - Svg::new("icons/error.svg") - .with_color(style.error_icon.color) - .constrained() - .with_width(style.error_icon.width) - .contained() - .with_style(style.error_icon.container) - .with_tooltip::( - message_id.0, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), + div() + .id("error") + .tooltip(|cx| Tooltip::text(error, cx)) + .child(IconElement::new(Icon::XCircle)), ) } else { None }, ) - .aligned() - .left() - .contained() - .with_style(style.message_header) - .into_any() + .into_any_element() } }), disposition: BlockDisposition::Above, @@ -2491,78 +2459,48 @@ impl ConversationEditor { .unwrap_or_else(|| "New Conversation".into()) } - fn render_current_model( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> impl Element { - enum Model {} - - MouseEventHandler::new::(0, cx, |state, cx| { - let style = style.model.style_for(state); - let model_display_name = self.conversation.read(cx).model.short_name(); - Label::new(model_display_name, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { + Button::new( + "current_model", + self.conversation.read(cx).model.short_name(), + ) + .tooltip(move |cx| Tooltip::text("Change Model", cx)) + .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) } - fn render_remaining_tokens( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { + fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_style = if remaining_tokens <= 0 { - &style.no_remaining_tokens + let remaining_tokens_color = if remaining_tokens <= 0 { + Color::Error } else if remaining_tokens <= 500 { - &style.low_remaining_tokens + Color::Warning } else { - &style.remaining_tokens + Color::Default }; Some( - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container), + div() + .border() + .border_color(gpui::red()) + .child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)), ) } } impl EventEmitter for ConversationEditor {} -impl View for ConversationEditor { - fn ui_name() -> &'static str { - "ConversationEditor" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).assistant; - Stack::new() - .with_child( - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container), - ) - .with_child( - Flex::row() - .with_child(self.render_current_model(theme, cx)) - .with_children(self.render_remaining_tokens(theme, cx)) - .aligned() - .top() - .right(), - ) - .into_any() - } +impl Render for ConversationEditor { + type Element = Div; - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().relative().child(self.editor.clone()).child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) } } @@ -2616,7 +2554,7 @@ struct InlineAssistant { prompt_editor: View, workspace: WeakView, confirmed: bool, - has_focus: bool, + focus_handle: FocusHandle, include_conversation: bool, measurements: Rc>, prompt_history: VecDeque, @@ -2631,124 +2569,63 @@ struct InlineAssistant { maintain_rate_limit: Option>, } -impl Entity for InlineAssistant { - type Event = InlineAssistantEvent; -} +impl EventEmitter for InlineAssistant {} -impl View for InlineAssistant { - fn ui_name() -> &'static str { - "InlineAssistant" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum ErrorIcon {} - let theme = theme::current(cx); - - Flex::row() - .with_children([Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() - .aligned(), - ) - .with_children(if SemanticIndex::enabled(cx) { - Some( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), - ) - } else { - None - }) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), +impl Render for InlineAssistant { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let measurements = self.measurements.get(); + h_stack() + .child( + h_stack() + .justify_center() + .w(measurements.gutter_width) + .child( + IconButton::new("include_conversation", Icon::Ai) + .action(ToggleIncludeConversation) + .selected(self.include_conversation) + .tooltip(Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, cx, - ) - .aligned(), + )), ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } - } - })]) - .with_child(Empty::new().constrained().dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.min.y(), - ), - max: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.max.y(), - ), - } - } - })) - .with_child( - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .flex(1., true), + .children(if SemanticIndex::enabled(cx) { + Some( + IconButton::new("retrieve_context", Icon::MagnifyingGlass) + .action(ToggleRetrieveContext) + .selected(self.retrieve_context) + .tooltip(Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + )), + ) + } else { + None + }) + .children(if let Some(error) = self.codegen.read(cx).error() { + Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text(error.to_string(), cx)) + .child(IconElement::new(Icon::XCircle).color(Color::Error)), + ) + } else { + None + }), ) - .with_children(if self.retrieve_context { - Some( - Flex::row() - .with_children(self.retrieve_context_status(cx)) - .flex(1., true) - .aligned(), - ) + .child( + div() + .ml(measurements.anchor_x - measurements.gutter_width) + .child(self.prompt_editor.clone()), + ) + .children(if self.retrieve_context { + self.retrieve_context_status(cx) } else { None }) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - cx.focus(&self.prompt_editor); - self.has_focus = true; - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; } } @@ -2765,11 +2642,8 @@ impl InlineAssistant { semantic_index: Option>, project: Model, ) -> Self { - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); + let prompt_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); let placeholder = match codegen.read(cx).kind() { CodegenKind::Transform { .. } => "Enter transformation prompt…", CodegenKind::Generate { .. } => "Enter generation prompt…", @@ -2777,9 +2651,15 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); + + let focus_handle = cx.focus_handle(); let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + cx.on_focus( + &focus_handle, + cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)), + ), ]; if let Some(semantic_index) = semantic_index.clone() { @@ -2791,7 +2671,7 @@ impl InlineAssistant { prompt_editor, workspace, confirmed: false, - has_focus: false, + focus_handle, include_conversation, measurements, prompt_history, @@ -3008,10 +2888,7 @@ impl InlineAssistant { anyhow::Ok(()) } - fn retrieve_context_status( - &self, - cx: &mut ViewContext, - ) -> Option> { + fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { enum ContextStatusIcon {} let Some(project) = self.project.upgrade() else { @@ -3020,47 +2897,27 @@ impl InlineAssistant { if let Some(semantic_index) = SemanticIndex::global(cx) { let status = semantic_index.update(cx, |index, _| index.status(&project)); - let theme = theme::current(cx); match status { SemanticIndexStatus::NotAuthenticated {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() ), + SemanticIndexStatus::NotIndexed {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.inline.context_status.error_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.error_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.error_icon.container) - .with_tooltip::( - self.id, - "Not Indexed", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() ), + SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry, } => { - let mut status_text = if remaining_files == 0 { "Indexing...".to_string() } else { @@ -3079,6 +2936,11 @@ impl InlineAssistant { } } Some( + div() + .id("update") + .tooltip(|cx| Tooltip::text(status_text, cx)) + .child(IconElement::new(Icon::Update).color(color)) + .into_any_element() Svg::new("icons/update.svg") .with_color(theme.assistant.inline.context_status.in_progress_icon.color) .constrained() @@ -3096,6 +2958,7 @@ impl InlineAssistant { .into_any(), ) } + SemanticIndexStatus::Indexed {} => Some( Svg::new("icons/check.svg") .with_color(theme.assistant.inline.context_status.complete_icon.color) @@ -3119,42 +2982,6 @@ impl InlineAssistant { } } - // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { - // let project = self.project.clone(); - // if let Some(semantic_index) = self.semantic_index.clone() { - // let status = semantic_index.update(cx, |index, cx| index.status(&project)); - // return match status { - // // This theoretically shouldnt be a valid code path - // // As the inline assistant cant be launched without an API key - // // We keep it here for safety - // semantic_index::SemanticIndexStatus::NotAuthenticated => { - // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexed => { - // "Indexing Complete!".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { - - // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); - - // 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) { - // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); - // } - // } - // status - // } - // semantic_index::SemanticIndexStatus::NotIndexed => { - // "Not Indexed for Context Retrieval".to_string() - // } - // }; - // } - - // "".to_string() - // } - fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, @@ -3208,8 +3035,8 @@ impl InlineAssistant { // This wouldn't need to exist if we could pass parameters when rendering child views. #[derive(Copy, Clone, Default)] struct BlockMeasurements { - anchor_x: f32, - gutter_width: f32, + anchor_x: Pixels, + gutter_width: Pixels, } struct PendingInlineAssist { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..29e743eace6fcea7274be0f7bc85081a8672e4d3 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -81,6 +81,7 @@ pub enum Icon { Shift, Option, Return, + Update, } impl Icon { @@ -155,6 +156,7 @@ impl Icon { Icon::Shift => "icons/shift.svg", Icon::Option => "icons/option.svg", Icon::Return => "icons/return.svg", + Icon::Update => "icons/update.svg", } } } From dccdcd322101461fbdd83d83bf1736a3bd01b47c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 12:41:54 -0500 Subject: [PATCH 18/78] Add indicator component Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/ui2/src/components.rs | 2 + crates/ui2/src/components/indicator.rs | 59 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 crates/ui2/src/components/indicator.rs diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 17271de48d4993c111c5cececd50499c6ef801b3..583b30a2e05fd647d7603d41ee4f0edbf8510713 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -5,6 +5,7 @@ mod context_menu; mod disclosure; mod divider; mod icon; +mod indicator; mod keybinding; mod label; mod list; @@ -24,6 +25,7 @@ pub use context_menu::*; pub use disclosure::*; pub use divider::*; pub use icon::*; +pub use indicator::*; pub use keybinding::*; pub use label::*; pub use list::*; diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..af62f9d989c12f1420afe5d054a7305cfd6e3a2b --- /dev/null +++ b/crates/ui2/src/components/indicator.rs @@ -0,0 +1,59 @@ +use gpui::{AnyView, Div, Position}; + +use crate::prelude::*; + +#[derive(Default)] +pub enum IndicatorStyle { + #[default] + Dot, + Bar, +} + +#[derive(IntoElement)] +pub struct Indicator { + position: Position, + style: IndicatorStyle, + color: Color, +} + +impl Indicator { + pub fn dot() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn bar() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + pub fn absolute(mut self) -> Self { + self.position = Position::Absolute; + self + } +} + +impl RenderOnce for Indicator { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .map(|this| match self.style { + IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(), + IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(), + }) + .when(self.position == Position::Absolute, |this| this.absolute()) + .bg(self.color.color(cx)) + } +} From 5e79807f6fc779f051b374744bbe433ab8c28dad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:14:40 -0800 Subject: [PATCH 19/78] Fix tree branch rendering in collab panel --- crates/collab_ui2/src/collab_panel.rs | 37 ++++++++++----------------- crates/gpui2/src/elements/canvas.rs | 10 +++++--- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c55bfa8cf5a7beec1c516598f242fcfa2fefe018..1de95f64b74de48b61b6a7ad0d01985afa45529b 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -169,7 +169,7 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action, + actions, canvas, div, img, 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, @@ -1204,14 +1204,9 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .left_child(IconButton::new(0, Icon::Folder)) - .child( - h_stack() - .w_full() - .justify_between() - .child(render_tree_branch(is_last, cx)) - .child(Label::new(project_name.clone())), - ) + .left_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)) // enum JoinProject {} @@ -3119,30 +3114,24 @@ impl CollabPanel { } fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { - let text_style = cx.text_style(); let rem_size = cx.rem_size(); - let text_system = cx.text_system(); - let font_id = text_system.font_id(&text_style.font()).unwrap(); - let font_size = text_style.font_size.to_pixels(rem_size); - let line_height = text_style.line_height_in_pixels(rem_size); - let cap_height = text_system.cap_height(font_id, font_size); - let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height); - let width = cx.rem_size() * 2.5; + let line_height = cx.text_style().line_height_in_pixels(rem_size); + let width = rem_size * 1.5; let thickness = px(2.); let color = cx.theme().colors().text; canvas(move |bounds, cx| { - let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.); - let end_x = bounds.right(); - let start_y = bounds.top(); - let end_y = bounds.top() + baseline_offset - (cap_height / 2.); + let start_x = (bounds.left() + bounds.right() - thickness) / 2.; + let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; + let right = bounds.right(); + let top = bounds.top(); cx.paint_quad( Bounds::from_corners( - point(start_x, start_y), + point(start_x, top), point( start_x + thickness, - if is_last { end_y } else { bounds.bottom() }, + if is_last { start_y } else { bounds.bottom() }, ), ), Default::default(), @@ -3151,7 +3140,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement Hsla::transparent_black(), ); cx.paint_quad( - Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)), + Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), Default::default(), color, Default::default(), diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs index 4761b04f3f84abae558038b6830d709deb06532e..287a3b4b5a38fdc0c7c90c75763bb9a0921dfb7e 100644 --- a/crates/gpui2/src/elements/canvas.rs +++ b/crates/gpui2/src/elements/canvas.rs @@ -1,9 +1,11 @@ -use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext}; +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 { Canvas { paint_callback: Box::new(callback), - style: Default::default(), + style: StyleRefinement::default(), } } @@ -32,7 +34,9 @@ impl Element for Canvas { _: Option, cx: &mut WindowContext, ) -> (crate::LayoutId, Self::State) { - let layout_id = cx.request_layout(&self.style.clone().into(), []); + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = cx.request_layout(&style, []); (layout_id, ()) } From d86da04584f2336f5d7a177e7a6db63d7e137f8c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 19:27:15 +0100 Subject: [PATCH 20/78] WIP --- .../ai2/src/providers/open_ai/completion.rs | 6 +- crates/assistant2/src/assistant_panel.rs | 521 +++++++++--------- crates/editor2/src/editor.rs | 93 ++-- crates/gpui2/src/window.rs | 6 + crates/util/src/arc_cow.rs | 8 +- 5 files changed, 307 insertions(+), 327 deletions(-) diff --git a/crates/ai2/src/providers/open_ai/completion.rs b/crates/ai2/src/providers/open_ai/completion.rs index 3e49fc5290e438bdad69cb01607e86a9791c4178..c9a2abd0c8c3bc170ff39f16294f2fb976f00465 100644 --- a/crates/ai2/src/providers/open_ai/completion.rs +++ b/crates/ai2/src/providers/open_ai/completion.rs @@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent { pub async fn stream_completion( credential: ProviderCredential, - executor: Arc, + executor: BackgroundExecutor, request: Box, ) -> Result>> { let api_key = match credential { @@ -197,11 +197,11 @@ pub async fn stream_completion( pub struct OpenAICompletionProvider { model: OpenAILanguageModel, credential: Arc>, - executor: Arc, + executor: BackgroundExecutor, } impl OpenAICompletionProvider { - pub fn new(model_name: &str, executor: Arc) -> Self { + pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self { let model = OpenAILanguageModel::load(model_name); let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); Self { diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index a6a04421b0bd15cae36b5a96d426e50e78ffd5d7..bcf85a6948eb8cba330a0a54bae24440b7462fff 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -27,8 +27,8 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, - ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView, + actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, + ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, @@ -51,7 +51,7 @@ use std::{ }; use ui::{ h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, - IconElement, Label, Selectable, StyledExt, Tooltip, + IconElement, Label, Selectable, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -76,49 +76,18 @@ actions!( pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); - cx.add_action( - |this: &mut AssistantPanel, - _: &workspace::NewFile, - cx: &mut ViewContext| { - this.new_conversation(cx); - }, - ); - cx.add_action(ConversationEditor::assist); - cx.capture_action(ConversationEditor::cancel_last_assist); - cx.capture_action(ConversationEditor::save); - cx.add_action(ConversationEditor::quote_selection); - cx.capture_action(ConversationEditor::copy); - cx.add_action(ConversationEditor::split); - cx.capture_action(ConversationEditor::cycle_message_role); - cx.add_action(AssistantPanel::save_credentials); - cx.add_action(AssistantPanel::reset_credentials); - cx.add_action(AssistantPanel::toggle_zoom); - cx.add_action(AssistantPanel::deploy); - cx.add_action(AssistantPanel::select_next_match); - cx.add_action(AssistantPanel::select_prev_match); - cx.add_action(AssistantPanel::handle_editor_cancel); - cx.add_action( - |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); + cx.observe_new_views( + |workspace: &mut Workspace, cx: &mut ViewContext| { + workspace + .register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }) + .register_action(AssistantPanel::inline_assist) + .register_action(AssistantPanel::cancel_last_inline_assist) + .register_action(ConversationEditor::quote_selection); }, - ); - cx.add_action(AssistantPanel::inline_assist); - cx.add_action(AssistantPanel::cancel_last_inline_assist); - cx.add_action(InlineAssistant::confirm); - cx.add_action(InlineAssistant::cancel); - cx.add_action(InlineAssistant::toggle_include_conversation); - cx.add_action(InlineAssistant::toggle_retrieve_context); - cx.add_action(InlineAssistant::move_up); - cx.add_action(InlineAssistant::move_down); -} - -#[derive(Debug)] -pub enum AssistantPanelEvent { - ZoomIn, - ZoomOut, - Focus, - Close, - DockPositionChanged, + ) + .detach(); } pub struct AssistantPanel { @@ -131,7 +100,6 @@ pub struct AssistantPanel { saved_conversations: Vec, saved_conversations_scroll_handle: UniformListScrollHandle, zoomed: bool, - // todo!("remove has_focus field") focus_handle: FocusHandle, toolbar: View, completion_provider: Arc, @@ -152,9 +120,12 @@ pub struct AssistantPanel { impl AssistantPanel { const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; - pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { + pub fn load( + workspace: WeakView, + mut cx: AsyncWindowContext, + ) -> Task>> { cx.spawn(|mut cx| async move { - let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; let saved_conversations = SavedConversationMetadata::list(fs.clone()) .await .log_err() @@ -163,7 +134,7 @@ impl AssistantPanel { // TODO: deserialize state. let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { - cx.add_view::(|cx| { + cx.build_view::(|cx| { const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { let mut events = fs @@ -184,10 +155,10 @@ impl AssistantPanel { anyhow::Ok(()) }); - let toolbar = cx.add_view(|cx| { + let toolbar = cx.build_view(|cx| { let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar.add_item(cx.build_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); @@ -199,8 +170,8 @@ impl AssistantPanel { )); let focus_handle = cx.focus_handle(); - cx.on_focus_in(Self::focus_in).detach(); - cx.on_focus_out(Self::focus_out).detach(); + cx.on_focus_in(&focus_handle, Self::focus_in).detach(); + cx.on_focus_out(&focus_handle, Self::focus_out).detach(); let mut this = Self { workspace: workspace_handle, @@ -231,11 +202,11 @@ impl AssistantPanel { let mut old_dock_position = this.position(cx); this.subscriptions = - vec![cx.observe_global::(move |this, cx| { + vec![cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(AssistantPanelEvent::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } cx.notify(); })]; @@ -343,7 +314,7 @@ impl AssistantPanel { // Retrieve Credentials Authenticates the Provider provider.retrieve_credentials(cx); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -353,14 +324,14 @@ impl AssistantPanel { let previously_indexed = semantic_index .update(&mut cx, |index, cx| { index.project_previously_indexed(&project, cx) - }) + })? .await .unwrap_or(false); if previously_indexed { let _ = semantic_index .update(&mut cx, |index, cx| { index.index_project(project.clone(), cx) - }) + })? .await; } anyhow::Ok(()) @@ -369,7 +340,7 @@ impl AssistantPanel { } let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.add_view(|cx| { + let inline_assistant = cx.build_view(|cx| { let assistant = InlineAssistant::new( inline_assist_id, measurements.clone(), @@ -382,7 +353,7 @@ impl AssistantPanel { self.semantic_index.clone(), project.clone(), ); - cx.focus_self(); + assistant.focus_handle.focus(cx); assistant }); let block_id = editor.update(cx, |editor, cx| { @@ -429,8 +400,13 @@ impl AssistantPanel { move |_, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade() { if let EditorEvent::SelectionsChanged { local } = event { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); + if *local + && inline_assistant + .read(cx) + .focus_handle + .contains_focused(cx) + { + cx.focus_view(&editor); } } } @@ -555,7 +531,7 @@ impl AssistantPanel { } } - cx.propagate_action(); + cx.propagate(); } fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { @@ -709,13 +685,17 @@ impl AssistantPanel { let snippets = cx.spawn(|_, mut cx| async move { let mut snippets = Vec::new(); for result in search_results.await { - snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx)); + snippets.push(PromptCodeSnippet::new( + result.buffer, + result.range, + &mut cx, + )?); } - snippets + anyhow::Ok(snippets) }); snippets } else { - Task::ready(Vec::new()) + Task::ready(Ok(Vec::new())) }; let mut model = AssistantSettings::get_global(cx) @@ -724,7 +704,7 @@ impl AssistantPanel { let model_name = model.full_name(); let prompt = cx.background_executor().spawn(async move { - let snippets = snippets.await; + let snippets = snippets.await?; let language_name = language_name.as_deref(); generate_content_prompt( @@ -799,7 +779,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| theme.assistant.inline.pending_edit_background, + |theme| gpui::red(), // todo!("use the appropriate color") cx, ); } @@ -820,7 +800,7 @@ impl AssistantPanel { } fn new_conversation(&mut self, cx: &mut ViewContext) -> View { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { ConversationEditor::new( self.completion_provider.clone(), self.languages.clone(), @@ -854,8 +834,8 @@ impl AssistantPanel { self.toolbar.update(cx, |toolbar, cx| { toolbar.set_active_item(Some(&editor), cx); }); - if self.has_focus(cx) { - cx.focus(&editor); + if self.focus_handle.contains_focused(cx) { + cx.focus_view(&editor); } } else { self.toolbar.update(cx, |toolbar, cx| { @@ -891,31 +871,31 @@ impl AssistantPanel { self.completion_provider.save_credentials(cx, credential); self.api_key_editor.take(); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } } else { - cx.propagate_action(); + cx.propagate(); } } fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { self.completion_provider.delete_credentials(cx); self.api_key_editor = Some(build_api_key_editor(cx)); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { if self.zoomed { - cx.emit(AssistantPanelEvent::ZoomOut) + cx.emit(PanelEvent::ZoomOut) } else { - cx.emit(AssistantPanelEvent::ZoomIn) + cx.emit(PanelEvent::ZoomIn) } } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; + let mut propagate = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { if search_bar.show(cx) { @@ -924,12 +904,12 @@ impl AssistantPanel { search_bar.select_query(cx); cx.focus_self(); } - propagate_action = false + propagate = false } }); } - if propagate_action { - cx.propagate_action(); + if propagate { + cx.propagate(); } } @@ -942,7 +922,7 @@ impl AssistantPanel { return; } } - cx.propagate_action(); + cx.propagate(); } fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { @@ -976,9 +956,9 @@ impl AssistantPanel { fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { if self.active_editor().is_some() { vec![ - Self::render_split_button(cx).into_any(), - Self::render_quote_button(cx).into_any(), - Self::render_assist_button(cx).into_any(), + Self::render_split_button(cx).into_any_element(), + Self::render_quote_button(cx).into_any_element(), + Self::render_assist_button(cx).into_any_element(), ] } else { Default::default() @@ -1028,16 +1008,13 @@ impl AssistantPanel { } fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let zoomed = self.zoomed; IconButton::new("zoom_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) - .tooltip(|cx| { - Tooltip::for_action( - if self.zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) + .tooltip(move |cx| { + Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) }) } @@ -1072,16 +1049,16 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let saved_conversation = fs.load(&path).await?; let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.add_model(|cx| { + let conversation = cx.build_model(|cx| { Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - }); + })?; this.update(&mut cx, |this, cx| { // If, by the time we've loaded the conversation, the user has already opened // the same conversation, we don't want to open it again. if let Some(ix) = this.editor_index_for_path(&path, cx) { this.set_active_editor_index(Some(ix), cx); } else { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { ConversationEditor::for_conversation(conversation, fs, workspace, cx) }); this.add_conversation(editor, cx); @@ -1120,6 +1097,7 @@ impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { if let Some(api_key_editor) = self.api_key_editor.clone() { v_stack() + .on_action(cx.listener(AssistantPanel::save_credentials)) .track_focus(&self.focus_handle) .child(Label::new( "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", @@ -1159,6 +1137,15 @@ impl Render for AssistantPanel { } v_stack() + .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { + this.new_conversation(cx); + })) + .on_action(cx.listener(AssistantPanel::reset_credentials)) + .on_action(cx.listener(AssistantPanel::toggle_zoom)) + .on_action(cx.listener(AssistantPanel::deploy)) + .on_action(cx.listener(AssistantPanel::select_next_match)) + .on_action(cx.listener(AssistantPanel::select_prev_match)) + .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) .track_focus(&self.focus_handle) .child(header) .children(if self.toolbar.read(cx).hidden() { @@ -1175,7 +1162,7 @@ impl Render for AssistantPanel { self.saved_conversations.len(), |this, range, cx| { range - .map(|ix| this.render_saved_conversation(ix, cx).into_any()) + .map(|ix| this.render_saved_conversation(ix, cx)) .collect() }, ) @@ -1311,17 +1298,14 @@ impl Conversation { completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + let buffer = cx.build_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1409,8 +1393,8 @@ impl Conversation { let markdown = language_registry.language_for_name("Markdown"); let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + let buffer = cx.build_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -1419,14 +1403,11 @@ impl Conversation { next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); } buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1497,26 +1478,24 @@ impl Conversation { }) .collect::>(); let model = self.model.clone(); - self.pending_token_count = cx.spawn_weak(|this, mut cx| { + self.pending_token_count = cx.spawn(|this, mut cx| { async move { cx.background_executor() .timer(Duration::from_millis(200)) .await; let token_count = cx - .background() + .background_executor() .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) }) .await?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - }); + this.update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + })?; anyhow::Ok(()) } .log_err() @@ -1603,7 +1582,7 @@ impl Conversation { .unwrap(); user_messages.push(user_message); - let task = cx.spawn_weak({ + let task = cx.spawn({ |this, mut cx| async move { let assistant_message_id = assistant_message.id; let stream_completion = async { @@ -1612,59 +1591,55 @@ impl Conversation { while let Some(message) = messages.next().await { let text = message?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors + this.update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + })?; smol::future::yield_now().await; } - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); + this.update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + })?; anyhow::Ok(()) }; let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = - MessageStatus::Error(error.to_string().trim().into()); - } + + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); } - cx.notify(); } - }); - } + cx.notify(); + } + }) + .ok(); } }); @@ -1999,10 +1974,10 @@ impl Conversation { None }; (path, summary) - }); + })?; if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; let path = if let Some(old_path) = old_path { old_path } else { @@ -2026,7 +2001,7 @@ impl Conversation { fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) .await?; - this.update(&mut cx, |this, _| this.path = Some(path)); + this.update(&mut cx, |this, _| this.path = Some(path))?; } Ok(()) @@ -2069,7 +2044,7 @@ impl ConversationEditor { cx: &mut ViewContext, ) -> Self { let conversation = - cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + cx.build_model(|cx| Conversation::new(language_registry, cx, completion_provider)); Self::for_conversation(conversation, fs, workspace, cx) } @@ -2079,7 +2054,7 @@ impl ConversationEditor { workspace: WeakView, cx: &mut ViewContext, ) -> Self { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); @@ -2093,7 +2068,7 @@ impl ConversationEditor { cx.observe(&conversation, |_, _, cx| cx.notify()), cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), - cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)), + cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.editor)), ]; let mut this = Self { @@ -2155,7 +2130,7 @@ impl ConversationEditor { .conversation .update(cx, |conversation, _| conversation.cancel_last_assist()) { - cx.propagate_action(); + cx.propagate(); } } @@ -2247,8 +2222,8 @@ impl ConversationEditor { .anchor() .scroll_position(&snapshot.display_snapshot); - let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y..scroll_bottom).contains(&cursor_row) { Some(ScrollPosition { cursor, offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), @@ -2286,7 +2261,7 @@ impl ConversationEditor { }) .on_click({ let conversation = conversation.clone(); - move |_, _, cx| { + move |_, cx| { conversation.update(cx, |conversation, cx| { conversation.cycle_message_roles( HashSet::from_iter(Some(message_id)), @@ -2302,18 +2277,16 @@ impl ConversationEditor { .border_color(gpui::red()) .child(sender) .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) - .with_children( - if let MessageStatus::Error(error) = &message.status { - Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text(error, cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }, - ) + .children(if let MessageStatus::Error(error) = &message.status { + Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text(error, cx)) + .child(IconElement::new(Icon::XCircle)), + ) + } else { + None + }) .into_any_element() } }), @@ -2342,36 +2315,35 @@ impl ConversationEditor { return; }; - let text = editor.read_with(cx, |editor, cx| { - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + let editor = editor.read(cx); + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - let selected_text = buffer.text_for_range(range).collect::(); - if selected_text.is_empty() { - None + let selected_text = buffer.text_for_range(range).collect::(); + let text = if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - } - }); + format!("```{language_name}\n{selected_text}\n```") + }) + }; // Activate the panel - if !panel.read(cx).has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { workspace.toggle_panel_focus::(cx); } @@ -2415,13 +2387,12 @@ impl ConversationEditor { } if spanned_messages > 1 { - cx.platform() - .write_to_clipboard(ClipboardItem::new(copied_text)); + cx.write_to_clipboard(ClipboardItem::new(copied_text)); return; } } - cx.propagate_action(); + cx.propagate(); } fn split(&mut self, _: &Split, cx: &mut ViewContext) { @@ -2492,15 +2463,30 @@ impl Render for ConversationEditor { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().relative().child(self.editor.clone()).child( - h_stack() - .absolute() - .gap_1() - .top_3() - .right_5() - .child(self.render_current_model(cx)) - .children(self.render_remaining_tokens(cx)), - ) + div() + .relative() + .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) + .capture_action(cx.listener(ConversationEditor::save)) + .capture_action(cx.listener(ConversationEditor::copy)) + .capture_action(cx.listener(ConversationEditor::cycle_message_role)) + .on_action(cx.listener(ConversationEditor::assist)) + .on_action(cx.listener(ConversationEditor::split)) + .child(self.editor.clone()) + .child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) + } +} + +impl FocusableView for ConversationEditor { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } @@ -2577,30 +2563,40 @@ impl Render for InlineAssistant { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let measurements = self.measurements.get(); h_stack() + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::toggle_include_conversation)) + .on_action(cx.listener(Self::toggle_retrieve_context)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) .child( h_stack() .justify_center() .w(measurements.gutter_width) .child( IconButton::new("include_conversation", Icon::Ai) - .action(ToggleIncludeConversation) + .action(Box::new(ToggleIncludeConversation)) .selected(self.include_conversation) - .tooltip(Tooltip::for_action( - "Include Conversation", - &ToggleIncludeConversation, - cx, - )), + .tooltip(|cx| { + Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, + cx, + ) + }), ) .children(if SemanticIndex::enabled(cx) { Some( IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .action(ToggleRetrieveContext) + .action(Box::new(ToggleRetrieveContext)) .selected(self.retrieve_context) - .tooltip(Tooltip::for_action( - "Retrieve Context", - &ToggleRetrieveContext, - cx, - )), + .tooltip(|cx| { + Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + ) + }), ) } else { None @@ -2629,6 +2625,12 @@ impl Render for InlineAssistant { } } +impl FocusableView for InlineAssistant { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + impl InlineAssistant { fn new( id: usize, @@ -2656,10 +2658,7 @@ impl InlineAssistant { let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), - cx.on_focus( - &focus_handle, - cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)), - ), + cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.prompt_editor)), ]; if let Some(semantic_index) = semantic_index.clone() { @@ -2939,42 +2938,17 @@ impl InlineAssistant { div() .id("update") .tooltip(|cx| Tooltip::text(status_text, cx)) - .child(IconElement::new(Icon::Update).color(color)) + .child(IconElement::new(Icon::Update).color(Color::Info)) .into_any_element() - Svg::new("icons/update.svg") - .with_color(theme.assistant.inline.context_status.in_progress_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.in_progress_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.in_progress_icon.container) - .with_tooltip::( - self.id, - status_text, - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), ) } SemanticIndexStatus::Indexed {} => Some( - Svg::new("icons/check.svg") - .with_color(theme.assistant.inline.context_status.complete_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.complete_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.complete_icon.container) - .with_tooltip::( - self.id, - "Index up to date", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(IconElement::new(Icon::Check).color(Color::Success)) + .into_any_element() ), } } else { @@ -3083,7 +3057,8 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3213,7 +3188,8 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3310,7 +3286,8 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3394,7 +3371,7 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = - cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + cx.build_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_0 = conversation.read(cx).message_anchors[0].id; let message_1 = conversation.update(cx, |conversation, cx| { @@ -3427,7 +3404,7 @@ mod tests { ] ); - let deserialized_conversation = cx.add_model(|cx| { + let deserialized_conversation = cx.build_model(|cx| { Conversation::deserialize( conversation.read(cx).serialize(cx), Default::default(), diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648ab0a1a2495f60a261112ef73847d90b..50dae22eaec2f19acfe929b64aa8d3ccd31bd98a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1961,14 +1961,14 @@ impl Editor { cx.notify(); } - // pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { - // self.cursor_shape = cursor_shape; - // cx.notify(); - // } + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { + self.cursor_shape = cursor_shape; + cx.notify(); + } - // pub fn set_collapse_matches(&mut self, collapse_matches: bool) { - // self.collapse_matches = collapse_matches; - // } + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } pub fn range_for_match(&self, range: &Range) -> Range { if self.collapse_matches { @@ -1977,56 +1977,47 @@ impl Editor { range.clone() } - // pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { - // if self.display_map.read(cx).clip_at_line_ends != clip { - // self.display_map - // .update(cx, |map, _| map.clip_at_line_ends = clip); - // } - // } - - // pub fn set_keymap_context_layer( - // &mut self, - // context: KeymapContext, - // cx: &mut ViewContext, - // ) { - // self.keymap_context_layers - // .insert(TypeId::of::(), context); - // cx.notify(); - // } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + if self.display_map.read(cx).clip_at_line_ends != clip { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + } - // pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { - // self.keymap_context_layers.remove(&TypeId::of::()); - // cx.notify(); - // } + pub fn set_keymap_context_layer( + &mut self, + context: KeyContext, + cx: &mut ViewContext, + ) { + self.keymap_context_layers + .insert(TypeId::of::(), context); + cx.notify(); + } - // pub fn set_input_enabled(&mut self, input_enabled: bool) { - // self.input_enabled = input_enabled; - // } + pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { + self.keymap_context_layers.remove(&TypeId::of::()); + cx.notify(); + } - // pub fn set_autoindent(&mut self, autoindent: bool) { - // if autoindent { - // self.autoindent_mode = Some(AutoindentMode::EachLine); - // } else { - // self.autoindent_mode = None; - // } - // } + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } - // pub fn read_only(&self) -> bool { - // self.read_only - // } + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } - // pub fn set_read_only(&mut self, read_only: bool) { - // self.read_only = read_only; - // } + pub fn read_only(&self) -> bool { + self.read_only + } - // pub fn set_field_editor_style( - // &mut self, - // style: Option>, - // cx: &mut ViewContext, - // ) { - // self.get_field_editor_theme = style; - // cx.notify(); - // } + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } fn selections_did_change( &mut self, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..f68046b2508e576dfa6d816068a3cc0b5f66b4fc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId { ElementId::NamedInteger(name.into(), id.as_u64() as usize) } } + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} diff --git a/crates/util/src/arc_cow.rs b/crates/util/src/arc_cow.rs index 86b998ff066b8dad6367921f2fed59a6d75a6e19..c6afabbbaa80046ea391e17ca85b522871f8c80c 100644 --- a/crates/util/src/arc_cow.rs +++ b/crates/util/src/arc_cow.rs @@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> { } } -impl From> for ArcCow<'_, T> { +impl From> for ArcCow<'_, T> { fn from(s: Arc) -> Self { Self::Owned(s) } } +impl From<&'_ Arc> for ArcCow<'_, T> { + fn from(s: &'_ Arc) -> Self { + Self::Owned(s.clone()) + } +} + impl From for ArcCow<'_, str> { fn from(value: String) -> Self { Self::Owned(value.into()) From 38d41acf9bfd76274ea93679f906e9e4fc320ea9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:29:19 -0800 Subject: [PATCH 21/78] Fix rendering of shared screens in collab panel --- crates/collab_ui2/src/collab_panel.rs | 76 +++++---------------------- crates/gpui2/src/window.rs | 6 +++ 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 1de95f64b74de48b61b6a7ad0d01985afa45529b..bdddc8288af9dfd6780a72a7893e9bf371d65200 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1294,70 +1294,20 @@ impl CollabPanel { is_last: bool, cx: &mut ViewContext, ) -> impl IntoElement { - // enum OpenSharedScreen {} + let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - // let tree_branch = theme.tree_branch; - - // let handler = MouseEventHandler::new::( - // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, - // cx, - // |mouse_state, cx| { - // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - // let row = theme - // .project_row - // .in_state(is_selected) - // .style_for(mouse_state); - - // Flex::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // is_last, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/desktop.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("Screen", row.name.text.clone()) - // .aligned() - // .left() - // .contained() - // .with_style(row.name.container) - // .flex(1., false), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(row.container) - // }, - // ); - // if peer_id.is_none() { - // return handler.into_any(); - // } - // handler - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.open_shared_screen(peer_id.unwrap(), cx) - // }); - // } - // }) - // .into_any() - - div() + ListItem::new(("screen", id)) + .left_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| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(peer_id.unwrap(), cx) + }); + })) + .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx)) + }) } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..f68046b2508e576dfa6d816068a3cc0b5f66b4fc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId { ElementId::NamedInteger(name.into(), id.as_u64() as usize) } } + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} From 7b4b068230cee43d4dbdfb40aed89b5776e887b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:40:30 -0800 Subject: [PATCH 22/78] Render chat and notes buttons below the current channel --- crates/collab_ui2/src/collab_panel.rs | 111 ++++---------------------- 1 file changed, 16 insertions(+), 95 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index bdddc8288af9dfd6780a72a7893e9bf371d65200..4ce04b131be561054f5d6cc5db83dc4c1710feb0 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1360,54 +1360,14 @@ impl CollabPanel { channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { - // enum ChannelNotes {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // false, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("notes", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - - div() + ListItem::new("channel-notes") + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx); + })) + .left_child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::File)) + .child(Label::new("notes")) + .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } fn render_channel_chat( @@ -1415,53 +1375,14 @@ impl CollabPanel { channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { - // enum ChannelChat {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // true, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/conversations.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("chat", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - div() + ListItem::new("channel-chat") + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx); + })) + .left_child(render_tree_branch(true, cx)) + .child(IconButton::new(0, Icon::MessageBubbles)) + .child(Label::new("chat")) + .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } // fn render_channel_invite( From 27703a327912b7cd2faedb21e3606ad47567f281 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:04:12 -0500 Subject: [PATCH 23/78] Update tabs rendering, fix tab spacing bug Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/editor2/src/items.rs | 43 +++--- crates/ui2/src/components/indicator.rs | 3 +- crates/workspace2/src/pane.rs | 178 ++++++++++++------------- crates/workspace2/src/toolbar.rs | 6 +- 4 files changed, 114 insertions(+), 116 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 93bb37c6222932f395d738e4a8bac9ec20d7076c..1d1740bb1bc729dd84b90077c1982f3349ea8a28 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -32,7 +32,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Color, Label}; +use ui::{h_stack, Color, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, @@ -586,28 +586,25 @@ impl Item for Editor { fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { let theme = cx.theme(); - AnyElement::new( - div() - .flex() - .flex_row() - .items_center() - .gap_2() - .child(Label::new(self.title(cx).to_string())) - .children(detail.and_then(|detail| { - let path = path_for_buffer(&self.buffer, detail, false, cx)?; - let description = path.to_string_lossy(); - - Some( - div().child( - Label::new(util::truncate_and_trailoff( - &description, - MAX_TAB_TITLE_LEN, - )) - .color(Color::Muted), - ), - ) - })), - ) + let description = detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + let description = description.trim(); + + if description.is_empty() { + return None; + } + + Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + }); + + h_stack() + .gap_2() + .child(Label::new(self.title(cx).to_string())) + .when_some(description, |this, description| { + this.child(Label::new(description).color(Color::Muted)) + }) + .into_any_element() } fn for_each_project_item( diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs index af62f9d989c12f1420afe5d054a7305cfd6e3a2b..4a94650dfc6cdd4a17290413587c2fd17898e20f 100644 --- a/crates/ui2/src/components/indicator.rs +++ b/crates/ui2/src/components/indicator.rs @@ -1,4 +1,4 @@ -use gpui::{AnyView, Div, Position}; +use gpui::{Div, Position}; use crate::prelude::*; @@ -49,6 +49,7 @@ impl RenderOnce for Indicator { fn render(self, cx: &mut WindowContext) -> Self::Rendered { div() + .flex_none() .map(|this| match self.style { IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(), IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(), diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 599c1d88c78d48dcb31a5784ab44afa3d74015c9..a2eb3d41ac28f0852a2c2d4b8872a7982b237852 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,5 +1,5 @@ use crate::{ - item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, + item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, workspace_settings::{AutosaveSetting, WorkspaceSettings}, NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace, @@ -27,8 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, IconSize, Label, - Tooltip, + h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, + Indicator, Label, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1416,39 +1416,7 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let label = item.tab_content(Some(detail), cx); - let close_right = ItemSettings::get_global(cx).close_position.right(); - - let close_icon = || { - let id = item.item_id(); - - h_stack() - .id(ix) - .justify_center() - .w_4() - .h_4() - .rounded_md() - .absolute() - .map(|this| { - if close_right { - this.right_1() - } else { - this.left_1() - } - }) - .invisible() - .group_hover("", |style| style.visible()) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .on_click(cx.listener(move |pane, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - })) - .child( - IconElement::new(Icon::Close) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - }; + let close_side = &ItemSettings::get_global(cx).close_position; let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( @@ -1467,82 +1435,114 @@ impl Pane { let is_active = ix == self.active_item_index; - let tab = h_stack() - .group("") - .id(ix) - .relative() - .cursor_pointer() - .when_some(item.tab_tooltip_text(cx), |div, text| { - div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) - }) - .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))) - // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) - // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) - // .on_drop(|_view, state: View, cx| { - // eprintln!("{:?}", state.read(cx)); - // }) - .flex() - .items_center() - .justify_center() - .px_5() - .h(rems(1.875)) - .bg(tab_bg) + let indicator = { + let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + + h_stack() + .w_3() + .h_3() + .justify_center() + .absolute() + .bg(gpui::red()) + .map(|this| match close_side { + ClosePosition::Left => this.right_1(), + ClosePosition::Right => this.left_1(), + }) + .when_some(indicator_color, |this, indicator_color| { + this.child(Indicator::dot().color(indicator_color)) + }) + }; + + let close_button = { + let id = item.item_id(); + + h_stack() + .invisible() + .w_3() + .h_3() + .justify_center() + .absolute() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .group_hover("", |style| style.visible()) + .child( + // TODO: Fix button size + IconButton::new("close tab", Icon::Close) + .icon_color(Color::Muted) + .size(ButtonSize::None) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(move |pane, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + })), + ) + }; + + let tab = div() .border_color(cx.theme().colors().border) - .text_color(if is_active { - cx.theme().colors().text - } else { - cx.theme().colors().text_muted - }) + .bg(tab_bg) + // 30px @ 16px/rem + .h(rems(1.875)) .map(|this| { let is_first_item = ix == 0; let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { cmp::Ordering::Less => { if is_first_item { - this.ml_px().mr_px().border_b() + this.pl_px().pr_px().border_b() } else { - this.border_l().mr_px().border_b() + this.border_l().pr_px().border_b() } } cmp::Ordering::Greater => { if is_last_item { - this.mr_px().ml_px().border_b() + this.pr_px().pl_px().border_b() } else { - this.border_r().ml_px().border_b() + this.border_r().pl_px().border_b() } } cmp::Ordering::Equal => { if is_first_item { - this.ml_px().border_r().mb_px() + this.pl_px().border_r().pb_px() } else { - this.border_l().border_r().mb_px() + this.border_l().border_r().pb_px() } } } }) - // .hover(|h| h.bg(tab_hover_bg)) - // .active(|a| a.bg(tab_active_bg)) - .gap_1() - .text_color(text_color) - .children( - item.has_conflict(cx) - .then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Warning), - ) + .child( + h_stack() + .group("") + .id(ix) + .relative() + .h_full() + .cursor_pointer() + .when_some(item.tab_tooltip_text(cx), |div, text| { + div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) }) - .or(item.is_dirty(cx).then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Info), - ) - })), - ) - .child(label) - .child(close_icon()); + .on_click( + cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)), + ) + // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) + // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) + // .on_drop(|_view, state: View, cx| { + // eprintln!("{:?}", state.read(cx)); + // }) + .px_5() + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) + .gap_1() + .text_color(text_color) + .child(indicator) + .child(close_button) + .child(div().bg(gpui::green()).child(label)), + ); right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d47e99cb2027bfea51775e2060a4ea948342d0e7..1cc71e4d849e0d43bc94782d547e9289da180011 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,10 +1,10 @@ use crate::ItemHandle; use gpui::{ - div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, + AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext, WindowContext, }; -use ui::{h_stack, v_stack, Icon, IconButton}; -use ui::{prelude::*, Tooltip}; +use ui::prelude::*; +use ui::{h_stack, v_stack}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), From dc7e4a4b1750d645bca1f598fec688236d8cac50 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:09:29 -0500 Subject: [PATCH 24/78] Remove debugging colors Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/workspace2/src/pane.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index a2eb3d41ac28f0852a2c2d4b8872a7982b237852..8c7d860a8edbf01ee19f013d1ed83d75f95ee282 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1447,7 +1447,6 @@ impl Pane { .h_3() .justify_center() .absolute() - .bg(gpui::red()) .map(|this| match close_side { ClosePosition::Left => this.right_1(), ClosePosition::Right => this.left_1(), @@ -1541,7 +1540,7 @@ impl Pane { .text_color(text_color) .child(indicator) .child(close_button) - .child(div().bg(gpui::green()).child(label)), + .child(label), ); right_click_menu(ix).trigger(tab).menu(|cx| { @@ -1588,7 +1587,6 @@ impl Pane { .border_b() .border_r() .border_color(cx.theme().colors().border) - .bg(gpui::red()) // Nav Buttons .child( IconButton::new("navigate_backward", Icon::ArrowLeft) @@ -1615,7 +1613,6 @@ impl Pane { .flex_1() .h_full() .overflow_hidden_x() - .bg(gpui::green()) .child( div() .absolute() @@ -1639,7 +1636,6 @@ impl Pane { // Right Side .child( h_stack() - .bg(gpui::blue()) .flex() .flex_none() .gap_1() From 4c4b235b137d3c52086d31e356485e144cf892a8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:09:42 -0500 Subject: [PATCH 25/78] make ci happy Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/quick_action_bar2/src/quick_action_bar.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 6b8f15d4c95f361ab2e881ab32174b474f16713a..3232de08adea814fcf33e7fc54b598a93bf18c25 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -5,7 +5,7 @@ use gpui::{ Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, Styled, Subscription, View, ViewContext, WeakView, }; -use search::{buffer_search, BufferSearchBar}; +use search::BufferSearchBar; use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -15,6 +15,7 @@ pub struct QuickActionBar { buffer_search_bar: View, active_item: Option>, _inlay_hints_enabled_subscription: Option, + #[allow(unused)] workspace: WeakView, } @@ -28,6 +29,7 @@ impl QuickActionBar { } } + #[allow(dead_code)] fn active_editor(&self) -> Option> { self.active_item .as_ref() @@ -172,6 +174,7 @@ impl QuickActionBarButton { } } + #[allow(dead_code)] pub fn meta(mut self, meta: Option>) -> Self { self.tooltip_meta = meta.map(|meta| meta.into()); self From 8141f4fd86ca06f95e0cb04875dfbe0fd5c7600b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 14:17:22 -0500 Subject: [PATCH 26/78] Format code --- crates/theme2/src/default_theme.rs | 4 +++- crates/theme2/src/registry.rs | 4 ++-- crates/zed2/src/zed2.rs | 7 +++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 269414b36a0747e5e1bfed677bfb69378ec2ab03..ab953b121a714f1bb7454c096b777f1b742b4827 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,8 +1,10 @@ use std::sync::Arc; use crate::{ + default_color_scales, one_themes::{one_dark, one_family}, - Theme, ThemeFamily, Appearance, ThemeStyles, SystemColors, ThemeColors, StatusColors, PlayerColors, SyntaxTheme, default_color_scales, + Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, + ThemeFamily, ThemeStyles, }; fn zed_pro_daylight() -> Theme { diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 8e2a4d401fd201515baa5bfd42d4d2a506798b93..cb7814cb6fb88886cb95490544c00f0dc7f5612e 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, - Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, zed_pro_family, + one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, + SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index ec9d76449b6a911ff77deea1880669d17427af9b..8ce1d1d90e80d9a838f8fc5d3fb55edfc2e85b75 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -102,10 +102,9 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let quick_action_bar = cx.build_view(|_| { - QuickActionBar::new(buffer_search_bar, workspace) - }); - toolbar.add_item(quick_action_bar, cx); + let quick_action_bar = cx + .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); + toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); From 631e264e3ccdd9716180e8cbb9479700d1923540 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 13:17:59 -0700 Subject: [PATCH 27/78] Start on app menus --- crates/gpui2/src/app.rs | 40 ++++ crates/gpui2/src/key_dispatch.rs | 20 +- crates/gpui2/src/platform.rs | 4 +- crates/gpui2/src/platform/app_menu.rs | 96 +++++++++ crates/gpui2/src/platform/mac/platform.rs | 230 +++++++++++---------- crates/gpui2/src/platform/mac/window.rs | 2 +- crates/gpui2/src/platform/test/platform.rs | 2 +- crates/gpui2/src/window.rs | 4 +- 8 files changed, 277 insertions(+), 121 deletions(-) create mode 100644 crates/gpui2/src/platform/app_menu.rs diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..8d4dc371e6a3bfd560046db2f562cb9f8ebf991d 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -425,6 +425,10 @@ impl AppContext { .collect() } + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + /// Opens a new window with the given option and the root view returned by the given function. /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// functionality. @@ -1015,6 +1019,42 @@ impl AppContext { activate(); subscription } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, cx| { + cx.window + .current_frame + .dispatch_tree + .clear_pending_keystrokes() + }) + .ok(); + } + } + + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + if let Some(window) = self.active_window() { + let window_action_available = window + .update(self, |_, cx| { + if let Some(focus_id) = cx.window.focus { + cx.window + .current_frame + .dispatch_tree + .is_action_available(action, focus_id) + } else { + false + } + }) + .unwrap_or(false); + if window_action_available { + return true; + } + } + + self.global_action_listeners + .contains_key(&action.as_any().type_id()) + } } impl Context for AppContext { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 4838b1a612ce65ba33c03ac25da878a752f716d3..0df052dfdf9e0f066666f56a94295d803dfab613 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -82,13 +82,13 @@ impl DispatchTree { } } - pub fn clear_keystroke_matchers(&mut self) { + pub fn clear_pending_keystrokes(&mut self) { self.keystroke_matchers.clear(); } /// Preserve keystroke matchers from previous frames to support multi-stroke /// bindings across multiple frames. - pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option) { + pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option) { if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { let dispatch_path = self.dispatch_path(node_id); @@ -163,6 +163,22 @@ impl DispatchTree { actions } + pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool { + if let Some(node) = self.focusable_node_ids.get(&target) { + for node_id in self.dispatch_path(*node) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + } + false + } + pub fn bindings_for_action( &self, action: &dyn Action, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 40c555301bdf00cbad7d2ef36b2c99511daa6d90..5d3a92f052eb8eafebe7e93b2d47e82c586c270a 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -1,3 +1,4 @@ +mod app_menu; mod keystroke; #[cfg(target_os = "macos")] mod mac; @@ -32,6 +33,7 @@ use std::{ }; use uuid::Uuid; +pub use app_menu::*; pub use keystroke::*; #[cfg(target_os = "macos")] pub use mac::*; @@ -59,7 +61,7 @@ pub trait Platform: 'static { fn displays(&self) -> Vec>; fn display(&self, id: DisplayId) -> Option>; - fn main_window(&self) -> Option; + fn active_window(&self) -> Option; fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f784f6585f9233c953bad50dbd6d6a2bc3207f9 --- /dev/null +++ b/crates/gpui2/src/platform/app_menu.rs @@ -0,0 +1,96 @@ +use crate::{Action, AppContext, Platform}; +use util::ResultExt; + +pub struct Menu<'a> { + pub name: &'a str, + pub items: Vec>, +} + +pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), + Action { + name: &'a str, + action: Box, + os_action: Option, + }, +} + +impl<'a> MenuItem<'a> { + pub fn separator() -> Self { + Self::Separator + } + + pub fn submenu(menu: Menu<'a>) -> Self { + Self::Submenu(menu) + } + + pub fn action(name: &'a str, action: impl Action) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: None, + } + } + + pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: Some(os_action), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + Cut, + Copy, + Paste, + SelectAll, + Undo, + Redo, +} + +pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { + platform.on_will_open_menu(Box::new({ + let cx = cx.to_async(); + move || { + cx.update(|cx| cx.clear_pending_keystrokes()).ok(); + } + })); + + platform.on_validate_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.is_action_available(action)) + .unwrap_or(false) + } + })); + + platform.on_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| { + // if let Some(main_window) = cx.active_window() { + // let dispatched = main_window + // .update(&mut *cx, |cx| { + // if let Some(view_id) = cx.focused_view_id() { + // cx.dispatch_action(Some(view_id), action); + // true + // } else { + // false + // } + // }) + // .unwrap_or(false); + + // if dispatched { + // return; + // } + // } + // cx.dispatch_global_action_any(action); + }) + .log_err(); + } + })); +} diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 9d02c8fb938e9b6f4cd1014e5ebce4f037911bcd..6dae0afc0e59af2abaeba861bb6c22638d3b0847 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,16 +1,17 @@ use super::BoolExt; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, - MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, - Result, SemanticVersion, VideoTimestamp, WindowOptions, + ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker, + MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow, + NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, + NSSavePanel, NSWindow, }, base::{id, nil, BOOL, YES}, foundation::{ @@ -237,114 +238,115 @@ impl MacPlatform { // application_menu // } - // unsafe fn create_menu_item( - // &self, - // item: MenuItem, - // delegate: id, - // actions: &mut Vec>, - // keystroke_matcher: &KeymapMatcher, - // ) -> id { - // match item { - // MenuItem::Separator => NSMenuItem::separatorItem(nil), - // MenuItem::Action { - // name, - // action, - // os_action, - // } => { - // // TODO - // let keystrokes = keystroke_matcher - // .bindings_for_action(action.id()) - // .find(|binding| binding.action().eq(action.as_ref())) - // .map(|binding| binding.keystrokes()); - // let selector = match os_action { - // Some(crate::OsAction::Cut) => selector("cut:"), - // Some(crate::OsAction::Copy) => selector("copy:"), - // Some(crate::OsAction::Paste) => selector("paste:"), - // Some(crate::OsAction::SelectAll) => selector("selectAll:"), - // Some(crate::OsAction::Undo) => selector("undo:"), - // Some(crate::OsAction::Redo) => selector("redo:"), - // None => selector("handleGPUIMenuItem:"), - // }; - - // let item; - // if let Some(keystrokes) = keystrokes { - // if keystrokes.len() == 1 { - // let keystroke = &keystrokes[0]; - // let mut mask = NSEventModifierFlags::empty(); - // for (modifier, flag) in &[ - // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), - // ] { - // if *modifier { - // mask |= *flag; - // } - // } - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(key_to_native(&keystroke.key).as_ref()), - // ) - // .autorelease(); - // item.setKeyEquivalentModifierMask_(mask); - // } - // // For multi-keystroke bindings, render the keystroke as part of the title. - // else { - // use std::fmt::Write; - - // let mut name = format!("{name} ["); - // for (i, keystroke) in keystrokes.iter().enumerate() { - // if i > 0 { - // name.push(' '); - // } - // write!(&mut name, "{}", keystroke).unwrap(); - // } - // name.push(']'); - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(&name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - // } else { - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - - // let tag = actions.len() as NSInteger; - // let _: () = msg_send![item, setTag: tag]; - // actions.push(action); - // item - // } - // MenuItem::Submenu(Menu { name, items }) => { - // let item = NSMenuItem::new(nil).autorelease(); - // let submenu = NSMenu::new(nil).autorelease(); - // submenu.setDelegate_(delegate); - // for item in items { - // submenu.addItem_(self.create_menu_item( - // item, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - // item.setSubmenu_(submenu); - // item.setTitle_(ns_string(name)); - // item - // } - // } - // } + unsafe fn create_menu_item( + &self, + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keystroke_matcher: &KeystrokeMatcher, + ) -> id { + todo!() + // match item { + // MenuItem::Separator => NSMenuItem::separatorItem(nil), + // MenuItem::Action { + // name, + // action, + // os_action, + // } => { + // // TODO + // let keystrokes = keystroke_matcher + // .bindings_for_action(action.id()) + // .find(|binding| binding.action().eq(action.as_ref())) + // .map(|binding| binding.keystrokes()); + // let selector = match os_action { + // Some(crate::OsAction::Cut) => selector("cut:"), + // Some(crate::OsAction::Copy) => selector("copy:"), + // Some(crate::OsAction::Paste) => selector("paste:"), + // Some(crate::OsAction::SelectAll) => selector("selectAll:"), + // Some(crate::OsAction::Undo) => selector("undo:"), + // Some(crate::OsAction::Redo) => selector("redo:"), + // None => selector("handleGPUIMenuItem:"), + // }; + + // let item; + // if let Some(keystrokes) = keystrokes { + // if keystrokes.len() == 1 { + // let keystroke = &keystrokes[0]; + // let mut mask = NSEventModifierFlags::empty(); + // for (modifier, flag) in &[ + // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), + // ] { + // if *modifier { + // mask |= *flag; + // } + // } + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(key_to_native(&keystroke.key).as_ref()), + // ) + // .autorelease(); + // item.setKeyEquivalentModifierMask_(mask); + // } + // // For multi-keystroke bindings, render the keystroke as part of the title. + // else { + // use std::fmt::Write; + + // let mut name = format!("{name} ["); + // for (i, keystroke) in keystrokes.iter().enumerate() { + // if i > 0 { + // name.push(' '); + // } + // write!(&mut name, "{}", keystroke).unwrap(); + // } + // name.push(']'); + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(&name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + // } else { + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + + // let tag = actions.len() as NSInteger; + // let _: () = msg_send![item, setTag: tag]; + // actions.push(action); + // item + // } + // MenuItem::Submenu(Menu { name, items }) => { + // let item = NSMenuItem::new(nil).autorelease(); + // let submenu = NSMenu::new(nil).autorelease(); + // submenu.setDelegate_(delegate); + // for item in items { + // submenu.addItem_(self.create_menu_item( + // item, + // delegate, + // actions, + // keystroke_matcher, + // )); + // } + // item.setSubmenu_(submenu); + // item.setTitle_(ns_string(name)); + // item + // } + // } + } } impl Platform for MacPlatform { @@ -479,8 +481,8 @@ impl Platform for MacPlatform { MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) } - fn main_window(&self) -> Option { - MacWindow::main_window() + fn active_window(&self) -> Option { + MacWindow::active_window() } fn open_window( diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index 5b72c10851ff555b08669d8db96e143509e8ad46..ba9a67e1580c16dfdbfc0849d8d1678af308c96a 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -662,7 +662,7 @@ impl MacWindow { } } - pub fn main_window() -> Option { + pub fn active_window() -> Option { unsafe { let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 6fa706f617119bfad728078a378309c16ab19df4..264273730510365d319120e3faaa0efd35076a1d 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -127,7 +127,7 @@ impl Platform for TestPlatform { self.displays().iter().find(|d| d.id() == id).cloned() } - fn main_window(&self) -> Option { + fn active_window(&self) -> Option { unimplemented!() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..8645554e5af83600b6ff6438869048fee09b46d1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .clear_keystroke_matchers(); + .clear_pending_keystrokes(); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(focus_id), @@ -1177,7 +1177,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .preserve_keystroke_matchers( + .preserve_pending_keystrokes( &mut self.window.previous_frame.dispatch_tree, self.window.focus, ); From 863222edc5524e864cae584d918854bb4708d217 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 12:57:23 -0800 Subject: [PATCH 28/78] Get following working Restore a single event type on Item trait, so that the workspace can subscribe to it and handle following events. --- crates/diagnostics2/src/diagnostics.rs | 47 +++----------- crates/editor2/src/editor.rs | 27 ++------ crates/editor2/src/editor_tests.rs | 4 +- crates/editor2/src/items.rs | 75 +++++++++++++++------- crates/terminal_view2/src/terminal_view.rs | 6 ++ crates/welcome2/src/welcome.rs | 6 ++ crates/workspace2/src/item.rs | 48 +++++++------- crates/workspace2/src/shared_screen.rs | 10 ++- crates/workspace2/src/workspace2.rs | 4 -- 9 files changed, 117 insertions(+), 110 deletions(-) diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index dd01f90b9f0623b3658673304464229411e3b801..44acc285e8231a48e20c791f38f09f6619e99d08 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -88,7 +88,7 @@ struct DiagnosticGroupState { block_count: usize, } -impl EventEmitter for ProjectDiagnosticsEditor {} +impl EventEmitter for ProjectDiagnosticsEditor {} impl Render for ProjectDiagnosticsEditor { type Element = Focusable
; @@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor { }); let editor_event_subscription = cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - Self::emit_item_event_for_editor_event(event, cx); + cx.emit(event.clone()); if event == &EditorEvent::Focused && this.path_states.is_empty() { cx.focus(&this.focus_handle); } @@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor { this } - fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext) { - match event { - EditorEvent::Closed => cx.emit(ItemEvent::CloseItem), - - EditorEvent::Saved | EditorEvent::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::SelectionsChanged { local } if *local => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - - EditorEvent::BufferEdited => { - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { - cx.emit(ItemEvent::Edit); - } - - _ => {} - } - } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, cx); @@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor { this.update(&mut cx, |this, cx| { this.summary = this.project.read(cx).diagnostic_summary(false, cx); - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })?; anyhow::Ok(()) } @@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor { } impl Item for ProjectDiagnosticsEditor { + type Event = EditorEvent; + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + fn deactivated(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648ab0a1a2495f60a261112ef73847d90b..a77e1dcc3b9621080e4e78ef94f80fff2c18112e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1675,8 +1675,7 @@ impl Editor { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { project_subscriptions.push(cx.observe(project, |_, _, cx| { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { @@ -2141,10 +2140,6 @@ impl Editor { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - cx.notify(); } @@ -8573,8 +8568,6 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(EditorEvent::BufferEdited); - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(SearchEvent::MatchesInvalidated); if *sigleton_buffer_edited { @@ -8622,20 +8615,14 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) } - multi_buffer::Event::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - multi_buffer::Event::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - multi_buffer::Event::Saved - | multi_buffer::Event::FileHandleChanged - | multi_buffer::Event::Reloaded => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { + cx.emit(EditorEvent::TitleChanged) } multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), - multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 424da8987eb6d673f0e789d4b8ae8b1620967045..571cbd84bb179be0b1562dd07f5c7a0114e1b8e4 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -32,7 +32,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -6478,7 +6478,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { cx.subscribe( &follower.root_view(cx).unwrap(), move |_, _, event: &EditorEvent, cx| { - if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { + if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) { *is_still_following.borrow_mut() = false; } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 93bb37c6222932f395d738e4a8bac9ec20d7076c..b5eb99a32da740bd16cc89ff3c45ca4527f52d45 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -35,7 +35,7 @@ use theme::{ActiveTheme, Theme}; use ui::{Color, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ - item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, + item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, StatusItemView, }; use workspace::{ @@ -46,27 +46,7 @@ use workspace::{ pub const MAX_TAB_TITLE_LEN: usize = 24; -impl FollowableEvents for EditorEvent { - fn to_follow_event(&self) -> Option { - match self { - EditorEvent::Edited => Some(FollowEvent::Unfollow), - EditorEvent::SelectionsChanged { local } - | EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - Some(FollowEvent::Unfollow) - } else { - None - } - } - _ => None, - } - } -} - -impl EventEmitter for Editor {} - impl FollowableItem for Editor { - type FollowableEvent = EditorEvent; fn remote_id(&self) -> Option { self.remote_id } @@ -241,9 +221,24 @@ impl FollowableItem for Editor { })) } + fn to_follow_event(event: &EditorEvent) -> Option { + match event { + EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::SelectionsChanged { local } + | EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + Some(FollowEvent::Unfollow) + } else { + None + } + } + _ => None, + } + } + fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &EditorEvent, update: &mut Option, cx: &WindowContext, ) -> bool { @@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) } impl Item for Editor { + type Event = EditorEvent; + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { let newest_selection = self.selections.newest::(cx); @@ -841,6 +838,40 @@ impl Item for Editor { Some("Editor") } + fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { + match event { + EditorEvent::Closed => f(ItemEvent::CloseItem), + + EditorEvent::Saved | EditorEvent::TitleChanged => { + f(ItemEvent::UpdateTab); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::Reparsed => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::SelectionsChanged { local } if *local => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::DirtyChanged => { + f(ItemEvent::UpdateTab); + } + + EditorEvent::BufferEdited => { + f(ItemEvent::Edit); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { + f(ItemEvent::Edit); + } + + _ => {} + } + } + fn deserialize( project: Model, _workspace: WeakView, diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index e184fa68762b3480732c222f713069b517b8412b..570b37ba098b86c159b7acce1e6941402336ec97 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -736,6 +736,8 @@ impl InputHandler for TerminalView { } impl Item for TerminalView { + type Event = ItemEvent; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(self.terminal().read(cx).title().into()) } @@ -843,6 +845,10 @@ impl Item for TerminalView { // .detach(); self.workspace_id = workspace.database_id(); } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } } impl SearchableItem for TerminalView { diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs index 441c2bf69663084e40d354eccefb8d7bbb66ce49..db348ab0a1a7115586f38ffb7acb37671c4b15a9 100644 --- a/crates/welcome2/src/welcome.rs +++ b/crates/welcome2/src/welcome.rs @@ -259,6 +259,8 @@ impl FocusableView for WelcomePage { } impl Item for WelcomePage { + type Event = ItemEvent; + fn tab_content(&self, _: Option, _: &WindowContext) -> AnyElement { "Welcome to Zed!".into_any() } @@ -278,4 +280,8 @@ impl Item for WelcomePage { _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), })) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } } diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index e7cdb2f861b9f911019ed1b191653885d76b6402..536ebd980e6cc66fae6cb56d15f0bdead58fda1d 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -78,7 +78,7 @@ impl Settings for ItemSettings { } } -#[derive(Eq, PartialEq, Hash, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, UpdateTab, @@ -92,7 +92,9 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: FocusableView + EventEmitter { +pub trait Item: FocusableView + EventEmitter { + type Event; + fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { @@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter { unimplemented!("reload() must be implemented if can_save() returns true") } + fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent)); + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter { } pub trait ItemHandle: 'static + Send { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; @@ -285,20 +289,20 @@ impl dyn ItemHandle { } impl ItemHandle for View { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { - self.focus_handle(cx) - } - fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { - handler(event, cx); + T::to_item_events(event, |item_event| handler(item_event, cx)); }) } + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.focus_handle(cx) + } + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { self.read(cx).tab_tooltip_text(cx) } @@ -461,7 +465,7 @@ impl ItemHandle for View { } } - match event { + T::to_item_events(event, |event| match event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx) @@ -489,7 +493,7 @@ impl ItemHandle for View { } _ => {} - } + }); })); cx.on_blur(&self.focus_handle(cx), move |workspace, cx| { @@ -655,12 +659,7 @@ pub enum FollowEvent { Unfollow, } -pub trait FollowableEvents { - fn to_follow_event(&self) -> Option; -} - pub trait FollowableItem: Item { - type FollowableEvent: FollowableEvents; fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &WindowContext) -> Option; fn from_state_proto( @@ -670,9 +669,10 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut WindowContext, ) -> Option>>>; + fn to_follow_event(event: &Self::Event) -> Option; fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &Self::Event, update: &mut Option, cx: &WindowContext, ) -> bool; @@ -683,7 +683,6 @@ pub trait FollowableItem: Item { cx: &mut ViewContext, ) -> Task>; fn is_project_item(&self, cx: &WindowContext) -> bool; - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); } @@ -739,10 +738,7 @@ impl FollowableItemHandle for View { } fn to_follow_event(&self, event: &dyn Any) -> Option { - event - .downcast_ref() - .map(T::FollowableEvent::to_follow_event) - .flatten() + T::to_follow_event(event.downcast_ref()?) } fn apply_update_proto( @@ -929,6 +925,12 @@ pub mod test { } impl Item for TestItem { + type Event = ItemEvent; + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } + fn tab_description(&self, detail: usize, _: &AppContext) -> Option { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs index c4bcb31958afcaf3e69b37ea116df7baa9a91f41..134dfc66bb82a42867c7fdb9d32b4cca359a0337 100644 --- a/crates/workspace2/src/shared_screen.rs +++ b/crates/workspace2/src/shared_screen.rs @@ -59,7 +59,6 @@ impl SharedScreen { } impl EventEmitter for SharedScreen {} -impl EventEmitter for SharedScreen {} impl FocusableView for SharedScreen { fn focus_handle(&self, _: &AppContext) -> FocusHandle { @@ -79,9 +78,12 @@ impl Render for SharedScreen { } impl Item for SharedScreen { + type Event = Event; + fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some(format!("{}'s screen", self.user.github_login).into()) } + fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_mut() { nav_history.push::<()>(None, cx); @@ -111,4 +113,10 @@ impl Item for SharedScreen { let track = self.track.upgrade()?; Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e6b259eaf65983878902428b976ee2a14220703d..3780f56b843103438e7646311435516675cfd734 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2625,8 +2625,6 @@ impl Workspace { update: proto::UpdateFollowers, cx: &mut AsyncWindowContext, ) -> Result<()> { - dbg!("process_leader_update", &update); - match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(cx, |this, _| { @@ -3880,8 +3878,6 @@ impl WorkspaceStore { let leader_id = envelope.original_sender_id()?; let update = envelope.payload; - dbg!("handle_upate_followers"); - this.update(&mut cx, |this, cx| { for workspace in &this.workspaces { workspace.update(cx, |workspace, cx| { From f2faa70f736252f7377935dee2fe72f7801a40de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 13:34:12 -0800 Subject: [PATCH 29/78] Make Window::on_next_frame work in tests --- .../gpui2/src/platform/mac/display_linker.rs | 3 +- crates/gpui2/src/platform/test/platform.rs | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/gpui2/src/platform/mac/display_linker.rs b/crates/gpui2/src/platform/mac/display_linker.rs index b63cf24e2689d1d249016d86b82a62ffd2d3946b..d8f5a675a58f4204e644f5661dfc5078b9ae4295 100644 --- a/crates/gpui2/src/platform/mac/display_linker.rs +++ b/crates/gpui2/src/platform/mac/display_linker.rs @@ -7,6 +7,7 @@ use std::{ use crate::DisplayId; use collections::HashMap; use parking_lot::Mutex; +pub use sys::CVSMPTETime as SmtpeTime; pub use sys::CVTimeStamp as VideoTimestamp; pub(crate) struct MacDisplayLinker { @@ -153,7 +154,7 @@ mod sys { kCVTimeStampTopField | kCVTimeStampBottomField; #[repr(C)] - #[derive(Clone, Copy)] + #[derive(Clone, Copy, Default)] pub struct CVSMPTETime { pub subframes: i16, pub subframe_divisor: i16, diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index fa4b6e18c587521d88d8d4a4fd4041952c584f4a..2cbc228c72b2c51761982d0eb0e70f5a7450e5b4 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -147,18 +147,25 @@ impl Platform for TestPlatform { fn set_display_link_output_callback( &self, _display_id: DisplayId, - _callback: Box, + mut callback: Box, ) { - unimplemented!() - } - - fn start_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } - - fn stop_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } + let timestamp = crate::VideoTimestamp { + version: 0, + video_time_scale: 0, + video_time: 0, + host_time: 0, + rate_scalar: 0.0, + video_refresh_period: 0, + smpte_time: crate::SmtpeTime::default(), + flags: 0, + reserved: 0, + }; + callback(×tamp, ×tamp) + } + + fn start_display_link(&self, _display_id: DisplayId) {} + + fn stop_display_link(&self, _display_id: DisplayId) {} fn open_url(&self, _url: &str) { unimplemented!() From 02e507b97384e97493ffe0092bf009e6a6601987 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 16:34:21 -0500 Subject: [PATCH 30/78] Update breadcrumb rendering (#3505) This PR updates the rendering of the breadcrumb. Release Notes: - N/A Co-authored-by: Nate Butler --- crates/breadcrumbs2/src/breadcrumbs.rs | 81 +++++++++++++------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs index 75195a315930e2525ed1b01aea36f57e6d30b699..1c577fa3105ea2d5b3d1aa81aa070dce136d3d06 100644 --- a/crates/breadcrumbs2/src/breadcrumbs.rs +++ b/crates/breadcrumbs2/src/breadcrumbs.rs @@ -1,10 +1,10 @@ use gpui::{ - Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, ViewContext, WeakView, }; use itertools::Itertools; use theme::ActiveTheme; -use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label}; +use ui::{prelude::*, ButtonLike, ButtonStyle, Label}; use workspace::{ item::{ItemEvent, ItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -36,54 +36,51 @@ impl EventEmitter for Breadcrumbs {} impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { - type Element = Component; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let button = ButtonLike::new("breadcrumbs") - .style(ButtonStyle::Transparent) - .disabled(true); + let element = h_stack().text_ui(); + + let Some(active_item) = &self + .active_item + .as_ref() + .filter(|item| item.downcast::().is_some()) + else { + return element; + }; - let active_item = match &self.active_item { - Some(active_item) => active_item, - None => return button.into_element(), + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element; }; - let not_editor = active_item.downcast::().is_none(); - let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) { - Some(breadcrumbs) => breadcrumbs, - None => return button.into_element(), - } - .into_iter() - .map(|breadcrumb| { - StyledText::new(breadcrumb.text) - .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default()) + let highlighted_segments = segments.into_iter().map(|segment| { + StyledText::new(segment.text) + .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default()) .into_any() }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").into_any_element() + }); - let button = button.children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" › ").into_any_element() - })); - - if not_editor || !self.pane_focused { - return button.into_element(); - } - - // let this = cx.view().downgrade(); - button - .style(ButtonStyle::Filled) - .disabled(false) - .on_click(move |_, _cx| { - todo!("outline::toggle"); - // this.update(cx, |this, cx| { - // if let Some(workspace) = this.workspace.upgrade() { - // workspace.update(cx, |_workspace, _cx| { - // outline::toggle(workspace, &Default::default(), cx) - // }) - // } - // }) - // .ok(); - }) - .into_element() + element.child( + ButtonLike::new("toggle outline view") + .style(ButtonStyle::Subtle) + .child(h_stack().gap_1().children(breadcrumbs)) + // We disable the button when it is not focused + // due to ... @julia what was the reason again? + .disabled(!self.pane_focused) + .on_click(move |_, _cx| { + todo!("outline::toggle"); + // this.update(cx, |this, cx| { + // if let Some(workspace) = this.workspace.upgrade() { + // workspace.update(cx, |_workspace, _cx| { + // outline::toggle(workspace, &Default::default(), cx) + // }) + // } + // }) + // .ok(); + }), + ) } } From 79567d1c87cf9cd7c46fa3cc92561dfd3911204e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 15:49:06 -0700 Subject: [PATCH 31/78] Add AppContext::dispatch_action and use it for app menu actions Co-Authored-By: Marshall Co-Authored-By: Julia --- crates/gpui2/src/app.rs | 61 +++++++++++++++++++++- crates/gpui2/src/platform.rs | 6 +-- crates/gpui2/src/platform/app_menu.rs | 27 ++-------- crates/gpui2/src/platform/mac/platform.rs | 6 +-- crates/gpui2/src/platform/test/platform.rs | 6 +-- 5 files changed, 73 insertions(+), 33 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 8d4dc371e6a3bfd560046db2f562cb9f8ebf991d..74712feb287468ef67917235b544ffd85ecf8400 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -39,7 +39,10 @@ use std::{ sync::{atomic::Ordering::SeqCst, Arc}, time::Duration, }; -use util::http::{self, HttpClient}; +use util::{ + http::{self, HttpClient}, + ResultExt, +}; /// Temporary(?) wrapper around RefCell to help us debug any double borrows. /// Strongly consider removing after stabilization. @@ -1055,6 +1058,62 @@ impl AppContext { self.global_action_listeners .contains_key(&action.as_any().type_id()) } + + pub fn dispatch_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action, DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .log_err(); + } + } + + if self.propagate_event { + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action, DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + } } impl Context for AppContext { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 5d3a92f052eb8eafebe7e93b2d47e82c586c270a..96be670af7dbc92af50571ab228dcaa663f90a75 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -92,9 +92,9 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); - fn on_menu_command(&self, callback: Box); - fn on_will_open_menu(&self, callback: Box); - fn on_validate_menu_command(&self, callback: Box bool>); + fn on_app_menu_action(&self, callback: Box); + fn on_will_open_app_menu(&self, callback: Box); + fn on_validate_app_menu_command(&self, callback: Box bool>); fn os_name(&self) -> &'static str; fn os_version(&self) -> Result; diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs index 0f784f6585f9233c953bad50dbd6d6a2bc3207f9..bfac1366b286ce58f1c423a2da13157881e7c3c8 100644 --- a/crates/gpui2/src/platform/app_menu.rs +++ b/crates/gpui2/src/platform/app_menu.rs @@ -53,14 +53,14 @@ pub enum OsAction { } pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { - platform.on_will_open_menu(Box::new({ + platform.on_will_open_app_menu(Box::new({ let cx = cx.to_async(); move || { cx.update(|cx| cx.clear_pending_keystrokes()).ok(); } })); - platform.on_validate_menu_command(Box::new({ + platform.on_validate_app_menu_command(Box::new({ let cx = cx.to_async(); move |action| { cx.update(|cx| cx.is_action_available(action)) @@ -68,29 +68,10 @@ pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { } })); - platform.on_menu_command(Box::new({ + platform.on_app_menu_action(Box::new({ let cx = cx.to_async(); move |action| { - cx.update(|cx| { - // if let Some(main_window) = cx.active_window() { - // let dispatched = main_window - // .update(&mut *cx, |cx| { - // if let Some(view_id) = cx.focused_view_id() { - // cx.dispatch_action(Some(view_id), action); - // true - // } else { - // false - // } - // }) - // .unwrap_or(false); - - // if dispatched { - // return; - // } - // } - // cx.dispatch_global_action_any(action); - }) - .log_err(); + cx.update(|cx| cx.dispatch_action(action)).log_err(); } })); } diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 6dae0afc0e59af2abaeba861bb6c22638d3b0847..d7fc37f0def0736d189a891c733ed8e09997be49 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -683,15 +683,15 @@ impl Platform for MacPlatform { } } - fn on_menu_command(&self, callback: Box) { + fn on_app_menu_action(&self, callback: Box) { self.0.lock().menu_command = Some(callback); } - fn on_will_open_menu(&self, callback: Box) { + fn on_will_open_app_menu(&self, callback: Box) { self.0.lock().will_open_menu = Some(callback); } - fn on_validate_menu_command(&self, callback: Box bool>) { + fn on_validate_app_menu_command(&self, callback: Box bool>) { self.0.lock().validate_menu_command = Some(callback); } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 264273730510365d319120e3faaa0efd35076a1d..952a9a96cac98e85863931f3e726d71b85e58896 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -205,15 +205,15 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_menu_command(&self, _callback: Box) { + fn on_app_menu_action(&self, _callback: Box) { unimplemented!() } - fn on_will_open_menu(&self, _callback: Box) { + fn on_will_open_app_menu(&self, _callback: Box) { unimplemented!() } - fn on_validate_menu_command(&self, _callback: Box bool>) { + fn on_validate_app_menu_command(&self, _callback: Box bool>) { unimplemented!() } From 82534b66125286c6355cdb40c19fd698471e8b52 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 16:37:01 -0700 Subject: [PATCH 32/78] Get app menus basically working - Everything is still disabled when there is no active window. Co-Authored-By: Marshall --- crates/editor2/src/display_map/block_map.rs | 8 +- crates/editor2/src/display_map/wrap_map.rs | 2 +- crates/editor2/src/editor_tests.rs | 6 +- crates/gpui2/src/app.rs | 21 +- crates/gpui2/src/app/test_context.rs | 24 +- crates/gpui2/src/platform.rs | 11 +- crates/gpui2/src/platform/app_menu.rs | 2 +- crates/gpui2/src/platform/mac/platform.rs | 341 ++++++++++---------- crates/gpui2/src/platform/test/platform.rs | 9 +- crates/settings2/src/settings_file.rs | 36 --- crates/zed2/src/app_menus.rs | 175 ++++++++++ crates/zed2/src/main.rs | 10 +- crates/zed2/src/zed2.rs | 41 ++- 13 files changed, 447 insertions(+), 239 deletions(-) create mode 100644 crates/zed2/src/app_menus.rs diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 64e46549fd6c7b9ae2576ca68d7c0f2af52b750e..cc0095bca94f30f6d65866c640092cb384d2cce3 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -993,7 +993,7 @@ mod tests { use super::*; use crate::display_map::inlay_map::InlayMap; use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; - use gpui::{div, font, px, Element, Platform as _}; + use gpui::{div, font, px, Element}; use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; @@ -1185,11 +1185,7 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(|cx| init_test(cx)); - let font_id = cx - .test_platform - .text_system() - .font_id(&font("Helvetica")) - .unwrap(); + let font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); let text = "one two three\nfour five six\nseven eight"; diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index a2ac0ec849bfb9b26983c897a2ae3cc2ebd9878c..ca9db7754baec0b1477c19d0f8efaa98fd8f14c3 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1032,7 +1032,7 @@ mod tests { display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, MultiBuffer, }; - use gpui::{font, px, test::observe, Platform}; + use gpui::{font, px, test::observe}; use rand::prelude::*; use settings::SettingsStore; use smol::stream::StreamExt; diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 424da8987eb6d673f0e789d4b8ae8b1620967045..b5f156f494936cba312d9b44365d1a903a803c4e 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -12,7 +12,7 @@ use futures::StreamExt; use gpui::{ div, serde_json::{self, json}, - Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, + Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -3238,9 +3238,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { the lazy dog"}); cx.update_editor(|e, cx| e.copy(&Copy, cx)); assert_eq!( - cx.test_platform - .read_from_clipboard() - .map(|item| item.text().to_owned()), + cx.read_from_clipboard().map(|item| item.text().to_owned()), Some("fox jumps over\n".to_owned()) ); diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 74712feb287468ef67917235b544ffd85ecf8400..79beca75da7f89e83ff2dcd9079f45c29ff153e2 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -15,10 +15,10 @@ use smol::future::FutureExt; pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView, - AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, + current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any, + AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, - ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, + ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId, @@ -278,6 +278,8 @@ impl AppContext { }), }); + init_app_menus(platform.as_ref(), &mut *app.borrow_mut()); + platform.on_quit(Box::new({ let cx = app.clone(); move || { @@ -1059,6 +1061,19 @@ impl AppContext { .contains_key(&action.as_any().type_id()) } + pub fn set_menus(&mut self, menus: Vec) { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| { + cx.platform + .set_menus(menus, Some(&cx.window.current_frame.dispatch_tree)); + }) + .ok(); + } else { + self.platform.set_menus(menus, None); + } + } + pub fn dispatch_action(&mut self, action: &dyn Action) { self.propagate_event = true; diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index a9403de9bce63a62dfedf455d997de34dff4d856..cbd70e52ffab4d539abfe6a177f8a112825b8402 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,9 +1,10 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, - KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result, - Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, - VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions, + BackgroundExecutor, Bounds, ClipboardItem, Context, Div, Entity, EventEmitter, + ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform, + PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, + TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -16,6 +17,7 @@ pub struct TestAppContext { pub foreground_executor: ForegroundExecutor, pub dispatcher: TestDispatcher, pub test_platform: Rc, + text_system: Arc, } impl Context for TestAppContext { @@ -82,6 +84,7 @@ impl TestAppContext { let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new(platform.text_system())); Self { app: AppContext::new(platform.clone(), asset_source, http_client), @@ -89,6 +92,7 @@ impl TestAppContext { foreground_executor, dispatcher: dispatcher.clone(), test_platform: platform, + text_system, } } @@ -155,6 +159,18 @@ impl TestAppContext { (view, Box::leak(cx)) } + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.test_platform.write_to_clipboard(item) + } + + pub fn read_from_clipboard(&self) -> Option { + self.test_platform.read_from_clipboard() + } + pub fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 96be670af7dbc92af50571ab228dcaa663f90a75..7bcd91a5e07dbc03a52fedd6837b9e06c542c6f4 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -6,10 +6,10 @@ mod mac; mod test; use crate::{ - point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, - FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, - Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, - SharedString, Size, TaskLabel, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, DispatchTree, + Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, + LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, + Scene, SharedString, Size, TaskLabel, }; use anyhow::{anyhow, bail}; use async_task::Runnable; @@ -46,7 +46,7 @@ pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } -pub trait Platform: 'static { +pub(crate) trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor; fn text_system(&self) -> Arc; @@ -92,6 +92,7 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); + fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>); fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs index bfac1366b286ce58f1c423a2da13157881e7c3c8..10fe2cf33ae6d8b830efc92f72bba7f5157de4b9 100644 --- a/crates/gpui2/src/platform/app_menu.rs +++ b/crates/gpui2/src/platform/app_menu.rs @@ -52,7 +52,7 @@ pub enum OsAction { Redo, } -pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) { platform.on_will_open_app_menu(Box::new({ let cx = cx.to_async(); move || { diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index d7fc37f0def0736d189a891c733ed8e09997be49..8a5ee676f7c9e76364e6aea27a9b064a917cca36 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,8 +1,8 @@ -use super::BoolExt; +use super::{events::key_to_native, BoolExt}; use crate::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker, - MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, + DisplayId, ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, + MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; @@ -10,10 +10,10 @@ use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, - NSSavePanel, NSWindow, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, + NSPasteboardTypeString, NSSavePanel, NSWindow, }, - base::{id, nil, BOOL, YES}, + base::{id, nil, selector, BOOL, YES}, foundation::{ NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL, @@ -201,151 +201,155 @@ impl MacPlatform { } } - // unsafe fn create_menu_bar( - // &self, - // menus: Vec, - // delegate: id, - // actions: &mut Vec>, - // keystroke_matcher: &KeymapMatcher, - // ) -> id { - // let application_menu = NSMenu::new(nil).autorelease(); - // application_menu.setDelegate_(delegate); - - // for menu_config in menus { - // let menu = NSMenu::new(nil).autorelease(); - // menu.setTitle_(ns_string(menu_config.name)); - // menu.setDelegate_(delegate); - - // for item_config in menu_config.items { - // menu.addItem_(self.create_menu_item( - // item_config, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - - // let menu_item = NSMenuItem::new(nil).autorelease(); - // menu_item.setSubmenu_(menu); - // application_menu.addItem_(menu_item); - - // if menu_config.name == "Window" { - // let app: id = msg_send![APP_CLASS, sharedApplication]; - // app.setWindowsMenu_(menu); - // } - // } - - // application_menu - // } + unsafe fn create_menu_bar( + &self, + menus: Vec, + delegate: id, + actions: &mut Vec>, + dispatch_tree: Option<&DispatchTree>, + ) -> id { + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); + + for menu_config in menus { + let menu = NSMenu::new(nil).autorelease(); + menu.setTitle_(ns_string(menu_config.name)); + menu.setDelegate_(delegate); + + for item_config in menu_config.items { + menu.addItem_(self.create_menu_item(item_config, delegate, actions, dispatch_tree)); + } + + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); + + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } + + application_menu + } unsafe fn create_menu_item( &self, item: MenuItem, delegate: id, actions: &mut Vec>, - keystroke_matcher: &KeystrokeMatcher, + dispatch_tree: Option<&DispatchTree>, ) -> id { - todo!() - // match item { - // MenuItem::Separator => NSMenuItem::separatorItem(nil), - // MenuItem::Action { - // name, - // action, - // os_action, - // } => { - // // TODO - // let keystrokes = keystroke_matcher - // .bindings_for_action(action.id()) - // .find(|binding| binding.action().eq(action.as_ref())) - // .map(|binding| binding.keystrokes()); - // let selector = match os_action { - // Some(crate::OsAction::Cut) => selector("cut:"), - // Some(crate::OsAction::Copy) => selector("copy:"), - // Some(crate::OsAction::Paste) => selector("paste:"), - // Some(crate::OsAction::SelectAll) => selector("selectAll:"), - // Some(crate::OsAction::Undo) => selector("undo:"), - // Some(crate::OsAction::Redo) => selector("redo:"), - // None => selector("handleGPUIMenuItem:"), - // }; - - // let item; - // if let Some(keystrokes) = keystrokes { - // if keystrokes.len() == 1 { - // let keystroke = &keystrokes[0]; - // let mut mask = NSEventModifierFlags::empty(); - // for (modifier, flag) in &[ - // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), - // ] { - // if *modifier { - // mask |= *flag; - // } - // } - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(key_to_native(&keystroke.key).as_ref()), - // ) - // .autorelease(); - // item.setKeyEquivalentModifierMask_(mask); - // } - // // For multi-keystroke bindings, render the keystroke as part of the title. - // else { - // use std::fmt::Write; - - // let mut name = format!("{name} ["); - // for (i, keystroke) in keystrokes.iter().enumerate() { - // if i > 0 { - // name.push(' '); - // } - // write!(&mut name, "{}", keystroke).unwrap(); - // } - // name.push(']'); - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(&name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - // } else { - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - - // let tag = actions.len() as NSInteger; - // let _: () = msg_send![item, setTag: tag]; - // actions.push(action); - // item - // } - // MenuItem::Submenu(Menu { name, items }) => { - // let item = NSMenuItem::new(nil).autorelease(); - // let submenu = NSMenu::new(nil).autorelease(); - // submenu.setDelegate_(delegate); - // for item in items { - // submenu.addItem_(self.create_menu_item( - // item, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - // item.setSubmenu_(submenu); - // item.setTitle_(ns_string(name)); - // item - // } - // } + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { + name, + action, + os_action, + } => { + let bindings = dispatch_tree + .map(|tree| tree.bindings_for_action(action.as_ref(), &tree.context_stack)) + .unwrap_or_default(); + let keystrokes = bindings + .iter() + .find(|binding| binding.action().partial_eq(action.as_ref())) + .map(|binding| binding.keystrokes()); + + let selector = match os_action { + Some(crate::OsAction::Cut) => selector("cut:"), + Some(crate::OsAction::Copy) => selector("copy:"), + Some(crate::OsAction::Paste) => selector("paste:"), + Some(crate::OsAction::SelectAll) => selector("selectAll:"), + Some(crate::OsAction::Undo) => selector("undo:"), + Some(crate::OsAction::Redo) => selector("redo:"), + None => selector("handleGPUIMenuItem:"), + }; + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + ( + keystroke.modifiers.command, + NSEventModifierFlags::NSCommandKeyMask, + ), + ( + keystroke.modifiers.control, + NSEventModifierFlags::NSControlKeyMask, + ), + ( + keystroke.modifiers.alt, + NSEventModifierFlags::NSAlternateKeyMask, + ), + ( + keystroke.modifiers.shift, + NSEventModifierFlags::NSShiftKeyMask, + ), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(key_to_native(&keystroke.key).as_ref()), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } + // For multi-keystroke bindings, render the keystroke as part of the title. + else { + use std::fmt::Write; + + let mut name = format!("{name} ["); + for (i, keystroke) in keystrokes.iter().enumerate() { + if i > 0 { + name.push(' '); + } + write!(&mut name, "{}", keystroke).unwrap(); + } + name.push(']'); + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(&name), + selector, + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action); + item + } + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(self.create_menu_item(item, delegate, actions, dispatch_tree)); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item + } + } } } @@ -633,6 +637,18 @@ impl Platform for MacPlatform { self.0.lock().event = Some(callback); } + fn on_app_menu_action(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } + fn os_name(&self) -> &'static str { "macOS" } @@ -675,6 +691,15 @@ impl Platform for MacPlatform { } } + fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, dispatch_tree)); + } + } + fn local_timezone(&self) -> UtcOffset { unsafe { let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; @@ -683,32 +708,6 @@ impl Platform for MacPlatform { } } - fn on_app_menu_action(&self, callback: Box) { - self.0.lock().menu_command = Some(callback); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.0.lock().will_open_menu = Some(callback); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.0.lock().validate_menu_command = Some(callback); - } - - // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { - // unsafe { - // let app: id = msg_send![APP_CLASS, sharedApplication]; - // let mut state = self.0.lock(); - // let actions = &mut state.menu_actions; - // app.setMainMenu_(self.create_menu_bar( - // menus, - // app.delegate(), - // actions, - // keystroke_matcher, - // )); - // } - // } - fn path_for_auxiliary_executable(&self, name: &str) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 952a9a96cac98e85863931f3e726d71b85e58896..b2a9279df4fbbaee20a507d3c795d596fa142a2a 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,6 +1,7 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, - Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, DisplayId, + ForegroundExecutor, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, + WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -205,6 +206,10 @@ impl Platform for TestPlatform { unimplemented!() } + fn set_menus(&self, _menus: Vec, _dispatch_tree: Option<&DispatchTree>) { + unimplemented!() + } + fn on_app_menu_action(&self, _callback: Box) { unimplemented!() } diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index c28e281895771a398e8a214961687df5002b1ccd..590079c51b52fe77a2c83ec4a862b27a0202ad1a 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -132,39 +132,3 @@ pub fn load_default_keymap(cx: &mut AppContext) { // KeymapFile::load_asset(asset_path, cx).unwrap(); // } } - -pub fn handle_keymap_file_changes( - mut user_keymap_file_rx: mpsc::UnboundedReceiver, - cx: &mut AppContext, -) { - cx.spawn(move |cx| async move { - // let mut settings_subscription = None; - while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { - cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok(); - - // todo!() - // let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); - // drop(settings_subscription); - // settings_subscription = Some(cx.update(|cx| { - // cx.observe_global::(move |cx| { - // let new_base_keymap = *settings::get::(cx); - // if new_base_keymap != old_base_keymap { - // old_base_keymap = new_base_keymap.clone(); - // reload_keymaps(cx, &keymap_content); - // } - // }) - // })); - } - } - }) - .detach(); -} - -fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { - // todo!() - // cx.clear_bindings(); - load_default_keymap(cx); - keymap_content.clone().add_to_cx(cx).log_err(); - // cx.set_menus(menus::menus()); -} diff --git a/crates/zed2/src/app_menus.rs b/crates/zed2/src/app_menus.rs new file mode 100644 index 0000000000000000000000000000000000000000..70b04e8f9be774d3e7d28e610f65bbabfb80c9b0 --- /dev/null +++ b/crates/zed2/src/app_menus.rs @@ -0,0 +1,175 @@ +use gpui::{Menu, MenuItem, OsAction}; + +#[cfg(target_os = "macos")] +pub fn app_menus() -> Vec> { + vec![ + Menu { + name: "Zed", + items: vec![ + MenuItem::action("About Zed…", super::About), + MenuItem::action("Check for Updates", auto_update::Check), + MenuItem::separator(), + MenuItem::submenu(Menu { + name: "Preferences", + items: vec![ + MenuItem::action("Open Settings", super::OpenSettings), + MenuItem::action("Open Key Bindings", super::OpenKeymap), + MenuItem::action("Open Default Settings", super::OpenDefaultSettings), + MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), + MenuItem::action("Open Local Settings", super::OpenLocalSettings), + MenuItem::action("Select Theme", theme_selector::Toggle), + ], + }), + MenuItem::action("Install CLI", install_cli::Install), + MenuItem::separator(), + MenuItem::action("Hide Zed", super::Hide), + MenuItem::action("Hide Others", super::HideOthers), + MenuItem::action("Show All", super::ShowAll), + MenuItem::action("Quit", super::Quit), + ], + }, + Menu { + name: "File", + items: vec![ + MenuItem::action("New", workspace::NewFile), + MenuItem::action("New Window", workspace::NewWindow), + MenuItem::separator(), + MenuItem::action("Open…", workspace::Open), + // MenuItem::action("Open Recent...", recent_projects::OpenRecent), + MenuItem::separator(), + MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), + MenuItem::action("Save", workspace::Save { save_intent: None }), + MenuItem::action("Save As…", workspace::SaveAs), + MenuItem::action("Save All", workspace::SaveAll { save_intent: None }), + MenuItem::action( + "Close Editor", + workspace::CloseActiveItem { save_intent: None }, + ), + MenuItem::action("Close Window", workspace::CloseWindow), + ], + }, + Menu { + name: "Edit", + items: vec![ + MenuItem::os_action("Undo", editor::Undo, OsAction::Undo), + MenuItem::os_action("Redo", editor::Redo, OsAction::Redo), + MenuItem::separator(), + MenuItem::os_action("Cut", editor::Cut, OsAction::Cut), + MenuItem::os_action("Copy", editor::Copy, OsAction::Copy), + MenuItem::os_action("Paste", editor::Paste, OsAction::Paste), + MenuItem::separator(), + MenuItem::action("Find", search::buffer_search::Deploy { focus: true }), + MenuItem::action("Find In Project", workspace::NewSearch), + MenuItem::separator(), + MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()), + MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette), + ], + }, + Menu { + name: "Selection", + items: vec![ + MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll), + MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode), + MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode), + MenuItem::separator(), + MenuItem::action("Add Cursor Above", editor::AddSelectionAbove), + MenuItem::action("Add Cursor Below", editor::AddSelectionBelow), + MenuItem::action( + "Select Next Occurrence", + editor::SelectNext { + replace_newest: false, + }, + ), + MenuItem::separator(), + MenuItem::action("Move Line Up", editor::MoveLineUp), + MenuItem::action("Move Line Down", editor::MoveLineDown), + MenuItem::action("Duplicate Selection", editor::DuplicateLine), + ], + }, + Menu { + name: "View", + items: vec![ + MenuItem::action("Zoom In", super::IncreaseBufferFontSize), + MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), + MenuItem::action("Reset Zoom", super::ResetBufferFontSize), + MenuItem::separator(), + MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), + MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), + MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), + MenuItem::action("Close All Docks", workspace::CloseAllDocks), + MenuItem::submenu(Menu { + name: "Editor Layout", + items: vec![ + MenuItem::action("Split Up", workspace::SplitUp), + MenuItem::action("Split Down", workspace::SplitDown), + MenuItem::action("Split Left", workspace::SplitLeft), + MenuItem::action("Split Right", workspace::SplitRight), + ], + }), + MenuItem::separator(), + MenuItem::action("Project Panel", project_panel::ToggleFocus), + MenuItem::action("Command Palette", command_palette::Toggle), + MenuItem::action("Diagnostics", diagnostics::Deploy), + MenuItem::separator(), + ], + }, + Menu { + name: "Go", + items: vec![ + MenuItem::action("Back", workspace::GoBack), + MenuItem::action("Forward", workspace::GoForward), + MenuItem::separator(), + MenuItem::action("Go to File", file_finder::Toggle), + // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), + MenuItem::action("Go to Symbol in Editor", outline::Toggle), + MenuItem::action("Go to Definition", editor::GoToDefinition), + MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition), + MenuItem::action("Find All References", editor::FindAllReferences), + MenuItem::action("Go to Line/Column", go_to_line::Toggle), + MenuItem::separator(), + MenuItem::action("Next Problem", editor::GoToDiagnostic), + MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic), + ], + }, + Menu { + name: "Window", + items: vec![ + MenuItem::action("Minimize", super::Minimize), + MenuItem::action("Zoom", super::Zoom), + MenuItem::separator(), + ], + }, + Menu { + name: "Help", + items: vec![ + MenuItem::action("Command Palette", command_palette::Toggle), + MenuItem::separator(), + MenuItem::action("View Telemetry", crate::OpenTelemetryLog), + MenuItem::action("View Dependency Licenses", crate::OpenLicenses), + MenuItem::action("Show Welcome", workspace::Welcome), + MenuItem::separator(), + // todo!(): Needs `feedback2` crate. + // MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback), + // MenuItem::action( + // "Copy System Specs Into Clipboard", + // feedback::CopySystemSpecsIntoClipboard, + // ), + // MenuItem::action("File Bug Report", feedback::FileBugReport), + // MenuItem::action("Request Feature", feedback::RequestFeature), + MenuItem::separator(), + MenuItem::action( + "Documentation", + crate::OpenBrowser { + url: "https://zed.dev/docs".into(), + }, + ), + MenuItem::action( + "Zed Twitter", + crate::OpenBrowser { + url: "https://twitter.com/zeddotdev".into(), + }, + ), + ], + }, + ] +} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6ca5d1a805b53ab1dabd43591b7353c0231aa365..7faafb2440c7e311641219461c2d79934702ddb9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -22,8 +22,7 @@ use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use settings::{ - default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file, - Settings, SettingsStore, + default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, }; use simplelog::ConfigBuilder; use smol::process::Command; @@ -51,8 +50,9 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed2::{ - build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, - languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, + app_menus, build_window_options, ensure_only_instance, handle_cli_connection, + handle_keymap_file_changes, initialize_workspace, languages, Assets, IsOnlyInstance, + OpenListener, OpenRequest, }; mod open_listener; @@ -224,7 +224,7 @@ fn main() { // feedback::init(cx); welcome::init(cx); - // cx.set_menus(menus::menus()); + cx.set_menus(app_menus()); initialize_workspace(app_state.clone(), cx); if stdout_is_a_pty() { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index abe8e7a86f19802f2ab8e7347ba87ba4b6a08050..7e69a2aee3ec799a7e5c6876da39e45386c6487f 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,11 +1,13 @@ #![allow(unused_variables, unused_mut)] //todo!() +mod app_menus; mod assets; pub mod languages; mod only_instance; mod open_listener; +pub use app_menus::*; pub use assets::*; use breadcrumbs::Breadcrumbs; use collections::VecDeque; @@ -18,8 +20,9 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::{anyhow, Context as _}; +use futures::{channel::mpsc, StreamExt}; use project_panel::ProjectPanel; -use settings::{initial_local_settings_content, Settings}; +use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; use util::{ @@ -561,6 +564,42 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { .detach(); } +pub fn handle_keymap_file_changes( + mut user_keymap_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + cx.spawn(move |cx| async move { + // let mut settings_subscription = None; + while let Some(user_keymap_content) = user_keymap_file_rx.next().await { + if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { + cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok(); + + // todo!() + // let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); + // drop(settings_subscription); + // settings_subscription = Some(cx.update(|cx| { + // cx.observe_global::(move |cx| { + // let new_base_keymap = *settings::get::(cx); + // if new_base_keymap != old_base_keymap { + // old_base_keymap = new_base_keymap.clone(); + // reload_keymaps(cx, &keymap_content); + // } + // }) + // })); + } + } + }) + .detach(); +} + +fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { + // todo!() + // cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + cx.set_menus(app_menus()); +} + fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings, From d8757845a988bc8d5f1de338962ad8fcf1d7129e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:21:35 -0500 Subject: [PATCH 33/78] Wire up `NewFile` action --- crates/editor2/src/editor.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a77e1dcc3b9621080e4e78ef94f80fff2c18112e..4b14ec42f1ed11b73220b4e785c4ceb1ad7cae45 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -92,6 +92,7 @@ use std::{ ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::Path, sync::Arc, + sync::Weak, time::{Duration, Instant}, }; pub use sum_tree::Bias; @@ -420,6 +421,16 @@ pub fn init(cx: &mut AppContext) { }, ) .detach(); + + cx.on_action(move |_: &workspace::NewFile, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }); } trait InvalidationRegion { From ed31d36ac1013c06d07e142092b9618cbc7fbf4a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:24:01 -0500 Subject: [PATCH 34/78] Wire up `NewWindow` action --- crates/editor2/src/editor.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 4b14ec42f1ed11b73220b4e785c4ceb1ad7cae45..3fdccb2de42f578c1a9857a9b25403119f617e00 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -431,6 +431,15 @@ pub fn init(cx: &mut AppContext) { .detach(); } }); + cx.on_action(move |_: &workspace::NewWindow, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }) } trait InvalidationRegion { From 5660c8f655359286c4f3138c925dec847b877e5a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:24:43 -0500 Subject: [PATCH 35/78] Add missing semicolon --- crates/editor2/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3fdccb2de42f578c1a9857a9b25403119f617e00..07f00198de2dcb4a5281b868e1c1727c83fe3c43 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -439,7 +439,7 @@ pub fn init(cx: &mut AppContext) { }) .detach(); } - }) + }); } trait InvalidationRegion { From b29cea287ba69a2841d0fd9e6c8ef5264a1b5c88 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 12:12:28 +0100 Subject: [PATCH 36/78] WIP --- crates/assistant2/src/assistant_panel.rs | 6 +-- crates/assistant2/src/codegen.rs | 53 ++++++++++++------------ crates/assistant2/src/prompts.rs | 4 +- crates/search2/src/buffer_search.rs | 12 ++++-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index bcf85a6948eb8cba330a0a54bae24440b7462fff..31264186bd92dc2f378c0b6eadbdc071799a004d 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2835,7 +2835,7 @@ impl InlineAssistant { ) })?; - if answer.next().await == Some(0) { + if answer.await? == 0 { this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; @@ -2875,7 +2875,7 @@ impl InlineAssistant { // This has to be updated to accomodate for semantic_permissions if semantic_permissioned.await.unwrap_or(false) { semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx)) + .update(&mut cx, |index, cx| index.index_project(project, cx))? .await } else { Err(anyhow!("project is not permissioned for semantic indexing")) @@ -3453,7 +3453,7 @@ fn report_assistant_event( .default_open_ai_model .clone(); - let telemetry_settings = TelemetrySettings::get_global(cx); + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); telemetry.report_assistant_event( telemetry_settings, diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs index 9696c629ac910010d193057adcdb1db3f2070935..98b43719f3d7f58b1479d9612d843a09ed68c3b1 100644 --- a/crates/assistant2/src/codegen.rs +++ b/crates/assistant2/src/codegen.rs @@ -109,13 +109,13 @@ impl Codegen { .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); let response = self.provider.complete(prompt); - self.generation = cx.spawn_weak(|this, mut cx| { + self.generation = cx.spawn(|this, mut cx| { async move { let generate = async { let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { + let diff = cx.background_executor().spawn(async move { let chunks = strip_invalid_spans_from_codeblock(response.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -181,7 +181,7 @@ impl Codegen { }); while let Some(hunks) = hunks_rx.next().await { - let this = if let Some(this) = this.upgrade(&cx) { + let this = if let Some(this) = this.upgrade() { this } else { break; @@ -251,17 +251,16 @@ impl Codegen { }; let result = generate.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.last_equal_ranges.clear(); - this.idle = true; - if let Err(error) = result { - this.error = Some(error); - } - cx.emit(Event::Finished); - cx.notify(); - }); - } + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }) + .ok(); } }); self.error.take(); @@ -370,7 +369,7 @@ mod tests { use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; - use gpui::TestAppContext; + use gpui::{Context, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use rand::prelude::*; @@ -390,7 +389,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -402,14 +401,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Transform { range }, @@ -459,7 +458,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -468,14 +467,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -524,7 +523,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = concat!( @@ -533,14 +532,14 @@ mod tests { "}\n" // ); let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs index b678c6fe3b402129692b02f0094841cafe92ffca..ac3b175e644aeac15cdb3d18c5112c6996d3cf7f 100644 --- a/crates/assistant2/src/prompts.rs +++ b/crates/assistant2/src/prompts.rs @@ -176,7 +176,7 @@ pub(crate) mod tests { use super::*; use std::sync::Arc; - use gpui::AppContext; + use gpui::{AppContext, Context}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use settings::SettingsStore; @@ -253,7 +253,7 @@ pub(crate) mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); assert_eq!( diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index b3d600611327f67802db738f57411b322593420d..b9fa36ef34e4b7413043a8d1c76357c55953270a 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -10,9 +10,9 @@ use collections::HashMap; use editor::{Editor, EditorMode}; use futures::channel::oneshot; use gpui::{ - actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _, - WeakView, WindowContext, + actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView, + InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task, + View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -251,6 +251,12 @@ impl Render for BufferSearchBar { } } +impl FocusableView for BufferSearchBar { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.query_editor.focus_handle(cx) + } +} + impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item( &mut self, From 1de02cf6e5f34383baf1feaedda28251e1d95426 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 12:51:25 +0100 Subject: [PATCH 37/78] Start wiring up assistant2 --- Cargo.lock | 2 + crates/assistant2/src/assistant_panel.rs | 229 ++++++++++++----------- crates/assistant2/src/codegen.rs | 8 +- crates/assistant2/src/prompts.rs | 3 +- crates/zed2/Cargo.toml | 4 +- crates/zed2/src/main.rs | 12 +- 6 files changed, 137 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3808d17f4f908eaabea911d39513261723ff8343..945ff79edd63f913f01700e3b07aefd912337786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11881,6 +11881,7 @@ dependencies = [ "activity_indicator2", "ai2", "anyhow", + "assistant2", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -11939,6 +11940,7 @@ dependencies = [ "rust-embed", "schemars", "search2", + "semantic_index2", "serde", "serde_derive", "serde_json", diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 31264186bd92dc2f378c0b6eadbdc071799a004d..c9a9d0d2519536f921d667e1f4e5434cd04b1973 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -22,16 +22,18 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorEvent, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, + ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, - ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, - PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, + actions, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, + AsyncWindowContext, ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, + FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, + StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, + View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; @@ -49,6 +51,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use theme::{ActiveTheme, ThemeSettings}; use ui::{ h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, IconElement, Label, Selectable, Tooltip, @@ -77,7 +80,7 @@ actions!( pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); cx.observe_new_views( - |workspace: &mut Workspace, cx: &mut ViewContext| { + |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace .register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); @@ -122,7 +125,7 @@ impl AssistantPanel { pub fn load( workspace: WeakView, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; @@ -540,7 +543,7 @@ impl AssistantPanel { if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor) + .entry(pending_assist.editor.clone()) { entry.get_mut().retain(|id| *id != assist_id); if entry.get().is_empty() { @@ -747,7 +750,7 @@ impl AssistantPanel { temperature, }); - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; anyhow::Ok(()) }) .detach(); @@ -779,7 +782,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| gpui::red(), // todo!("use the appropriate color") + |theme| theme.editor_active_line_background, // todo!("use the appropriate color") cx, ); } @@ -1240,7 +1243,7 @@ impl Panel for AssistantPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _cx: &WindowContext) -> Option { Some(Icon::Ai) } @@ -1862,7 +1865,7 @@ impl Conversation { .text .push_str(&text); cx.emit(ConversationEvent::SummaryChanged); - }); + })?; } this.update(&mut cx, |this, cx| { @@ -1870,7 +1873,7 @@ impl Conversation { summary.done = true; cx.emit(ConversationEvent::SummaryChanged); } - }); + })?; anyhow::Ok(()) } @@ -2249,7 +2252,7 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - move |cx| { + move |_cx| { let message_id = message.id; let sender = ButtonLike::new("role") .child(match message.role { @@ -2277,16 +2280,18 @@ impl ConversationEditor { .border_color(gpui::red()) .child(sender) .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) - .children(if let MessageStatus::Error(error) = &message.status { - Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text(error, cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }) + .children( + if let MessageStatus::Error(error) = message.status.clone() { + Some( + div() + .id("error") + .tooltip(move |cx| Tooltip::text(&error, cx)) + .child(IconElement::new(Icon::XCircle)), + ) + } else { + None + }, + ) .into_any_element() } }), @@ -2602,10 +2607,11 @@ impl Render for InlineAssistant { None }) .children(if let Some(error) = self.codegen.read(cx).error() { + let error_message = SharedString::from(error.to_string()); Some( div() .id("error") - .tooltip(|cx| Tooltip::text(error.to_string(), cx)) + .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) .child(IconElement::new(Icon::XCircle).color(Color::Error)), ) } else { @@ -2615,7 +2621,7 @@ impl Render for InlineAssistant { .child( div() .ml(measurements.anchor_x - measurements.gutter_width) - .child(self.prompt_editor.clone()), + .child(self.render_prompt_editor(cx)), ) .children(if self.retrieve_context { self.retrieve_context_status(cx) @@ -2752,24 +2758,14 @@ impl InlineAssistant { fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); - self.prompt_editor.update(cx, |editor, cx| { + self.prompt_editor.update(cx, |editor, _cx| { let was_read_only = editor.read_only(); if was_read_only != is_read_only { if is_read_only { editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); } else { self.confirmed = false; editor.set_read_only(false); - editor.set_field_editor_style( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); } } }); @@ -2787,15 +2783,8 @@ impl InlineAssistant { report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor.update(cx, |editor, cx| { - editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); - }); + self.prompt_editor + .update(cx, |editor, _cx| editor.set_read_only(true)); cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, @@ -2827,7 +2816,7 @@ impl InlineAssistant { cx.spawn(|this, mut cx| async move { // If Necessary prompt user if !semantic_permissioned.await.unwrap_or(false) { - let mut answer = this.update(&mut cx, |_, cx| { + let answer = this.update(&mut cx, |_, cx| { cx.prompt( PromptLevel::Info, prompt_text.as_str(), @@ -2888,71 +2877,68 @@ impl InlineAssistant { } fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { - enum ContextStatusIcon {} - let Some(project) = self.project.upgrade() else { return None; }; - if let Some(semantic_index) = SemanticIndex::global(cx) { - let status = semantic_index.update(cx, |index, _| index.status(&project)); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), + let semantic_index = SemanticIndex::global(cx)?; + let status = semantic_index.update(cx, |index, _| index.status(&project)); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - SemanticIndexStatus::NotIndexed {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Indexed", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; + SemanticIndexStatus::NotIndexed {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - 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) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + 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) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); } - Some( - div() - .id("update") - .tooltip(|cx| Tooltip::text(status_text, cx)) - .child(IconElement::new(Icon::Update).color(Color::Info)) - .into_any_element() - ) } - SemanticIndexStatus::Indexed {} => Some( + let status_text = SharedString::from(status_text); + Some( div() - .id("check") - .tooltip(|cx| Tooltip::text("Index up to date", cx)) - .child(IconElement::new(Icon::Check).color(Color::Success)) + .id("update") + .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) + .child(IconElement::new(Icon::Update).color(Color::Info)) .into_any_element() - ), + ) } - } else { - None + + SemanticIndexStatus::Indexed {} => Some( + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(IconElement::new(Icon::Check).color(Color::Success)) + .into_any_element() + ), } } @@ -3004,6 +2990,35 @@ impl InlineAssistant { }); }); } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt_editor.read(cx).read_only() { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + EditorElement::new( + &self.prompt_editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } } // This wouldn't need to exist if we could pass parameters when rendering child views. @@ -3052,7 +3067,8 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); @@ -3183,7 +3199,8 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); @@ -3282,7 +3299,8 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); @@ -3366,7 +3384,8 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs index 98b43719f3d7f58b1479d9612d843a09ed68c3b1..2f7b2f03785e65a8ad3dd4a672b2c7fd820ec0bc 100644 --- a/crates/assistant2/src/codegen.rs +++ b/crates/assistant2/src/codegen.rs @@ -181,12 +181,6 @@ impl Codegen { }); while let Some(hunks) = hunks_rx.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - this.update(&mut cx, |this, cx| { this.last_equal_ranges.clear(); @@ -243,7 +237,7 @@ impl Codegen { } cx.notify(); - }); + })?; } diff.await?; diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs index ac3b175e644aeac15cdb3d18c5112c6996d3cf7f..06881ad9c27935bae19ec9a3eaaed2e4f133d9fc 100644 --- a/crates/assistant2/src/prompts.rs +++ b/crates/assistant2/src/prompts.rs @@ -227,7 +227,8 @@ pub(crate) mod tests { #[gpui::test] fn test_outline_for_prompt(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); language_settings::init(cx); let text = indoc! {" struct X { diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ee9416e234188400164f2ebd1f713bc145364862..9fef3232402b7f04e342313b28529f1b38d7c376 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -49,7 +49,7 @@ lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } -# assistant = { path = "../assistant" } +assistant = { package = "assistant2", path = "../assistant2" } outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } @@ -68,7 +68,7 @@ terminal_view = { package = "terminal_view2", path = "../terminal_view2" } theme = { package = "theme2", path = "../theme2" } theme_selector = { package = "theme_selector2", path = "../theme_selector2" } util = { path = "../util" } -# semantic_index = { path = "../semantic_index" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } welcome = { package = "welcome2", path = "../welcome2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6ca5d1a805b53ab1dabd43591b7353c0231aa365..eafd4924c726d1fd92273d26199dbbf2411ecdd9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -161,11 +161,11 @@ fn main() { node_runtime.clone(), cx, ); - // assistant::init(cx); + assistant::init(cx); // component_test::init(cx); - // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) - // .detach(); + cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) + .detach(); watch_file_types(fs.clone(), cx); languages.set_theme(cx.theme().clone()); @@ -186,10 +186,10 @@ fn main() { .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { - languages, + languages: languages.clone(), client: client.clone(), user_store: user_store.clone(), - fs, + fs: fs.clone(), build_window_options, workspace_store, node_runtime, @@ -210,7 +210,7 @@ fn main() { channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); search::init(cx); - // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); + semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); // vim::init(cx); terminal_view::init(cx); From 3f9fe58c48a9722a3dd8ccf62081db2dbbc404a1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:00:57 +0100 Subject: [PATCH 38/78] Signed out state is looking good --- Cargo.lock | 1 + crates/copilot2/Cargo.toml | 1 + crates/copilot2/src/sign_in.rs | 278 +++++++++++++++++------------- crates/ui2/src/components/icon.rs | 3 + 4 files changed, 164 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e689388a132306158b634fd1776476233d7a03f3..6b123f9061d4ba7b15d606e7896c65c22a3c8245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2121,6 +2121,7 @@ dependencies = [ "settings2", "smol", "theme2", + "ui2", "util", ] diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 9a9243b32eecb766451a3f7f89940227fe6059fa..b04a7d1246ebfbcbb03dbdf179d2633934cecd98 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -28,6 +28,7 @@ theme = { package = "theme2", path = "../theme2" } lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } +ui = { package = "ui2", path = "../ui2" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" anyhow.workspace = true diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 7973d935af1be1c455bb2b90a0ed8dcf2c1b3566..e39deeff83b168d653546c237eba8464f1516c1e 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -11,11 +11,15 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; -use crate::{Copilot, Status}; +use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - px, size, AppContext, Bounds, Div, GlobalPixels, Point, Render, ViewContext, VisualContext, - WindowBounds, WindowHandle, WindowKind, WindowOptions, + div, px, red, size, AnyElement, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, + InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Render, Stateful, + StatefulInteractiveElement, Styled, ViewContext, VisualContext, WindowBounds, WindowHandle, + WindowKind, WindowOptions, }; +use theme::ActiveTheme; +use ui::{h_stack, v_stack, Button, Clickable, Icon, IconElement, Label}; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { @@ -72,13 +76,14 @@ fn create_copilot_auth_window( center: true, focus: true, show: true, - kind: WindowKind::Normal, + kind: WindowKind::PopUp, is_movable: true, display_id: None, }; - cx.open_window(window_options, |cx| { + let window = cx.open_window(window_options, |cx| { cx.build_view(|_| CopilotCodeVerification::new(status.clone())) - }) + }); + window } pub struct CopilotCodeVerification { @@ -99,119 +104,138 @@ impl CopilotCodeVerification { cx.notify(); } - // fn render_device_code( - // data: &PromptUserDeviceFlow, - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> impl IntoAnyElement { - // let copied = cx - // .read_from_clipboard() - // .map(|item| item.text() == &data.user_code) - // .unwrap_or(false); - - // let device_code_style = &style.auth.prompting.device_code; - - // MouseEventHandler::new::(0, cx, |state, _cx| { - // Flex::row() - // .with_child( - // Label::new(data.user_code.clone(), device_code_style.text.clone()) - // .aligned() - // .contained() - // .with_style(device_code_style.left_container) - // .constrained() - // .with_width(device_code_style.left), - // ) - // .with_child( - // Label::new( - // if copied { "Copied!" } else { "Copy" }, - // device_code_style.cta.style_for(state).text.clone(), - // ) - // .aligned() - // .contained() - // .with_style(*device_code_style.right_container.style_for(state)) - // .constrained() - // .with_width(device_code_style.right), - // ) - // .contained() - // .with_style(device_code_style.cta.style_for(state).container) - // }) - // .on_click(gpui::platform::MouseButton::Left, { - // let user_code = data.user_code.clone(); - // move |_, _, cx| { - // cx.platform() - // .write_to_clipboard(ClipboardItem::new(user_code.clone())); - // cx.notify(); - // } - // }) - // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) - // } + fn render_device_code( + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl IntoElement { + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &data.user_code) + .unwrap_or(false); + h_stack() + .cursor_pointer() + .justify_between() + .on_mouse_down(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + dbg!("Copied"); + cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .child(Label::new(data.user_code.clone())) + .child(Label::new(if copied { "Copied!" } else { "Copy" })) - // fn render_prompting_modal( - // connect_clicked: bool, - // data: &PromptUserDeviceFlow, - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ConnectButton {} + // MouseEventHandler::new::(0, cx, |state, _cx| { + // Flex::row() + // .with_child( + // Label::new(data.user_code.clone(), device_code_style.text.clone()) + // .aligned() + // .contained() + // .with_style(device_code_style.left_container) + // .constrained() + // .with_width(device_code_style.left), + // ) + // .with_child( + // Label::new( + // if copied { "Copied!" } else { "Copy" }, + // device_code_style.cta.style_for(state).text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(*device_code_style.right_container.style_for(state)) + // .constrained() + // .with_width(device_code_style.right), + // ) + // .contained() + // .with_style(device_code_style.cta.style_for(state).container) + // }) + // .on_click(gpui::platform::MouseButton::Left, { + // + // move |_, _, cx| { + // + // } + // }) + // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) + } - // Flex::column() - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Enable Copilot by connecting", - // style.auth.prompting.subheading.text.clone(), - // ) - // .aligned(), - // Label::new( - // "your existing license.", - // style.auth.prompting.subheading.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.subheading.container), - // ) - // .with_child(Self::render_device_code(data, &style, cx)) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Paste this code into GitHub after", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "clicking the button below.", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.hint.container.clone()), - // ) - // .with_child(theme::ui::cta_button::( - // if connect_clicked { - // "Waiting for connection..." - // } else { - // "Connect to GitHub" - // }, - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // { - // let verification_uri = data.verification_uri.clone(); - // move |_, verification, cx| { - // cx.platform().open_url(&verification_uri); - // verification.connect_clicked = true; - // } - // }, - // )) - // .align_children_center() - // .into_any() - // } + fn render_prompting_modal( + connect_clicked: bool, + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl Element { + let connect_button_label = if connect_clicked { + "Waiting for connection..." + } else { + "Connect to Github" + }; + v_stack() + .child( + v_stack() + .flex_1() + .w_full() + .items_center() + .justify_between() + .children([ + h_stack() + .items_center() + .child(Label::new("Enable Copilot by connecting")), + h_stack() + .items_center() + .child(Label::new("your existing license")), + ]), + ) + .child(Self::render_device_code(data, cx)) + .child(Label::new("Paste this code into GitHub after").size(ui::LabelSize::Small)) + .child(Label::new("clicking the button below.").size(ui::LabelSize::Small)) + .child( + Button::new("connect-button", connect_button_label).on_click({ + let verification_uri = data.verification_uri.clone(); + cx.listener(move |this, _, cx| { + cx.open_url(&verification_uri); + this.connect_clicked = true; + }) + }), + ) + // Flex::column() + // .with_child(Self::render_device_code(data, &style, cx)) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Paste this code into GitHub after", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "clicking the button below.", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.hint.container.clone()), + // ) + // .with_child(theme::ui::cta_button::( + // if connect_clicked { + // "Waiting for connection..." + // } else { + // "Connect to GitHub" + // }, + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // { + // let verification_uri = data.verification_uri.clone(); + // move |_, verification, cx| { + // cx.platform().open_url(&verification_uri); + // verification.connect_clicked = true; + // } + // }, + // )) + // .align_children_center() + } // fn render_enabled_modal( // style: &theme::Copilot, @@ -316,10 +340,26 @@ impl CopilotCodeVerification { } impl Render for CopilotCodeVerification { - type Element = Div; + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + let prompt = match &self.status { + Status::SigningIn { prompt } => prompt.as_ref(), + _ => None, + }; + div() + .id("copilot code verification") + .flex() + .flex_col() + .size_full() + .items_center() + .p_10() + .bg(cx.theme().colors().element_background) + .child(ui::Label::new("Connect Copilot to Zed")) + .child(IconElement::new(Icon::ZedXCopilot)) + .children( + prompt.map(|data| Self::render_prompting_modal(self.connect_clicked, data, cx)), + ) } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..e9a13bd009b0afd6b23e89e773f81d93684d8989 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -81,6 +81,7 @@ pub enum Icon { Shift, Option, Return, + ZedXCopilot, } impl Icon { @@ -109,6 +110,7 @@ impl Icon { Icon::Close => "icons/x.svg", Icon::Collab => "icons/user_group_16.svg", Icon::Copilot => "icons/copilot.svg", + Icon::CopilotInit => "icons/copilot_init.svg", Icon::CopilotError => "icons/copilot_error.svg", Icon::CopilotDisabled => "icons/copilot_disabled.svg", @@ -155,6 +157,7 @@ impl Icon { Icon::Shift => "icons/shift.svg", Icon::Option => "icons/option.svg", Icon::Return => "icons/return.svg", + Icon::ZedXCopilot => "icons/zed_x_copilot.svg", } } } From 7998e8281c384eb58a6099df0111699856b84079 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:14:18 +0100 Subject: [PATCH 39/78] Barebones Copilot prompt Filter out sign in/sign out when user is signed in/not signed in --- crates/copilot2/src/copilot2.rs | 14 ++++-- crates/copilot2/src/sign_in.rs | 87 ++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index d23d25119b152be1c232304d2992af81af3ab9e4..a829f68f41b266dc9a5896a2fffe2a926ac3c805 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -60,8 +60,8 @@ pub fn init( TypeId::of::(), TypeId::of::(), ]; - let copilot_auth_action_types = [TypeId::of::(), TypeId::of::()]; - + let copilot_auth_action_types = [TypeId::of::()]; + let copilot_no_auth_action_types = [TypeId::of::()]; let status = handle.read(cx).status(); let filter = cx.default_global::(); @@ -69,8 +69,14 @@ pub fn init( Status::Disabled => { filter.hidden_action_types.extend(copilot_action_types); filter.hidden_action_types.extend(copilot_auth_action_types); + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); } Status::Authorized => { + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); for type_id in copilot_action_types .iter() .chain(&copilot_auth_action_types) @@ -80,7 +86,8 @@ pub fn init( } _ => { filter.hidden_action_types.extend(copilot_action_types); - for type_id in &copilot_auth_action_types { + filter.hidden_action_types.extend(copilot_auth_action_types); + for type_id in &copilot_no_auth_action_types { filter.hidden_action_types.remove(type_id); } } @@ -97,6 +104,7 @@ pub fn init( } }); cx.on_action(|_: &SignOut, cx| { + dbg!("Signing out"); if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index e39deeff83b168d653546c237eba8464f1516c1e..dd107532fa1f55cf2baa9d5d1305c53f37814022 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -13,13 +13,12 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - div, px, red, size, AnyElement, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, - InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Render, Stateful, - StatefulInteractiveElement, Styled, ViewContext, VisualContext, WindowBounds, WindowHandle, - WindowKind, WindowOptions, + div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement, + IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext, + WindowBounds, WindowHandle, WindowKind, WindowOptions, }; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Button, Clickable, Icon, IconElement, Label}; +use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { @@ -56,7 +55,9 @@ pub fn init(cx: &mut AppContext) { } _ => { if let Some(code_verification) = verification_window.take() { - code_verification.update(cx, |_, cx| cx.remove_window()); + code_verification + .update(cx, |_, cx| cx.remove_window()) + .ok(); } } } @@ -118,12 +119,12 @@ impl CopilotCodeVerification { .on_mouse_down(gpui::MouseButton::Left, { let user_code = data.user_code.clone(); move |_, cx| { - dbg!("Copied"); cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); cx.notify(); } }) .child(Label::new(data.user_code.clone())) + .child(div()) .child(Label::new(if copied { "Copied!" } else { "Copy" })) // MouseEventHandler::new::(0, cx, |state, _cx| { @@ -170,24 +171,18 @@ impl CopilotCodeVerification { "Connect to Github" }; v_stack() + .flex_1() + .items_center() + .justify_between() + .w_full() + .child(Label::new( + "Enable Copilot by connecting your existing license", + )) + .child(Self::render_device_code(data, cx)) .child( - v_stack() - .flex_1() - .w_full() - .items_center() - .justify_between() - .children([ - h_stack() - .items_center() - .child(Label::new("Enable Copilot by connecting")), - h_stack() - .items_center() - .child(Label::new("your existing license")), - ]), + Label::new("Paste this code into GitHub after clicking the button below.") + .size(ui::LabelSize::Small), ) - .child(Self::render_device_code(data, cx)) - .child(Label::new("Paste this code into GitHub after").size(ui::LabelSize::Small)) - .child(Label::new("clicking the button below.").size(ui::LabelSize::Small)) .child( Button::new("connect-button", connect_button_label).on_click({ let verification_uri = data.verification_uri.clone(); @@ -236,6 +231,17 @@ impl CopilotCodeVerification { // )) // .align_children_center() } + fn render_enabled_modal() -> impl Element { + v_stack() + .child(Label::new("Copilot Enabled!")) + .child(Label::new( + "You can update your settings or sign out from the Copilot menu in the status bar.", + )) + .child( + Button::new("copilot-enabled-done-button", "Done") + .on_click(|_, cx| cx.remove_window()), + ) + } // fn render_enabled_modal( // style: &theme::Copilot, @@ -280,7 +286,22 @@ impl CopilotCodeVerification { // .align_children_center() // .into_any() // } - + fn render_unauthorized_modal() -> impl Element { + v_stack() + .child(Label::new( + "Enable Copilot by connecting your existing license.", + )) + .child( + Label::new("You must have an active Copilot license to use it in Zed.") + .color(Color::Warning), + ) + .child( + Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| { + cx.remove_window(); + cx.open_url(COPILOT_SIGN_UP_URL) + }), + ) + } // fn render_unauthorized_modal( // style: &theme::Copilot, // cx: &mut ViewContext, @@ -344,8 +365,18 @@ impl Render for CopilotCodeVerification { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let prompt = match &self.status { - Status::SigningIn { prompt } => prompt.as_ref(), - _ => None, + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal().into_any_element() + } + Status::Authorized => { + self.connect_clicked = false; + Self::render_enabled_modal().into_any_element() + } + _ => div().into_any_element(), }; div() .id("copilot code verification") @@ -357,9 +388,7 @@ impl Render for CopilotCodeVerification { .bg(cx.theme().colors().element_background) .child(ui::Label::new("Connect Copilot to Zed")) .child(IconElement::new(Icon::ZedXCopilot)) - .children( - prompt.map(|data| Self::render_prompting_modal(self.connect_clicked, data, cx)), - ) + .child(prompt) } } From 1b0ec82caa66bcabd55b2fde2fd9ba789fbd27ce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:16:19 +0100 Subject: [PATCH 40/78] Remove old UI code, remove dbg! --- crates/copilot2/src/copilot2.rs | 1 - crates/copilot2/src/sign_in.rs | 241 +------------------------------- 2 files changed, 2 insertions(+), 240 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index a829f68f41b266dc9a5896a2fffe2a926ac3c805..9c5483d634c201019873d9aa99fdaccd4b361cec 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -104,7 +104,6 @@ pub fn init( } }); cx.on_action(|_: &SignOut, cx| { - dbg!("Signing out"); if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index dd107532fa1f55cf2baa9d5d1305c53f37814022..8da34c427f9eb98c4b2bee031761b4304b498f13 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -1,16 +1,3 @@ -// TODO add logging in -// use crate::{request::PromptUserDeviceFlow, Copilot, Status}; -// use gpui::{ -// elements::*, -// geometry::rect::RectF, -// platform::{WindowBounds, WindowKind, WindowOptions}, -// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, -// WindowHandle, -// }; -// use theme::ui::modal; - -const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; - use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement, @@ -20,6 +7,8 @@ use gpui::{ use theme::ActiveTheme; use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; + pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { let mut verification_window: Option> = None; @@ -126,38 +115,6 @@ impl CopilotCodeVerification { .child(Label::new(data.user_code.clone())) .child(div()) .child(Label::new(if copied { "Copied!" } else { "Copy" })) - - // MouseEventHandler::new::(0, cx, |state, _cx| { - // Flex::row() - // .with_child( - // Label::new(data.user_code.clone(), device_code_style.text.clone()) - // .aligned() - // .contained() - // .with_style(device_code_style.left_container) - // .constrained() - // .with_width(device_code_style.left), - // ) - // .with_child( - // Label::new( - // if copied { "Copied!" } else { "Copy" }, - // device_code_style.cta.style_for(state).text.clone(), - // ) - // .aligned() - // .contained() - // .with_style(*device_code_style.right_container.style_for(state)) - // .constrained() - // .with_width(device_code_style.right), - // ) - // .contained() - // .with_style(device_code_style.cta.style_for(state).container) - // }) - // .on_click(gpui::platform::MouseButton::Left, { - // - // move |_, _, cx| { - // - // } - // }) - // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) } fn render_prompting_modal( @@ -192,44 +149,6 @@ impl CopilotCodeVerification { }) }), ) - // Flex::column() - // .with_child(Self::render_device_code(data, &style, cx)) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Paste this code into GitHub after", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "clicking the button below.", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.hint.container.clone()), - // ) - // .with_child(theme::ui::cta_button::( - // if connect_clicked { - // "Waiting for connection..." - // } else { - // "Connect to GitHub" - // }, - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // { - // let verification_uri = data.verification_uri.clone(); - // move |_, verification, cx| { - // cx.platform().open_url(&verification_uri); - // verification.connect_clicked = true; - // } - // }, - // )) - // .align_children_center() } fn render_enabled_modal() -> impl Element { v_stack() @@ -243,49 +162,6 @@ impl CopilotCodeVerification { ) } - // fn render_enabled_modal( - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum DoneButton {} - - // let enabled_style = &style.auth.authorized; - // Flex::column() - // .with_child( - // Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) - // .contained() - // .with_style(enabled_style.subheading.container) - // .aligned(), - // ) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "You can update your settings or", - // enabled_style.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "sign out from the Copilot menu in", - // enabled_style.hint.text.clone(), - // ) - // .aligned(), - // Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(enabled_style.hint.container), - // ) - // .with_child(theme::ui::cta_button::( - // "Done", - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // |_, _, cx| cx.remove_window(), - // )) - // .align_children_center() - // .into_any() - // } fn render_unauthorized_modal() -> impl Element { v_stack() .child(Label::new( @@ -302,62 +178,6 @@ impl CopilotCodeVerification { }), ) } - // fn render_unauthorized_modal( - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let unauthorized_style = &style.auth.not_authorized; - - // Flex::column() - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Enable Copilot by connecting", - // unauthorized_style.subheading.text.clone(), - // ) - // .aligned(), - // Label::new( - // "your existing license.", - // unauthorized_style.subheading.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(unauthorized_style.subheading.container), - // ) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "You must have an active copilot", - // unauthorized_style.warning.text.clone(), - // ) - // .aligned(), - // Label::new( - // "license to use it in Zed.", - // unauthorized_style.warning.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(unauthorized_style.warning.container), - // ) - // .with_child(theme::ui::cta_button::( - // "Subscribe on GitHub", - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // |_, _, cx| { - // cx.remove_window(); - // cx.platform().open_url(COPILOT_SIGN_UP_URL) - // }, - // )) - // .align_children_center() - // .into_any() - // } } impl Render for CopilotCodeVerification { @@ -391,60 +211,3 @@ impl Render for CopilotCodeVerification { .child(prompt) } } - -// impl Entity for CopilotCodeVerification { -// type Event = (); -// } - -// impl View for CopilotCodeVerification { -// fn ui_name() -> &'static str { -// "CopilotCodeVerification" -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// cx.notify() -// } - -// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// cx.notify() -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// enum ConnectModal {} - -// let style = theme::current(cx).clone(); - -// modal::( -// "Connect Copilot to Zed", -// &style.copilot.modal, -// cx, -// |cx| { -// Flex::column() -// .with_children([ -// theme::ui::icon(&style.copilot.auth.header).into_any(), -// match &self.status { -// Status::SigningIn { -// prompt: Some(prompt), -// } => Self::render_prompting_modal( -// self.connect_clicked, -// &prompt, -// &style.copilot, -// cx, -// ), -// Status::Unauthorized => { -// self.connect_clicked = false; -// Self::render_unauthorized_modal(&style.copilot, cx) -// } -// Status::Authorized => { -// self.connect_clicked = false; -// Self::render_enabled_modal(&style.copilot, cx) -// } -// _ => Empty::new().into_any(), -// }, -// ]) -// .align_children_center() -// }, -// ) -// .into_any() -// } -// } From 5f172a52a4f27bac0363eaa8913aaed4f2129335 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 14:23:05 +0100 Subject: [PATCH 41/78] Load assistant panel --- crates/workspace2/src/status_bar.rs | 23 ++++++------------ crates/zed2/src/zed2.rs | 37 +++++++++++++++-------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 1bc84e04117d2fff8ba0d50188fb53e0cc0ca336..07c48293b5ca720dbc295003e2a3b578f999655c 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -62,22 +62,13 @@ impl Render for StatusBar { ) .child( // Right Dock - h_stack() - .gap_1() - .child( - // Terminal - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-assistant", Icon::Ai)), - ) - .child( - // Terminal - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-chat", Icon::MessageBubbles)), - ), + h_stack().gap_1().child( + // Terminal + div() + .border() + .border_color(gpui::red()) + .child(IconButton::new("status-chat", Icon::MessageBubbles)), + ), ) .child(self.render_right_tools(cx)), ) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 5f2099154cc8f298baedceeee3ab282fe8eca079..9cb3f5c0de44b1e788398529f10a1d69c35bec07 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -7,6 +7,7 @@ mod only_instance; mod open_listener; pub use assets::*; +use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; @@ -177,7 +178,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.spawn(|workspace_handle, mut cx| async move { let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let channels_panel = collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); // let chat_panel = @@ -189,14 +190,14 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let ( project_panel, terminal_panel, - // assistant_panel, + assistant_panel, channels_panel, // chat_panel, // notification_panel, ) = futures::try_join!( project_panel, terminal_panel, - // assistant_panel, + assistant_panel, channels_panel, // chat_panel, // notification_panel, @@ -206,25 +207,25 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); - // workspace.add_panel(assistant_panel, cx); + workspace.add_panel(assistant_panel, cx); workspace.add_panel(channels_panel, cx); // workspace.add_panel(chat_panel, cx); // workspace.add_panel(notification_panel, cx); - // if !was_deserialized - // && workspace - // .project() - // .read(cx) - // .visible_worktrees(cx) - // .any(|tree| { - // tree.read(cx) - // .root_entry() - // .map_or(false, |entry| entry.is_dir()) - // }) - // { - // workspace.toggle_dock(project_panel_position, cx); - // } - // cx.focus_self(); + // if !was_deserialized + // && workspace + // .project() + // .read(cx) + // .visible_worktrees(cx) + // .any(|tree| { + // tree.read(cx) + // .root_entry() + // .map_or(false, |entry| entry.is_dir()) + // }) + // { + // workspace.toggle_dock(project_panel_position, cx); + // } + cx.focus_self(); }) }) .detach(); From 0ef97edd6edbab2cdf8b0028b6a82c06c26e81ea Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:57:16 +0200 Subject: [PATCH 42/78] Format the CI file with Zed's default prettier --- .github/workflows/ci.yml | 272 +++++++++++++------------- .github/workflows/release_nightly.yml | 160 +++++++-------- 2 files changed, 216 insertions(+), 216 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bfc0ab683e859f2675846499a518b8df220c7e0..c499b1933a26376458c8bd02376734fd7911a9ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,144 +1,144 @@ name: CI on: - push: - branches: - - main - - "v[0-9]+.[0-9]+.x" - tags: - - "v*" - pull_request: - branches: - - "**" + push: + branches: + - main + - "v[0-9]+.[0-9]+.x" + tags: + - "v*" + pull_request: + branches: + - "**" env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting - runs-on: - - self-hosted - - test - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Set up default .cargo/config.toml - run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml - - - name: Run rustfmt - uses: ./.github/actions/check_formatting - - tests: - name: Run tests - runs-on: - - self-hosted - - test - needs: rustfmt - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Run tests - uses: ./.github/actions/run_tests - - - name: Build collab - run: cargo build -p collab - - - name: Build other binaries - run: cargo build --workspace --bins --all-features - - bundle: - name: Bundle app - runs-on: - - self-hosted - - bundle - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - needs: tests - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi - - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 100 - - - name: Determine version and release channel - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - run: | - set -eu - - version=$(script/get-crate-version zed) - channel=$(cat crates/zed/RELEASE_CHANNEL) - echo "Publishing version: ${version} on release channel ${channel}" - echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV - - expected_tag_name="" - case ${channel} in - stable) - expected_tag_name="v${version}";; - preview) - expected_tag_name="v${version}-pre";; - nightly) - expected_tag_name="v${version}-nightly";; - *) - echo "can't publish a release on channel ${channel}" - exit 1;; - esac - if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then - echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" - exit 1 - fi - - - name: Generate license file - run: script/generate-licenses - - - name: Create app bundle - run: script/bundle - - - name: Upload app bundle to workflow run if main branch or specific label - uses: actions/upload-artifact@v3 - if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg - path: target/release/Zed.dmg - - - uses: softprops/action-gh-release@v1 - name: Upload app bundle to release - if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} - with: - draft: true - prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} - files: target/release/Zed.dmg - body: "" + rustfmt: + name: Check formatting + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Set up default .cargo/config.toml + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + + - name: Run rustfmt + uses: ./.github/actions/check_formatting + + tests: + name: Run tests + runs-on: + - self-hosted + - test + needs: rustfmt + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Run tests + uses: ./.github/actions/run_tests + + - name: Build collab + run: cargo build -p collab + + - name: Build other binaries + run: cargo build --workspace --bins --all-features + + bundle: + name: Bundle app + runs-on: + - self-hosted + - bundle + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + needs: tests env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + steps: + - name: Install Rust + run: | + rustup set profile minimal + rustup update stable + rustup target add aarch64-apple-darwin + rustup target add x86_64-apple-darwin + rustup target add wasm32-wasi + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 100 + + - name: Determine version and release channel + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + set -eu + + version=$(script/get-crate-version zed) + channel=$(cat crates/zed/RELEASE_CHANNEL) + echo "Publishing version: ${version} on release channel ${channel}" + echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV + + expected_tag_name="" + case ${channel} in + stable) + expected_tag_name="v${version}";; + preview) + expected_tag_name="v${version}-pre";; + nightly) + expected_tag_name="v${version}-nightly";; + *) + echo "can't publish a release on channel ${channel}" + exit 1;; + esac + if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then + echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" + exit 1 + fi + + - name: Generate license file + run: script/generate-licenses + + - name: Create app bundle + run: script/bundle + + - name: Upload app bundle to workflow run if main branch or specific label + uses: actions/upload-artifact@v3 + if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + with: + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg + path: target/release/Zed.dmg + + - uses: softprops/action-gh-release@v1 + name: Upload app bundle to release + if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} + with: + draft: true + prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} + files: target/release/Zed.dmg + body: "" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 7b08c52c61395b09e9925ddb99a31470049b021b..38552646c343d5f44975e33146a4cd38dc3ab4d3 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -1,98 +1,98 @@ name: Release Nightly on: - schedule: - # Fire every night at 1:00am - - cron: "0 1 * * *" - push: - tags: - - "nightly" + schedule: + # Fire every night at 1:00am + - cron: "0 1 * * *" + push: + tags: + - "nightly" env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting - runs-on: - - self-hosted - - test - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + rustfmt: + name: Check formatting + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Run rustfmt - uses: ./.github/actions/check_formatting + - name: Run rustfmt + uses: ./.github/actions/check_formatting - tests: - name: Run tests - runs-on: - - self-hosted - - test - needs: rustfmt - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + tests: + name: Run tests + runs-on: + - self-hosted + - test + needs: rustfmt + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Run tests - uses: ./.github/actions/run_tests + - name: Run tests + uses: ./.github/actions/run_tests - bundle: - name: Bundle app - runs-on: - - self-hosted - - bundle - needs: tests - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} - steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi + bundle: + name: Bundle app + runs-on: + - self-hosted + - bundle + needs: tests + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} + DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} + steps: + - name: Install Rust + run: | + rustup set profile minimal + rustup update stable + rustup target add aarch64-apple-darwin + rustup target add x86_64-apple-darwin + rustup target add wasm32-wasi - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 100 + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 100 - - name: Set release channel to nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL + - name: Set release channel to nightly + run: | + set -eu + version=$(git rev-parse --short HEAD) + echo "Publishing version: ${version} on release channel nightly" + echo "nightly" > crates/zed/RELEASE_CHANNEL - - name: Generate license file - run: script/generate-licenses + - name: Generate license file + run: script/generate-licenses - - name: Create app bundle - run: script/bundle -2 + - name: Create app bundle + run: script/bundle -2 - - name: Upload Zed Nightly - run: script/upload-nightly + - name: Upload Zed Nightly + run: script/upload-nightly From a58f3934581e943fedce70579f674c5e528750c2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:58:49 +0200 Subject: [PATCH 43/78] Do not bundle Zed on `main` branch commits --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c499b1933a26376458c8bd02376734fd7911a9ed..8ac2912424bb8bf10014ac1b8961405cd2ba47c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: runs-on: - self-hosted - bundle - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} needs: tests env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} From d09dfe01f5ad14f864063efd660f551f4377fff5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:15:53 +0100 Subject: [PATCH 44/78] Wire up global actions Added an ephemeral root node so that even if there's no window/focused handle we still have something to dispatch to. Co-authored-by: Antonio --- crates/editor2/src/element.rs | 63 ++++++----- crates/gpui2/src/app.rs | 6 +- crates/gpui2/src/elements/div.rs | 6 +- crates/gpui2/src/key_dispatch.rs | 25 +++-- crates/gpui2/src/window.rs | 186 +++++++++++++++++-------------- 5 files changed, 158 insertions(+), 128 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d7b9d0bb40498cd8fb6c51f4cd33d5e6489f4ad1..ab11f5ffb5427d39905675756be5d9996e984fc1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2803,35 +2803,46 @@ impl Element for EditorElement { let focus_handle = editor.focus_handle(cx); let dispatch_context = self.editor.read(cx).dispatch_context(cx); - cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| { - self.register_actions(cx); - self.register_key_listeners(cx); - - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor - // take precedence. - cx.with_z_index(0, |cx| { - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); - }); - let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); - cx.handle_input(&focus_handle, input_handler); + cx.with_key_dispatch( + Some(dispatch_context), + Some(focus_handle.clone()), + |_, cx| { + self.register_actions(cx); + self.register_key_listeners(cx); + + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor + // take precedence. + cx.with_z_index(0, |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout, + cx, + ); + }); + let input_handler = + ElementInputHandler::new(bounds, self.editor.clone(), cx); + cx.handle_input(&focus_handle, input_handler); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, cx); - } - self.paint_text(text_bounds, &mut layout, cx); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, cx); + } + self.paint_text(text_bounds, &mut layout, cx); - if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); - }) - } + if !layout.blocks.is_empty() { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }) + } + }); }); - }); - }) + }, + ) } } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..4803eb8b97c8cf86bfb56843f739670e1f81cb52 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -201,7 +201,7 @@ pub struct AppContext { pub(crate) windows: SlotMap>, pub(crate) keymap: Arc>, pub(crate) global_action_listeners: - HashMap>>, + HashMap>>, pending_effects: VecDeque, pub(crate) pending_notifications: HashSet, pub(crate) pending_global_notifications: HashSet, @@ -962,9 +962,9 @@ impl AppContext { self.global_action_listeners .entry(TypeId::of::()) .or_default() - .push(Box::new(move |action, phase, cx| { + .push(Rc::new(move |action, phase, cx| { if phase == DispatchPhase::Bubble { - let action = action.as_any().downcast_ref().unwrap(); + let action = action.downcast_ref().unwrap(); listener(action, cx) } })); diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ce457fc6931246ee1e2e4d19a4a1639b37998395..c95a7f890f986b19ba7fdbac6b35c36ed8db36df 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -55,7 +55,7 @@ pub trait InteractiveElement: Sized + Element { E: Debug, { if let Some(key_context) = key_context.try_into().log_err() { - self.interactivity().key_context = key_context; + self.interactivity().key_context = Some(key_context); } self } @@ -722,7 +722,7 @@ impl DivState { pub struct Interactivity { pub element_id: Option, - pub key_context: KeyContext, + pub key_context: Option, pub focusable: bool, pub tracked_focus_handle: Option, pub scroll_handle: Option, @@ -1238,7 +1238,7 @@ impl Default for Interactivity { fn default() -> Self { Self { element_id: None, - key_context: KeyContext::default(), + key_context: None, focusable: false, tracked_focus_handle: None, scroll_handle: None, diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 4838b1a612ce65ba33c03ac25da878a752f716d3..a79a358a1c38453fe7a8501e1ae0cae5ccf832c3 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -61,7 +61,7 @@ impl DispatchTree { self.keystroke_matchers.clear(); } - pub fn push_node(&mut self, context: KeyContext) { + pub fn push_node(&mut self, context: Option) { let parent = self.node_stack.last().copied(); let node_id = DispatchNodeId(self.nodes.len()); self.nodes.push(DispatchNode { @@ -69,7 +69,7 @@ impl DispatchTree { ..Default::default() }); self.node_stack.push(node_id); - if !context.is_empty() { + if let Some(context) = context { self.active_node().context = context.clone(); self.context_stack.push(context); } @@ -148,16 +148,14 @@ impl DispatchTree { false } - pub fn available_actions(&self, target: FocusId) -> Vec> { + pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { let mut actions = Vec::new(); - if let Some(node) = self.focusable_node_ids.get(&target) { - for node_id in self.dispatch_path(*node) { - let node = &self.nodes[node_id.0]; - for DispatchActionListener { action_type, .. } in &node.action_listeners { - // Intentionally silence these errors without logging. - // If an action cannot be built by default, it's not available. - actions.extend(self.action_registry.build_action_type(action_type).ok()); - } + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + actions.extend(self.action_registry.build_action_type(action_type).ok()); } } actions @@ -236,6 +234,11 @@ impl DispatchTree { self.focusable_node_ids.get(&target).copied() } + pub fn root_node_id(&self) -> DispatchNodeId { + debug_assert!(!self.nodes.is_empty()); + DispatchNodeId(0) + } + fn active_node_id(&self) -> DispatchNodeId { *self.node_stack.last().unwrap() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 271b09b8b950d777400f403fa59d5b81ad4e5ed6..6323eb962f101daa66fcfa8ae1482408507b7670 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -453,19 +453,21 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { - if let Some(focus_handle) = self.focused() { - self.defer(move |cx| { - if let Some(node_id) = cx - .window - .current_frame - .dispatch_tree - .focusable_node_id(focus_handle.id) - { - cx.propagate_event = true; - cx.dispatch_action_on_node(node_id, action); - } - }) - } + let focus_handle = self.focused(); + + self.defer(move |cx| { + let node_id = focus_handle + .and_then(|handle| { + cx.window + .current_frame + .dispatch_tree + .focusable_node_id(handle.id) + }) + .unwrap_or_else(|| cx.window.current_frame.dispatch_tree.root_node_id()); + + cx.propagate_event = true; + cx.dispatch_action_on_node(node_id, action); + }) } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities @@ -1154,8 +1156,19 @@ impl<'a> WindowContext<'a> { self.start_frame(); self.with_z_index(0, |cx| { - let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(Point::zero(), available_space, cx); + cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { + for (action_type, action_listeners) in &cx.app.global_action_listeners { + for action_listener in action_listeners.iter().cloned() { + cx.window.current_frame.dispatch_tree.on_action( + *action_type, + Rc::new(move |action, phase, cx| action_listener(action, phase, cx)), + ) + } + } + + let available_space = cx.window.viewport_size.map(Into::into); + root_view.draw(Point::zero(), available_space, cx); + }) }); if let Some(active_drag) = self.app.active_drag.take() { @@ -1338,73 +1351,77 @@ impl<'a> WindowContext<'a> { } fn dispatch_key_event(&mut self, event: &dyn Any) { - if let Some(node_id) = self.window.focus.and_then(|focus_id| { - self.window - .current_frame - .dispatch_tree - .focusable_node_id(focus_id) - }) { - let dispatch_path = self - .window - .current_frame - .dispatch_tree - .dispatch_path(node_id); + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); - let mut actions: Vec> = Vec::new(); + let dispatch_path = self + .window + .current_frame + .dispatch_tree + .dispatch_path(node_id); - // Capture phase - let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); - self.propagate_event = true; + let mut actions: Vec> = Vec::new(); - for node_id in &dispatch_path { - let node = self.window.current_frame.dispatch_tree.node(*node_id); + // Capture phase + let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); + self.propagate_event = true; - if !node.context.is_empty() { - context_stack.push(node.context.clone()); - } + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self); - if !self.propagate_event { - return; - } + if !node.context.is_empty() { + context_stack.push(node.context.clone()); + } + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; } } + } - // Bubble phase - for node_id in dispatch_path.iter().rev() { - // Handle low level key events - let node = self.window.current_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self); - if !self.propagate_event { - return; - } + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; } + } - // Match keystrokes - let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if let Some(found) = self - .window - .current_frame - .dispatch_tree - .dispatch_key(&key_down_event.keystroke, &context_stack) - { - actions.push(found.boxed_clone()) - } + // Match keystrokes + let node = self.window.current_frame.dispatch_tree.node(*node_id); + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if let Some(found) = self + .window + .current_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &context_stack) + { + actions.push(found.boxed_clone()) } - - context_stack.pop(); } + + context_stack.pop(); } + } - for action in actions { - self.dispatch_action_on_node(node_id, action); - if !self.propagate_event { - return; - } + for action in actions { + self.dispatch_action_on_node(node_id, action); + if !self.propagate_event { + return; } } } @@ -1490,22 +1507,21 @@ impl<'a> WindowContext<'a> { } pub fn available_actions(&self) -> Vec> { - if let Some(focus_id) = self.window.focus { - let mut actions = self - .window - .current_frame - .dispatch_tree - .available_actions(focus_id); - actions.extend( - self.app - .global_action_listeners - .keys() - .filter_map(|type_id| self.app.actions.build_action_type(type_id).ok()), - ); - actions - } else { - Vec::new() - } + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); + + self.window + .current_frame + .dispatch_tree + .available_actions(node_id) } pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { @@ -1561,7 +1577,7 @@ impl<'a> WindowContext<'a> { //========== ELEMENT RELATED FUNCTIONS =========== pub fn with_key_dispatch( &mut self, - context: KeyContext, + context: Option, focus_handle: Option, f: impl FnOnce(Option, &mut Self) -> R, ) -> R { From a1c8f01ff3592130dc9dce77388617d00b9b02e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 16:34:07 +0100 Subject: [PATCH 45/78] WIP --- crates/assistant2/src/assistant_panel.rs | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index c9a9d0d2519536f921d667e1f4e5434cd04b1973..2a589afc4f519fa51e2567d55851443f1a538703 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1140,6 +1140,7 @@ impl Render for AssistantPanel { } v_stack() + .size_full() .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { this.new_conversation(cx); })) @@ -1156,22 +1157,26 @@ impl Render for AssistantPanel { } else { Some(self.toolbar.clone()) }) - .child(if let Some(editor) = self.active_editor() { - editor.clone().into_any_element() - } else { - uniform_list( - cx.view().clone(), - "saved_conversations", - self.saved_conversations.len(), - |this, range, cx| { - range - .map(|ix| this.render_saved_conversation(ix, cx)) - .collect() - }, - ) - .track_scroll(self.saved_conversations_scroll_handle.clone()) - .into_any_element() - }) + .child( + div() + .flex_1() + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + uniform_list( + cx.view().clone(), + "saved_conversations", + self.saved_conversations.len(), + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx)) + .collect() + }, + ) + .track_scroll(self.saved_conversations_scroll_handle.clone()) + .into_any_element() + }), + ) .border() .border_color(gpui::red()) } @@ -2469,6 +2474,7 @@ impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() + .size_full() .relative() .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) .capture_action(cx.listener(ConversationEditor::save)) From f833cd7c160239b9f72e1ca394cdd6209e35ede2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 10:41:49 -0500 Subject: [PATCH 46/78] Use specified color for non-highlighted text in `HighlightedLabel` (#3509) This PR fixes an issue where the specified color for a `HighlightedLabel` was not respected as the default color for non-highlighted text. Release Notes: - N/A --- crates/ui2/src/components/label.rs | 5 ++++- crates/ui2/src/components/stories/label.rs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 7aeda3e850fbe91463f6f3f6a0298cb3fc7fa5a7..d455a041ee6e8f861a3cdacf1c155c2f7fc57871 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -129,6 +129,9 @@ impl RenderOnce for HighlightedLabel { )); } + let mut text_style = cx.text_style().clone(); + text_style.color = self.color.color(cx); + div() .flex() .when(self.strikethrough, |this| { @@ -146,7 +149,7 @@ impl RenderOnce for HighlightedLabel { LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), }) - .child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights)) + .child(StyledText::new(self.label).with_highlights(&text_style, highlights)) } } diff --git a/crates/ui2/src/components/stories/label.rs b/crates/ui2/src/components/stories/label.rs index 2417bee6e1cd77646ceddd2c4febe4c6e45b7e19..e026d388fde62545f47aab22985fa7efba4f3c7a 100644 --- a/crates/ui2/src/components/stories/label.rs +++ b/crates/ui2/src/components/stories/label.rs @@ -23,5 +23,9 @@ impl Render for LabelStory { "Héllo, world!", vec![0, 1, 3, 8, 9, 13], )) + .child(Story::label("Highlighted with `color`")) + .child( + HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error), + ) } } From 6549a9a091512f6db53e08efbd3a94d53d524f00 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:52:52 +0100 Subject: [PATCH 47/78] Let WindowContext::dispatch_action handle global actions Co-authored-by: Antonio --- crates/gpui2/src/app.rs | 82 ++++++++++++++------------------ crates/gpui2/src/key_dispatch.rs | 20 ++++---- crates/gpui2/src/window.rs | 16 +++++++ 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 0abdf4b17410419529c705bfd808271f5100a7c0..d23d6e3d9d15003ab3236a6d491143f25d1f48de 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1040,20 +1040,10 @@ impl AppContext { pub fn is_action_available(&mut self, action: &dyn Action) -> bool { if let Some(window) = self.active_window() { - let window_action_available = window - .update(self, |_, cx| { - if let Some(focus_id) = cx.window.focus { - cx.window - .current_frame - .dispatch_tree - .is_action_available(action, focus_id) - } else { - false - } - }) - .unwrap_or(false); - if window_action_available { - return true; + if let Ok(window_action_available) = + window.update(self, |_, cx| cx.is_action_available(action)) + { + return window_action_available; } } @@ -1075,44 +1065,19 @@ impl AppContext { } pub fn dispatch_action(&mut self, action: &dyn Action) { - self.propagate_event = true; - - if let Some(mut global_listeners) = self - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in &global_listeners { - listener(action, DispatchPhase::Capture, self); - if !self.propagate_event { - break; - } - } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - - if self.propagate_event { - if let Some(active_window) = self.active_window() { - active_window - .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) - .log_err(); - } - } + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .log_err(); + } else { + self.propagate_event = true; - if self.propagate_event { if let Some(mut global_listeners) = self .global_action_listeners .remove(&action.as_any().type_id()) { - for listener in global_listeners.iter().rev() { - listener(action, DispatchPhase::Bubble, self); + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); if !self.propagate_event { break; } @@ -1127,6 +1092,29 @@ impl AppContext { self.global_action_listeners .insert(action.as_any().type_id(), global_listeners); } + + if self.propagate_event { + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } } } } diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 07561644f7a36164aa328349aaf7c08ad2c74621..80e662ad3ef6477b4cd0d925b2852957a4c04d00 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -161,17 +161,15 @@ impl DispatchTree { actions } - pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool { - if let Some(node) = self.focusable_node_ids.get(&target) { - for node_id in self.dispatch_path(*node) { - let node = &self.nodes[node_id.0]; - if node - .action_listeners - .iter() - .any(|listener| listener.action_type == action.as_any().type_id()) - { - return true; - } + pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; } } false diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 1bd8b4653797669519349a0edb6fb783c361c392..8995d04b64de4abac4653e228691f3784eb89a6e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -804,6 +804,22 @@ impl<'a> WindowContext<'a> { ); } + pub fn is_action_available(&self, action: &dyn Action) -> bool { + let target = self + .focused() + .and_then(|focused_handle| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focused_handle.id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); + self.window + .current_frame + .dispatch_tree + .is_action_available(action, target) + } + /// The position of the mouse relative to the window. pub fn mouse_position(&self) -> Point { self.window.mouse_position From 8f1c74b8bc0cee150b33b76309b9f21735bfda4f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 11:17:12 -0500 Subject: [PATCH 48/78] Factor out `LabelLike` to share common label styles (#3510) This PR factors out a new `LabelLike` component to share common styles between the `Label` and `HighlightedLabel` components. Release Notes: - N/A --- crates/copilot2/src/sign_in.rs | 2 +- crates/diagnostics2/src/diagnostics.rs | 2 +- crates/editor2/src/items.rs | 2 +- crates/go_to_line2/src/go_to_line.rs | 2 +- crates/ui2/src/components/label.rs | 188 +----------------- .../src/components/label/highlighted_label.rs | 86 ++++++++ crates/ui2/src/components/label/label.rs | 48 +++++ crates/ui2/src/components/label/label_like.rs | 102 ++++++++++ crates/ui2/src/prelude.rs | 2 +- 9 files changed, 247 insertions(+), 187 deletions(-) create mode 100644 crates/ui2/src/components/label/highlighted_label.rs create mode 100644 crates/ui2/src/components/label/label.rs create mode 100644 crates/ui2/src/components/label/label_like.rs diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 8da34c427f9eb98c4b2bee031761b4304b498f13..4fa93ffcf89a496ae1b150334c9b7a310b779cec 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -5,7 +5,7 @@ use gpui::{ WindowBounds, WindowHandle, WindowKind, WindowOptions, }; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; +use ui::{prelude::*, Button, Icon, IconElement, Label}; const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 44acc285e8231a48e20c791f38f09f6619e99d08..f725fb7c4f58906f4263a5bcc86e6dd1ec24af6f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -36,7 +36,7 @@ use std::{ }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; -use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label}; +use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label}; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 70d4d6bf255fad8b056a19ba5d5b41dc97a66178..12feb31696922558d61eb00d0a995039a21b2cea 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -32,7 +32,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{h_stack, Color, Label}; +use ui::{h_stack, prelude::*, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 5ad95c1f6ea6cf0fb49e185a044d8be7eaf383fe..aff9942c265da765431460d9f381abf131e3573a 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -6,7 +6,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Color, Label, StyledExt}; +use ui::{h_stack, prelude::*, v_stack, Label}; use util::paths::FILE_ROW_COLUMN_DELIMITER; actions!(Toggle); diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index d455a041ee6e8f861a3cdacf1c155c2f7fc57871..bda97be6490d20309941e9de980f4a0fb2350bb1 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,183 +1,7 @@ -use std::ops::Range; +mod highlighted_label; +mod label; +mod label_like; -use crate::prelude::*; -use crate::styled_ext::StyledExt; -use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum LabelSize { - #[default] - Default, - Small, -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LineHeightStyle { - #[default] - TextLabel, - /// Sets the line height to 1 - UILabel, -} - -#[derive(IntoElement, Clone)] -pub struct Label { - label: SharedString, - size: LabelSize, - line_height_style: LineHeightStyle, - color: Color, - strikethrough: bool, -} - -impl RenderOnce for Label { - type Rendered = Div; - - fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_1_2() - .w_full() - .h_px() - .bg(Color::Hidden.color(cx)), - ) - }) - .map(|this| match self.size { - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - }) - .when(self.line_height_style == LineHeightStyle::UILabel, |this| { - this.line_height(relative(1.)) - }) - .text_color(self.color.color(cx)) - .child(self.label.clone()) - } -} - -impl Label { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - size: LabelSize::Default, - line_height_style: LineHeightStyle::default(), - color: Color::Default, - strikethrough: false, - } - } - - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.line_height_style = line_height_style; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } -} - -#[derive(IntoElement)] -pub struct HighlightedLabel { - label: SharedString, - size: LabelSize, - color: Color, - highlight_indices: Vec, - strikethrough: bool, -} - -impl RenderOnce for HighlightedLabel { - type Rendered = Div; - - fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let highlight_color = cx.theme().colors().text_accent; - - let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); - - while let Some(start_ix) = highlight_indices.next() { - let mut end_ix = start_ix; - - loop { - end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8(); - if let Some(&next_ix) = highlight_indices.peek() { - if next_ix == end_ix { - end_ix = next_ix; - highlight_indices.next(); - continue; - } - } - break; - } - - highlights.push(( - start_ix..end_ix, - HighlightStyle { - color: Some(highlight_color), - ..Default::default() - }, - )); - } - - let mut text_style = cx.text_style().clone(); - text_style.color = self.color.color(cx); - - div() - .flex() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_px() - .my_auto() - .w_full() - .h_px() - .bg(Color::Hidden.color(cx)), - ) - }) - .map(|this| match self.size { - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - }) - .child(StyledText::new(self.label).with_highlights(&text_style, highlights)) - } -} - -impl HighlightedLabel { - /// shows a label with the given characters highlighted. - /// characters are identified by utf8 byte position. - pub fn new(label: impl Into, highlight_indices: Vec) -> Self { - Self { - label: label.into(), - size: LabelSize::Default, - color: Color::Default, - highlight_indices, - strikethrough: false, - } - } - - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } -} +pub use highlighted_label::*; +pub use label::*; +pub use label_like::*; diff --git a/crates/ui2/src/components/label/highlighted_label.rs b/crates/ui2/src/components/label/highlighted_label.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7fbb0d8167cd0f78fd40f01454621a3e26ca33d --- /dev/null +++ b/crates/ui2/src/components/label/highlighted_label.rs @@ -0,0 +1,86 @@ +use std::ops::Range; + +use gpui::{HighlightStyle, StyledText}; + +use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; + +#[derive(IntoElement)] +pub struct HighlightedLabel { + base: LabelLike, + label: SharedString, + highlight_indices: Vec, +} + +impl HighlightedLabel { + /// Constructs a label with the given characters highlighted. + /// Characters are identified by UTF-8 byte position. + pub fn new(label: impl Into, highlight_indices: Vec) -> Self { + Self { + base: LabelLike::new(), + label: label.into(), + highlight_indices, + } + } +} + +impl LabelCommon for HighlightedLabel { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.base = self.base.strikethrough(strikethrough); + self + } +} + +impl RenderOnce for HighlightedLabel { + type Rendered = LabelLike; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + let highlight_color = cx.theme().colors().text_accent; + + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); + let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); + + while let Some(start_ix) = highlight_indices.next() { + let mut end_ix = start_ix; + + loop { + end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8(); + if let Some(&next_ix) = highlight_indices.peek() { + if next_ix == end_ix { + end_ix = next_ix; + highlight_indices.next(); + continue; + } + } + break; + } + + highlights.push(( + start_ix..end_ix, + HighlightStyle { + color: Some(highlight_color), + ..Default::default() + }, + )); + } + + let mut text_style = cx.text_style().clone(); + text_style.color = self.base.color.color(cx); + + LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights)) + } +} diff --git a/crates/ui2/src/components/label/label.rs b/crates/ui2/src/components/label/label.rs new file mode 100644 index 0000000000000000000000000000000000000000..8272340888837ca3f2ec2a0294a3168c14009cd0 --- /dev/null +++ b/crates/ui2/src/components/label/label.rs @@ -0,0 +1,48 @@ +use gpui::WindowContext; + +use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; + +#[derive(IntoElement)] +pub struct Label { + base: LabelLike, + label: SharedString, +} + +impl Label { + pub fn new(label: impl Into) -> Self { + Self { + base: LabelLike::new(), + label: label.into(), + } + } +} + +impl LabelCommon for Label { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.base = self.base.strikethrough(strikethrough); + self + } +} + +impl RenderOnce for Label { + type Rendered = LabelLike; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + self.base.child(self.label) + } +} diff --git a/crates/ui2/src/components/label/label_like.rs b/crates/ui2/src/components/label/label_like.rs new file mode 100644 index 0000000000000000000000000000000000000000..72a48adea451f49cd10dd2e2bd224f16d4ae51a9 --- /dev/null +++ b/crates/ui2/src/components/label/label_like.rs @@ -0,0 +1,102 @@ +use gpui::{relative, AnyElement, Div, Styled}; +use smallvec::SmallVec; + +use crate::prelude::*; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum LabelSize { + #[default] + Default, + Small, +} + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum LineHeightStyle { + #[default] + TextLabel, + /// Sets the line height to 1 + UILabel, +} + +pub trait LabelCommon { + fn size(self, size: LabelSize) -> Self; + fn line_height_style(self, line_height_style: LineHeightStyle) -> Self; + fn color(self, color: Color) -> Self; + fn strikethrough(self, strikethrough: bool) -> Self; +} + +#[derive(IntoElement)] +pub struct LabelLike { + size: LabelSize, + line_height_style: LineHeightStyle, + pub(crate) color: Color, + strikethrough: bool, + children: SmallVec<[AnyElement; 2]>, +} + +impl LabelLike { + pub fn new() -> Self { + Self { + size: LabelSize::Default, + line_height_style: LineHeightStyle::default(), + color: Color::Default, + strikethrough: false, + children: SmallVec::new(), + } + } +} + +impl LabelCommon for LabelLike { + fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.line_height_style = line_height_style; + self + } + + fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self + } +} + +impl ParentElement for LabelLike { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for LabelLike { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .when(self.strikethrough, |this| { + this.relative().child( + div() + .absolute() + .top_1_2() + .w_full() + .h_px() + .bg(Color::Hidden.color(cx)), + ) + }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) + .when(self.line_height_style == LineHeightStyle::UILabel, |this| { + this.line_height(relative(1.)) + }) + .text_color(self.color.color(cx)) + .children(self.children) + } +} diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 38065b62754b5facb5ed9440ad2b74535f40d445..a71efa4dc3ebc8e4065260575beac78972f3f89c 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -9,5 +9,5 @@ pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; pub use crate::{h_stack, v_stack}; -pub use crate::{ButtonCommon, Color, StyledExt}; +pub use crate::{ButtonCommon, Color, LabelCommon, StyledExt}; pub use theme::ActiveTheme; From c8ddc95caa4860948fd6f9a7dc7801cc828a6b64 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:26:54 +0100 Subject: [PATCH 49/78] Take a `Keymap` when setting app menus For a brief period on this branch, we were taking a `DispatchTree`. Doing so resulted in more accurate key bindings but it meant that we would have had to recompute the app menus every time the key context changed. We decided to err on the side of keeping things simple and work in the same way they worked back in zed1. Co-Authored-By: Marshall --- crates/gpui2/src/app.rs | 11 +---------- crates/gpui2/src/platform.rs | 6 +++--- crates/gpui2/src/platform/mac/platform.rs | 23 ++++++++++------------ crates/gpui2/src/platform/test/platform.rs | 7 +++---- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index d23d6e3d9d15003ab3236a6d491143f25d1f48de..0715ace9eaf8ca0de7fabcb635264e361fc09dad 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1052,16 +1052,7 @@ impl AppContext { } pub fn set_menus(&mut self, menus: Vec) { - if let Some(active_window) = self.active_window() { - active_window - .update(self, |_, cx| { - cx.platform - .set_menus(menus, Some(&cx.window.current_frame.dispatch_tree)); - }) - .ok(); - } else { - self.platform.set_menus(menus, None); - } + self.platform.set_menus(menus, &self.keymap.lock()); } pub fn dispatch_action(&mut self, action: &dyn Action) { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 7bcd91a5e07dbc03a52fedd6837b9e06c542c6f4..66cf7c14efb95ad520083122a900f9ff900c7e94 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -6,8 +6,8 @@ mod mac; mod test; use crate::{ - point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, DispatchTree, - Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, + FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap, LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, TaskLabel, }; @@ -92,7 +92,7 @@ pub(crate) trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); - fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>); + fn set_menus(&self, menus: Vec, keymap: &Keymap); fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 8a5ee676f7c9e76364e6aea27a9b064a917cca36..2deea545e164b3445dcd6388048eaf97b66e9102 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,7 +1,7 @@ use super::{events::key_to_native, BoolExt}; use crate::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, - DisplayId, ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; @@ -206,7 +206,7 @@ impl MacPlatform { menus: Vec, delegate: id, actions: &mut Vec>, - dispatch_tree: Option<&DispatchTree>, + keymap: &Keymap, ) -> id { let application_menu = NSMenu::new(nil).autorelease(); application_menu.setDelegate_(delegate); @@ -217,7 +217,7 @@ impl MacPlatform { menu.setDelegate_(delegate); for item_config in menu_config.items { - menu.addItem_(self.create_menu_item(item_config, delegate, actions, dispatch_tree)); + menu.addItem_(self.create_menu_item(item_config, delegate, actions, keymap)); } let menu_item = NSMenuItem::new(nil).autorelease(); @@ -238,7 +238,7 @@ impl MacPlatform { item: MenuItem, delegate: id, actions: &mut Vec>, - dispatch_tree: Option<&DispatchTree>, + keymap: &Keymap, ) -> id { match item { MenuItem::Separator => NSMenuItem::separatorItem(nil), @@ -247,11 +247,8 @@ impl MacPlatform { action, os_action, } => { - let bindings = dispatch_tree - .map(|tree| tree.bindings_for_action(action.as_ref(), &tree.context_stack)) - .unwrap_or_default(); - let keystrokes = bindings - .iter() + let keystrokes = keymap + .bindings_for_action(action.type_id()) .find(|binding| binding.action().partial_eq(action.as_ref())) .map(|binding| binding.keystrokes()); @@ -343,7 +340,7 @@ impl MacPlatform { let submenu = NSMenu::new(nil).autorelease(); submenu.setDelegate_(delegate); for item in items { - submenu.addItem_(self.create_menu_item(item, delegate, actions, dispatch_tree)); + submenu.addItem_(self.create_menu_item(item, delegate, actions, keymap)); } item.setSubmenu_(submenu); item.setTitle_(ns_string(name)); @@ -691,12 +688,12 @@ impl Platform for MacPlatform { } } - fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>) { + fn set_menus(&self, menus: Vec, keymap: &Keymap) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; let mut state = self.0.lock(); let actions = &mut state.menu_actions; - app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, dispatch_tree)); + app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, keymap)); } } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index c76796b5225f9e67ac51fe0898ffef975fc02cad..10fd9f0ff38e7abb57a11d201c9745371cef3f49 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,7 +1,6 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, DisplayId, - ForegroundExecutor, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, - WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, + Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -213,7 +212,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn set_menus(&self, _menus: Vec, _dispatch_tree: Option<&DispatchTree>) { + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) { unimplemented!() } From 886ec79d587ffa00b0d96f14f5114dac8c2e8124 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:45:59 +0100 Subject: [PATCH 50/78] Make TestPlatform::set_menus a no-op --- crates/gpui2/src/platform/test/platform.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 10fd9f0ff38e7abb57a11d201c9745371cef3f49..edbe39480994a9425f733ea5a5ed967f63535f11 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -212,9 +212,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) { - unimplemented!() - } + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} fn on_app_menu_action(&self, _callback: Box) { unimplemented!() From 80c8fd1f4c09ec049ae2b96fa0aa584e3d70fed2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 11:54:59 -0500 Subject: [PATCH 51/78] Fix toolbar not appearing for initial pane (#3512) This PR fixes an issues where the toolbar would not appear for the center pane when Zed2 initially loads. We resolved this by adding a call to initialize the center pane when the workspace is initialized Due to changes in the way subscriptions work we can on longer observe an event that is emitted in the same event cycle in which the subscription is created. Because of this we need to explicitly initialize the center pane, as it won't get performed by the subscription. Release Notes: - N/A --------- Co-authored-by: Antonio --- crates/zed2/src/zed2.rs | 64 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 5f2099154cc8f298baedceeee3ab282fe8eca079..daa25b8eb95948c2aa281831529c20bd07c6d2d8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -11,7 +11,7 @@ use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; use gpui::{ - actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, + actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; @@ -30,6 +30,7 @@ use util::{ ResultExt, }; use uuid::Uuid; +use workspace::Pane; use workspace::{ create_and_open_local_file, dock::PanelHandle, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, @@ -92,37 +93,12 @@ pub fn build_window_options( pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.observe_new_views(move |workspace: &mut Workspace, cx| { let workspace_handle = cx.view().clone(); + let center_pane = workspace.active_pane().clone(); + initialize_pane(workspace, ¢er_pane, cx); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace)); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); - toolbar.add_item(buffer_search_bar.clone(), cx); - - let quick_action_bar = cx - .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); - toolbar.add_item(quick_action_bar, cx); - let diagnostic_editor_controls = - cx.build_view(|_| diagnostics::ToolbarControls::new()); - // toolbar.add_item(diagnostic_editor_controls, cx); - // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - // toolbar.add_item(project_search_bar, cx); - // let submit_feedback_button = - // cx.add_view(|_| SubmitFeedbackButton::new()); - // toolbar.add_item(submit_feedback_button, cx); - // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); - // toolbar.add_item(feedback_info_text, cx); - // let lsp_log_item = - // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); - // toolbar.add_item(lsp_log_item, cx); - // let syntax_tree_item = cx - // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); - // toolbar.add_item(syntax_tree_item, cx); - }) - }); + initialize_pane(workspace, pane, cx); } } }) @@ -434,6 +410,36 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .detach(); } +fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewContext) { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace)); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); + toolbar.add_item(buffer_search_bar.clone(), cx); + + let quick_action_bar = + cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); + toolbar.add_item(quick_action_bar, cx); + let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); + // toolbar.add_item(diagnostic_editor_controls, cx); + // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + // toolbar.add_item(project_search_bar, cx); + // let submit_feedback_button = + // cx.add_view(|_| SubmitFeedbackButton::new()); + // toolbar.add_item(submit_feedback_button, cx); + // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + // toolbar.add_item(feedback_info_text, cx); + // let lsp_log_item = + // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); + // toolbar.add_item(lsp_log_item, cx); + // let syntax_tree_item = cx + // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + // toolbar.add_item(syntax_tree_item, cx); + }) + }); +} + fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { use std::fmt::Write as _; From 80f315106d88beb7c9bca8afe49dc2760d67a8ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:56:05 +0100 Subject: [PATCH 52/78] Add key context to ConversationEditor Co-Authored-By: Marshall --- crates/assistant2/src/assistant_panel.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2a589afc4f519fa51e2567d55851443f1a538703..b926cb51efd9445498da5c63790117f040e2af13 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2474,6 +2474,7 @@ impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() + .key_context("ConversationEditor") .size_full() .relative() .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) From 5e558e2a58e6b1e38df3df6480a2efd2288f3ec0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:57:18 +0100 Subject: [PATCH 53/78] Make more menu-related platform methods no-ops --- crates/gpui2/src/platform/test/platform.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index edbe39480994a9425f733ea5a5ed967f63535f11..876120b62626657541fabd1b790e857e16bd5865 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -214,17 +214,11 @@ impl Platform for TestPlatform { fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn on_app_menu_action(&self, _callback: Box) { - unimplemented!() - } + fn on_app_menu_action(&self, _callback: Box) {} - fn on_will_open_app_menu(&self, _callback: Box) { - unimplemented!() - } + fn on_will_open_app_menu(&self, _callback: Box) {} - fn on_validate_app_menu_command(&self, _callback: Box bool>) { - unimplemented!() - } + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} fn os_name(&self) -> &'static str { "test" From 2aee3e3192cd7fd682a288bc27444ef5954089c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 18:02:45 +0100 Subject: [PATCH 54/78] Make `Node::context` optional as well This was an oversight in d09dfe0. Co-Authored-By: Marshall --- crates/gpui2/src/key_dispatch.rs | 10 +++++----- crates/gpui2/src/window.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 80e662ad3ef6477b4cd0d925b2852957a4c04d00..7b8d506d03b2feb5c349838625a51e9d2d27b4b7 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -28,7 +28,7 @@ pub(crate) struct DispatchTree { pub(crate) struct DispatchNode { pub key_listeners: SmallVec<[KeyListener; 2]>, pub action_listeners: SmallVec<[DispatchActionListener; 16]>, - pub context: KeyContext, + pub context: Option, parent: Option, } @@ -70,14 +70,14 @@ impl DispatchTree { }); self.node_stack.push(node_id); if let Some(context) = context { - self.active_node().context = context.clone(); + self.active_node().context = Some(context.clone()); self.context_stack.push(context); } } pub fn pop_node(&mut self) { let node_id = self.node_stack.pop().unwrap(); - if !self.nodes[node_id.0].context.is_empty() { + if self.nodes[node_id.0].context.is_some() { self.context_stack.pop(); } } @@ -95,8 +95,8 @@ impl DispatchTree { self.context_stack.clear(); for node_id in dispatch_path { let node = self.node(node_id); - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); + if let Some(context) = node.context.clone() { + self.context_stack.push(context); } if let Some((context_stack, matcher)) = old_tree diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8995d04b64de4abac4653e228691f3784eb89a6e..2f4708984355661c90764e1a00daf31084259486 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1393,8 +1393,8 @@ impl<'a> WindowContext<'a> { for node_id in &dispatch_path { let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { - context_stack.push(node.context.clone()); + if let Some(context) = node.context.clone() { + context_stack.push(context); } for key_listener in node.key_listeners.clone() { @@ -1418,7 +1418,7 @@ impl<'a> WindowContext<'a> { // Match keystrokes let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { + if node.context.is_some() { if let Some(key_down_event) = event.downcast_ref::() { if let Some(found) = self .window @@ -1563,7 +1563,7 @@ impl<'a> WindowContext<'a> { let context_stack = dispatch_tree .dispatch_path(node_id) .into_iter() - .map(|node_id| dispatch_tree.node(node_id).context.clone()) + .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) .collect(); dispatch_tree.bindings_for_action(action, &context_stack) } From f6a7a6c4d4df8ec1cfcc7fc299a494c1718c26d6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 12:03:59 -0500 Subject: [PATCH 55/78] v0.117.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 004214c9c85f0642e579c60672b40b04626ed1f8..0d4bb71f9826e4f35f096ef3661afaceb60f4fff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11706,7 +11706,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.116.0" +version = "0.117.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 245bb4cd5895e6f72641241b193987780bdec768..6d9cb3c7502bc539cedc26d45c97fde5e5b7099f 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.116.0" +version = "0.117.0" publish = false [lib] From 5ae20607833b27ef22b6566c10516aba022fde95 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 12:34:47 -0500 Subject: [PATCH 56/78] collab 0.30.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 0d4bb71f9826e4f35f096ef3661afaceb60f4fff..f67b3dd0ae0fdd1bcec98b4f59e407021ef01451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,7 +1702,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.29.1" +version = "0.30.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 33c3c14ddd6552fc53a873694bcd1541f945ebe0..50491704c9792a241f10b2c2ac3dbcf146b77c61 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.29.1" +version = "0.30.0" publish = false [[bin]] From e4884f1d7600740e97c9ea46ac2416d51ab444fb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 18:39:50 +0100 Subject: [PATCH 57/78] Move assistant actions to the top of the crate Co-Authored-By: Marshall --- crates/assistant2/src/assistant.rs | 15 +++++++++- crates/assistant2/src/assistant_panel.rs | 35 ++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 91d61a19f98b4cf4a61257a68d8f7212c6a33586..910eeda9e115d853cfe2a7ccaea041245050484e 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -12,12 +12,25 @@ use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; use futures::StreamExt; -use gpui::AppContext; +use gpui::{actions, AppContext}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use util::paths::CONVERSATIONS_DIR; +actions!( + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, +); + #[derive( Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b926cb51efd9445498da5c63790117f040e2af13..cea7199759477ee441a7f4d4427255199268fc44 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2,8 +2,9 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, prompts::generate_content_prompt, - MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, + Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, + NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, }; use ai::{ @@ -28,12 +29,12 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, - AsyncWindowContext, ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, - FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, - ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, - StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, - View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, + div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, + ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, FontStyle, + FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, + ParentElement, Pixels, PromptLevel, Render, SharedString, StatefulInteractiveElement, Styled, + Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, + WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; @@ -51,10 +52,9 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use theme::{ActiveTheme, ThemeSettings}; +use theme::ThemeSettings; use ui::{ - h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, - IconElement, Label, Selectable, Tooltip, + h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -64,19 +64,6 @@ use workspace::{ Save, Toast, ToggleZoom, Toolbar, Workspace, }; -actions!( - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, -); - pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); cx.observe_new_views( From cc9eff89f5226c419a9e697660208b964c1cf544 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 19:12:01 +0100 Subject: [PATCH 58/78] Use a handler instead of an action for clicks This prevents dispatching actions on buttons that were not the target of the click. Co-Authored-By: Marshall --- crates/assistant2/src/assistant_panel.rs | 8 ++- .../quick_action_bar2/src/quick_action_bar.rs | 19 ++++--- crates/search2/src/buffer_search.rs | 50 +++++++++---------- .../ui2/src/components/button/icon_button.rs | 6 +-- crates/workspace2/src/dock.rs | 5 +- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index cea7199759477ee441a7f4d4427255199268fc44..202f8a2092152158341cc5aaf0752fa74ea23b2c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2574,7 +2574,9 @@ impl Render for InlineAssistant { .w(measurements.gutter_width) .child( IconButton::new("include_conversation", Icon::Ai) - .action(Box::new(ToggleIncludeConversation)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_include_conversation(&ToggleIncludeConversation, cx) + })) .selected(self.include_conversation) .tooltip(|cx| { Tooltip::for_action( @@ -2587,7 +2589,9 @@ impl Render for InlineAssistant { .children(if SemanticIndex::enabled(cx) { Some( IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .action(Box::new(ToggleRetrieveContext)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_retrieve_context(&ToggleRetrieveContext, cx) + })) .selected(self.retrieve_context) .tooltip(|cx| { Tooltip::for_action( diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 3232de08adea814fcf33e7fc54b598a93bf18c25..3686ace2fb0b73a530a98681a5639c3a1bfc71e4 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -2,8 +2,8 @@ use editor::Editor; use gpui::{ - Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, - Styled, Subscription, View, ViewContext, WeakView, + Action, ClickEvent, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, + Stateful, Styled, Subscription, View, ViewContext, WeakView, }; use search::BufferSearchBar; use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; @@ -41,19 +41,24 @@ impl Render for QuickActionBar { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let buffer_search_bar = self.buffer_search_bar.clone(); let search_button = QuickActionBarButton::new( "toggle buffer search", Icon::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(search::buffer_search::Deploy { focus: false }), "Buffer Search", + move |_, cx| { + buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + }, ); let assistant_button = QuickActionBarButton::new( - "toggle inline assitant", + "toggle inline assistant", Icon::MagicWand, false, Box::new(gpui::NoAction), "Inline assistant", + |_, _cx| todo!(), ); h_stack() .id("quick action bar") @@ -154,6 +159,7 @@ struct QuickActionBarButton { action: Box, tooltip: SharedString, tooltip_meta: Option, + on_click: Box, } impl QuickActionBarButton { @@ -163,6 +169,7 @@ impl QuickActionBarButton { toggled: bool, action: Box, tooltip: impl Into, + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { Self { id: id.into(), @@ -171,6 +178,7 @@ impl QuickActionBarButton { action, tooltip: tooltip.into(), tooltip_meta: None, + on_click: Box::new(on_click), } } @@ -201,10 +209,7 @@ impl RenderOnce for QuickActionBarButton { Tooltip::for_action(tooltip.clone(), &*action, cx) } }) - .on_click({ - let action = self.action.boxed_clone(); - move |_, cx| cx.dispatch_action(action.boxed_clone()) - }) + .on_click(move |event, cx| (self.on_click)(event, cx)) } } diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index b9fa36ef34e4b7413043a8d1c76357c55953270a..da32f51194cc68d869a76ba8b88023f5450b2844 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -18,7 +18,7 @@ use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; -use ui::{h_stack, Icon, IconButton, IconElement}; +use ui::{h_stack, Clickable, Icon, IconButton, IconElement}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -161,16 +161,6 @@ impl Render for BufferSearchBar { Some(ui::Label::new(message)) }); - let nav_button_for_direction = |icon, direction| { - render_nav_button( - icon, - self.active_match_index.is_some(), - cx.listener(move |this, _, cx| match direction { - Direction::Prev => this.select_prev_match(&Default::default(), cx), - Direction::Next => this.select_next_match(&Default::default(), cx), - }), - ) - }; let should_show_replace_input = self.replace_enabled && supported_options.replacement; let replace_all = should_show_replace_input .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); @@ -237,15 +227,21 @@ impl Render for BufferSearchBar { h_stack() .gap_0p5() .flex_none() - .child(self.render_action_button()) + .child(self.render_action_button(cx)) .children(match_count) - .child(nav_button_for_direction( + .child(render_nav_button( ui::Icon::ChevronLeft, - Direction::Prev, + self.active_match_index.is_some(), + cx.listener(move |this, _, cx| { + this.select_prev_match(&Default::default(), cx); + }), )) - .child(nav_button_for_direction( + .child(render_nav_button( ui::Icon::ChevronRight, - Direction::Next, + self.active_match_index.is_some(), + cx.listener(move |this, _, cx| { + this.select_next_match(&Default::default(), cx); + }), )), ) } @@ -317,13 +313,7 @@ impl BufferSearchBar { pane.update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, |this, cx| { - if this.is_dismissed() { - this.show(cx); - } else { - this.dismiss(&Dismiss, cx); - } - }); + search_bar.update(cx, |this, cx| this.toggle(cx)); return; } let view = cx.build_view(|cx| BufferSearchBar::new(cx)); @@ -487,6 +477,14 @@ impl BufferSearchBar { false } + pub fn toggle(&mut self, cx: &mut ViewContext) { + if self.is_dismissed() { + self.show(cx); + } else { + self.dismiss(&Dismiss, cx); + } + } + pub fn show(&mut self, cx: &mut ViewContext) -> bool { if self.active_searchable_item.is_none() { return false; @@ -588,12 +586,14 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button(&self) -> impl IntoElement { + fn render_action_button(&self, cx: &mut ViewContext) -> impl IntoElement { // let tooltip_style = theme.tooltip.clone(); // let style = theme.search.action_button.clone(); - IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches)) + IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| { + this.select_all_matches(&SelectAllMatches, cx); + })) } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index 94431ef642e08840034ff7f2e4025be5f220daac..f49120e90c1c1afea3857152ee6d286cb7051596 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -1,4 +1,4 @@ -use gpui::{Action, AnyView, DefiniteLength}; +use gpui::{AnyView, DefiniteLength}; use crate::prelude::*; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; @@ -39,10 +39,6 @@ impl IconButton { self.selected_icon = icon.into(); self } - - pub fn action(self, action: Box) -> Self { - self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone())) - } } impl Disableable for IconButton { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index abcf5c49bc929c5954c3e40557e419e4ecaca702..a0a90293d603a3afde61dda2c4ca59ec5fa273a1 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -724,7 +724,10 @@ impl Render for PanelButtons { .trigger( IconButton::new(name, icon) .selected(is_active_button) - .action(action.boxed_clone()) + .on_click({ + let action = action.boxed_clone(); + move |_, cx| cx.dispatch_action(action.boxed_clone()) + }) .tooltip(move |cx| { Tooltip::for_action(tooltip.clone(), &*action, cx) }), From 7e2ff63270e6779350fdbf7b0f6d49a3e7668612 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 19:15:09 +0100 Subject: [PATCH 59/78] Paint blocks on top of the editor This ensures blocks get mouse events before the editor beneath them. Co-Authored-By: Marshall --- crates/editor2/src/element.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab11f5ffb5427d39905675756be5d9996e984fc1..d7badd4ab5cc080a9f5bb2d58ef586416539632f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2835,8 +2835,10 @@ impl Element for EditorElement { self.paint_text(text_bounds, &mut layout, cx); if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); + cx.with_z_index(1, |cx| { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }) }) } }); From fdd64832e75f34a1277505274b5b01953a867acf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 13:52:38 -0500 Subject: [PATCH 60/78] Update inline assist styles --- crates/assistant2/src/assistant_panel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 202f8a2092152158341cc5aaf0752fa74ea23b2c..e7c9d4c21a30ac98f4cd2e03fbd965cc74d1e40c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2562,6 +2562,10 @@ impl Render for InlineAssistant { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let measurements = self.measurements.get(); h_stack() + .w_full() + .py_2() + .border_y_1() + .border_color(cx.theme().colors().border) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::toggle_include_conversation)) @@ -2617,7 +2621,8 @@ impl Render for InlineAssistant { }), ) .child( - div() + h_stack() + .w_full() .ml(measurements.anchor_x - measurements.gutter_width) .child(self.render_prompt_editor(cx)), ) From 147c99f1a750a035a66cad8b5b571f499eb131d2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 14:28:31 -0500 Subject: [PATCH 61/78] Fix layout for terminal tabs (#3514) This PR fixes the layout for terminal tabs. We need to use an `h_stack` here to get the icon and the label to position themselves next to each other instead of one on top of the other. Release Notes: - N/A --- Cargo.lock | 1 + crates/terminal_view2/Cargo.toml | 1 + crates/terminal_view2/src/terminal_view.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9d6fb877fa10a256c6c746451cfcd58e9112136..4b5af36a19b8fcf1049e7d762db189a50730359a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9531,6 +9531,7 @@ dependencies = [ "terminal2", "theme2", "thiserror", + "ui2", "util", "workspace2", ] diff --git a/crates/terminal_view2/Cargo.toml b/crates/terminal_view2/Cargo.toml index 12e2c06504fad5adc7e502c4225a28c9bf985f30..9654bed7f56f236d0dc57196e1e25a9452c507e3 100644 --- a/crates/terminal_view2/Cargo.toml +++ b/crates/terminal_view2/Cargo.toml @@ -21,6 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2" } db = { package = "db2", path = "../db2" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } terminal = { package = "terminal2", path = "../terminal2" } +ui = { package = "ui2", path = "../ui2" } smallvec.workspace = true smol.workspace = true mio-extras = "2.0.6" diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 570b37ba098b86c159b7acce1e6941402336ec97..49da703addd1e4380341fd8590589610706cd536 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,10 +9,9 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent, - FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement, - KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, - SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle, + Focusable, FocusableElement, FocusableView, InputHandler, KeyDownEvent, Keystroke, Model, + MouseButton, MouseDownEvent, Pixels, Render, Task, View, VisualContext, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; @@ -25,13 +24,13 @@ use terminal::{ terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, Event, MaybeNavigationTarget, Terminal, }; +use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, Icon, IconElement, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -745,7 +744,8 @@ impl Item for TerminalView { fn tab_content(&self, _detail: Option, cx: &WindowContext) -> AnyElement { let title = self.terminal().read(cx).title(); - div() + h_stack() + .gap_2() .child(IconElement::new(Icon::Terminal)) .child(Label::new(title)) .into_any() From ac07e230faf4f3d84fd176775908045c39a6d4b9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 6 Dec 2023 12:28:44 -0700 Subject: [PATCH 62/78] Document geometry --- crates/editor2/src/editor_tests.rs | 56 +- crates/editor2/src/element.rs | 4 +- crates/gpui2/src/elements/overlay.rs | 2 +- crates/gpui2/src/geometry.rs | 1247 ++++++++++++++++++++- crates/gpui2/src/platform/test/display.rs | 2 +- crates/gpui2/src/platform/test/window.rs | 4 +- crates/gpui2/src/style.rs | 2 +- crates/gpui2/src/window.rs | 2 +- 8 files changed, 1285 insertions(+), 34 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 571cbd84bb179be0b1562dd07f5c7a0114e1b8e4..9ffa3e523bff4aafb840abfe0c699f62776707e6 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -345,7 +345,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -356,7 +361,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -368,7 +378,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { editor.update(cx, |view, cx| { view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -380,7 +395,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(0, 0), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -423,7 +443,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { }); view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -432,7 +457,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -643,11 +673,21 @@ fn test_cancel(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); view.end_selection(cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(0, 3), + 0, + gpui::Point::::default(), + cx, + ); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab11f5ffb5427d39905675756be5d9996e984fc1..7c7d7472f2ad22ecdd592c9b79fc62ca6a0b56ed 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -485,7 +485,7 @@ impl EditorElement { let modifiers = event.modifiers; if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { let point_for_position = position_map.point_for_position(text_bounds, event.position); - let mut scroll_delta = gpui::Point::::zero(); + let mut scroll_delta = gpui::Point::::default(); let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); let top = text_bounds.origin.y + vertical_margin; let bottom = text_bounds.lower_left().y - vertical_margin; @@ -511,7 +511,7 @@ impl EditorElement { position: point_for_position.previous_valid, goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) - .clamp(&gpui::Point::zero(), &position_map.scroll_max), + .clamp(&gpui::Point::default(), &position_map.scroll_max), }, cx, ); diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 764bdfabcd6695d3ee0b4dd71cc51e472567c09e..7d4b90963760abb1ad9df260ad418fba47819bb4 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -102,7 +102,7 @@ impl Element for Overlay { let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { - origin: Point::zero(), + origin: Point::default(), size: cx.viewport_size(), }; diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 20afd2d288b29bc8953c41c79389b9331a80244b..50f680f493cae5ee2c2d634c982ad30b357f5880 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -8,6 +8,18 @@ use std::{ ops::{Add, Div, Mul, MulAssign, Sub}, }; +/// Describes a location in a 2D cartesian coordinate space. +/// +/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. +/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let point = Point { x: 10, y: 20 }; +/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } +/// ``` #[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)] #[refineable(Debug)] #[repr(C)] @@ -16,19 +28,66 @@ pub struct Point { pub y: T, } +/// Constructs a new `Point` with the given x and y coordinates. +/// +/// # Arguments +/// +/// * `x` - The x coordinate of the point. +/// * `y` - The y coordinate of the point. +/// +/// # Returns +/// +/// Returns a `Point` with the specified coordinates. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let p = point(10, 20); +/// assert_eq!(p.x, 10); +/// assert_eq!(p.y, 20); +/// ``` pub fn point(x: T, y: T) -> Point { Point { x, y } } impl Point { + /// Creates a new `Point` with the specified `x` and `y` coordinates. + /// + /// # Arguments + /// + /// * `x` - The horizontal coordinate of the point. + /// * `y` - The vertical coordinate of the point. + /// + /// # Examples + /// + /// ``` + /// let p = Point::new(10, 20); + /// assert_eq!(p.x, 10); + /// assert_eq!(p.y, 20); + /// ``` pub const fn new(x: T, y: T) -> Self { Self { x, y } } - pub fn zero() -> Self { - Self::new(T::default(), T::default()) - } - + /// Transforms the point to a `Point` by applying the given function to both coordinates. + /// + /// This method allows for converting a `Point` to a `Point` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `x` + /// and `y` coordinates, resulting in a new point of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 3, y: 4 }; + /// let p_float = p.map(|coord| coord as f32); + /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); + /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Point { Point { x: f(self.x.clone()), @@ -38,6 +97,21 @@ impl Point { } impl Point { + /// Scales the point by a given factor, which is typically derived from the resolution + /// of a target display to ensure proper sizing of UI elements. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to both the x and y coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Pixels, ScaledPixels}; + /// let p = Point { x: Pixels(10.0), y: Pixels(20.0) }; + /// let scaled_p = p.scale(1.5); + /// assert_eq!(scaled_p, Point { x: ScaledPixels(15.0), y: ScaledPixels(30.0) }); + /// ``` pub fn scale(&self, factor: f32) -> Point { Point { x: self.x.scale(factor), @@ -45,6 +119,16 @@ impl Point { } } + /// Calculates the Euclidean distance from the origin (0, 0) to this point. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// # use zed::Pixels; + /// let p = Point { x: Pixels(3.0), y: Pixels(4.0) }; + /// assert_eq!(p.magnitude(), 5.0); + /// ``` pub fn magnitude(&self) -> f64 { ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() } @@ -95,14 +179,29 @@ impl Point where T: PartialOrd + Clone + Default + Debug, { + /// Returns a new point with the maximum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let max_point = p1.max(&p2); + /// assert_eq!(max_point, Point { x: 5, y: 7 }); + /// ``` pub fn max(&self, other: &Self) -> Self { Point { - x: if self.x >= other.x { + x: if self.x > other.x { self.x.clone() } else { other.x.clone() }, - y: if self.y >= other.y { + y: if self.y > other.y { self.y.clone() } else { other.y.clone() @@ -110,6 +209,21 @@ where } } + /// Returns a new point with the minimum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let min_point = p1.min(&p2); + /// assert_eq!(min_point, Point { x: 3, y: 2 }); + /// ``` pub fn min(&self, other: &Self) -> Self { Point { x: if self.x <= other.x { @@ -125,6 +239,32 @@ where } } + /// Clamps the point to a specified range. + /// + /// Given a minimum point and a maximum point, this method constrains the current point + /// such that its coordinates do not exceed the range defined by the minimum and maximum points. + /// If the current point's coordinates are less than the minimum, they are set to the minimum. + /// If they are greater than the maximum, they are set to the maximum. + /// + /// # Arguments + /// + /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. + /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 10, y: 20 }; + /// let min = Point { x: 0, y: 5 }; + /// let max = Point { x: 15, y: 25 }; + /// let clamped_p = p.clamp(&min, &max); + /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); + /// + /// let p_out_of_bounds = Point { x: -5, y: 30 }; + /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); + /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); + /// ``` pub fn clamp(&self, min: &Self, max: &Self) -> Self { self.max(min).min(max) } @@ -139,6 +279,10 @@ impl Clone for Point { } } +/// A structure representing a two-dimensional size with width and height in a given unit. +/// +/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. +/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] #[refineable(Debug)] #[repr(C)] @@ -147,6 +291,21 @@ pub struct Size { pub height: T, } +/// Constructs a new `Size` with the provided width and height. +/// +/// # Arguments +/// +/// * `width` - The width component of the `Size`. +/// * `height` - The height component of the `Size`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Size; +/// let my_size = size(10, 20); +/// assert_eq!(my_size.width, 10); +/// assert_eq!(my_size.height, 20); +/// ``` pub fn size(width: T, height: T) -> Size where T: Clone + Default + Debug, @@ -158,6 +317,24 @@ impl Size where T: Clone + Default + Debug, { + /// Applies a function to the width and height of the size, producing a new `Size`. + /// + /// This method allows for converting a `Size` to a `Size` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `width` + /// and `height`, resulting in a new size of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let my_size = Size { width: 10, height: 20 }; + /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); + /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); + /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Size where U: Clone + Default + Debug, @@ -170,6 +347,24 @@ where } impl Size { + /// Scales the size by a given factor. + /// + /// This method multiplies both the width and height by the provided scaling factor, + /// resulting in a new `Size` that is proportionally larger or smaller + /// depending on the factor. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the width and height. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Size, Pixels, ScaledPixels}; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let scaled_size = size.scale(2.0); + /// assert_eq!(scaled_size, Size { width: ScaledPixels(200.0), height: ScaledPixels(100.0) }); + /// ``` pub fn scale(&self, factor: f32) -> Size { Size { width: self.width.scale(factor), @@ -182,6 +377,21 @@ impl Size where T: PartialOrd + Clone + Default + Debug, { + /// Returns a new `Size` with the maximum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let max_size = size1.max(&size2); + /// assert_eq!(max_size, Size { width: 50, height: 40 }); + /// ``` pub fn max(&self, other: &Self) -> Self { Size { width: if self.width >= other.width { @@ -286,6 +496,14 @@ impl From> for Size { } impl Size { + /// Returns a `Size` with both width and height set to fill the available space. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, + /// which represents 100% of the available space in both dimensions. + /// + /// # Returns + /// + /// A `Size` that will fill the available space when used in a layout. pub fn full() -> Self { Self { width: relative(1.).into(), @@ -294,16 +512,16 @@ impl Size { } } -impl Size { - pub fn zero() -> Self { - Self { - width: px(0.).into(), - height: px(0.).into(), - } - } -} - impl Size { + /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, + /// indicating that their size should be computed based on the layout context, such as the content size or + /// available space. + /// + /// # Returns + /// + /// A `Size` with width and height set to `Length::Auto`. pub fn auto() -> Self { Self { width: Length::Auto, @@ -312,6 +530,23 @@ impl Size { } } +/// Represents a rectangular area in a 2D space with an origin point and a size. +/// +/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. +/// The origin is represented as a `Point` which defines the upper-left corner of the rectangle, +/// and the size is represented as a `Size` which defines the width and height of the rectangle. +/// +/// # Examples +/// +/// ``` +/// # use zed::{Bounds, Point, Size}; +/// let origin = Point { x: 0, y: 0 }; +/// let size = Size { width: 10, height: 20 }; +/// let bounds = Bounds::new(origin, size); +/// +/// assert_eq!(bounds.origin, origin); +/// assert_eq!(bounds.size, size); +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -324,6 +559,33 @@ impl Bounds where T: Clone + Debug + Sub + Default, { + /// Constructs a `Bounds` from two corner points: the upper-left and lower-right corners. + /// + /// This function calculates the origin and size of the `Bounds` based on the provided corner points. + /// The origin is set to the upper-left corner, and the size is determined by the difference between + /// the x and y coordinates of the lower-right and upper-left points. + /// + /// # Arguments + /// + /// * `upper_left` - A `Point` representing the upper-left corner of the rectangle. + /// * `lower_right` - A `Point` representing the lower-right corner of the rectangle. + /// + /// # Returns + /// + /// Returns a `Bounds` that encompasses the area defined by the two corner points. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point}; + /// let upper_left = Point { x: 0, y: 0 }; + /// let lower_right = Point { x: 10, y: 10 }; + /// let bounds = Bounds::from_corners(upper_left, lower_right); + /// + /// assert_eq!(bounds.origin, upper_left); + /// assert_eq!(bounds.size.width, 10); + /// assert_eq!(bounds.size.height, 10); + /// ``` pub fn from_corners(upper_left: Point, lower_right: Point) -> Self { let origin = Point { x: upper_left.x.clone(), @@ -336,6 +598,16 @@ where Bounds { origin, size } } + /// Creates a new `Bounds` with the specified origin and size. + /// + /// # Arguments + /// + /// * `origin` - A `Point` representing the origin of the bounds. + /// * `size` - A `Size` representing the size of the bounds. + /// + /// # Returns + /// + /// Returns a `Bounds` that has the given origin and size. pub fn new(origin: Point, size: Size) -> Self { Bounds { origin, size } } @@ -345,6 +617,39 @@ impl Bounds where T: Clone + Debug + PartialOrd + Add + Sub + Default + Half, { + /// Checks if this `Bounds` intersects with another `Bounds`. + /// + /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. + /// This method checks if there is any overlapping area between the two bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to check for intersection with. + /// + /// # Returns + /// + /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds3 = Bounds { + /// origin: Point { x: 20, y: 20 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds + /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds + /// ``` pub fn intersects(&self, other: &Bounds) -> bool { let my_lower_right = self.lower_right(); let their_lower_right = other.lower_right(); @@ -355,6 +660,32 @@ where && my_lower_right.y > other.origin.y } + /// Dilates the bounds by a specified amount in all directions. + /// + /// This method expands the bounds by the given `amount`, increasing the size + /// and adjusting the origin so that the bounds grow outwards equally in all directions. + /// The resulting bounds will have its width and height increased by twice the `amount` + /// (since it grows in both directions), and the origin will be moved by `-amount` + /// in both the x and y directions. + /// + /// # Arguments + /// + /// * `amount` - The amount by which to dilate the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let mut bounds = Bounds { + /// origin: Point { x: 10, y: 10 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// bounds.dilate(5); + /// assert_eq!(bounds, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` pub fn dilate(&mut self, amount: T) { self.origin.x = self.origin.x.clone() - amount.clone(); self.origin.y = self.origin.y.clone() - amount.clone(); @@ -363,6 +694,27 @@ where self.size.height = self.size.height.clone() + double_amount; } + /// Returns the center point of the bounds. + /// + /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height + /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the + /// coordinate system. + /// + /// # Returns + /// + /// A `Point` representing the center of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let center = bounds.center(); + /// assert_eq!(center, Point { x: 5, y: 10 }); + /// ``` pub fn center(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone().half(), @@ -372,12 +724,78 @@ where } impl + Sub> Bounds { + /// Calculates the intersection of two `Bounds` objects. + /// + /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, + /// the resulting `Bounds` will have a size with width and height of zero. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to intersect with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the intersection area. If there is no intersection, + /// the returned `Bounds` will have a size with width and height of zero. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let intersection = bounds1.intersect(&bounds2); + /// + /// assert_eq!(intersection, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 5, height: 5 }, + /// }); + /// ``` pub fn intersect(&self, other: &Self) -> Self { let upper_left = self.origin.max(&other.origin); let lower_right = self.lower_right().min(&other.lower_right()); Self::from_corners(upper_left, lower_right) } + /// Computes the union of two `Bounds`. + /// + /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. + /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, + /// and a size that encompasses the furthest extents of both `Bounds`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to create a union with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the union of the two `Bounds`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 15, height: 15 }, + /// }; + /// let union_bounds = bounds1.union(&bounds2); + /// + /// assert_eq!(union_bounds, Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` pub fn union(&self, other: &Self) -> Self { let top_left = self.origin.min(&other.origin); let bottom_right = self.lower_right().max(&other.lower_right()); @@ -432,22 +850,59 @@ impl Bounds where T: Add + Clone + Default + Debug, { + /// Returns the top edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the top edge of the bounds. pub fn top(&self) -> T { self.origin.y.clone() } + /// Returns the bottom edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. pub fn bottom(&self) -> T { self.origin.y.clone() + self.size.height.clone() } + /// Returns the left edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the left edge of the bounds. pub fn left(&self) -> T { self.origin.x.clone() } + /// Returns the right edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the right edge of the bounds. pub fn right(&self) -> T { self.origin.x.clone() + self.size.width.clone() } + /// Returns the upper-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the upper-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let upper_right = bounds.upper_right(); + /// assert_eq!(upper_right, Point { x: 10, y: 0 }); + /// ``` pub fn upper_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), @@ -455,6 +910,23 @@ where } } + /// Returns the lower-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_right = bounds.lower_right(); + /// assert_eq!(lower_right, Point { x: 10, y: 20 }); + /// ``` pub fn lower_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), @@ -462,6 +934,23 @@ where } } + /// Returns the lower-left corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-left corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_left = bounds.lower_left(); + /// assert_eq!(lower_left, Point { x: 0, y: 20 }); + /// ``` pub fn lower_left(&self) -> Point { Point { x: self.origin.x.clone(), @@ -474,6 +963,35 @@ impl Bounds where T: Add + PartialOrd + Clone + Default + Debug, { + /// Checks if the given point is within the bounds. + /// + /// This method determines whether a point lies inside the rectangle defined by the bounds, + /// including the edges. The point is considered inside if its x-coordinate is greater than + /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate + /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. + /// + /// # Arguments + /// + /// * `point` - A reference to a `Point` that represents the point to check. + /// + /// # Returns + /// + /// Returns `true` if the point is within the bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Bounds}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let inside_point = Point { x: 5, y: 5 }; + /// let outside_point = Point { x: 15, y: 15 }; + /// + /// assert!(bounds.contains_point(&inside_point)); + /// assert!(!bounds.contains_point(&outside_point)); + /// ``` pub fn contains_point(&self, point: &Point) -> bool { point.x >= self.origin.x && point.x <= self.origin.x.clone() + self.size.width.clone() @@ -481,6 +999,34 @@ where && point.y <= self.origin.y.clone() + self.size.height.clone() } + /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. + /// + /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to the `origin` and + /// `size` fields, resulting in new bounds of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Bounds` with the origin and size mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map(|value| value as f64 * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); pub fn map(&self, f: impl Fn(T) -> U) -> Bounds where U: Clone + Default + Debug, @@ -493,6 +1039,36 @@ where } impl Bounds { + /// Scales the bounds by a given factor, typically used to adjust for display scaling. + /// + /// This method multiplies the origin and size of the bounds by the provided scaling factor, + /// resulting in a new `Bounds` that is proportionally larger or smaller + /// depending on the scaling factor. This can be used to ensure that the bounds are properly + /// scaled for different display densities. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. + /// + /// # Returns + /// + /// Returns a new `Bounds` that represents the scaled bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size, Pixels}; + /// let bounds = Bounds { + /// origin: Point { x: Pixels(10.0), y: Pixels(20.0) }, + /// size: Size { width: Pixels(30.0), height: Pixels(40.0) }, + /// }; + /// let display_scale_factor = 2.0; + /// let scaled_bounds = bounds.scale(display_scale_factor); + /// assert_eq!(scaled_bounds, Bounds { + /// origin: Point { x: ScaledPixels(20.0), y: ScaledPixels(40.0) }, + /// size: Size { width: ScaledPixels(60.0), height: ScaledPixels(80.0) }, + /// }); + /// ``` pub fn scale(&self, factor: f32) -> Bounds { Bounds { origin: self.origin.scale(factor), @@ -503,6 +1079,26 @@ impl Bounds { impl Copy for Bounds {} +/// Represents the edges of a box in a 2D space, such as padding or margin. +/// +/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Edges; +/// let edges = Edges { +/// top: 10.0, +/// right: 20.0, +/// bottom: 30.0, +/// left: 40.0, +/// }; +/// +/// assert_eq!(edges.top, 10.0); +/// assert_eq!(edges.right, 20.0); +/// assert_eq!(edges.bottom, 30.0); +/// assert_eq!(edges.left, 40.0); +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -545,6 +1141,30 @@ where impl Copy for Edges {} impl Edges { + /// Constructs `Edges` where all sides are set to the same specified value. + /// + /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, + /// such as padding or margin with the same size on all sides. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four sides of the edges. + /// + /// # Returns + /// + /// An `Edges` instance with all sides set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let uniform_edges = Edges::all(10.0); + /// assert_eq!(uniform_edges.top, 10.0); + /// assert_eq!(uniform_edges.right, 10.0); + /// assert_eq!(uniform_edges.bottom, 10.0); + /// assert_eq!(uniform_edges.left, 10.0); + /// ``` pub fn all(value: T) -> Self { Self { top: value.clone(), @@ -554,6 +1174,28 @@ impl Edges { } } + /// Applies a function to each field of the `Edges`, producing a new `Edges`. + /// + /// This method allows for converting an `Edges` to an `Edges` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Edges` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; + /// let edges_float = edges.map(|&value| value as f32 * 1.1); + /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); + /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Edges where U: Clone + Default + Debug, @@ -566,6 +1208,33 @@ impl Edges { } } + /// Checks if any of the edges satisfy a given predicate. + /// + /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. + /// + /// # Arguments + /// + /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. + /// + /// # Returns + /// + /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { + /// top: 10, + /// right: 0, + /// bottom: 5, + /// left: 0, + /// }; + /// + /// assert!(edges.any(|value| *value == 0)); + /// assert!(edges.any(|value| *value > 0)); + /// assert!(!edges.any(|value| *value > 10)); + /// ``` pub fn any bool>(&self, predicate: F) -> bool { predicate(&self.top) || predicate(&self.right) @@ -575,6 +1244,24 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. + /// + /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to `Length::Auto`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let auto_edges = Edges::auto(); + /// assert_eq!(auto_edges.top, Length::Auto); + /// assert_eq!(auto_edges.right, Length::Auto); + /// assert_eq!(auto_edges.bottom, Length::Auto); + /// assert_eq!(auto_edges.left, Length::Auto); + /// ``` pub fn auto() -> Self { Self { top: Length::Auto, @@ -584,6 +1271,25 @@ impl Edges { } } + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -595,6 +1301,25 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.right, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.bottom, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.left, DefiniteLength::from(zed::px(0.))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -604,6 +1329,42 @@ impl Edges { } } + /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. + /// + /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account + /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). + /// + /// # Arguments + /// + /// * `parent_size` - `Size` representing the size of the parent element. + /// * `rem_size` - `Pixels` representing the size of one REM unit. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, DefiniteLength, px, AbsoluteLength, Size}; + /// let edges = Edges { + /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), + /// right: DefiniteLength::Fraction(0.5), + /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), + /// left: DefiniteLength::Fraction(0.25), + /// }; + /// let parent_size = Size { + /// width: AbsoluteLength::Pixels(px(200.0)), + /// height: AbsoluteLength::Pixels(px(100.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels + /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width + /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems + /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width + /// ``` pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(parent_size.height, rem_size), @@ -615,6 +1376,25 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels(0.0))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -624,6 +1404,37 @@ impl Edges { } } + /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. + /// + /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. + /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem unit in pixels. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, AbsoluteLength, Pixels, px}; + /// let edges = Edges { + /// top: AbsoluteLength::Pixels(px(10.0)), + /// right: AbsoluteLength::Rems(rems(1.0)), + /// bottom: AbsoluteLength::Pixels(px(20.0)), + /// left: AbsoluteLength::Rems(rems(2.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels + /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels + /// ``` pub fn to_pixels(&self, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(rem_size), @@ -635,6 +1446,34 @@ impl Edges { } impl Edges { + /// Scales the `Edges` by a given factor, returning `Edges`. + /// + /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each edge. + /// + /// # Returns + /// + /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, Pixels}; + /// let edges = Edges { + /// top: Pixels(10.0), + /// right: Pixels(20.0), + /// bottom: Pixels(30.0), + /// left: Pixels(40.0), + /// }; + /// let scaled_edges = edges.scale(2.0); + /// assert_eq!(scaled_edges.top, ScaledPixels(20.0)); + /// assert_eq!(scaled_edges.right, ScaledPixels(40.0)); + /// assert_eq!(scaled_edges.bottom, ScaledPixels(60.0)); + /// assert_eq!(scaled_edges.left, ScaledPixels(80.0)); + /// ``` pub fn scale(&self, factor: f32) -> Edges { Edges { top: self.top.scale(factor), @@ -645,6 +1484,10 @@ impl Edges { } } +/// 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`. +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -659,6 +1502,30 @@ impl Corners where T: Clone + Default + Debug, { + /// Constructs `Corners` where all sides are set to the same specified value. + /// + /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, + /// such as a uniform border radius on a rectangle. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four corners. + /// + /// # Returns + /// + /// An `Corners` instance with all corners set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Corners; + /// let uniform_corners = Corners::all(5.0); + /// assert_eq!(uniform_corners.top_left, 5.0); + /// assert_eq!(uniform_corners.top_right, 5.0); + /// assert_eq!(uniform_corners.bottom_right, 5.0); + /// assert_eq!(uniform_corners.bottom_left, 5.0); + /// ``` pub fn all(value: T) -> Self { Self { top_left: value.clone(), @@ -670,6 +1537,42 @@ where } impl Corners { + /// Converts the `AbsoluteLength` to `Pixels` based on the provided size and rem size, ensuring the resulting + /// `Pixels` do not exceed half of the maximum of the provided size's width and height. + /// + /// This method is particularly useful when dealing with corner radii, where the radius in pixels should not + /// exceed half the size of the box it applies to, to avoid the corners overlapping. + /// + /// # Arguments + /// + /// * `size` - The `Size` against which the maximum allowable radius is determined. + /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. + /// + /// # Returns + /// + /// Returns a `Corners` instance with each corner's length converted to pixels and clamped to the + /// maximum allowable radius based on the provided size. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, AbsoluteLength, Pixels, Size}; + /// let corners = Corners { + /// top_left: AbsoluteLength::Pixels(Pixels(15.0)), + /// top_right: AbsoluteLength::Rems(Rems(1.0)), + /// bottom_right: AbsoluteLength::Pixels(Pixels(20.0)), + /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), + /// }; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let rem_size = Pixels(16.0); + /// let corners_in_pixels = corners.to_pixels(size, rem_size); + /// + /// // The resulting corners should not exceed half the size of the smallest dimension (50.0 / 2.0 = 25.0). + /// assert_eq!(corners_in_pixels.top_left, Pixels(15.0)); + /// assert_eq!(corners_in_pixels.top_right, Pixels(16.0)); // 1 rem converted to pixels + /// assert_eq!(corners_in_pixels.bottom_right, Pixels(20.0).min(Pixels(25.0))); // Clamped to 25.0 + /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0).min(Pixels(25.0))); // 2 rems converted to pixels and clamped + /// ``` pub fn to_pixels(&self, size: Size, rem_size: Pixels) -> Corners { let max = size.width.max(size.height) / 2.; Corners { @@ -682,6 +1585,34 @@ impl Corners { } impl Corners { + /// Scales the `Corners` by a given factor, returning `Corners`. + /// + /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each corner. + /// + /// # Returns + /// + /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let scaled_corners = corners.scale(2.0); + /// assert_eq!(scaled_corners.top_left, ScaledPixels(20.0)); + /// assert_eq!(scaled_corners.top_right, ScaledPixels(40.0)); + /// assert_eq!(scaled_corners.bottom_right, ScaledPixels(60.0)); + /// assert_eq!(scaled_corners.bottom_left, ScaledPixels(80.0)); + /// ``` pub fn scale(&self, factor: f32) -> Corners { Corners { top_left: self.top_left.scale(factor), @@ -693,6 +1624,38 @@ impl Corners { } impl Corners { + /// Applies a function to each field of the `Corners`, producing a new `Corners`. + /// + /// This method allows for converting a `Corners` to a `Corners` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Corners` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let corners_in_rems = corners.map(|&px| Rems(px.0 / 16.0)); + /// assert_eq!(corners_in_rems, Corners { + /// top_left: Rems(0.625), + /// top_right: Rems(1.25), + /// bottom_right: Rems(1.875), + /// bottom_left: Rems(2.5), + /// }); + /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Corners where U: Clone + Default + Debug, @@ -737,6 +1700,28 @@ where impl Copy for Corners where T: Copy + Clone + Default + Debug {} +/// 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 +/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit +/// of measurement for all visual elements and layout calculations. +/// +/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for +/// anti-aliasing and animations. However, when applied to actual pixel grids, the value +/// is typically rounded to the nearest integer. +/// +/// # Examples +/// +/// ``` +/// use zed::Pixels; +/// +/// // Define a length of 10 pixels +/// let length = Pixels(10.0); +/// +/// // Define a length and scale it by a factor of 2 +/// let scaled_length = length.scale(2.0); +/// assert_eq!(scaled_length, Pixels(20.0)); +/// ``` #[derive( Clone, Copy, @@ -815,29 +1800,68 @@ impl MulAssign for Pixels { } impl Pixels { + /// Represents zero pixels. pub const ZERO: Pixels = Pixels(0.0); + /// The maximum value that can be represented by `Pixels`. pub const MAX: Pixels = Pixels(f32::MAX); + /// Floors the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the floored value. pub fn floor(&self) -> Self { Self(self.0.floor()) } + /// Rounds the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the rounded value. pub fn round(&self) -> Self { Self(self.0.round()) } + /// Returns the ceiling of the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the ceiling value. pub fn ceil(&self) -> Self { Self(self.0.ceil()) } + /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. + /// + /// This method is used when adjusting pixel values for display scaling factors, + /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and + /// thus requires scaling to maintain visual consistency and readability. + /// + /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering + /// calculations where display scaling is considered. pub fn scale(&self, factor: f32) -> ScaledPixels { ScaledPixels(self.0 * factor) } + /// Raises the `Pixels` value to a given power. + /// + /// # Arguments + /// + /// * `exponent` - The exponent to raise the `Pixels` value by. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the value raised to the given exponent. pub fn pow(&self, exponent: f32) -> Self { Self(self.0.powf(exponent)) } + /// Returns the absolute value of the `Pixels`. + /// + /// # Returns + /// + /// A new `Pixels` instance with the absolute value of the original `Pixels`. pub fn abs(&self) -> Self { Self(self.0.abs()) } @@ -925,6 +1949,13 @@ impl From for Pixels { } } +/// Represents physical pixels on the display. +/// +/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. +/// This type is used when precise pixel manipulation is required, such as rendering graphics or +/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be +/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the +/// display. #[derive( Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, )] @@ -932,6 +1963,28 @@ impl From for Pixels { pub struct DevicePixels(pub(crate) i32); impl DevicePixels { + /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. + /// + /// This function is useful when working with graphical data that needs to be stored in a buffer, + /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. + /// + /// # Arguments + /// + /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. + /// + /// # Returns + /// + /// The number of bytes required to represent the `DevicePixels` value in memory. + /// + /// # Examples + /// + /// ``` + /// # use zed::DevicePixels; + /// let pixels = DevicePixels(10); // 10 device pixels + /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) + /// let total_bytes = pixels.to_bytes(bytes_per_pixel); + /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes + /// ``` pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { self.0 as u32 * bytes_per_pixel as u32 } @@ -991,15 +2044,32 @@ impl From for DevicePixels { } } +/// Represents scaled pixels that take into account the device's scale factor. +/// +/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices +/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), +/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, +/// dimensions and positions can be specified in a way that scales appropriately across different +/// display resolutions. #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct ScaledPixels(pub(crate) f32); impl ScaledPixels { + /// Floors the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the floored value. pub fn floor(&self) -> Self { Self(self.0.floor()) } + /// Rounds the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the rounded value. pub fn ceil(&self) -> Self { Self(self.0.ceil()) } @@ -1031,6 +2101,12 @@ impl From for f64 { } } +/// Represents pixels in a global coordinate space, which can span across multiple displays. +/// +/// `GlobalPixels` is used when dealing with a coordinate system that is not limited to a single +/// display's boundaries. This type is particularly useful in multi-monitor setups where +/// positioning and measurements need to be consistent and relative to a "global" origin point +/// rather than being relative to any individual display. #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct GlobalPixels(pub(crate) f32); @@ -1065,6 +2141,14 @@ impl sqlez::bindable::Bind for GlobalPixels { } } +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [WindowContext::set_rem_size]. +/// +/// Rems are used for defining lengths that are scalable and consistent across different UI elements. +/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), +/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar +/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. +/// +/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] pub struct Rems(pub f32); @@ -1082,17 +2166,26 @@ impl Debug for Rems { } } +/// Represents an absolute length in pixels or rems. +/// +/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not +/// affected by the current font size, or a number of rems, which is relative to the font size of +/// the root element. It is used for specifying dimensions that are either independent of or +/// related to the typographic scale. #[derive(Clone, Copy, Debug, Neg)] pub enum AbsoluteLength { + /// A length in pixels. Pixels(Pixels), + /// A length in rems. Rems(Rems), } impl AbsoluteLength { + /// Checks if the absolute length is zero. pub fn is_zero(&self) -> bool { match self { - AbsoluteLength::Pixels(px) => px.0 == 0., - AbsoluteLength::Rems(rems) => rems.0 == 0., + AbsoluteLength::Pixels(px) => px.0 == 0.0, + AbsoluteLength::Rems(rems) => rems.0 == 0.0, } } } @@ -1110,6 +2203,27 @@ impl From for AbsoluteLength { } impl AbsoluteLength { + /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{AbsoluteLength, Pixels}; + /// let length_in_pixels = AbsoluteLength::Pixels(Pixels(42.0)); + /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); + /// let rem_size = Pixels(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); + /// ``` pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { match self { AbsoluteLength::Pixels(pixels) => *pixels, @@ -1125,14 +2239,47 @@ impl Default for AbsoluteLength { } /// A non-auto length that can be defined in pixels, rems, or percent of parent. +/// +/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically +/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a +/// fraction of the parent's size. #[derive(Clone, Copy, Neg)] pub enum DefiniteLength { + /// An absolute length specified in pixels or rems. Absolute(AbsoluteLength), - /// A fraction of the parent's size between 0 and 1. + /// A relative length specified as a fraction of the parent's size, between 0 and 1. Fraction(f32), } impl DefiniteLength { + /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. + /// + /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. + /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. + /// + /// # Arguments + /// + /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. + /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. + /// + /// # Returns + /// + /// Returns the `DefiniteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; + /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); + /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); + /// let length_as_fraction = DefiniteLength::Fraction(0.5); + /// let base_size = AbsoluteLength::Pixels(px(100.0)); + /// let rem_size = px(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); + /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); + /// ``` pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { match self { DefiniteLength::Absolute(size) => size.to_pixels(rem_size), @@ -1180,7 +2327,9 @@ impl Default for DefiniteLength { /// A length that can be defined in pixels, rems, percent of parent, or auto. #[derive(Clone, Copy)] pub enum Length { + /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. Definite(DefiniteLength), + /// An automatic length that is determined by the context in which it is used. Auto, } @@ -1193,6 +2342,18 @@ impl Debug for Length { } } +/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. +/// +/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. +/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. +/// +/// # Arguments +/// +/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. +/// +/// # Returns +/// +/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. pub fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction).into() } @@ -1202,14 +2363,43 @@ pub fn phi() -> DefiniteLength { relative(1.61803398875) } +/// Constructs a `Rems` value representing a length in rems. +/// +/// # Arguments +/// +/// * `rems` - The number of rems for the length. +/// +/// # Returns +/// +/// A `Rems` representing the specified number of rems. pub fn rems(rems: f32) -> Rems { Rems(rems) } +/// Constructs a `Pixels` value representing a length in pixels. +/// +/// # Arguments +/// +/// * `pixels` - The number of pixels for the length. +/// +/// # Returns +/// +/// A `Pixels` representing the specified number of pixels. pub const fn px(pixels: f32) -> Pixels { Pixels(pixels) } +/// Returns a `Length` representing an automatic length. +/// +/// The `auto` length is often used in layout calculations where the length should be determined +/// by the layout context itself rather than being explicitly set. This is commonly used in CSS +/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used +/// to instruct the layout engine to calculate the size based on other factors like the size of the +/// container or the intrinsic size of the content. +/// +/// # Returns +/// +/// A `Length` variant set to `Auto`. pub fn auto() -> Length { Length::Auto } @@ -1250,7 +2440,17 @@ impl From<()> for Length { } } +/// Provides a trait for types that can calculate half of their value. +/// +/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type +/// representing half of the original value. This is commonly used for types that represent measurements or sizes, +/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. pub trait Half { + /// Returns half of the current value. + /// + /// # Returns + /// + /// A new instance of the implementing type, representing half of the original value. fn half(&self) -> Self; } @@ -1290,7 +2490,18 @@ impl Half for GlobalPixels { } } +/// A trait for checking if a value is zero. +/// +/// This trait provides a method to determine if a value is considered to be zero. +/// It is implemented for various numeric and length-related types where the concept +/// of zero is applicable. This can be useful for comparisons, optimizations, or +/// determining if an operation has a neutral effect. pub trait IsZero { + /// Determines if the value is zero. + /// + /// # Returns + /// + /// Returns `true` if the value is zero, `false` otherwise. fn is_zero(&self) -> bool; } diff --git a/crates/gpui2/src/platform/test/display.rs b/crates/gpui2/src/platform/test/display.rs index 78d75296e66d3fa18f1c7ba6403537369ff013a7..95f1daf8e92fc8bc620a91d3c1aa1ed12818c384 100644 --- a/crates/gpui2/src/platform/test/display.rs +++ b/crates/gpui2/src/platform/test/display.rs @@ -15,7 +15,7 @@ impl TestDisplay { id: DisplayId(1), uuid: uuid::Uuid::new_v4(), bounds: Bounds::from_corners( - Point::zero(), + Point::default(), Point::new(GlobalPixels(1920.), GlobalPixels(1080.)), ), } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index b1bfebad06745f51899421707d2ead4da57bfcb6..245b36da56d8d585a3f4125a2d0c1d30a27144ef 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -78,7 +78,7 @@ impl PlatformWindow for TestWindow { } fn mouse_position(&self) -> Point { - Point::zero() + Point::default() } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { @@ -223,7 +223,7 @@ impl PlatformAtlas for TestAtlas { }, tile_id: TileId(tile_id), bounds: crate::Bounds { - origin: Point::zero(), + origin: Point::default(), size, }, }, diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 9254eaeb85246253180e6ffc59c3f1f12895ae59..5c511b41a4982489f8fcd807d7a21b6767ee99ac 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -385,7 +385,7 @@ impl Default for Style { min_size: Size::auto(), max_size: Size::auto(), aspect_ratio: None, - gap: Size::zero(), + gap: Size::default(), // Aligment align_items: None, align_self: None, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6323eb962f101daa66fcfa8ae1482408507b7670..d8ef5cb674a3d02b54b3e3a30297011be357ce66 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1167,7 +1167,7 @@ impl<'a> WindowContext<'a> { } let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(Point::zero(), available_space, cx); + root_view.draw(Point::default(), available_space, cx); }) }); From 489c25ac6ab43ab14919fbfd5e4cee4a52e6eb56 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 11:32:05 -0800 Subject: [PATCH 63/78] Put ToggleZoom key binding back into the block with no context --- assets/keymaps/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 25fafa755e842359fd882b416f5e33e06c42a202..b8d3711132b1063543e7554b88d014df21460361 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -18,6 +18,7 @@ "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "cmd-shift-w": "workspace::CloseWindow", + "shift-escape": "workspace::ToggleZoom", "cmd-o": "workspace::Open", "cmd-=": "zed::IncreaseBufferFontSize", "cmd-+": "zed::IncreaseBufferFontSize", @@ -388,7 +389,6 @@ "cmd-n": "workspace::NewFile", "cmd-shift-n": "workspace::NewWindow", "ctrl-`": "terminal_panel::ToggleFocus", - "shift-escape": "workspace::ToggleZoom", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], From f76e1cfd91a4bbf88fc32fcca750dfcceb7a615d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 21:38:30 +0200 Subject: [PATCH 64/78] Pass proper theme colors for inlays and suggestions --- crates/editor2/src/display_map.rs | 6 +++--- crates/editor2/src/editor.rs | 27 +++++++++++++++++++++++++++ crates/language2/src/highlight_map.rs | 2 -- crates/theme2/src/one_themes.rs | 2 -- crates/theme2/src/styles/syntax.rs | 12 ------------ 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 1aee04dd0ae02b8d4ea98025be177a82e3801ef7..9cc16933711c221a3605bbeca01ba13931048a7b 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -24,7 +24,7 @@ use lsp::DiagnosticSeverity; use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; -use theme::{SyntaxTheme, Theme}; +use theme::{StatusColors, SyntaxTheme, Theme}; use wrap_map::WrapMap; pub use block_map::{ @@ -513,8 +513,8 @@ impl DisplaySnapshot { self.chunks( display_rows, language_aware, - Some(editor_style.syntax.inlay_style), - Some(editor_style.syntax.suggestion_style), + Some(editor_style.hints_style), + Some(editor_style.suggestions_style), ) .map(|chunk| { let mut highlight_style = chunk diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3994990f6414a6a59582e7bba712623bd88255d4..3dbdbf5e3c400ce4b99c441d60ed44770a0f9164 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -499,6 +499,8 @@ pub struct EditorStyle { pub scrollbar_width: Pixels, pub syntax: Arc, pub diagnostic_style: DiagnosticStyle, + pub hints_style: HighlightStyle, + pub suggestions_style: HighlightStyle, } type CompletionId = usize; @@ -7640,6 +7642,18 @@ impl Editor { .editor_style .diagnostic_style .clone(), + // todo!("what about the rest of the highlight style parts for inlays and suggestions?") + hints_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, }, )) .into_any_element() @@ -9302,6 +9316,19 @@ impl Render for Editor { scrollbar_width: px(12.), syntax: cx.theme().syntax().clone(), diagnostic_style: cx.theme().diagnostic_style(), + // TODO kb find `HighlightStyle` usages + // todo!("what about the rest of the highlight style parts?") + hints_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, }, ) } diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index 8e7a35233cf2e702536241099619cb0bff53459e..270ac259c9d78eff8d36a2ee8c8038f117d33260 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -95,8 +95,6 @@ mod tests { .iter() .map(|(name, color)| (name.to_string(), (*color).into())) .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), }; let capture_names = &[ diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index e1fb5f1bed21422f64eb210899b3850f7bb1c6d2..fbcabc0ff3897a9b5156f19af8c2cf08361f33f3 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -191,8 +191,6 @@ pub(crate) fn one_dark() -> Theme { ("variable.special".into(), red.into()), ("variant".into(), HighlightStyle::default()), ], - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), }), }, } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index cc73caa6dfca3c920cf79af89eb7d1993d670688..0f35bf60a73aa634bf0953e19f4beea1354131c3 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -8,12 +8,6 @@ use crate::{ #[derive(Clone, Default)] pub struct SyntaxTheme { pub highlights: Vec<(String, HighlightStyle)>, - // todo!("Remove this in favor of StatusColor.hint") - // If this should be overridable we should move it to ThemeColors - pub inlay_style: HighlightStyle, - // todo!("Remove this in favor of StatusColor.prediction") - // If this should be overridable we should move it to ThemeColors - pub suggestion_style: HighlightStyle, } impl SyntaxTheme { @@ -72,8 +66,6 @@ impl SyntaxTheme { ("variable.special".into(), red().light().step_9().into()), ("variant".into(), red().light().step_9().into()), ], - inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") - suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") } } @@ -132,8 +124,6 @@ impl SyntaxTheme { ("variable.special".into(), red().dark().step_11().into()), ("variant".into(), red().dark().step_11().into()), ], - inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") - suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") } } @@ -152,8 +142,6 @@ impl SyntaxTheme { ) }) .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), } } From 9e1d79744554fc9f84dc70db68a6d9453b8f62c8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:17:10 +0200 Subject: [PATCH 65/78] Use distinct version for zed2, append git hash to its nightly version --- .github/workflows/release_nightly.yml | 3 ++- Cargo.lock | 2 +- crates/zed2/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 38552646c343d5f44975e33146a4cd38dc3ab4d3..0e0fd18e25324984f64405facb2d876f2e68405a 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -81,11 +81,12 @@ jobs: - name: Limit target directory size run: script/clear-target-dir-if-larger-than 100 - - name: Set release channel to nightly + - name: Set release channel to nightly, add nightly prefix to the final version run: | set -eu version=$(git rev-parse --short HEAD) echo "Publishing version: ${version} on release channel nightly" + sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml echo "nightly" > crates/zed/RELEASE_CHANNEL - name: Generate license file diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..6935b2810acc40bbd19d36ca8bde66a5c43806c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11889,7 +11889,7 @@ dependencies = [ [[package]] name = "zed2" -version = "0.109.0" +version = "2.0.0" dependencies = [ "activity_indicator2", "ai2", diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 8c0c932f288bbf6a186869257ccdbe99d19bf13a..0fcbcc40fc281b45ad304d48149aec89506c8275 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition = "2021" name = "zed2" -version = "0.109.0" +version = "2.0.0" publish = false [lib] From 5644815c4c291ed787729e18860f706b5a39bb8d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 22:06:30 +0200 Subject: [PATCH 66/78] Use a better name for zed2 inlay style field --- crates/editor2/src/display_map.rs | 2 +- crates/editor2/src/editor.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 9cc16933711c221a3605bbeca01ba13931048a7b..60975a7a5caec6552b3154e528b9629e9beb094c 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -513,7 +513,7 @@ impl DisplaySnapshot { self.chunks( display_rows, language_aware, - Some(editor_style.hints_style), + Some(editor_style.inlays_style), Some(editor_style.suggestions_style), ) .map(|chunk| { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3dbdbf5e3c400ce4b99c441d60ed44770a0f9164..94ae8abc71a2d99ff2b5bde104809a824cab3877 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -499,7 +499,7 @@ pub struct EditorStyle { pub scrollbar_width: Pixels, pub syntax: Arc, pub diagnostic_style: DiagnosticStyle, - pub hints_style: HighlightStyle, + pub inlays_style: HighlightStyle, pub suggestions_style: HighlightStyle, } @@ -7643,7 +7643,7 @@ impl Editor { .diagnostic_style .clone(), // todo!("what about the rest of the highlight style parts for inlays and suggestions?") - hints_style: HighlightStyle { + inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), fade_out: Some(0.6), @@ -9318,7 +9318,7 @@ impl Render for Editor { diagnostic_style: cx.theme().diagnostic_style(), // TODO kb find `HighlightStyle` usages // todo!("what about the rest of the highlight style parts?") - hints_style: HighlightStyle { + inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), fade_out: Some(0.6), From bcdefb8ec84ce3f721c4f7f851b3dca6425a76fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:36:26 -0800 Subject: [PATCH 67/78] Bring back channel notes --- crates/collab_ui2/src/channel_view.rs | 898 +++++++++++++------------- crates/collab_ui2/src/collab_panel.rs | 4 +- 2 files changed, 446 insertions(+), 456 deletions(-) diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs index d2ffc0de57d865d6c8eb047ff85bad007074e4f3..8d2c037f9d3f5807e7d5375df6accb0b676377d6 100644 --- a/crates/collab_ui2/src/channel_view.rs +++ b/crates/collab_ui2/src/channel_view.rs @@ -1,454 +1,444 @@ -// use anyhow::{anyhow, Result}; -// use call::report_call_event_for_channel; -// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; -// use client::{ -// proto::{self, PeerId}, -// Collaborator, ParticipantIndex, -// }; -// use collections::HashMap; -// use editor::{CollaborationHub, Editor}; -// use gpui::{ -// actions, -// elements::{ChildView, Label}, -// geometry::vector::Vector2F, -// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, -// ViewContext, ViewHandle, -// }; -// use project::Project; -// use smallvec::SmallVec; -// use std::{ -// any::{Any, TypeId}, -// sync::Arc, -// }; -// use util::ResultExt; -// use workspace::{ -// item::{FollowableItem, Item, ItemEvent, ItemHandle}, -// register_followable_item, -// searchable::SearchableItemHandle, -// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, -// }; - -// actions!(channel_view, [Deploy]); - -// pub fn init(cx: &mut AppContext) { -// register_followable_item::(cx) -// } - -// pub struct ChannelView { -// pub editor: ViewHandle, -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// remote_id: Option, -// _editor_event_subscription: Subscription, -// } - -// impl ChannelView { -// pub fn open( -// channel_id: ChannelId, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// let pane = workspace.read(cx).active_pane().clone(); -// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); -// cx.spawn(|mut cx| async move { -// let channel_view = channel_view.await?; -// pane.update(&mut cx, |pane, cx| { -// report_call_event_for_channel( -// "open channel notes", -// channel_id, -// &workspace.read(cx).app_state().client, -// cx, -// ); -// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); -// }); -// anyhow::Ok(channel_view) -// }) -// } - -// pub fn open_in_pane( -// channel_id: ChannelId, -// pane: ViewHandle, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// let workspace = workspace.read(cx); -// let project = workspace.project().to_owned(); -// let channel_store = ChannelStore::global(cx); -// let language_registry = workspace.app_state().languages.clone(); -// let markdown = language_registry.language_for_name("Markdown"); -// let channel_buffer = -// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); - -// cx.spawn(|mut cx| async move { -// let channel_buffer = channel_buffer.await?; -// let markdown = markdown.await.log_err(); - -// channel_buffer.update(&mut cx, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.set_language_registry(language_registry); -// if let Some(markdown) = markdown { -// buffer.set_language(Some(markdown), cx); -// } -// }) -// }); - -// pane.update(&mut cx, |pane, cx| { -// let buffer_id = channel_buffer.read(cx).remote_id(cx); - -// let existing_view = pane -// .items_of_type::() -// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); - -// // If this channel buffer is already open in this pane, just return it. -// if let Some(existing_view) = existing_view.clone() { -// if existing_view.read(cx).channel_buffer == channel_buffer { -// return existing_view; -// } -// } - -// let view = cx.add_view(|cx| { -// let mut this = Self::new(project, channel_store, channel_buffer, cx); -// this.acknowledge_buffer_version(cx); -// this -// }); - -// // If the pane contained a disconnected view for this channel buffer, -// // replace that. -// if let Some(existing_item) = existing_view { -// if let Some(ix) = pane.index_for_item(&existing_item) { -// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) -// .detach(); -// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); -// } -// } - -// view -// }) -// .ok_or_else(|| anyhow!("pane was dropped")) -// }) -// } - -// pub fn new( -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// cx: &mut ViewContext, -// ) -> Self { -// let buffer = channel_buffer.read(cx).buffer(); -// let editor = cx.add_view(|cx| { -// let mut editor = Editor::for_buffer(buffer, None, cx); -// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( -// channel_buffer.clone(), -// ))); -// editor.set_read_only( -// !channel_buffer -// .read(cx) -// .channel(cx) -// .is_some_and(|c| c.can_edit_notes()), -// ); -// editor -// }); -// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); - -// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) -// .detach(); - -// Self { -// editor, -// project, -// channel_store, -// channel_buffer, -// remote_id: None, -// _editor_event_subscription, -// } -// } - -// pub fn channel(&self, cx: &AppContext) -> Option> { -// self.channel_buffer.read(cx).channel(cx) -// } - -// fn handle_channel_buffer_event( -// &mut self, -// _: ModelHandle, -// event: &ChannelBufferEvent, -// cx: &mut ViewContext, -// ) { -// match event { -// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { -// editor.set_read_only(true); -// cx.notify(); -// }), -// ChannelBufferEvent::ChannelChanged => { -// self.editor.update(cx, |editor, cx| { -// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); -// cx.emit(editor::Event::TitleChanged); -// cx.notify() -// }); -// } -// ChannelBufferEvent::BufferEdited => { -// if cx.is_self_focused() || self.editor.is_focused(cx) { -// self.acknowledge_buffer_version(cx); -// } else { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.notes_changed( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// } -// } -// ChannelBufferEvent::CollaboratorsChanged => {} -// } -// } - -// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.acknowledge_notes_version( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// self.channel_buffer.update(cx, |buffer, cx| { -// buffer.acknowledge_buffer_version(cx); -// }); -// } -// } - -// impl Entity for ChannelView { -// type Event = editor::Event; -// } - -// impl View for ChannelView { -// fn ui_name() -> &'static str { -// "ChannelView" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// ChildView::new(self.editor.as_any(), cx).into_any() -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// self.acknowledge_buffer_version(cx); -// cx.focus(self.editor.as_any()) -// } -// } -// } - -// impl Item for ChannelView { -// fn act_as_type<'a>( -// &'a self, -// type_id: TypeId, -// self_handle: &'a ViewHandle, -// _: &'a AppContext, -// ) -> Option<&'a AnyViewHandle> { -// if type_id == TypeId::of::() { -// Some(self_handle) -// } else if type_id == TypeId::of::() { -// Some(&self.editor) -// } else { -// None -// } -// } - -// fn tab_content( -// &self, -// _: Option, -// style: &theme::Tab, -// cx: &gpui::AppContext, -// ) -> AnyElement { -// let label = if let Some(channel) = self.channel(cx) { -// match ( -// channel.can_edit_notes(), -// self.channel_buffer.read(cx).is_connected(), -// ) { -// (true, true) => format!("#{}", channel.name), -// (false, true) => format!("#{} (read-only)", channel.name), -// (_, false) => format!("#{} (disconnected)", channel.name), -// } -// } else { -// format!("channel notes (disconnected)") -// }; -// Label::new(label, style.label.to_owned()).into_any() -// } - -// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { -// Some(Self::new( -// self.project.clone(), -// self.channel_store.clone(), -// self.channel_buffer.clone(), -// cx, -// )) -// } - -// fn is_singleton(&self, _cx: &AppContext) -> bool { -// false -// } - -// fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { -// self.editor -// .update(cx, |editor, cx| editor.navigate(data, cx)) -// } - -// fn deactivated(&mut self, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::deactivated(editor, cx)) -// } - -// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) -// } - -// fn as_searchable(&self, _: &ViewHandle) -> Option> { -// Some(Box::new(self.editor.clone())) -// } - -// fn show_toolbar(&self) -> bool { -// true -// } - -// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { -// self.editor.read(cx).pixel_position_of_cursor(cx) -// } - -// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { -// editor::Editor::to_item_events(event) -// } -// } - -// impl FollowableItem for ChannelView { -// fn remote_id(&self) -> Option { -// self.remote_id -// } - -// fn to_state_proto(&self, cx: &AppContext) -> Option { -// let channel_buffer = self.channel_buffer.read(cx); -// if !channel_buffer.is_connected() { -// return None; -// } - -// Some(proto::view::Variant::ChannelView( -// proto::view::ChannelView { -// channel_id: channel_buffer.channel_id, -// editor: if let Some(proto::view::Variant::Editor(proto)) = -// self.editor.read(cx).to_state_proto(cx) -// { -// Some(proto) -// } else { -// None -// }, -// }, -// )) -// } - -// fn from_state_proto( -// pane: ViewHandle, -// workspace: ViewHandle, -// remote_id: workspace::ViewId, -// state: &mut Option, -// cx: &mut AppContext, -// ) -> Option>>> { -// let Some(proto::view::Variant::ChannelView(_)) = state else { -// return None; -// }; -// let Some(proto::view::Variant::ChannelView(state)) = state.take() else { -// unreachable!() -// }; - -// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); - -// Some(cx.spawn(|mut cx| async move { -// let this = open.await?; - -// let task = this -// .update(&mut cx, |this, cx| { -// this.remote_id = Some(remote_id); - -// if let Some(state) = state.editor { -// Some(this.editor.update(cx, |editor, cx| { -// editor.apply_update_proto( -// &this.project, -// proto::update_view::Variant::Editor(proto::update_view::Editor { -// selections: state.selections, -// pending_selection: state.pending_selection, -// scroll_top_anchor: state.scroll_top_anchor, -// scroll_x: state.scroll_x, -// scroll_y: state.scroll_y, -// ..Default::default() -// }), -// cx, -// ) -// })) -// } else { -// None -// } -// }) -// .ok_or_else(|| anyhow!("window was closed"))?; - -// if let Some(task) = task { -// task.await?; -// } - -// Ok(this) -// })) -// } - -// fn add_event_to_update_proto( -// &self, -// event: &Self::Event, -// update: &mut Option, -// cx: &AppContext, -// ) -> bool { -// self.editor -// .read(cx) -// .add_event_to_update_proto(event, update, cx) -// } - -// fn apply_update_proto( -// &mut self, -// project: &ModelHandle, -// message: proto::update_view::Variant, -// cx: &mut ViewContext, -// ) -> gpui::Task> { -// self.editor.update(cx, |editor, cx| { -// editor.apply_update_proto(project, message, cx) -// }) -// } - -// fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { -// self.editor.update(cx, |editor, cx| { -// editor.set_leader_peer_id(leader_peer_id, cx) -// }) -// } - -// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { -// Editor::should_unfollow_on_event(event, cx) -// } - -// fn is_project_item(&self, _cx: &AppContext) -> bool { -// false -// } -// } - -// struct ChannelBufferCollaborationHub(ModelHandle); - -// impl CollaborationHub for ChannelBufferCollaborationHub { -// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { -// self.0.read(cx).collaborators() -// } - -// fn user_participant_indices<'a>( -// &self, -// cx: &'a AppContext, -// ) -> &'a HashMap { -// self.0.read(cx).user_store().read(cx).participant_indices() -// } -// } +use anyhow::Result; +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, +}; +use collections::HashMap; +use editor::{CollaborationHub, Editor, EditorEvent}; +use gpui::{ + actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView, + IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, +}; +use project::Project; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use ui::Label; +use util::ResultExt; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, +}; + +actions!(Deploy); + +pub fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: View, + project: Model, + channel_store: Model, + channel_buffer: Model, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + })?; + anyhow::Ok(channel_view) + }) + } + + pub fn open_in_pane( + channel_id: ChannelId, + pane: View, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = ChannelStore::global(cx); + let language_registry = workspace.app_state().languages.clone(); + let markdown = language_registry.language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); + + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { + buffer.set_language(Some(markdown), cx); + } + }) + })?; + + pane.update(&mut cx, |pane, cx| { + let buffer_id = channel_buffer.read(cx).remote_id(cx); + + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + + // If this channel buffer is already open in this pane, just return it. + if let Some(existing_view) = existing_view.clone() { + if existing_view.read(cx).channel_buffer == channel_buffer { + return existing_view; + } + } + + let view = cx.build_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }); + + // If the pane contained a disconnected view for this channel buffer, + // replace that. + if let Some(existing_item) = existing_view { + if let Some(ix) = pane.index_for_item(&existing_item) { + pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx) + .detach(); + pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); + } + } + + view + }) + }) + } + + pub fn new( + project: Model, + channel_store: Model, + channel_buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + let editor = cx.build_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); + editor + }); + let _editor_event_subscription = + cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone())); + + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + Self { + editor, + project, + channel_store, + channel_buffer, + remote_id: None, + _editor_event_subscription, + } + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) + } + + fn handle_channel_buffer_event( + &mut self, + _: Model, + event: &ChannelBufferEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::EditorEvent::TitleChanged); + cx.notify() + }); + } + ChannelBufferEvent::BufferEdited => { + if self.editor.read(cx).is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + ChannelBufferEvent::CollaboratorsChanged => {} + } + } + + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } +} + +impl EventEmitter for ChannelView {} + +impl Render for ChannelView { + type Element = AnyView; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.editor.clone().into() + } +} + +impl FocusableView for ChannelView { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.read(cx).focus_handle(cx) + } +} + +impl Item for ChannelView { + type Event = EditorEvent; + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn tab_content(&self, _: Option, cx: &WindowContext) -> AnyElement { + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } + } else { + format!("channel notes (disconnected)") + }; + Label::new(label).into_any_element() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option> { + Some(cx.build_view(|cx| { + Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + ) + })) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &View) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + self.editor.read(cx).pixel_position_of_cursor(cx) + } + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &WindowContext) -> Option { + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } + + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel_buffer.channel_id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: View, + workspace: View, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut WindowContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; + + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this.update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + })?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &EditorEvent, + update: &mut Option, + cx: &WindowContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } + + fn is_project_item(&self, _cx: &WindowContext) -> bool { + false + } + + fn to_follow_event(event: &Self::Event) -> Option { + Editor::to_follow_event(event) + } +} + +struct ChannelBufferCollaborationHub(Model); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } +} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 8c0a92ad5252c5b12bc56ee174329a15a5a384c2..272f02bb49105bc55c2b4ca8a0755a4e045e0342 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -191,6 +191,7 @@ use workspace::{ Workspace, }; +use crate::channel_view::ChannelView; use crate::{face_pile::FacePile, CollaborationPanelSettings}; use self::channel_modal::ChannelModal; @@ -1935,8 +1936,7 @@ impl CollabPanel { fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { - todo!(); - // ChannelView::open(action.channel_id, workspace, cx).detach(); + ChannelView::open(channel_id, workspace, cx).detach(); } } From 1ec81e02da8b7d24648186ad3c58d02583ed5ffb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:44:07 -0800 Subject: [PATCH 68/78] Allow opening channel notes from the channel list --- crates/collab_ui2/src/collab_panel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 272f02bb49105bc55c2b4ca8a0755a4e045e0342..bfef193cf73cc3b4129815bf305d88653d7e88ba 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2619,6 +2619,9 @@ impl CollabPanel { } 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) }), From e9dcca7712569f7573610c62a0d5376c09c54d6e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:59:54 -0800 Subject: [PATCH 69/78] Initialize channel notes to register the view as followable --- crates/collab_ui2/src/collab_ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index efd3ff869225aced36002a3bdb4f1f5905579c5a..fba76932fc01749eaa8ddee7c19264aaecae455e 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -33,6 +33,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { // vcs_menu::init(cx); collab_titlebar_item::init(cx); collab_panel::init(cx); + channel_view::init(cx); // chat_panel::init(cx); notifications::init(&app_state, cx); From 6bbb1642b86545da18119d1e729145cfbc2e33b1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 12:18:48 -0800 Subject: [PATCH 70/78] Fix propagation of active item to followers Enable channel buffer integration tests. --- Cargo.lock | 2 +- crates/collab2/Cargo.toml | 2 +- .../collab2/src/tests/channel_buffer_tests.rs | 1756 +++++++++-------- crates/collab2/src/tests/test_server.rs | 14 +- crates/gpui2/src/app/test_context.rs | 4 + crates/workspace2/src/workspace2.rs | 25 +- 6 files changed, 906 insertions(+), 897 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..773c2a3df552928594755ca398e38ac6861f6a66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,7 +1830,7 @@ dependencies = [ "clap 3.2.25", "client2", "clock", - "collab_ui", + "collab_ui2", "collections", "ctor", "dashmap", diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml index b8e6a45b06959e44958d7cd3d9e77d7fa8bb49e2..06443afdbcf05190d67efcdccc8cba8cb03bbcdd 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup theme = { package = "theme2", path = "../theme2" } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -collab_ui = { path = "../collab_ui", features = ["test-support"] } +collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] } async-trait.workspace = true pretty_assertions.workspace = true diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index 63057cbd415f011f9269c0f61410afbd12fb0721..b0af360fd52ee78300060e669ba38620d07ec4e7 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,875 +1,881 @@ -//todo(partially ported) -// use std::ops::Range; - -// use crate::{ -// rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, -// tests::TestServer, -// }; -// use client::{Collaborator, ParticipantIndex, UserId}; -// use collections::HashMap; -// use editor::{Anchor, Editor, ToOffset}; -// use futures::future; -// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; -// use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; - -// #[gpui::test] -// async fn test_core_channel_buffers( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) -// .await; - -// // Client A joins the channel buffer -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// // Client A edits the buffer -// let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..0, "hello world")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(5..5, ", cruel")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..5, "goodbye")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); -// executor.run_until_parked(); - -// // Client B joins the channel buffer -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client B sees the correct text, and then edits it -// let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); -// assert_eq!( -// buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), -// buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) -// ); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); -// buffer_b.update(cx_b, |buffer, cx| { -// buffer.edit([(7..12, "beautiful")], None, cx) -// }); - -// // Both A and B see the new edit -// executor.run_until_parked(); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - -// // Client A closes the channel buffer. -// cx_a.update(|_| drop(channel_buffer_a)); -// executor.run_until_parked(); - -// // Client B sees that client A is gone from the channel buffer. -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // Client A rejoins the channel buffer -// let _channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// // Sanity test, make sure we saw A rejoining -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// &buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client A loses connection. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// // Client B observes A disconnect -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // TODO: -// // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects -// // - Test interaction with channel deletion while buffer is open -// } - -// // todo!("collab_ui") -// // #[gpui::test] -// // async fn test_channel_notes_participant_indices( -// // executor: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&executor).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // client_a -// // .fs() -// // .insert_tree("/root", json!({"file.txt": "123"})) -// // .await; -// // let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let project_c = client_c.build_empty_local_project(cx_c); -// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // // Clients A, B, and C open the channel notes -// // let channel_view_a = cx_a -// // .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_c = cx_c -// // .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) -// // .await -// // .unwrap(); - -// // // Clients A, B, and C all insert and select some text -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("a", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("b", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![1..2]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_c.update(cx_c, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("c", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // }); - -// // // Client A sees clients B and C without assigned colors, because they aren't -// // // in a call together. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); -// // }); -// // }); - -// // // Clients A and B join the same call. -// // for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { -// // call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) -// // .await -// // .unwrap(); -// // } - -// // // Clients A and B see each other with two different assigned colors. Client C -// // // still doesn't have a color. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); - -// // // Client A shares a project, and client B joins. -// // let project_id = active_call_a -// // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// // .await -// // .unwrap(); -// // let project_b = client_b.build_remote_project(project_id, cx_b).await; -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - -// // // Clients A and B open the same file. -// // let editor_a = workspace_a -// // .update(cx_a, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); -// // let editor_b = workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); - -// // editor_a.update(cx_a, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // executor.run_until_parked(); - -// // // Clients A and B see each other with the same colors as in the channel notes. -// // editor_a.update(cx_a, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); -// // }); -// // } - -// #[track_caller] -// fn assert_remote_selections( -// editor: &mut Editor, -// expected_selections: &[(Option, Range)], -// cx: &mut ViewContext, -// ) { -// let snapshot = editor.snapshot(cx); -// let range = Anchor::min()..Anchor::max(); -// let remote_selections = snapshot -// .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) -// .map(|s| { -// let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); -// let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); -// (s.participant_index, start..end) -// }) -// .collect::>(); -// assert_eq!( -// remote_selections, expected_selections, -// "incorrect remote selections" -// ); -// } - -// #[gpui::test] -// async fn test_multiple_handles_to_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; - -// let channel_id = server -// .make_channel("the-channel", None, (&client_a, cx_a), &mut []) -// .await; - -// let channel_buffer_1 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_2 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_3 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - -// // All concurrent tasks for opening a channel buffer return the same model handle. -// let (channel_buffer, channel_buffer_2, channel_buffer_3) = -// future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) -// .await -// .unwrap(); -// let channel_buffer_model_id = channel_buffer.entity_id(); -// assert_eq!(channel_buffer, channel_buffer_2); -// assert_eq!(channel_buffer, channel_buffer_3); - -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "hello")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// cx_a.update(|_| { -// drop(channel_buffer); -// drop(channel_buffer_2); -// drop(channel_buffer_3); -// }); -// deterministic.run_until_parked(); - -// // The channel buffer can be reopened after dropping it. -// let channel_buffer = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, _| { -// assert_eq!(buffer.text(), "hello"); -// }) -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffer_disconnect( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); -// assert!(!buffer.is_connected()); -// }); - -// deterministic.run_until_parked(); - -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// deterministic.run_until_parked(); - -// client_a -// .channel_store() -// .update(cx_a, |channel_store, _| { -// channel_store.remove_channel(channel_id) -// }) -// .await -// .unwrap(); -// deterministic.run_until_parked(); - -// // Channel buffer observed the deletion -// channel_buffer_b.update(cx_b, |buffer, cx| { -// assert!(buffer.channel(cx).is_none()); -// assert!(!buffer.is_connected()); -// }); -// } - -// #[gpui::test] -// async fn test_rejoin_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client A disconnects. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); - -// // Both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Both clients see their own edit. -// deterministic.run_until_parked(); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "12"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "01"); -// }); - -// // Client A reconnects. Both clients see each other's edits, and see -// // the same collaborators. -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffers_and_server_restarts( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let _channel_buffer_c = client_c -// .channel_store() -// .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client C can't reconnect. -// client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - -// // Server stops. -// server.reset().await; -// deterministic.advance_clock(RECEIVE_TIMEOUT); - -// // While the server is down, both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Server restarts. -// server.start().await.unwrap(); -// deterministic.advance_clock(CLEANUP_TIMEOUT); - -// // Clients reconnects. Clients A and B see each other's edits, and see -// // that client C has disconnected. -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_collaborators( -// buffer_a.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// //todo!(collab_ui) -// // #[gpui::test(iterations = 10)] -// // async fn test_following_to_channel_notes_without_a_shared_project( -// // deterministic: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // mut cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); -// // cx_a.update(collab_ui::channel_view::init); -// // cx_b.update(collab_ui::channel_view::init); -// // cx_c.update(collab_ui::channel_view::init); - -// // let channel_1_id = server -// // .make_channel( -// // "channel-1", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; -// // let channel_2_id = server -// // .make_channel( -// // "channel-2", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // // Clients A, B, and C join a channel. -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); -// // let active_call_c = cx_c.read(ActiveCall::global); -// // for (call, cx) in [ -// // (&active_call_a, &mut cx_a), -// // (&active_call_b, &mut cx_b), -// // (&active_call_c, &mut cx_c), -// // ] { -// // call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) -// // .await -// // .unwrap(); -// // } -// // deterministic.run_until_parked(); - -// // // Clients A, B, and C all open their own unshared projects. -// // client_a.fs().insert_tree("/a", json!({})).await; -// // client_b.fs().insert_tree("/b", json!({})).await; -// // client_c.fs().insert_tree("/c", json!({})).await; -// // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// // let (project_b, _) = client_b.build_local_project("/b", cx_b).await; -// // let (project_c, _) = client_b.build_local_project("/c", cx_c).await; -// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // active_call_a -// // .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// // .await -// // .unwrap(); - -// // // Client A opens the notes for channel 1. -// // let channel_view_1_a = cx_a -// // .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_1_a.update(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("Hello from A.", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![3..4]); -// // }); -// // }); -// // }); - -// // // Client B follows client A. -// // workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// // }) -// // .await -// // .unwrap(); - -// // // Client B is taken to the notes for channel 1, with the same -// // // text selected as client A. -// // deterministic.run_until_parked(); -// // let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_1_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // let editor = notes.editor.read(cx); -// // assert_eq!(editor.text(cx), "Hello from A."); -// // assert_eq!(editor.selections.ranges::(cx), &[3..4]); -// // }); - -// // // Client A opens the notes for channel 2. -// // let channel_view_2_a = cx_a -// // .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_2_a.read_with(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); - -// // // Client B is taken to the notes for channel 2. -// // deterministic.run_until_parked(); -// // let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_2_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); -// // } - -// //todo!(collab_ui) -// // #[gpui::test] -// // async fn test_channel_buffer_changes( -// // deterministic: BackgroundExecutor, -// // cx_a: &mut TestAppContext, -// // cx_b: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b)], -// // ) -// // .await; - -// // let channel_buffer_a = client_a -// // .channel_store() -// // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// // .await -// // .unwrap(); - -// // // Client A makes an edit, and client B should see that the note has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "1")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); - -// // // Opening the buffer should clear the changed flag. -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Editing the channel while the buffer is open should not show that the buffer has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "2")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); - -// // // Test that the server is tracking things correctly, and we retain our 'not changed' -// // // state across a disconnect -// // server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Closing the buffer should re-enable change tracking -// // cx_b.update(|cx| { -// // workspace_b.update(cx, |workspace, cx| { -// // workspace.close_all_items_and_panes(&Default::default(), cx) -// // }); - -// // drop(channel_view_b) -// // }); - -// // deterministic.run_until_parked(); - -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "3")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); -// // } - -// #[track_caller] -// fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { -// let mut user_ids = collaborators -// .values() -// .map(|collaborator| collaborator.user_id) -// .collect::>(); -// user_ids.sort(); -// assert_eq!( -// user_ids, -// ids.into_iter().map(|id| id.unwrap()).collect::>() -// ); -// } - -// fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { -// channel_buffer.read_with(cx, |buffer, _| buffer.text()) -// } +use crate::{ + rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + tests::TestServer, +}; +use call::ActiveCall; +use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; +use client::{Collaborator, ParticipantIndex, UserId}; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; +use futures::future; +use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +use serde_json::json; +use std::ops::Range; + +#[gpui::test] +async fn test_core_channel_buffers( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Client A joins the channel buffer + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A edits the buffer + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + executor.run_until_parked(); + + // Client B joins the channel buffer + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); + + // Both A and B see the new edit + executor.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); + executor.run_until_parked(); + + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + executor.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open +} + +#[gpui::test] +async fn test_channel_notes_participant_indices( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + client_a + .fs() + .insert_tree("/root", json!({"file.txt": "123"})) + .await; + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + + let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b); + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) + .await + .unwrap(); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) + .await + .unwrap(); + + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + }); + executor.run_until_parked(); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); + }); + executor.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + }); + + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + }); + }); + + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } + + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + cx, + ); + }); + }); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + cx, + ); + }); + }); + + // Client A shares a project, and client B joins. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + executor.run_until_parked(); + + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); + }); + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + }); +} + +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.participant_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); +} + +#[gpui::test] +async fn test_multiple_handles_to_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let channel_id = server + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) + .await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let channel_buffer_model_id = channel_buffer.entity_id(); + assert_eq!(channel_buffer, channel_buffer_2); + assert_eq!(channel_buffer, channel_buffer_3); + + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, cx| { + assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, cx| { + assert!(buffer.channel(cx).is_none()); + assert!(!buffer.is_connected()); + }); +} + +#[gpui::test] +async fn test_rejoin_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client A disconnects. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + + // Both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Both clients see their own edit. + deterministic.run_until_parked(); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "12"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "01"); + }); + + // Client A reconnects. Both clients see each other's edits, and see + // the same collaborators. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test] +async fn test_channel_buffers_and_server_restarts( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let _channel_buffer_c = client_c + .channel_store() + .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client C can't reconnect. + client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + + // Server stops. + server.reset().await; + deterministic.advance_clock(RECEIVE_TIMEOUT); + + // While the server is down, both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Server restarts. + server.start().await.unwrap(); + deterministic.advance_clock(CLEANUP_TIMEOUT); + + // Clients reconnects. Clients A and B see each other's edits, and see + // that client C has disconnected. + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_to_channel_notes_without_a_shared_project( + deterministic: BackgroundExecutor, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, + mut cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let client_c = server.create_client(cx_c, "user_c").await; + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + cx_a.update(collab_ui::channel_view::init); + cx_b.update(collab_ui::channel_view::init); + cx_c.update(collab_ui::channel_view::init); + + let channel_1_id = server + .make_channel( + "channel-1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + let channel_2_id = server + .make_channel( + "channel-2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + // Clients A, B, and C join a channel. + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + for (call, cx) in [ + (&active_call_a, &mut cx_a), + (&active_call_b, &mut cx_b), + (&active_call_c, &mut cx_c), + ] { + call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) + .await + .unwrap(); + } + deterministic.run_until_parked(); + + // Clients A, B, and C all open their own unshared projects. + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; + client_c.fs().insert_tree("/c", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + let (project_c, _) = client_b.build_local_project("/c", cx_c).await; + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + // Client A opens the notes for channel 1. + let channel_view_1_a = cx_a + .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_1_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + notes.editor.update(cx, |editor, cx| { + editor.insert("Hello from A.", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![3..4]); + }); + }); + }); + + // Client B follows client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client B is taken to the notes for channel 1, with the same + // text selected as client A. + deterministic.run_until_parked(); + let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_1_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + let editor = notes.editor.read(cx); + assert_eq!(editor.text(cx), "Hello from A."); + assert_eq!(editor.selections.ranges::(cx), &[3..4]); + }); + + // Client A opens the notes for channel 2. + eprintln!("opening -------------------->"); + + let channel_view_2_a = cx_a + .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_2_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); + + // Client B is taken to the notes for channel 2. + deterministic.run_until_parked(); + + eprintln!("opening <--------------------"); + + let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_2_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); +} + +#[gpui::test] +async fn test_channel_buffer_changes( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A makes an edit, and client B should see that the note has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); + + // Opening the buffer should clear the changed flag. + let project_b = client_b.build_empty_local_project(cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Editing the channel while the buffer is open should not show that the buffer has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "2")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + + // Test that the server is tracking things correctly, and we retain our 'not changed' + // state across a disconnect + server + .simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone()); + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Closing the buffer should re-enable change tracking + cx_b.update(|cx| { + workspace_b.update(cx, |workspace, cx| { + workspace.close_all_items_and_panes(&Default::default(), cx) + }); + + drop(channel_view_b) + }); + + deterministic.run_until_parked(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "3")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); +} + +#[track_caller] +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { + let mut user_ids = collaborators + .values() + .map(|collaborator| collaborator.user_id) + .collect::>(); + user_ids.sort(); + assert_eq!( + user_ids, + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); +} + +fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 6bb57e11ab1d582031930f34b8bfe67b96a2581e..f7517369711d6fc53fc5c53041122134af797e72 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -13,7 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; @@ -602,14 +602,12 @@ impl TestClient { .unwrap() } - //todo(workspace) - #[allow(dead_code)] - pub fn build_workspace( - &self, + pub fn build_workspace<'a>( + &'a self, project: &Model, - cx: &mut TestAppContext, - ) -> WindowHandle { - cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index cbd70e52ffab4d539abfe6a177f8a112825b8402..9f0c7e6aca01a934277f2f11f22bee90c4d68d36 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -545,6 +545,10 @@ pub struct VisualTestContext<'a> { } impl<'a> VisualTestContext<'a> { + pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { + self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() + } + pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 9c886839c93811f5b6668c1e6ff60ae6875a8c14..d5583be0bceb409944d2f7fb1c473c3900f67938 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2077,6 +2077,7 @@ impl Workspace { } if &pane == self.active_pane() { self.active_item_path_changed(cx); + self.update_active_view_for_followers(cx); } } pane::Event::ChangeItemTitle => { @@ -2756,18 +2757,18 @@ impl Workspace { fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); - if self.active_pane.read(cx).has_focus(cx) { - let item = self - .active_item(cx) - .and_then(|item| item.to_followable_item_handle(cx)); - if let Some(item) = item { - is_project_item = item.is_project_item(cx); - update = proto::UpdateActiveView { - id: item - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()), - leader_id: self.leader_for_pane(&self.active_pane), - }; + + if let Some(item) = self.active_item(cx) { + if item.focus_handle(cx).contains_focused(cx) { + if let Some(item) = item.to_followable_item_handle(cx) { + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { + id: item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()), + leader_id: self.leader_for_pane(&self.active_pane), + }; + } } } From b94c335605c334c24221d53f04ba5af46d304a44 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 22:19:48 +0200 Subject: [PATCH 71/78] Do not run the same workflow concurrently on non-main branches --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ac2912424bb8bf10014ac1b8961405cd2ba47c5..5ba25dbf947c14b485e7303cd12dd17f8a206927 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ on: branches: - "**" +concurrency: + # Allow only one workflow per any non-`main` branch. + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 From d711087529938df85fadd7b3c2c472954cf077b4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 15:44:50 -0500 Subject: [PATCH 72/78] Wire up inline assist quick action --- Cargo.lock | 1 + crates/quick_action_bar2/Cargo.toml | 2 +- .../quick_action_bar2/src/quick_action_bar.rs | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..6eefc3309d76e4c4a078af40f7a0a2e0ae76bbeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7117,6 +7117,7 @@ dependencies = [ name = "quick_action_bar2" version = "0.1.0" dependencies = [ + "assistant2", "editor2", "gpui2", "search2", diff --git a/crates/quick_action_bar2/Cargo.toml b/crates/quick_action_bar2/Cargo.toml index 32f440d202648b5c0dba8071d7aa5e49d4da18db..678e5c16f1cbbb26ee5ab39113e1a1a8d572ea50 100644 --- a/crates/quick_action_bar2/Cargo.toml +++ b/crates/quick_action_bar2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] -#assistant = { path = "../assistant" } +assistant = { package = "assistant2", path = "../assistant2" } editor = { package = "editor2", path = "../editor2" } gpui = { package = "gpui2", path = "../gpui2" } search = { package = "search2", path = "../search2" } diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 3686ace2fb0b73a530a98681a5639c3a1bfc71e4..91e0f54e7fe3f04deb2540e1e59c8aac38a8fdea 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -1,4 +1,4 @@ -// use assistant::{assistant_panel::InlineAssist, AssistantPanel}; +use assistant::{AssistantPanel, InlineAssist}; use editor::Editor; use gpui::{ @@ -15,7 +15,6 @@ pub struct QuickActionBar { buffer_search_bar: View, active_item: Option>, _inlay_hints_enabled_subscription: Option, - #[allow(unused)] workspace: WeakView, } @@ -56,21 +55,25 @@ impl Render for QuickActionBar { "toggle inline assistant", Icon::MagicWand, false, - Box::new(gpui::NoAction), + Box::new(InlineAssist), "Inline assistant", - |_, _cx| todo!(), + { + let workspace = self.workspace.clone(); + move |_, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + AssistantPanel::inline_assist(workspace, &InlineAssist, cx); + }); + } + } + }, ); h_stack() .id("quick action bar") .p_1() .gap_2() .child(search_button) - .child( - div() - .border() - .border_color(gpui::red()) - .child(assistant_button), - ) + .child(assistant_button) } } From dbb501d7eb76e21d25f2694730be2b564895f3ac Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 15:45:03 -0500 Subject: [PATCH 73/78] Add gap between label and keybinding in `Tooltip` --- crates/ui2/src/components/tooltip.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index cc17a610f4bd532cfc25669352643e1b0dc6a648..7c502ac5cb13a4322db626602feedcd34edccdce 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -84,6 +84,7 @@ impl Render for Tooltip { .px_2() .child( h_stack() + .gap_2() .child(self.title.clone()) .when_some(self.key_binding.clone(), |this, key_binding| { this.justify_between().child(key_binding) From f7c995c4a08d56eb5c18d8e6d3ef2fa7e579dd62 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:14:39 -0500 Subject: [PATCH 74/78] Add "Toggle Inlay Hints" quick action (#3520) This PR adds the "Toggle Inlay Hints" quick action to the toolbar. Release Notes: - N/A --- .../quick_action_bar/src/quick_action_bar.rs | 8 +- .../quick_action_bar2/src/quick_action_bar.rs | 173 ++++-------------- 2 files changed, 39 insertions(+), 142 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index d648e83f8f1acbfe013ab411be89ca1bcd5380fe..074ef7d2f398ba557e7e101292a7d7cbb012eb94 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -13,7 +13,7 @@ use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspac pub struct QuickActionBar { buffer_search_bar: ViewHandle, active_item: Option>, - _inlay_hints_enabled_subscription: Option, + inlay_hints_enabled_subscription: Option, workspace: WeakViewHandle, } @@ -22,7 +22,7 @@ impl QuickActionBar { Self { buffer_search_bar, active_item: None, - _inlay_hints_enabled_subscription: None, + inlay_hints_enabled_subscription: None, workspace: workspace.weak_handle(), } } @@ -161,12 +161,12 @@ impl ToolbarItemView for QuickActionBar { match active_pane_item { Some(active_item) => { self.active_item = Some(active_item.boxed_clone()); - self._inlay_hints_enabled_subscription.take(); + self.inlay_hints_enabled_subscription.take(); if let Some(editor) = active_item.downcast::() { let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); - self._inlay_hints_enabled_subscription = + self.inlay_hints_enabled_subscription = Some(cx.observe(&editor, move |_, editor, cx| { let editor = editor.read(cx); let new_inlay_hints_enabled = editor.inlay_hints_enabled(); diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 91e0f54e7fe3f04deb2540e1e59c8aac38a8fdea..e933689e62ee2f59e49aeeaf080032c489702fe6 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -28,7 +28,6 @@ impl QuickActionBar { } } - #[allow(dead_code)] fn active_editor(&self) -> Option> { self.active_item .as_ref() @@ -40,23 +39,48 @@ impl Render for QuickActionBar { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let buffer_search_bar = self.buffer_search_bar.clone(); - let search_button = QuickActionBarButton::new( + let Some(editor) = self.active_editor() else { + return div().id("empty quick action bar"); + }; + + let inlay_hints_button = Some(QuickActionBarButton::new( + "toggle inlay hints", + Icon::InlayHint, + editor.read(cx).inlay_hints_enabled(), + Box::new(editor::ToggleInlayHints), + "Toggle Inlay Hints", + { + let editor = editor.clone(); + move |_, cx| { + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); + }); + } + }, + )) + .filter(|_| editor.read(cx).supports_inlay_hints(cx)); + + let search_button = Some(QuickActionBarButton::new( "toggle buffer search", Icon::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(search::buffer_search::Deploy { focus: false }), "Buffer Search", - move |_, cx| { - buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + { + let buffer_search_bar = self.buffer_search_bar.clone(); + move |_, cx| { + buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + } }, - ); + )) + .filter(|_| editor.is_singleton(cx)); + let assistant_button = QuickActionBarButton::new( "toggle inline assistant", Icon::MagicWand, false, Box::new(InlineAssist), - "Inline assistant", + "Inline Assist", { let workspace = self.workspace.clone(); move |_, cx| { @@ -68,92 +92,19 @@ impl Render for QuickActionBar { } }, ); + h_stack() .id("quick action bar") .p_1() .gap_2() - .child(search_button) + .children(inlay_hints_button) + .children(search_button) .child(assistant_button) } } impl EventEmitter for QuickActionBar {} -// impl View for QuickActionBar { -// fn ui_name() -> &'static str { -// "QuickActionsBar" -// } - -// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { -// let Some(editor) = self.active_editor() else { -// return div(); -// }; - -// let mut bar = Flex::row(); -// if editor.read(cx).supports_inlay_hints(cx) { -// bar = bar.with_child(render_quick_action_bar_button( -// 0, -// "icons/inlay_hint.svg", -// editor.read(cx).inlay_hints_enabled(), -// ( -// "Toggle Inlay Hints".to_string(), -// Some(Box::new(editor::ToggleInlayHints)), -// ), -// cx, -// |this, cx| { -// if let Some(editor) = this.active_editor() { -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); -// }); -// } -// }, -// )); -// } - -// if editor.read(cx).buffer().read(cx).is_singleton() { -// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); -// let search_action = buffer_search::Deploy { focus: true }; - -// bar = bar.with_child(render_quick_action_bar_button( -// 1, -// "icons/magnifying_glass.svg", -// search_bar_shown, -// ( -// "Buffer Search".to_string(), -// Some(Box::new(search_action.clone())), -// ), -// cx, -// move |this, cx| { -// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { -// if search_bar_shown { -// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); -// } else { -// buffer_search_bar.deploy(&search_action, cx); -// } -// }); -// }, -// )); -// } - -// bar.add_child(render_quick_action_bar_button( -// 2, -// "icons/magic-wand.svg", -// false, -// ("Inline Assist".into(), Some(Box::new(InlineAssist))), -// cx, -// move |this, cx| { -// if let Some(workspace) = this.workspace.upgrade(cx) { -// workspace.update(cx, |workspace, cx| { -// AssistantPanel::inline_assist(workspace, &Default::default(), cx); -// }); -// } -// }, -// )); - -// bar.into_any() -// } -// } - #[derive(IntoElement)] struct QuickActionBarButton { id: ElementId, @@ -161,7 +112,6 @@ struct QuickActionBarButton { toggled: bool, action: Box, tooltip: SharedString, - tooltip_meta: Option, on_click: Box, } @@ -180,16 +130,9 @@ impl QuickActionBarButton { toggled, action, tooltip: tooltip.into(), - tooltip_meta: None, on_click: Box::new(on_click), } } - - #[allow(dead_code)] - pub fn meta(mut self, meta: Option>) -> Self { - self.tooltip_meta = meta.map(|meta| meta.into()); - self - } } impl RenderOnce for QuickActionBarButton { @@ -198,63 +141,17 @@ impl RenderOnce for QuickActionBarButton { fn render(self, _: &mut WindowContext) -> Self::Rendered { let tooltip = self.tooltip.clone(); let action = self.action.boxed_clone(); - let tooltip_meta = self.tooltip_meta.clone(); IconButton::new(self.id.clone(), self.icon) .size(ButtonSize::Compact) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .selected(self.toggled) - .tooltip(move |cx| { - if let Some(meta) = &tooltip_meta { - Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx) - } else { - Tooltip::for_action(tooltip.clone(), &*action, cx) - } - }) + .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) .on_click(move |event, cx| (self.on_click)(event, cx)) } } -// fn render_quick_action_bar_button< -// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext), -// >( -// index: usize, -// icon: &'static str, -// toggled: bool, -// tooltip: (String, Option>), -// cx: &mut ViewContext, -// on_click: F, -// ) -> AnyElement { -// enum QuickActionBarButton {} - -// let theme = theme::current(cx); -// let (tooltip_text, action) = tooltip; - -// MouseEventHandler::new::(index, cx, |mouse_state, _| { -// let style = theme -// .workspace -// .toolbar -// .toggleable_tool -// .in_state(toggled) -// .style_for(mouse_state); -// Svg::new(icon) -// .with_color(style.color) -// .constrained() -// .with_width(style.icon_width) -// .aligned() -// .constrained() -// .with_width(style.button_width) -// .with_height(style.button_width) -// .contained() -// .with_style(style.container) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) -// .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) -// .into_any_named("quick action bar button") -// } - impl ToolbarItemView for QuickActionBar { fn set_active_pane_item( &mut self, From 41fc30f62e733b11c863c52ce15586d96146f9d5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:29:35 -0500 Subject: [PATCH 75/78] Always show tooltips on buttons (#3521) This PR fixes an issue where tooltips weren't being shown on selected buttons. We now always show tooltips on buttons that have one. Release Notes: - N/A --- crates/ui2/src/components/button/button_like.rs | 6 +----- crates/ui2/src/components/stories/icon_button.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 4bef6bff774571047dd1519c18b7ec61b32f1904..1a33eb2845eef9e767181bb2cc5f65ba7eb8957f 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -359,11 +359,7 @@ impl RenderOnce for ButtonLike { }, ) .when_some(self.tooltip, |this, tooltip| { - if !self.selected { - this.tooltip(move |cx| tooltip(cx)) - } else { - this - } + this.tooltip(move |cx| tooltip(cx)) }) .children(self.children) } diff --git a/crates/ui2/src/components/stories/icon_button.rs b/crates/ui2/src/components/stories/icon_button.rs index 3c4d68f8aff5500225be0f6cf83c096a5c6c6dff..583f453d188b9df5aaaf71b5bb1897814c0a6f51 100644 --- a/crates/ui2/src/components/stories/icon_button.rs +++ b/crates/ui2/src/components/stories/icon_button.rs @@ -51,5 +51,13 @@ impl Render for IconButtonStory { .tooltip(|cx| Tooltip::text("Open messages", cx)), ), ) + .child(Story::label("Selected with `tooltip`")) + .child( + div().w_8().child( + IconButton::new("selected_with_tooltip", Icon::InlayHint) + .selected(true) + .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)), + ), + ) } } From 89c8a7c2426a426fdda1af36e8121d77a01cc128 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 13:52:33 -0800 Subject: [PATCH 76/78] Enable buffer font size adjustment in zed2 Co-authored-by: Nathan --- crates/editor2/src/editor.rs | 2 +- crates/gpui2/src/app.rs | 1 - crates/theme2/src/settings.rs | 31 +++++++++++++++---------------- crates/ui2/src/styled_ext.rs | 3 +-- crates/zed2/src/zed2.rs | 15 +++++++-------- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 94ae8abc71a2d99ff2b5bde104809a824cab3877..8f9d22e6c9e6f7b4277d089e5be572f15415d76b 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9291,7 +9291,7 @@ impl Render for Editor { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features, - font_size: settings.buffer_font_size.into(), + font_size: settings.buffer_font_size(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(settings.buffer_line_height.value()), diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 0715ace9eaf8ca0de7fabcb635264e361fc09dad..9293302938c13b8948fab22dd4fc092e69da6c80 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -860,7 +860,6 @@ impl AppContext { } /// Remove the global of the given type from the app context. Does not notify global observers. - #[cfg(any(test, feature = "test-support"))] pub fn remove_global(&mut self) -> G { let global_type = TypeId::of::(); *self diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 15b578d4b0e10855d0368ce9d31167847ae8944b..624b14fe33fe194e2f8b7af3211ff4c9a144c100 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -27,7 +27,7 @@ pub struct ThemeSettings { } #[derive(Default)] -pub struct AdjustedBufferFontSize(Option); +pub struct AdjustedBufferFontSize(Pixels); #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { @@ -69,12 +69,10 @@ impl BufferLineHeight { } impl ThemeSettings { - pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels { - let font_size = *cx - .default_global::() - .0 - .get_or_insert(self.buffer_font_size.into()); - font_size.max(MIN_FONT_SIZE) + pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels { + cx.try_global::() + .map_or(self.buffer_font_size, |size| size.0) + .max(MIN_FONT_SIZE) } pub fn line_height(&self) -> f32 { @@ -83,9 +81,9 @@ impl ThemeSettings { } pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { - if let Some(adjusted_size) = cx.default_global::().0 { + if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::() { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let delta = adjusted_size - buffer_font_size; + let delta = *adjusted_size - buffer_font_size; size + delta } else { size @@ -95,18 +93,19 @@ pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let adjusted_size = cx - .default_global::() - .0 - .get_or_insert(buffer_font_size); - f(adjusted_size); - *adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size); + let mut adjusted_size = cx + .try_global::() + .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); + + f(&mut adjusted_size); + adjusted_size = adjusted_size.max(MIN_FONT_SIZE); + cx.set_global(AdjustedBufferFontSize(adjusted_size)); cx.refresh(); } pub fn reset_font_size(cx: &mut AppContext) { if cx.has_global::() { - cx.global_mut::().0 = None; + cx.remove_global::(); cx.refresh(); } } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index e567830d6ca9ddbc978d0bd08b43ab404625ae9b..0f882b496346252226f8224397c65340b6532280 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -70,8 +70,7 @@ pub trait StyledExt: Styled + Sized { /// or other places that text needs to match the user's buffer font size. fn text_buffer(self, cx: &mut WindowContext) -> Self { let settings = ThemeSettings::get_global(cx); - - self.text_size(settings.buffer_font_size) + self.text_size(settings.buffer_font_size(cx)) } /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index d220250b3dba8bdd55bc0be98e85a62565902225..45ace75ebcc16c3dadc6a639699ebde401a0139f 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -235,14 +235,13 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .open_urls(&[action.url.clone()]) }) .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) - //todo!(buffer font size) - // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size += 1.0) - // }); - // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size -= 1.0) - // }); - // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + .register_action(move |_, _: &IncreaseBufferFontSize, cx| { + theme::adjust_font_size(cx, |size| *size += px(1.0)) + }) + .register_action(move |_, _: &DecreaseBufferFontSize, cx| { + theme::adjust_font_size(cx, |size| *size -= px(1.0)) + }) + .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx)) .register_action(|_, _: &install_cli::Install, cx| { cx.spawn(|_, cx| async move { install_cli::install_cli(cx.deref()) From b72c54fc311c7bd56f6aab2eb6212a2d43a51afe Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:59:24 -0500 Subject: [PATCH 77/78] Don't load the Vim keymap temporarily (#3522) This PR removes the loading of the Vim keymap temporarily. This cuts down on the noise from all of the Vim-related action warnings. We can resume loading the Vim keymap once we're ready to add Vim support. Release Notes: - N/A --- crates/settings2/src/settings_file.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index 590079c51b52fe77a2c83ec4a862b27a0202ad1a..46450a9c282754678d6f7c26bbbc98a4122d82d1 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -124,6 +124,17 @@ pub fn update_settings_file( pub fn load_default_keymap(cx: &mut AppContext) { for path in ["keymaps/default.json", "keymaps/vim.json"] { + // TODO: Remove this conditional when we're ready to add Vim support. + // Right now we're avoiding loading the Vim keymap to silence the warnings + // about invalid action bindings. + if path.contains("vim") { + let _: Option<()> = Err(format!( + "TODO: Skipping {path} until we're ready to add Vim support" + )) + .log_err(); + continue; + } + KeymapFile::load_asset(path, cx).unwrap(); } From 2d18b949adb16786fb28f8c96c9293eb14f035d6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 14:29:09 -0800 Subject: [PATCH 78/78] Upgrade async-compression dep --- Cargo.lock | 4 ++-- Cargo.toml | 1 + crates/copilot/Cargo.toml | 2 +- crates/copilot2/Cargo.toml | 2 +- crates/node_runtime/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- crates/zed2/Cargo.toml | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9e1ab3f2b6c1dce78f675db5456c2ab7dbbea5a..59062e5e144a8b9a81b4e66b0d7c505d3ceb2e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.3.15" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" dependencies = [ "flate2", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 376e3ee62f9ab3e15a12aa3c8beb736763cbf276..6b154cc87fac27cbb6dc252c2a1099d2667f74b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ resolver = "2" [workspace.dependencies] anyhow = { version = "1.0.57" } async-trait = { version = "0.1" } +async-compression = { version = "0.4", features = ["gzip", "futures-io"] } # TODO: Switch back to the published version of `ctor` once: # 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295 # 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20). diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 2558974753f124c67e404f0065e8ab51fd83367b..985e784367459dc987ae5014a14f9e0e90df8a6c 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -28,7 +28,7 @@ theme = { path = "../theme" } lsp = { path = "../lsp" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true log.workspace = true diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index b04a7d1246ebfbcbb03dbdf179d2633934cecd98..ce169f3319a935fc061f6f06f53134366950b32f 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -29,7 +29,7 @@ lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } ui = { package = "ui2", path = "../ui2" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true log.workspace = true diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index faa837fb67062a8b1de7a1f4eabd3baae8a71665..0b4e7a262d9b2bb095f4772b57acf1cc31bbde0a 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] util = { path = "../util" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" futures.workspace = true async-trait.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6d9cb3c7502bc539cedc26d45c97fde5e5b7099f..f665cc36dbdc8f4b0fa8576768efb32388d07374 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -78,7 +78,7 @@ workspace = { path = "../workspace" } welcome = { path = "../welcome" } zed-actions = {path = "../zed-actions"} anyhow.workspace = true -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" async-recursion = "0.3" async-trait.workspace = true diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 0fcbcc40fc281b45ad304d48149aec89506c8275..427e72068e755d61161920c35652fa376a45b169 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -74,7 +74,7 @@ workspace = { package = "workspace2", path = "../workspace2" } welcome = { package = "welcome2", path = "../welcome2" } zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" async-recursion = "0.3" async-trait.workspace = true