From 379ba620b215e552e927bb606e1d168aae7e86e0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 15:59:40 -0700 Subject: [PATCH 01/48] 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/48] 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/48] 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 5ab6874ae9d1b6aeeace957deeb1407a694cdab6 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 4 Dec 2023 11:44:32 -0500 Subject: [PATCH 04/48] zed2: Port outline view --- Cargo.lock | 21 ++ Cargo.toml | 1 + .../src/language_selector.rs | 1 + crates/outline2/Cargo.toml | 29 ++ crates/outline2/src/outline.rs | 276 ++++++++++++++++++ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 7 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 crates/outline2/Cargo.toml create mode 100644 crates/outline2/src/outline.rs diff --git a/Cargo.lock b/Cargo.lock index 9ea9dcf1034dcbe005f7eeb3298fca453bb3cbe4..66125d770390222874f86b419bf3741147edea5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6164,6 +6164,26 @@ dependencies = [ "workspace", ] +[[package]] +name = "outline2" +version = "0.1.0" +dependencies = [ + "editor2", + "fuzzy2", + "gpui2", + "language2", + "ordered-float 2.10.0", + "picker2", + "postage", + "settings2", + "smol", + "text2", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "overload" version = "0.1.1" @@ -11818,6 +11838,7 @@ dependencies = [ "menu2", "node_runtime", "num_cpus", + "outline2", "parking_lot 0.11.2", "postage", "project2", diff --git a/Cargo.toml b/Cargo.toml index 0a7e4aa18f57e1d87c597392f086dcbc078815e2..3658ffad297f2c9d4fb3bf5eb6b03ede591d37e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ members = [ "crates/notifications", "crates/notifications2", "crates/outline", + "crates/outline2", "crates/picker", "crates/picker2", "crates/plugin", diff --git a/crates/language_selector2/src/language_selector.rs b/crates/language_selector2/src/language_selector.rs index 49be0c5418a826fbfc9a5623f3feb33770a571c9..e3970401d4c9201d631e0548ca306f6ca63e7a94 100644 --- a/crates/language_selector2/src/language_selector.rs +++ b/crates/language_selector2/src/language_selector.rs @@ -79,6 +79,7 @@ impl FocusableView for LanguageSelector { self.picker.focus_handle(cx) } } + impl EventEmitter for LanguageSelector {} pub struct LanguageSelectorDelegate { diff --git a/crates/outline2/Cargo.toml b/crates/outline2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7606fc46fe002d993b39a02fd151dee412ea14c7 --- /dev/null +++ b/crates/outline2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "outline2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/outline.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } +picker = { package = "picker2", path = "../picker2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } + +ordered-float.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/outline2/src/outline.rs b/crates/outline2/src/outline.rs new file mode 100644 index 0000000000000000000000000000000000000000..8442d6480d4d011cbb745fa8de815118538dbf96 --- /dev/null +++ b/crates/outline2/src/outline.rs @@ -0,0 +1,276 @@ +use editor::{ + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, + DisplayPoint, Editor, ToPoint, +}; +use fuzzy::StringMatch; +use gpui::{ + actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + FontWeight, ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, View, + ViewContext, VisualContext, WeakView, WindowContext, +}; +use language::Outline; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; +use theme::ActiveTheme; +use ui::{v_stack, ListItem, Selectable}; +use util::ResultExt; +use workspace::Workspace; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(OutlineView::register).detach(); +} + +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let outline = editor + .read(cx) + .buffer() + .read(cx) + .snapshot(cx) + .outline(Some(&cx.theme().syntax())); + + if let Some(outline) = outline { + workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx)); + } + } +} + +pub struct OutlineView { + picker: View>, +} + +impl FocusableView for OutlineView { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for OutlineView {} + +impl Render for OutlineView { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().min_w_96().child(self.picker.clone()) + } +} + +impl OutlineView { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(toggle); + } + + fn new( + outline: Outline, + editor: View, + cx: &mut ViewContext, + ) -> OutlineView { + let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx); + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + OutlineView { picker } + } +} + +struct OutlineViewDelegate { + outline_view: WeakView, + active_editor: View, + outline: Outline, + selected_match_index: usize, + prev_scroll_position: Option>, + matches: Vec, + last_query: String, +} + +impl OutlineViewDelegate { + fn new( + outline_view: WeakView, + outline: Outline, + editor: View, + cx: &mut ViewContext, + ) -> Self { + Self { + outline_view, + last_query: Default::default(), + matches: Default::default(), + selected_match_index: 0, + prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), + active_editor: editor, + outline, + } + } + + fn restore_active_editor(&mut self, cx: &mut WindowContext) { + self.active_editor.update(cx, |editor, cx| { + editor.highlight_rows(None); + if let Some(scroll_position) = self.prev_scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + }) + } + + fn set_selected_index( + &mut self, + ix: usize, + navigate: bool, + cx: &mut ViewContext>, + ) { + self.selected_match_index = ix; + + if navigate && !self.matches.is_empty() { + let selected_match = &self.matches[self.selected_match_index]; + let outline_item = &self.outline.items[selected_match.candidate_id]; + + self.active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let buffer_snapshot = &snapshot.buffer_snapshot; + let start = outline_item.range.start.to_point(buffer_snapshot); + let end = outline_item.range.end.to_point(buffer_snapshot); + let display_rows = start.to_display_point(&snapshot).row() + ..end.to_display_point(&snapshot).row() + 1; + active_editor.highlight_rows(Some(display_rows)); + active_editor.request_autoscroll(Autoscroll::center(), cx); + }); + } + } +} + +impl PickerDelegate for OutlineViewDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self) -> Arc { + "Search buffer symbols...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.set_selected_index(ix, true, cx); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> Task<()> { + let selected_index; + if query.is_empty() { + self.restore_active_editor(cx); + self.matches = self + .outline + .items + .iter() + .enumerate() + .map(|(index, _)| StringMatch { + candidate_id: index, + score: Default::default(), + positions: Default::default(), + string: Default::default(), + }) + .collect(); + + let editor = self.active_editor.read(cx); + let cursor_offset = editor.selections.newest::(cx).head(); + let buffer = editor.buffer().read(cx).snapshot(cx); + selected_index = self + .outline + .items + .iter() + .enumerate() + .map(|(ix, item)| { + let range = item.range.to_offset(&buffer); + let distance_to_closest_endpoint = cmp::min( + (range.start as isize - cursor_offset as isize).abs(), + (range.end as isize - cursor_offset as isize).abs(), + ); + let depth = if range.contains(&cursor_offset) { + Some(item.depth) + } else { + None + }; + (ix, depth, distance_to_closest_endpoint) + }) + .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) + .map(|(ix, _, _)| ix) + .unwrap_or(0); + } else { + self.matches = smol::block_on( + self.outline + .search(&query, cx.background_executor().clone()), + ); + selected_index = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); + } + self.last_query = query; + self.set_selected_index(selected_index, !self.last_query.is_empty(), cx); + Task::ready(()) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + self.prev_scroll_position.take(); + + self.active_editor.update(cx, |active_editor, cx| { + if let Some(rows) = active_editor.highlighted_rows() { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([position..position]) + }); + active_editor.highlight_rows(None); + } + }); + + self.dismissed(cx); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.outline_view + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + self.restore_active_editor(cx); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let mat = &self.matches[ix]; + let outline_item = &self.outline.items[mat.candidate_id]; + + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + outline_item.highlight_ranges.iter().cloned(), + ); + + let styled_text = StyledText::new(outline_item.text.clone()) + .with_highlights(&TextStyle::default(), highlights); + + Some( + ListItem::new(ix) + .inset(true) + .selected(selected) + .child(div().pl(rems(outline_item.depth as f32)).child(styled_text)), + ) + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index dc1597469b95565cd8f9be0ecfa0ffc3108a3487..18bfd8c2664e81efcc589fec3b184edc9160660a 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -50,7 +50,7 @@ menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } # assistant = { path = "../assistant" } -# outline = { path = "../outline" } +outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 1cf3793fe12ea73881a49d7999549845915417ac..90dd495a2624ead690fe621ce3744dc3eff070b4 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -205,7 +205,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); - // outline::init(cx); + outline::init(cx); // project_symbols::init(cx); project_panel::init(Assets, cx); channel::init(&client, user_store.clone(), cx); From 9a2fd184256e5b6b8df01dab240198f7904ace28 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 4 Dec 2023 15:37:34 -0500 Subject: [PATCH 05/48] Avoid panic with outline highlight ranges Previously we were using a function in `editor` to combine syntax highlighting and fuzzy match positions, it would operate on the full text as put into the label. However we now have a method `ranges` on `StringMatch` itself which operates on just the match text. The outline view has some pretty specific behavior around path/normal matches and how they are highlighted. So let's just give the match the full text before it leaves the search function so it can freely index --- crates/language2/src/outline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/language2/src/outline.rs b/crates/language2/src/outline.rs index 4bcbdcd27fa7e4290560944e411047097da06425..df1a3c629e75e7695fcf9bd1f6c6a796df2c01f1 100644 --- a/crates/language2/src/outline.rs +++ b/crates/language2/src/outline.rs @@ -81,6 +81,7 @@ impl Outline { let mut prev_item_ix = 0; for mut string_match in matches { let outline_match = &self.items[string_match.candidate_id]; + string_match.string = outline_match.text.clone(); if is_path_query { let prefix_len = self.path_candidate_prefixes[string_match.candidate_id]; From 3627ff87f008776ac1b63a42b0f4023374ce452f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 4 Dec 2023 15:53:38 -0500 Subject: [PATCH 06/48] Ensure the candidate keybinding matches the correct context --- crates/gpui2/src/key_dispatch.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 5fbf83bfbab8e45320ee856fd7542298abdc9649..1ab99ec487854b0081a960c5554635bcef58d014 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -167,7 +167,10 @@ impl DispatchTree { self.keymap .lock() .bindings_for_action(action.type_id()) - .filter(|candidate| candidate.action.partial_eq(action)) + .filter(|candidate| { + candidate.action.partial_eq(action) + && candidate.matches_context(&self.context_stack) + }) .cloned() .collect() } From 2c2e5144c9c034b3e15c480cab8711345671d23f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 4 Dec 2023 21:28:37 +0000 Subject: [PATCH 07/48] Fix context key matching * You need to check all layers of the context stack * When in command, the context should be based on where focus was (to match `available_actions`. --- .../command_palette2/src/command_palette.rs | 6 ++++- crates/gpui2/src/key_dispatch.rs | 19 +++++++++++--- crates/gpui2/src/window.rs | 26 ++++++++++++++++--- crates/ui2/src/components/keybinding.rs | 15 ++++++++--- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 04688b05492c8c298c33d32423cdb5e7ce1fe393..a2abadd5fdea652e7d3d8fda933dac549fde8ef2 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -311,7 +311,11 @@ impl PickerDelegate for CommandPaletteDelegate { command.name.clone(), r#match.positions.clone(), )) - .children(KeyBinding::for_action(&*command.action, cx)), + .children(KeyBinding::for_action_in( + &*command.action, + &self.previous_focus_handle, + cx, + )), ), ) } diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 1ab99ec487854b0081a960c5554635bcef58d014..95915b98ed3dedfb496b8bc85bb96b86682666ff 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -16,7 +16,7 @@ pub struct DispatchNodeId(usize); pub(crate) struct DispatchTree { node_stack: Vec, - context_stack: Vec, + pub(crate) context_stack: Vec, nodes: Vec, focusable_node_ids: HashMap, keystroke_matchers: HashMap, KeystrokeMatcher>, @@ -163,13 +163,24 @@ impl DispatchTree { actions } - pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + pub fn bindings_for_action( + &self, + action: &dyn Action, + context_stack: &Vec, + ) -> Vec { self.keymap .lock() .bindings_for_action(action.type_id()) .filter(|candidate| { - candidate.action.partial_eq(action) - && candidate.matches_context(&self.context_stack) + if !candidate.action.partial_eq(action) { + return false; + } + for i in 1..context_stack.len() { + if candidate.matches_context(&context_stack[0..i]) { + return true; + } + } + return false; }) .cloned() .collect() diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 5724f1e0701a2b960afb478fad0186649c29debd..b88e89ef55d6d41221491acaa70e0f83f5a98190 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1492,10 +1492,28 @@ impl<'a> WindowContext<'a> { } pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { - self.window - .current_frame - .dispatch_tree - .bindings_for_action(action) + self.window.current_frame.dispatch_tree.bindings_for_action( + action, + &self.window.current_frame.dispatch_tree.context_stack, + ) + } + + pub fn bindings_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Vec { + let dispatch_tree = &self.window.previous_frame.dispatch_tree; + + let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else { + return vec![]; + }; + let context_stack = dispatch_tree + .dispatch_path(node_id) + .into_iter() + .map(|node_id| dispatch_tree.node(node_id).context.clone()) + .collect(); + dispatch_tree.bindings_for_action(action, &context_stack) } pub fn listener_for( diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 993e2f323e7d2305cf4963f1fbd780a77441f208..c4054fa1a434e677c3480740c31ee55ec45cb419 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; -use gpui::{relative, rems, Action, Div, IntoElement, Keystroke}; +use gpui::{relative, rems, Action, Div, FocusHandle, IntoElement, Keystroke}; #[derive(IntoElement, Clone)] pub struct KeyBinding { @@ -49,12 +49,21 @@ impl RenderOnce for KeyBinding { impl KeyBinding { pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { - // todo! this last is arbitrary, we want to prefer users key bindings over defaults, - // and vim over normal (in vim mode), etc. let key_binding = cx.bindings_for_action(action).last().cloned()?; Some(Self::new(key_binding)) } + // like for_action(), but lets you specify the context from which keybindings + // are matched. + pub fn for_action_in( + action: &dyn Action, + focus: &FocusHandle, + cx: &mut WindowContext, + ) -> Option { + let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?; + Some(Self::new(key_binding)) + } + fn icon_for_key(keystroke: &Keystroke) -> Option { let mut icon: Option = None; From 79773178c836bce90ee839d97f9bd5f88d9eea96 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 4 Dec 2023 21:37:47 +0000 Subject: [PATCH 08/48] I was soooo close --- crates/gpui2/src/key_dispatch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 95915b98ed3dedfb496b8bc85bb96b86682666ff..4838b1a612ce65ba33c03ac25da878a752f716d3 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -176,7 +176,7 @@ impl DispatchTree { return false; } for i in 1..context_stack.len() { - if candidate.matches_context(&context_stack[0..i]) { + if candidate.matches_context(&context_stack[0..=i]) { return true; } } From c10d8a8110077213f61160b706bb68f123ae0187 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 4 Dec 2023 16:40:53 -0500 Subject: [PATCH 09/48] Fix cursor styles not displaying properly (#3493) This PR fixes an issue where an element with a cursor style set would not update the cursor when hovering over it. Previously the cursor style would only appear by interacting with the element in some way, for instance, by clicking on the element or by having a `.hover` with some other style being applied. Release Notes: - N/A --- crates/gpui2/src/elements/div.rs | 1 + crates/storybook2/src/stories/cursor.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 68dca4c9d144a571d894f63f382c2d9489f7251c..3d7feee21b632ee69df47d7cc192c8487c939fb2 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -866,6 +866,7 @@ impl Interactivity { } if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() { let bounds = bounds.intersect(&cx.content_mask().bounds); diff --git a/crates/storybook2/src/stories/cursor.rs b/crates/storybook2/src/stories/cursor.rs index d160fa4f4a0c15e86e4a5b91c7f011d4b5005d6e..7d4cf8145a034906bb87bb944ae926fff5ab9f3a 100644 --- a/crates/storybook2/src/stories/cursor.rs +++ b/crates/storybook2/src/stories/cursor.rs @@ -102,7 +102,6 @@ impl Render for CursorStory { .w_64() .h_8() .bg(gpui::red()) - .hover(|style| style.bg(gpui::blue())) .active(|style| style.bg(gpui::green())) .text_sm() .child(Story::label(name)), From 5bdaf0e074f2c034ff418ae713cfaa20be811eab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 17:54:37 -0500 Subject: [PATCH 10/48] 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 c82fea375dd383eba4299a4c6297fc947e07555d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 4 Dec 2023 22:54:48 +0000 Subject: [PATCH 11/48] Dispatch actions on focused node Allows us to implement context menu matching nicely --- crates/editor2/src/editor.rs | 1 + crates/gpui2/src/elements/div.rs | 31 ++++++----- crates/gpui2/src/window.rs | 17 ++++-- crates/ui2/src/components/context_menu.rs | 67 +++++++++++++++++++---- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 6b223243996e32946de9cdcdd15b3eb68e989d43..dfad00036e612bd79f6c6a9b682de10038725b20 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -3640,6 +3640,7 @@ impl Editor { } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { + dbg!("TOGGLE CODE ACTIONS"); let mut context_menu = self.context_menu.write(); if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { *context_menu = None; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 68dca4c9d144a571d894f63f382c2d9489f7251c..2551ddf4a840becff071d1eefa7b3d55c20694ee 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element { /// Add a listener for the given action, fires during the bubble event phase fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { - // NOTE: this debug assert has the side-effect of working around - // a bug where a crate consisting only of action definitions does - // not register the actions in debug builds: - // - // https://github.com/rust-lang/rust/issues/47384 - // https://github.com/mmastrac/rust-ctor/issues/280 - // - // if we are relying on this side-effect still, removing the debug_assert! - // likely breaks the command_palette tests. - // debug_assert!( - // A::is_registered(), - // "{:?} is not registered as an action", - // A::qualified_name() - // ); self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |action, phase, cx| { @@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element { self } + fn on_boxed_action( + mut self, + action: &Box, + listener: impl Fn(&Box, &mut WindowContext) + 'static, + ) -> Self { + let action = action.boxed_clone(); + self.interactivity().action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&action, cx) + } + }), + )); + self + } + fn on_key_down( mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b88e89ef55d6d41221491acaa70e0f83f5a98190..09bc2c561836f399ca4325b563494bda13cdd00c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1348,6 +1348,8 @@ impl<'a> WindowContext<'a> { .dispatch_tree .dispatch_path(node_id); + let mut actions: Vec> = Vec::new(); + // Capture phase let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); self.propagate_event = true; @@ -1382,22 +1384,26 @@ impl<'a> WindowContext<'a> { 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(action) = self + if let Some(found) = self .window .current_frame .dispatch_tree .dispatch_key(&key_down_event.keystroke, &context_stack) { - self.dispatch_action_on_node(*node_id, action); - if !self.propagate_event { - return; - } + actions.push(found.boxed_clone()) } } context_stack.pop(); } } + + for action in actions { + self.dispatch_action_on_node(node_id, action); + if !self.propagate_event { + return; + } + } } } @@ -1425,7 +1431,6 @@ impl<'a> WindowContext<'a> { } } } - // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 54c8d9337574c914f3545ae9066279eeb6027936..9a5390a8d5e93c177822b7b0be2d80d35d0f5189 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -7,7 +7,7 @@ use gpui::{ IntoElement, Render, View, VisualContext, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use std::rc::Rc; +use std::{rc::Rc, time::Duration}; pub enum ContextMenuItem { Separator, @@ -16,7 +16,7 @@ pub enum ContextMenuItem { label: SharedString, icon: Option, handler: Rc, - key_binding: Option, + action: Option>, }, } @@ -70,8 +70,8 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { label: label.into(), handler: Rc::new(on_click), - key_binding: None, icon: None, + action: None, }); self } @@ -84,7 +84,7 @@ impl ContextMenu { ) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), - key_binding: KeyBinding::for_action(&*action, cx), + action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), icon: None, }); @@ -99,7 +99,7 @@ impl ContextMenu { ) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), - key_binding: KeyBinding::for_action(&*action, cx), + action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), icon: Some(Icon::Link), }); @@ -161,6 +161,36 @@ impl ContextMenu { self.select_last(&Default::default(), cx); } } + + pub fn on_action_dispatch(&mut self, dispatched: &Box, cx: &mut ViewContext) { + if let Some(ix) = self.items.iter().position(|item| { + if let ContextMenuItem::Entry { + action: Some(action), + .. + } = item + { + action.partial_eq(&**dispatched) + } else { + false + } + }) { + self.selected_index = Some(ix); + cx.notify(); + let action = dispatched.boxed_clone(); + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + this.update(&mut cx, |this, cx| { + cx.dispatch_action(action); + this.cancel(&Default::default(), cx) + }) + }) + .detach_and_log_err(cx); + } else { + cx.propagate() + } + } } impl ContextMenuItem { @@ -185,6 +215,22 @@ impl Render for ContextMenu { .on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::cancel)) + .map(|mut el| { + for item in self.items.iter() { + if let ContextMenuItem::Entry { + action: Some(action), + .. + } = item + { + el = el.on_boxed_action( + action, + cx.listener(ContextMenu::on_action_dispatch), + ); + } + } + el + }) + .on_blur(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx))) .flex_none() .child( List::new().children(self.items.iter().enumerate().map( @@ -196,8 +242,8 @@ impl Render for ContextMenu { ContextMenuItem::Entry { label, handler, - key_binding, icon, + action, } => { let handler = handler.clone(); let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); @@ -218,11 +264,10 @@ impl Render for ContextMenu { .w_full() .justify_between() .child(label_element) - .children( - key_binding - .clone() - .map(|binding| div().ml_1().child(binding)), - ), + .children(action.as_ref().and_then(|action| { + KeyBinding::for_action(&**action, cx) + .map(|binding| div().ml_1().child(binding)) + })), ) .selected(Some(ix) == self.selected_index) .on_click(move |event, cx| { From fc16e4509a85ff69d76b648f9191ba4e50e27138 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 18:13:28 -0500 Subject: [PATCH 12/48] 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 13/48] 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 1c9b984738aededd18390e5f015ace63e36e1b67 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 4 Dec 2023 23:35:31 +0000 Subject: [PATCH 14/48] Remove cx param --- crates/copilot_button2/src/copilot_button.rs | 3 +- crates/editor2/src/editor.rs | 1 - crates/editor2/src/mouse_context_menu.rs | 11 +++---- crates/project_panel2/src/project_panel.rs | 25 +++++++------- crates/terminal_view2/src/terminal_view.rs | 7 ++-- crates/ui2/src/components/context_menu.rs | 14 ++------ crates/workspace2/src/pane.rs | 34 ++++++-------------- 7 files changed, 31 insertions(+), 64 deletions(-) diff --git a/crates/copilot_button2/src/copilot_button.rs b/crates/copilot_button2/src/copilot_button.rs index aab59a9cad500a97a93f6c12990ba43cdb25b528..dc6f8085339de18f40bd1c4262bc3b9e0d8c9a67 100644 --- a/crates/copilot_button2/src/copilot_button.rs +++ b/crates/copilot_button2/src/copilot_button.rs @@ -201,9 +201,8 @@ impl CopilotButton { url: COPILOT_SETTINGS_URL.to_string(), } .boxed_clone(), - cx, ) - .action("Sign Out", SignOut.boxed_clone(), cx) + .action("Sign Out", SignOut.boxed_clone()) }); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index dfad00036e612bd79f6c6a9b682de10038725b20..6b223243996e32946de9cdcdd15b3eb68e989d43 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -3640,7 +3640,6 @@ impl Editor { } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - dbg!("TOGGLE CODE ACTIONS"); let mut context_menu = self.context_menu.write(); if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { *context_menu = None; diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index fdeec9110b97ff5c23b945bf31da590bbe8a30dc..8b998ccb3a52d59bd1fb10ff5294085e6b27cfc0 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -37,19 +37,18 @@ pub fn deploy_context_menu( }); let context_menu = ui::ContextMenu::build(cx, |menu, cx| { - menu.action("Rename Symbol", Box::new(Rename), cx) - .action("Go to Definition", Box::new(GoToDefinition), cx) - .action("Go to Type Definition", Box::new(GoToTypeDefinition), cx) - .action("Find All References", Box::new(FindAllReferences), cx) + menu.action("Rename Symbol", Box::new(Rename)) + .action("Go to Definition", Box::new(GoToDefinition)) + .action("Go to Type Definition", Box::new(GoToTypeDefinition)) + .action("Find All References", Box::new(FindAllReferences)) .action( "Code Actions", Box::new(ToggleCodeActions { deployed_from_indicator: false, }), - cx, ) .separator() - .action("Reveal in Finder", Box::new(RevealInFinder), cx) + .action("Reveal in Finder", Box::new(RevealInFinder)) }); let context_menu_focus = context_menu.focus_handle(cx); cx.focus(&context_menu_focus); diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 0a5a63f14a289f5f8aeb44a20a454a35f8bea20b..ce039071cf830d3282dcc860fc6ce083576aafc1 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -397,7 +397,6 @@ impl ProjectPanel { menu = menu.action( "Add Folder to Project", Box::new(workspace::AddFolderToProject), - cx, ); if is_root { menu = menu.entry( @@ -412,35 +411,35 @@ impl ProjectPanel { } menu = menu - .action("New File", Box::new(NewFile), cx) - .action("New Folder", Box::new(NewDirectory), cx) + .action("New File", Box::new(NewFile)) + .action("New Folder", Box::new(NewDirectory)) .separator() - .action("Cut", Box::new(Cut), cx) - .action("Copy", Box::new(Copy), cx); + .action("Cut", Box::new(Cut)) + .action("Copy", Box::new(Copy)); if let Some(clipboard_entry) = self.clipboard_entry { if clipboard_entry.worktree_id() == worktree_id { - menu = menu.action("Paste", Box::new(Paste), cx); + menu = menu.action("Paste", Box::new(Paste)); } } menu = menu .separator() - .action("Copy Path", Box::new(CopyPath), cx) - .action("Copy Relative Path", Box::new(CopyRelativePath), cx) + .action("Copy Path", Box::new(CopyPath)) + .action("Copy Relative Path", Box::new(CopyRelativePath)) .separator() - .action("Reveal in Finder", Box::new(RevealInFinder), cx); + .action("Reveal in Finder", Box::new(RevealInFinder)); if is_dir { menu = menu - .action("Open in Terminal", Box::new(OpenInTerminal), cx) - .action("Search Inside", Box::new(NewSearchInDirectory), cx) + .action("Open in Terminal", Box::new(OpenInTerminal)) + .action("Search Inside", Box::new(NewSearchInDirectory)) } - menu = menu.separator().action("Rename", Box::new(Rename), cx); + menu = menu.separator().action("Rename", Box::new(Rename)); if !is_root { - menu = menu.action("Delete", Box::new(Delete), cx); + menu = menu.action("Delete", Box::new(Delete)); } menu diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index b007d58c34bcb2163f42bd2b88e1979a18152f56..4186a610bf0ae11cdb4bef306e2df2ef1b9b7925 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -299,11 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, cx| { - menu.action("Clear", Box::new(Clear), cx).action( - "Close", - Box::new(CloseActiveItem { save_intent: None }), - cx, - ) + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); dbg!(&position); // todo!() diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 9a5390a8d5e93c177822b7b0be2d80d35d0f5189..27aa73b4fe35d38457301fad78a15cb4b0986b23 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -76,12 +76,7 @@ impl ContextMenu { self } - pub fn action( - mut self, - label: impl Into, - action: Box, - cx: &mut WindowContext, - ) -> Self { + pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), action: Some(action.boxed_clone()), @@ -91,12 +86,7 @@ impl ContextMenu { self } - pub fn link( - mut self, - label: impl Into, - action: Box, - cx: &mut WindowContext, - ) -> Self { + pub fn link(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), action: Some(action.boxed_clone()), diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 438ad396936e740fe0c1cc3518e7bdca9f02e941..855ce0e931e4c13b78d6312df1b1ad79fb4bd433 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1531,24 +1531,17 @@ impl Pane { menu.action( "Close Active Item", 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 Inactive Items", CloseInactiveItems.boxed_clone()) + .action("Close Clean Items", CloseCleanItems.boxed_clone()) + .action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone()) .action( "Close Items To The Right", CloseItemsToTheRight.boxed_clone(), - cx, ) .action( "Close All Items", CloseAllItems { save_intent: None }.boxed_clone(), - cx, ) }) }) @@ -1627,17 +1620,12 @@ impl Pane { .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) + menu.action("New File", NewFile.boxed_clone()) .action( "New Terminal", NewCenterTerminal.boxed_clone(), - cx, - ) - .action( - "New Search", - NewSearch.boxed_clone(), - cx, ) + .action("New Search", NewSearch.boxed_clone()) }); cx.subscribe( &menu, @@ -1661,14 +1649,10 @@ impl Pane { .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) + menu.action("Split Right", SplitRight.boxed_clone()) + .action("Split Left", SplitLeft.boxed_clone()) + .action("Split Up", SplitUp.boxed_clone()) + .action("Split Down", SplitDown.boxed_clone()) }); cx.subscribe( &menu, From 63667ecf6f866cf8a295695ef1e5b5e1c4dcc8ad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2023 15:11:52 -0800 Subject: [PATCH 15/48] Start bringing back the current call section of the collab panel Co-authored-by: Nathan --- crates/collab_ui2/src/collab_panel.rs | 1324 +++++++++---------- crates/editor2/src/element.rs | 4 +- crates/gpui2/src/elements/canvas.rs | 48 + crates/gpui2/src/elements/mod.rs | 2 + crates/gpui2/src/text_system.rs | 32 +- crates/gpui2/src/text_system/line.rs | 4 +- crates/ui2/src/components/list/list_item.rs | 11 +- crates/workspace2/src/workspace2.rs | 185 +-- 8 files changed, 810 insertions(+), 800 deletions(-) create mode 100644 crates/gpui2/src/elements/canvas.rs diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index b90df68c2a8ac08ed3e7a2fdf0ff31fe0f920998..6f21937766df98045381f65f2794903d819fd7c1 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -18,7 +18,7 @@ mod contact_finder; // }; use contact_finder::ContactFinder; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; -use rpc::proto; +use rpc::proto::{self, PeerId}; use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; @@ -169,11 +169,12 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, - AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, - ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString, - Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action, + AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, + FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model, + MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, + ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -345,21 +346,21 @@ enum Section { #[derive(Clone, Debug)] enum ListEntry { Header(Section), - // CallParticipant { - // user: Arc, - // peer_id: Option, - // is_pending: bool, - // }, - // ParticipantProject { - // project_id: u64, - // worktree_root_names: Vec, - // host_user_id: u64, - // is_last: bool, - // }, - // ParticipantScreen { - // peer_id: Option, - // is_last: bool, - // }, + CallParticipant { + user: Arc, + peer_id: Option, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_last: bool, + }, + ParticipantScreen { + peer_id: Option, + is_last: bool, + }, IncomingRequest(Arc), OutgoingRequest(Arc), // ChannelInvite(Arc), @@ -368,12 +369,12 @@ enum ListEntry { depth: usize, has_children: bool, }, - // ChannelNotes { - // channel_id: ChannelId, - // }, - // ChannelChat { - // channel_id: ChannelId, - // }, + ChannelNotes { + channel_id: ChannelId, + }, + ChannelChat { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -706,136 +707,136 @@ impl CollabPanel { let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let old_entries = mem::take(&mut self.entries); - let scroll_to_top = false; - - // if let Some(room) = ActiveCall::global(cx).read(cx).room() { - // self.entries.push(ListEntry::Header(Section::ActiveCall)); - // if !old_entries - // .iter() - // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) - // { - // scroll_to_top = true; - // } + let mut scroll_to_top = false; - // if !self.collapsed_sections.contains(&Section::ActiveCall) { - // let room = room.read(cx); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + self.entries.push(ListEntry::Header(Section::ActiveCall)); + if !old_entries + .iter() + .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + { + scroll_to_top = true; + } - // if let Some(channel_id) = room.channel_id() { - // self.entries.push(ListEntry::ChannelNotes { channel_id }); - // self.entries.push(ListEntry::ChannelChat { channel_id }) - // } + if !self.collapsed_sections.contains(&Section::ActiveCall) { + let room = room.read(cx); - // // Populate the active user. - // if let Some(user) = user_store.current_user() { - // self.match_candidates.clear(); - // self.match_candidates.push(StringMatchCandidate { - // id: 0, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if !matches.is_empty() { - // let user_id = user.id; - // self.entries.push(ListEntry::CallParticipant { - // user, - // peer_id: None, - // is_pending: false, - // }); - // let mut projects = room.local_participant().projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: user_id, - // is_last: projects.peek().is_none() && !room.is_screen_sharing(), - // }); - // } - // if room.is_screen_sharing() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: None, - // is_last: true, - // }); - // } - // } - // } + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }) + } - // // Populate remote participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.remote_participants().iter().map(|(_, participant)| { - // StringMatchCandidate { - // id: participant.user.id as usize, - // string: participant.user.github_login.clone(), - // char_bag: participant.user.github_login.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // for mat in matches { - // let user_id = mat.candidate_id as u64; - // let participant = &room.remote_participants()[&user_id]; - // self.entries.push(ListEntry::CallParticipant { - // user: participant.user.clone(), - // peer_id: Some(participant.peer_id), - // is_pending: false, - // }); - // let mut projects = participant.projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: participant.user.id, - // is_last: projects.peek().is_none() - // && participant.video_tracks.is_empty(), - // }); - // } - // if !participant.video_tracks.is_empty() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: Some(participant.peer_id), - // is_last: true, - // }); - // } - // } + // Populate the active user. + if let Some(user) = user_store.current_user() { + self.match_candidates.clear(); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + let user_id = user.id; + self.entries.push(ListEntry::CallParticipant { + user, + peer_id: None, + is_pending: false, + }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + self.entries.push(ListEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_last: projects.peek().is_none() && !room.is_screen_sharing(), + }); + } + if room.is_screen_sharing() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: None, + is_last: true, + }); + } + } + } - // // Populate pending participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.pending_participants().iter().enumerate().map( - // |(id, participant)| StringMatchCandidate { - // id, - // string: participant.github_login.clone(), - // char_bag: participant.github_login.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // self.entries - // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { - // user: room.pending_participants()[mat.candidate_id].clone(), - // peer_id: None, - // is_pending: true, - // })); - // } - // } + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.remote_participants().iter().map(|(_, participant)| { + StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + for mat in matches { + let user_id = mat.candidate_id as u64; + let participant = &room.remote_participants()[&user_id]; + self.entries.push(ListEntry::CallParticipant { + user: participant.user.clone(), + peer_id: Some(participant.peer_id), + is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + self.entries.push(ListEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: participant.user.id, + is_last: projects.peek().is_none() + && participant.video_tracks.is_empty(), + }); + } + if !participant.video_tracks.is_empty() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: Some(participant.peer_id), + is_last: true, + }); + } + } + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.pending_participants().iter().enumerate().map( + |(id, participant)| StringMatchCandidate { + id, + string: participant.github_login.clone(), + char_bag: participant.github_login.chars().collect(), + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries + .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + peer_id: None, + is_pending: true, + })); + } + } let mut request_entries = Vec::new(); @@ -1133,290 +1134,235 @@ impl CollabPanel { cx.notify(); } - // fn render_call_participant( - // user: &User, - // peer_id: Option, - // user_store: ModelHandle, - // is_pending: bool, - // is_selected: bool, - // theme: &theme::Theme, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum CallParticipant {} - // enum CallParticipantTooltip {} - // enum LeaveCallButton {} - // enum LeaveCallTooltip {} - - // let collab_theme = &theme.collab_panel; - - // let is_current_user = - // user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); - - // let content = MouseEventHandler::new::( - // user.id as usize, - // cx, - // |mouse_state, cx| { - // let style = if is_current_user { - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(&mut Default::default()) - // } else { - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(mouse_state) - // }; - - // Flex::row() - // .with_children(user.avatar.clone().map(|avatar| { - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left() - // })) - // .with_child( - // Label::new( - // user.github_login.clone(), - // collab_theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(if is_pending { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned() - // .into_any(), - // ) - // } else if is_current_user { - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/exit.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // Self::leave_call(cx); - // }) - // .with_tooltip::( - // 0, - // "Leave call", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(style) - // }, - // ); - - // if is_current_user || is_pending || peer_id.is_none() { - // return content.into_any(); - // } - - // let tooltip = format!("Follow {}", user.github_login); - - // content - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // workspace - // .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) - // .map(|task| task.detach_and_log_err(cx)); - // } - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // user.id as usize, - // tooltip, - // Some(Box::new(FollowNextCollaborator)), - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } + fn render_call_participant( + &self, + user: Arc, + peer_id: Option, + is_pending: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let is_current_user = + self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + let tooltip = format!("Follow {}", user.github_login); - // fn render_participant_project( - // project_id: u64, - // worktree_root_names: &[String], - // host_user_id: u64, - // is_current: bool, - // is_last: bool, - // is_selected: bool, - // theme: &theme::Theme, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum JoinProject {} - // enum JoinProjectTooltip {} - - // let collab_theme = &theme.collab_panel; - // let host_avatar_width = collab_theme - // .contact_avatar - // .width - // .or(collab_theme.contact_avatar.height) - // .unwrap_or(0.); - // let tree_branch = collab_theme.tree_branch; - // let project_name = if worktree_root_names.is_empty() { - // "untitled".to_string() - // } else { - // worktree_root_names.join(", ") - // }; - - // let content = - // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { - // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - // let row = if is_current { - // collab_theme - // .project_row - // .in_state(true) - // .style_for(&mut Default::default()) - // } else { - // collab_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, collab_theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file_icons/folder.svg") - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new(project_name.clone(), row.name.text.clone()) - // .aligned() - // .left() - // .contained() - // .with_style(row.name.container) - // .flex(1., false), - // ) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(row.container) - // }); - - // if is_current { - // return content.into_any(); - // } - - // content - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // let app_state = workspace.read(cx).app_state().clone(); - // workspace::join_remote_project(project_id, host_user_id, app_state, cx) - // .detach_and_log_err(cx); - // } - // }) - // .with_tooltip::( - // project_id as usize, - // format!("Open {}", project_name), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } + ListItem::new(SharedString::from(user.github_login.clone())) + .left_child(Avatar::data(user.avatar.clone().unwrap())) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(user.github_login.clone())) + .child(if is_pending { + Label::new("Calling").color(Color::Muted).into_any_element() + } else if is_current_user { + IconButton::new("leave-call", Icon::ArrowRight) + .on_click(cx.listener(move |this, _, cx| { + Self::leave_call(cx); + })) + .tooltip(|cx| Tooltip::text("Leave Call", cx)) + .into_any_element() + } else { + div().into_any_element() + }), + ) + .when(!is_current_user, |this| { + this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .on_click(cx.listener(move |this, _, cx| { + this.workspace.update(cx, |workspace, cx| { + // workspace.follow(peer_id, cx) + }); + })) + }) + } - // fn render_participant_screen( - // peer_id: Option, - // is_last: bool, - // is_selected: bool, - // theme: &theme::CollabPanel, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum OpenSharedScreen {} - - // 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() - // } + fn render_participant_project( + &self, + project_id: u64, + worktree_root_names: &[String], + host_user_id: u64, + // is_current: bool, + is_last: bool, + // is_selected: bool, + // theme: &theme::Theme, + cx: &mut ViewContext, + ) -> impl IntoElement { + let project_name: SharedString = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + } + .into(); + + let theme = cx.theme(); + + ListItem::new(project_id as usize) + .on_click(cx.listener(move |this, _, cx| { + this.workspace.update(cx, |workspace, cx| { + let app_state = workspace.app_state().clone(); + workspace::join_remote_project(project_id, host_user_id, app_state, cx) + .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())), + ) + .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) + + // enum JoinProject {} + // enum JoinProjectTooltip {} + + // let collab_theme = &theme.collab_panel; + // let host_avatar_width = collab_theme + // .contact_avatar + // .width + // .or(collab_theme.contact_avatar.height) + // .unwrap_or(0.); + // let tree_branch = collab_theme.tree_branch; + + // let content = + // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { + // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + // let row = if is_current { + // collab_theme + // .project_row + // .in_state(true) + // .style_for(&mut Default::default()) + // } else { + // collab_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, collab_theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/file_icons/folder.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(project_name.clone(), row.name.text.clone()) + // .aligned() + // .left() + // .contained() + // .with_style(row.name.container) + // .flex(1., false), + // ) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(row.container) + // }); + + // if is_current { + // return content.into_any(); + // } + + // content + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let app_state = workspace.read(cx).app_state().clone(); + // workspace::join_remote_project(project_id, host_user_id, app_state, cx) + // .detach_and_log_err(cx); + // } + // }) + // .with_tooltip::( + // project_id as usize, + // format!("Open {}", project_name), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + } + + fn render_participant_screen( + &self, + peer_id: Option, + is_last: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + // enum OpenSharedScreen {} + + // 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() + } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { if let Some(_) = self.channel_editing_state.take() { @@ -1463,117 +1409,114 @@ impl CollabPanel { // .into_any() // } - // fn render_channel_notes( - // &self, - // channel_id: ChannelId, - // theme: &theme::CollabPanel, - // is_selected: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // 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() - // } + fn render_channel_notes( + &self, + 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() - // fn render_channel_chat( - // &self, - // channel_id: ChannelId, - // theme: &theme::CollabPanel, - // is_selected: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // 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() + } + + fn render_channel_chat( + &self, + 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() + } // fn render_channel_invite( // channel: Arc, @@ -2392,6 +2335,36 @@ impl CollabPanel { ListEntry::ChannelEditor { depth } => { self.render_channel_editor(depth, cx).into_any_element() } + ListEntry::CallParticipant { + user, + peer_id, + is_pending, + } => self + .render_call_participant(user, peer_id, is_pending, cx) + .into_any_element(), + ListEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_last, + } => self + .render_participant_project( + project_id, + &worktree_root_names, + host_user_id, + is_last, + cx, + ) + .into_any_element(), + ListEntry::ParticipantScreen { peer_id, is_last } => self + .render_participant_screen(peer_id, is_last, cx) + .into_any_element(), + ListEntry::ChannelNotes { channel_id } => { + self.render_channel_notes(channel_id, cx).into_any_element() + } + ListEntry::ChannelChat { channel_id } => { + self.render_channel_chat(channel_id, cx).into_any_element() + } } }), ), @@ -2405,37 +2378,36 @@ impl CollabPanel { is_collapsed: bool, cx: &ViewContext, ) -> impl IntoElement { - // let mut channel_link = None; - // let mut channel_tooltip_text = None; - // let mut channel_icon = None; + let mut channel_link = None; + let mut channel_tooltip_text = None; + let mut channel_icon = None; // let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { - // let channel_name = maybe!({ - // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - - // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - - // channel_link = Some(channel.link()); - // (channel_icon, channel_tooltip_text) = match channel.visibility { - // proto::ChannelVisibility::Public => { - // (Some("icons/public.svg"), Some("Copy public channel link.")) - // } - // proto::ChannelVisibility::Members => { - // (Some("icons/hash.svg"), Some("Copy private channel link.")) - // } - // }; - - // Some(channel.name.as_str()) - // }); - - // if let Some(name) = channel_name { - // SharedString::from(format!("{}", name)) - // } else { - // SharedString::from("Current Call") - // } - todo!() + let channel_name = maybe!({ + let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + channel_link = Some(channel.link()); + (channel_icon, channel_tooltip_text) = match channel.visibility { + proto::ChannelVisibility::Public => { + (Some("icons/public.svg"), Some("Copy public channel link.")) + } + proto::ChannelVisibility::Members => { + (Some("icons/hash.svg"), Some("Copy private channel link.")) + } + }; + + Some(channel.name.as_str()) + }); + + if let Some(name) = channel_name { + SharedString::from(format!("{}", name)) + } else { + SharedString::from("Current Call") + } } Section::ContactRequests => SharedString::from("Requests"), Section::Contacts => SharedString::from("Contacts"), @@ -2446,34 +2418,15 @@ impl CollabPanel { }; let button = match section { - Section::ActiveCall => - // channel_link.map(|channel_link| { - // let channel_link_copy = channel_link.clone(); - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/link.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // let item = ClipboardItem::new(channel_link_copy.clone()); - // cx.write_to_clipboard(item) - // }) - // .with_tooltip::( - // 0, - // channel_tooltip_text.unwrap(), - // None, - // tooltip_style.clone(), - // cx, - // ) - // }), - { - todo!() - } + Section::ActiveCall => channel_link.map(|channel_link| { + let channel_link_copy = channel_link.clone(); + IconButton::new("channel-link", Icon::Check) + .on_click(move |_, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) + }) + .tooltip(|cx| Tooltip::text("Copy channel link", cx)) + }), Section::Contacts => Some( IconButton::new("add-contact", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) @@ -3177,50 +3130,49 @@ impl CollabPanel { } } -// fn render_tree_branch( -// branch_style: theme::TreeBranch, -// row_style: &TextStyle, -// is_last: bool, -// size: Vector2F, -// font_cache: &FontCache, -// ) -> gpui::elements::ConstrainedBox { -// let line_height = row_style.line_height(font_cache); -// let cap_height = row_style.cap_height(font_cache); -// let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; - -// Canvas::new(move |bounds, _, _, cx| { -// cx.paint_layer(None, |cx| { -// let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); -// let end_x = bounds.max_x(); -// let start_y = bounds.min_y(); -// let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - -// cx.scene().push_quad(gpui::Quad { -// bounds: RectF::from_points( -// vec2f(start_x, start_y), -// vec2f( -// start_x + branch_style.width, -// if is_last { end_y } else { bounds.max_y() }, -// ), -// ), -// background: Some(branch_style.color), -// border: gpui::Border::default(), -// corner_radii: (0.).into(), -// }); -// cx.scene().push_quad(gpui::Quad { -// bounds: RectF::from_points( -// vec2f(start_x, end_y), -// vec2f(end_x, end_y + branch_style.width), -// ), -// background: Some(branch_style.color), -// border: gpui::Border::default(), -// corner_radii: (0.).into(), -// }); -// }) -// }) -// .constrained() -// .with_width(size.x()) -// } +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 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.); + + cx.paint_quad( + Bounds::from_corners( + point(start_x, start_y), + point( + start_x + thickness, + if is_last { end_y } else { bounds.bottom() }, + ), + ), + Default::default(), + color, + Default::default(), + Hsla::transparent_black(), + ); + cx.paint_quad( + Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)), + Default::default(), + color, + Default::default(), + Hsla::transparent_black(), + ); + }) + .w(width) + .h(line_height) +} impl Render for CollabPanel { type Element = Focusable
; @@ -3427,33 +3379,33 @@ impl PartialEq for ListEntry { return section_1 == section_2; } } - // ListEntry::CallParticipant { user: user_1, .. } => { - // if let ListEntry::CallParticipant { user: user_2, .. } = other { - // return user_1.id == user_2.id; - // } - // } - // ListEntry::ParticipantProject { - // project_id: project_id_1, - // .. - // } => { - // if let ListEntry::ParticipantProject { - // project_id: project_id_2, - // .. - // } = other - // { - // return project_id_1 == project_id_2; - // } - // } - // ListEntry::ParticipantScreen { - // peer_id: peer_id_1, .. - // } => { - // if let ListEntry::ParticipantScreen { - // peer_id: peer_id_2, .. - // } = other - // { - // return peer_id_1 == peer_id_2; - // } - // } + ListEntry::CallParticipant { user: user_1, .. } => { + if let ListEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } + ListEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ListEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } + ListEntry::ParticipantScreen { + peer_id: peer_id_1, .. + } => { + if let ListEntry::ParticipantScreen { + peer_id: peer_id_2, .. + } = other + { + return peer_id_1 == peer_id_2; + } + } ListEntry::Channel { channel: channel_1, .. } => { @@ -3464,22 +3416,22 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id; } } - // ListEntry::ChannelNotes { channel_id } => { - // if let ListEntry::ChannelNotes { - // channel_id: other_id, - // } = other - // { - // return channel_id == other_id; - // } - // } - // ListEntry::ChannelChat { channel_id } => { - // if let ListEntry::ChannelChat { - // channel_id: other_id, - // } = other - // { - // return channel_id == other_id; - // } - // } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } + ListEntry::ChannelChat { channel_id } => { + if let ListEntry::ChannelChat { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } // ListEntry::ChannelInvite(channel_1) => { // if let ListEntry::ChannelInvite(channel_2) = other { // return channel_1.id == channel_2.id; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3abe5a37f97b9ce2d23f0c0c1d215e05883588b9..cc3cbefcdbd3dd5a45f20be2c3e98be99f99805b 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1753,7 +1753,7 @@ impl EditorElement { let gutter_width; let gutter_margin; if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let descent = cx.text_system().descent(font_id, font_size); let gutter_padding_factor = 3.5; gutter_padding = (em_width * gutter_padding_factor).round(); @@ -3628,7 +3628,7 @@ fn compute_auto_height_layout( let gutter_width; let gutter_margin; if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let descent = cx.text_system().descent(font_id, font_size); let gutter_padding_factor = 3.5; gutter_padding = (em_width * gutter_padding_factor).round(); gutter_width = max_line_number_width + gutter_padding * 2.0; diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs new file mode 100644 index 0000000000000000000000000000000000000000..4761b04f3f84abae558038b6830d709deb06532e --- /dev/null +++ b/crates/gpui2/src/elements/canvas.rs @@ -0,0 +1,48 @@ +use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext}; + +pub fn canvas(callback: impl 'static + FnOnce(Bounds, &mut WindowContext)) -> Canvas { + Canvas { + paint_callback: Box::new(callback), + style: Default::default(), + } +} + +pub struct Canvas { + paint_callback: Box, &mut WindowContext)>, + style: StyleRefinement, +} + +impl IntoElement for Canvas { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Canvas { + type State = (); + + fn layout( + &mut self, + _: Option, + cx: &mut WindowContext, + ) -> (crate::LayoutId, Self::State) { + let layout_id = cx.request_layout(&self.style.clone().into(), []); + (layout_id, ()) + } + + fn paint(self, bounds: Bounds, _: &mut (), cx: &mut WindowContext) { + (self.paint_callback)(bounds, cx) + } +} + +impl Styled for Canvas { + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui2/src/elements/mod.rs b/crates/gpui2/src/elements/mod.rs index 12c57958eaaf1829664ee73500985d05037f9786..e986b0b3eaef37f65c55344a7a14ef234cb8539e 100644 --- a/crates/gpui2/src/elements/mod.rs +++ b/crates/gpui2/src/elements/mod.rs @@ -1,3 +1,4 @@ +mod canvas; mod div; mod img; mod overlay; @@ -5,6 +6,7 @@ mod svg; mod text; mod uniform_list; +pub use canvas::*; pub use div::*; pub use img::*; pub use overlay::*; diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index 440789dd472b35c02e1bbf3c2605e7b4c8ae3be3..b3f17bd057e2253fbba8d80439289cdc57f275d3 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -72,7 +72,7 @@ impl TextSystem { } } - pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result> { + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) } @@ -89,9 +89,9 @@ impl TextSystem { let bounds = self .platform_text_system .typographic_bounds(font_id, glyph_id)?; - self.read_metrics(font_id, |metrics| { + Ok(self.read_metrics(font_id, |metrics| { (bounds / metrics.units_per_em as f32 * font_size.0).map(px) - }) + })) } pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { @@ -100,28 +100,28 @@ impl TextSystem { .glyph_for_char(font_id, ch) .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?; let result = self.platform_text_system.advance(font_id, glyph_id)? - / self.units_per_em(font_id)? as f32; + / self.units_per_em(font_id) as f32; Ok(result * font_size) } - pub fn units_per_em(&self, font_id: FontId) -> Result { + pub fn units_per_em(&self, font_id: FontId) -> u32 { self.read_metrics(font_id, |metrics| metrics.units_per_em as u32) } - pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) } - pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) } - pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) } - pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.descent(font_size)) } @@ -130,24 +130,24 @@ impl TextSystem { font_id: FontId, font_size: Pixels, line_height: Pixels, - ) -> Result { - let ascent = self.ascent(font_id, font_size)?; - let descent = self.descent(font_id, font_size)?; + ) -> Pixels { + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); let padding_top = (line_height - ascent - descent) / 2.; - Ok(padding_top + ascent) + padding_top + ascent } - fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result { + fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { let lock = self.font_metrics.upgradable_read(); if let Some(metrics) = lock.get(&font_id) { - Ok(read(metrics)) + read(metrics) } else { let mut lock = RwLockUpgradableReadGuard::upgrade(lock); let metrics = lock .entry(font_id) .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); - Ok(read(metrics)) + read(metrics) } } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 0d15647b88fdfb112b72b150b0500d20ffac8b37..d62bee69c095139114ecf45561287d6ac218da2a 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -101,9 +101,7 @@ fn paint_line( let mut glyph_origin = origin; let mut prev_glyph_position = Point::default(); for (run_ix, run) in layout.runs.iter().enumerate() { - let max_glyph_size = text_system - .bounding_box(run.font_id, layout.font_size)? - .size; + let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { glyph_origin.x += glyph.position.x - prev_glyph_position.x; diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 85198416cd94e7b498fa2c9283a175d390b77a82..529f2c2a58765caeeb20ce7ecf6f12080c2edc0b 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,7 +1,8 @@ use std::rc::Rc; use gpui::{ - px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful, + px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, + Stateful, }; use smallvec::SmallVec; @@ -21,6 +22,7 @@ pub struct ListItem { inset: bool, on_click: Option>, on_toggle: Option>, + tooltip: Option AnyView + 'static>>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -38,6 +40,7 @@ impl ListItem { on_click: None, on_secondary_mouse_down: None, on_toggle: None, + tooltip: None, children: SmallVec::new(), } } @@ -55,6 +58,11 @@ impl ListItem { self } + pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { + self.tooltip = Some(Box::new(tooltip)); + self + } + pub fn inset(mut self, inset: bool) -> Self { self.inset = inset; self @@ -149,6 +157,7 @@ impl RenderOnce for ListItem { (on_mouse_down)(event, cx) }) }) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) .child( div() .when(self.inset, |this| this.px_2()) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5dcec2cabd5392a5d65695602a1ae6c05aabe63c..66eea706709169c2a88eccfccf9a0a81ed274f38 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -4314,101 +4314,102 @@ pub fn create_and_open_local_file( }) } -// pub fn join_remote_project( -// project_id: u64, -// follow_user_id: u64, -// app_state: Arc, -// cx: &mut AppContext, -// ) -> Task> { -// cx.spawn(|mut cx| async move { -// let windows = cx.windows(); -// let existing_workspace = windows.into_iter().find_map(|window| { -// window.downcast::().and_then(|window| { -// window -// .read_root_with(&cx, |workspace, cx| { -// if workspace.project().read(cx).remote_id() == Some(project_id) { -// Some(cx.handle().downgrade()) -// } else { -// None -// } -// }) -// .unwrap_or(None) -// }) -// }); - -// let workspace = if let Some(existing_workspace) = existing_workspace { -// existing_workspace -// } else { -// let active_call = cx.read(ActiveCall::global); -// let room = active_call -// .read_with(&cx, |call, _| call.room().cloned()) -// .ok_or_else(|| anyhow!("not in a call"))?; -// let project = room -// .update(&mut cx, |room, cx| { -// room.join_project( -// project_id, -// app_state.languages.clone(), -// app_state.fs.clone(), -// cx, -// ) -// }) -// .await?; - -// let window_bounds_override = window_bounds_env_override(&cx); -// let window = cx.add_window( -// (app_state.build_window_options)( -// window_bounds_override, -// None, -// cx.platform().as_ref(), -// ), -// |cx| Workspace::new(0, project, app_state.clone(), cx), -// ); -// let workspace = window.root(&cx).unwrap(); -// (app_state.initialize_workspace)( -// workspace.downgrade(), -// false, -// app_state.clone(), -// cx.clone(), -// ) -// .await -// .log_err(); +pub fn join_remote_project( + project_id: u64, + follow_user_id: u64, + app_state: Arc, + cx: &mut AppContext, +) -> Task> { + todo!() + // let windows = cx.windows(); + // cx.spawn(|mut cx| async move { + // let existing_workspace = windows.into_iter().find_map(|window| { + // window.downcast::().and_then(|window| { + // window + // .update(&mut cx, |workspace, cx| { + // if workspace.project().read(cx).remote_id() == Some(project_id) { + // Some(cx.view().downgrade()) + // } else { + // None + // } + // }) + // .unwrap_or(None) + // }) + // }); -// workspace.downgrade() -// }; + // let workspace = if let Some(existing_workspace) = existing_workspace { + // existing_workspace + // } else { + // let active_call = cx.update(ActiveCall::global); + // let room = active_call + // .read_with(&cx, |call, _| call.room().cloned()) + // .ok_or_else(|| anyhow!("not in a call"))?; + // let project = room + // .update(&mut cx, |room, cx| { + // room.join_project( + // project_id, + // app_state.languages.clone(), + // app_state.fs.clone(), + // cx, + // ) + // }) + // .await?; -// workspace.window().activate(&mut cx); -// cx.platform().activate(true); - -// workspace.update(&mut cx, |workspace, cx| { -// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { -// let follow_peer_id = room -// .read(cx) -// .remote_participants() -// .iter() -// .find(|(_, participant)| participant.user.id == follow_user_id) -// .map(|(_, p)| p.peer_id) -// .or_else(|| { -// // If we couldn't follow the given user, follow the host instead. -// let collaborator = workspace -// .project() -// .read(cx) -// .collaborators() -// .values() -// .find(|collaborator| collaborator.replica_id == 0)?; -// Some(collaborator.peer_id) -// }); - -// if let Some(follow_peer_id) = follow_peer_id { -// workspace -// .follow(follow_peer_id, cx) -// .map(|follow| follow.detach_and_log_err(cx)); -// } -// } -// })?; + // let window_bounds_override = window_bounds_env_override(&cx); + // let window = cx.add_window( + // (app_state.build_window_options)( + // window_bounds_override, + // None, + // cx.platform().as_ref(), + // ), + // |cx| Workspace::new(0, project, app_state.clone(), cx), + // ); + // let workspace = window.root(&cx).unwrap(); + // (app_state.initialize_workspace)( + // workspace.downgrade(), + // false, + // app_state.clone(), + // cx.clone(), + // ) + // .await + // .log_err(); + + // workspace.downgrade() + // }; + + // workspace.window().activate(&mut cx); + // cx.platform().activate(true); + + // workspace.update(&mut cx, |workspace, cx| { + // if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + // let follow_peer_id = room + // .read(cx) + // .remote_participants() + // .iter() + // .find(|(_, participant)| participant.user.id == follow_user_id) + // .map(|(_, p)| p.peer_id) + // .or_else(|| { + // // If we couldn't follow the given user, follow the host instead. + // let collaborator = workspace + // .project() + // .read(cx) + // .collaborators() + // .values() + // .find(|collaborator| collaborator.replica_id == 0)?; + // Some(collaborator.peer_id) + // }); + + // if let Some(follow_peer_id) = follow_peer_id { + // workspace + // .follow(follow_peer_id, cx) + // .map(|follow| follow.detach_and_log_err(cx)); + // } + // } + // })?; -// anyhow::Ok(()) -// }) -// } + // anyhow::Ok(()) + // }) +} pub fn restart(_: &Restart, cx: &mut AppContext) { let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; From 2c5603032db765ab5aaaf3e7fb5fff22523486d5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2023 15:46:32 -0800 Subject: [PATCH 16/48] Allow sharing projects Co-authored-by: Nathan --- crates/collab_ui2/src/collab_panel.rs | 1 + crates/collab_ui2/src/collab_titlebar_item.rs | 102 ++++++++---------- 2 files changed, 47 insertions(+), 56 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6f21937766df98045381f65f2794903d819fd7c1..03637a051f9510e6b4de5bddee0fc305098514b4 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1199,6 +1199,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.workspace.update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); + let call = workspace.call_state(); workspace::join_remote_project(project_id, host_user_id, app_state, cx) .detach_and_log_err(cx); }); diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2cdf32ca366b0fdeb8ed8fd44a84861df23daa84..3a0f0093bb416386e5c0f283a67cdf3ca097d9d9 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,9 +31,9 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, - ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, - ViewContext, VisualContext, WeakView, WindowBounds, + actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, + Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; use theme::ActiveTheme; @@ -49,6 +49,14 @@ use crate::face_pile::FacePile; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; +actions!( + ShareProject, + UnshareProject, + ToggleUserMenu, + ToggleProjectMenu, + SwitchBranch +); + // actions!( // collab, // [ @@ -204,7 +212,16 @@ impl Render for CollabTitlebarItem { "toggle_sharing", if is_shared { "Unshare" } else { "Share" }, ) - .style(ButtonStyle::Subtle), + .style(ButtonStyle::Subtle) + .on_click(cx.listener( + move |this, _, cx| { + if is_shared { + this.unshare_project(&Default::default(), cx); + } else { + this.share_project(&Default::default(), cx); + } + }, + )), ) .child( IconButton::new("leave-call", ui::Icon::Exit) @@ -451,46 +468,19 @@ impl CollabTitlebarItem { // render_project_owner -> resolve if you are in a room -> Option pub fn render_project_owner(&self, cx: &mut ViewContext) -> Option { - // TODO: We can't finish implementing this until project sharing works - // - [ ] Show the project owner when the project is remote (maybe done) - // - [x] Show the project owner when the project is local - // - [ ] Show the project owner with a lock icon when the project is local and unshared - - let remote_id = self.project.read(cx).remote_id(); - let is_local = remote_id.is_none(); - let is_shared = self.project.read(cx).is_shared(); - let (user_name, participant_index) = { - if let Some(host) = self.project.read(cx).host() { - debug_assert!(!is_local); - let (Some(host_user), Some(participant_index)) = ( - self.user_store.read(cx).get_cached_user(host.user_id), - self.user_store - .read(cx) - .participant_indices() - .get(&host.user_id), - ) else { - return None; - }; - (host_user.github_login.clone(), participant_index.0) - } else { - debug_assert!(is_local); - let name = self - .user_store - .read(cx) - .current_user() - .map(|user| user.github_login.clone())?; - (name, 0) - } - }; + let host = self.project.read(cx).host()?; + let host = self.user_store.read(cx).get_cached_user(host.user_id)?; + let participant_index = self + .user_store + .read(cx) + .participant_indices() + .get(&host.id)?; Some( div().border().border_color(gpui::red()).child( - Button::new( - "project_owner_trigger", - format!("{user_name} ({})", !is_shared), - ) - .color(Color::Player(participant_index)) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + Button::new("project_owner_trigger", host.github_login.clone()) + .color(Color::Player(participant_index.0)) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), ), ) } @@ -730,21 +720,21 @@ impl CollabTitlebarItem { cx.notify(); } - // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { - // let active_call = ActiveCall::global(cx); - // let project = self.project.clone(); - // active_call - // .update(cx, |call, cx| call.share_project(project, cx)) - // .detach_and_log_err(cx); - // } + fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { + let active_call = ActiveCall::global(cx); + let project = self.project.clone(); + active_call + .update(cx, |call, cx| call.share_project(project, cx)) + .detach_and_log_err(cx); + } - // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { - // let active_call = ActiveCall::global(cx); - // let project = self.project.clone(); - // active_call - // .update(cx, |call, cx| call.unshare_project(project, cx)) - // .log_err(); - // } + fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { + let active_call = ActiveCall::global(cx); + let project = self.project.clone(); + active_call + .update(cx, |call, cx| call.unshare_project(project, cx)) + .log_err(); + } // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { // self.user_menu.update(cx, |user_menu, cx| { From 9162f299a7100f8f07ef46a4848a9197bef413bb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 5 Dec 2023 00:31:14 +0000 Subject: [PATCH 17/48] Fix project panel context menu --- crates/gpui2/src/window.rs | 11 +++++++---- crates/ui2/src/components/context_menu.rs | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 09bc2c561836f399ca4325b563494bda13cdd00c..64e58ee7e4dd5aebe06a9bf0f05dd87745cedc62 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1497,10 +1497,13 @@ impl<'a> WindowContext<'a> { } pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { - self.window.current_frame.dispatch_tree.bindings_for_action( - action, - &self.window.current_frame.dispatch_tree.context_stack, - ) + self.window + .previous_frame + .dispatch_tree + .bindings_for_action( + action, + &self.window.previous_frame.dispatch_tree.context_stack, + ) } pub fn bindings_for_action_in( diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 27aa73b4fe35d38457301fad78a15cb4b0986b23..0d6a632db58f3d750bcd8e60cf6a9a92b5405468 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -24,6 +24,7 @@ pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, selected_index: Option, + delayed: bool, } impl FocusableView for ContextMenu { @@ -46,6 +47,7 @@ impl ContextMenu { items: Default::default(), focus_handle: cx.focus_handle(), selected_index: None, + delayed: false, }, cx, ) @@ -165,6 +167,7 @@ impl ContextMenu { } }) { self.selected_index = Some(ix); + self.delayed = true; cx.notify(); let action = dispatched.boxed_clone(); cx.spawn(|this, mut cx| async move { @@ -205,7 +208,7 @@ impl Render for ContextMenu { .on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::cancel)) - .map(|mut el| { + .when(!self.delayed, |mut el| { for item in self.items.iter() { if let ContextMenuItem::Entry { action: Some(action), From 959b2961ffdcb085aa414c6b10714f49aa46001a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2023 16:55:04 -0800 Subject: [PATCH 18/48] Revert "Decouple workspace from call (#3380)" This reverts commit 6da57cbc6e33a7d1ede533ecc11948292feb22f3, reversing changes made to 62b18437044c9e2b7e4ef2ba24fbbf12444a48c7. Also, adjust new code that was written using the "call handler". --- Cargo.lock | 4 - crates/call2/Cargo.toml | 4 +- crates/call2/src/call2.rs | 257 +------ crates/call2/src/participant.rs | 2 +- crates/collab2/src/tests/channel_tests.rs | 37 +- crates/collab2/src/tests/integration_tests.rs | 11 +- crates/collab2/src/tests/test_server.rs | 1 - crates/collab_ui2/src/collab_panel.rs | 32 +- crates/collab_ui2/src/collab_titlebar_item.rs | 128 ++-- crates/collab_ui2/src/collab_ui.rs | 115 ++-- crates/workspace2/Cargo.toml | 2 +- crates/workspace2/src/pane_group.rs | 4 + .../src/shared_screen.rs | 9 +- crates/workspace2/src/workspace2.rs | 647 ++++++++---------- crates/zed2/src/main.rs | 1 - 15 files changed, 440 insertions(+), 814 deletions(-) rename crates/{call2 => workspace2}/src/shared_screen.rs (94%) diff --git a/Cargo.lock b/Cargo.lock index 03945e457852910799290c7b290e0fb6101e000c..fe126973af05cc53d17cf5ae7b4bd5064da394ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,7 +1222,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", - "async-trait", "audio2", "client2", "collections", @@ -1242,9 +1241,7 @@ dependencies = [ "serde_json", "settings2", "smallvec", - "ui2", "util", - "workspace2", ] [[package]] @@ -11477,7 +11474,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", - "async-trait", "bincode", "call2", "client2", diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 8dc37f68dd7bd9b91c1d1fab240448eb25119cbf..c2d95c8b52b2705763c537e71285f521aea3148d 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,9 +31,7 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } -ui = {package = "ui2", path = "../ui2"} -workspace = {package = "workspace2", path = "../workspace2"} -async-trait.workspace = true + anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index a93305772312cab3624f995e0dd49751554867e1..14cb28c32d6b932c8db9f2fa54b5c0125bbf8011 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -1,32 +1,25 @@ pub mod call_settings; pub mod participant; pub mod room; -mod shared_screen; use anyhow::{anyhow, Result}; -use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto::{self, PeerId}, - Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, -}; +use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel, - Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, + WeakModel, }; -pub use participant::ParticipantLocation; use postage::watch; use project::Project; use room::Event; -pub use room::Room; use settings::Settings; -use shared_screen::SharedScreen; use std::sync::Arc; -use util::ResultExt; -use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; + +pub use participant::ParticipantLocation; +pub use room::Room; pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { CallSettings::register(cx); @@ -334,55 +327,12 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: u64, - requesting_window: Option>, cx: &mut ModelContext, ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return cx.spawn(|_, _| async move { - todo!(); - // let future = room.update(&mut cx, |room, cx| { - // room.most_active_project(cx).map(|(host, project)| { - // room.join_project(project, host, app_state.clone(), cx) - // }) - // }) - - // if let Some(future) = future { - // future.await?; - // } - - // Ok(Some(room)) - }); - } - - let should_prompt = room.update(cx, |room, _| { - room.channel_id().is_some() - && room.is_sharing_project() - && room.remote_participants().len() > 0 - }); - if should_prompt && requesting_window.is_some() { - return cx.spawn(|this, mut cx| async move { - let answer = requesting_window.unwrap().update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - ) - })?; - if answer.await? == 1 { - return Ok(None); - } - - room.update(&mut cx, |room, cx| room.clear_state(cx))?; - - this.update(&mut cx, |this, cx| { - this.join_channel(channel_id, requesting_window, cx) - })? - .await - }); - } - - if room.read(cx).channel_id().is_some() { + return Task::ready(Ok(Some(room))); + } else { room.update(cx, |room, cx| room.clear_state(cx)); } } @@ -555,197 +505,6 @@ pub fn report_call_event_for_channel( ) } -pub struct Call { - active_call: Option<(Model, Vec)>, -} - -impl Call { - pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box { - let mut active_call = None; - if cx.has_global::>() { - let call = cx.global::>().clone(); - let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); - } - Box::new(Self { active_call }) - } - fn on_active_call_event( - workspace: &mut Workspace, - _: Model, - event: &room::Event, - cx: &mut ViewContext, - ) { - match event { - room::Event::ParticipantLocationChanged { participant_id } - | room::Event::RemoteVideoTracksChanged { participant_id } => { - workspace.leader_updated(*participant_id, cx); - } - _ => {} - } - } -} - -#[async_trait(?Send)] -impl CallHandler for Call { - fn peer_state( - &mut self, - leader_id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(leader_id)?; - - let leader_in_this_app; - let leader_in_this_project; - match participant.location { - ParticipantLocation::SharedProject { project_id } => { - leader_in_this_app = true; - leader_in_this_project = Some(project_id) == project.read(cx).remote_id(); - } - ParticipantLocation::UnsharedProject => { - leader_in_this_app = true; - leader_in_this_project = false; - } - ParticipantLocation::External => { - leader_in_this_app = false; - leader_in_this_project = false; - } - }; - - Some((leader_in_this_project, leader_in_this_app)) - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(Box::new(item)); - } - } - - Some(Box::new(cx.build_view(|cx| { - SharedScreen::new(&track, peer_id, user.clone(), cx) - }))) - } - fn room_id(&self, cx: &AppContext) -> Option { - Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) - } - fn hang_up(&self, cx: &mut AppContext) -> Task> { - let Some((call, _)) = self.active_call.as_ref() else { - return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); - }; - - call.update(cx, |this, cx| this.hang_up(cx)) - } - fn active_project(&self, cx: &AppContext) -> Option> { - ActiveCall::global(cx).read(cx).location().cloned() - } - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task> { - ActiveCall::global(cx).update(cx, |this, cx| { - this.invite(called_user_id, initial_project, cx) - }) - } - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { - self.active_call - .as_ref() - .map(|call| { - call.0.read(cx).room().map(|room| { - room.read(cx) - .remote_participants() - .iter() - .map(|participant| { - (participant.1.user.clone(), participant.1.peer_id.clone()) - }) - .collect() - }) - }) - .flatten() - } - fn is_muted(&self, cx: &AppContext) -> Option { - self.active_call - .as_ref() - .map(|call| { - call.0 - .read(cx) - .room() - .map(|room| room.read(cx).is_muted(cx)) - }) - .flatten() - } - fn toggle_mute(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - let room = room.clone(); - cx.spawn(|_, mut cx| async move { - room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? - .await - }) - .detach_and_log_err(cx); - }) - }) - }); - } - fn toggle_screen_share(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - room.update(cx, |this, cx| { - if this.is_screen_sharing() { - this.unshare_screen(cx).log_err(); - } else { - let t = this.share_screen(cx); - cx.spawn(move |_, _| async move { - t.await.log_err(); - }) - .detach(); - } - }) - }) - }) - }); - } - fn toggle_deafen(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - room.update(cx, |this, cx| { - this.toggle_deafen(cx).log_err(); - }) - }) - }) - }); - } - fn is_deafened(&self, cx: &AppContext) -> Option { - self.active_call - .as_ref() - .map(|call| { - call.0 - .read(cx) - .room() - .map(|room| room.read(cx).is_deafened()) - }) - .flatten() - .flatten() - } -} - #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index 325a4f812b2f58c1b1bb0cc56f042e891df435d4..11a58b4b098cc6a255f8c1b061d76cf44c64684b 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -4,7 +4,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; pub use live_kit_client::Frame; -pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; diff --git a/crates/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs index 43d18ee7d13b850b634c67a0414831a64b455d5c..8ce5d99b80d3c630a81181e5f03f78d385186a10 100644 --- a/crates/collab2/src/tests/channel_tests.rs +++ b/crates/collab2/src/tests/channel_tests.rs @@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member( let active_call_b = cx_b.read(ActiveCall::global); assert!(active_call_b - .update(cx_b, |active_call, cx| active_call - .join_channel(sub_id, None, cx)) + .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx)) .await .is_ok()); } @@ -395,9 +394,7 @@ async fn test_channel_room( let active_call_b = cx_b.read(ActiveCall::global); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -445,9 +442,7 @@ async fn test_channel_room( }); active_call_b - .update(cx_b, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -564,16 +559,12 @@ async fn test_channel_room( }); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); active_call_b - .update(cx_b, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo let active_call_a = cx_a.read(ActiveCall::global); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo active_call_a .update(cx_a, |active_call, cx| { - active_call.join_channel(rust_id, None, cx) + active_call.join_channel(rust_id, cx) }) .await .unwrap(); @@ -804,7 +793,7 @@ async fn test_call_from_channel( let active_call_b = cx_b.read(ActiveCall::global); active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); @@ -1297,7 +1286,7 @@ async fn test_guest_access( // Non-members should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .is_err()); @@ -1319,7 +1308,7 @@ async fn test_guest_access( // Client B joins channel A as a guest active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .unwrap(); @@ -1352,7 +1341,7 @@ async fn test_guest_access( assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) .await .unwrap(); @@ -1383,7 +1372,7 @@ async fn test_invite_access( // should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .is_err()); @@ -1401,7 +1390,7 @@ async fn test_invite_access( .unwrap(); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .unwrap(); diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index 2268a51f2ba5f1671a02707101ecad8f65501d1c..7104d36b8de51e5cda88531c3a80ff7400c047b3 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( // Simultaneously join channel 1 and then channel 2 active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) .detach(); - let join_channel_2 = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx)); + let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); join_channel_2.await.unwrap(); @@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( call.invite(client_c.user_id().unwrap(), None, cx) }); - let join_channel = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); b_invite.await.unwrap(); c_invite.await.unwrap(); @@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( .unwrap(); // Simultaneously join channel 1 and call user B and user C from client A. - let join_channel = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); let b_invite = active_call_a.update(cx_a, |call, cx| { call.invite(client_b.user_id().unwrap(), None, cx) diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 5f95f00d6fcd5c74d81d90f9b4b455ab531862d5..6bb57e11ab1d582031930f34b8bfe67b96a2581e 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -221,7 +221,6 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 03637a051f9510e6b4de5bddee0fc305098514b4..9144298897561a449e7095a97d94fad0beaa43b3 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1199,7 +1199,6 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.workspace.update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); - let call = workspace.call_state(); workspace::join_remote_project(project_id, host_user_id, app_state, cx) .detach_and_log_err(cx); }); @@ -2219,20 +2218,19 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; let Some(handle) = cx.window_handle().downcast::() else { return; }; - let active_call = ActiveCall::global(cx); - cx.spawn(|_, mut cx| async move { - active_call - .update(&mut cx, |active_call, cx| { - active_call.join_channel(channel_id, Some(handle), cx) - }) - .log_err()? - .await - .notify_async_err(&mut cx) - }) - .detach() + workspace::join_channel( + channel_id, + workspace.read(cx).app_state().clone(), + Some(handle), + cx, + ) + .detach_and_log_err(cx) } fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { @@ -2500,15 +2498,7 @@ impl CollabPanel { let user_id = contact.user.id; let github_login = SharedString::from(contact.user.github_login.clone()); let mut item = ListItem::new(github_login.clone()) - .on_click(cx.listener(move |this, _, cx| { - this.workspace - .update(cx, |this, cx| { - this.call_state() - .invite(user_id, None, cx) - .detach_and_log_err(cx) - }) - .log_err(); - })) + .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) .child( h_stack() .w_full() diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 3a0f0093bb416386e5c0f283a67cdf3ca097d9d9..7e5354c6015bf9764380d7637377fa2b91482da1 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -99,37 +99,23 @@ impl Render for CollabTitlebarItem { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let is_in_room = self - .workspace - .update(cx, |this, cx| this.call_state().is_in_room(cx)) - .unwrap_or_default(); + let room = ActiveCall::global(cx).read(cx).room(); + let is_in_room = room.is_some(); let is_shared = is_in_room && self.project.read(cx).is_shared(); let current_user = self.user_store.read(cx).current_user(); let client = self.client.clone(); - let users = self - .workspace - .update(cx, |this, cx| this.call_state().remote_participants(cx)) - .log_err() - .flatten(); - let is_muted = self - .workspace - .update(cx, |this, cx| this.call_state().is_muted(cx)) - .log_err() - .flatten() - .unwrap_or_default(); - let is_deafened = self - .workspace - .update(cx, |this, cx| this.call_state().is_deafened(cx)) - .log_err() - .flatten() - .unwrap_or_default(); - let speakers_icon = if self - .workspace - .update(cx, |this, cx| this.call_state().is_deafened(cx)) - .log_err() - .flatten() - .unwrap_or_default() - { + let remote_participants = room.map(|room| { + room.read(cx) + .remote_participants() + .values() + .map(|participant| (participant.user.clone(), participant.peer_id)) + .collect::>() + }); + let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx)); + let is_deafened = room + .and_then(|room| room.read(cx).is_deafened()) + .unwrap_or(false); + let speakers_icon = if is_deafened { ui::Icon::AudioOff } else { ui::Icon::AudioOn @@ -165,7 +151,7 @@ impl Render for CollabTitlebarItem { .children(self.render_project_branch(cx)), ) .when_some( - users.zip(current_user.clone()), + remote_participants.zip(current_user.clone()), |this, (remote_participants, current_user)| { let mut pile = FacePile::default(); pile.extend( @@ -176,25 +162,30 @@ impl Render for CollabTitlebarItem { div().child(Avatar::data(avatar.clone())).into_any_element() }) .into_iter() - .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { - user.avatar.as_ref().map(|avatar| { - div() - .child( - Avatar::data(avatar.clone()).into_element().into_any(), - ) - .on_mouse_down(MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.open_shared_screen(peer_id, cx); - }) - .log_err(); - } - }) - .into_any_element() - }) - })), + .chain(remote_participants.into_iter().filter_map( + |(user, peer_id)| { + let avatar = user.avatar.as_ref()?; + Some( + div() + .child( + Avatar::data(avatar.clone()) + .into_element() + .into_any(), + ) + .on_mouse_down(MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.open_shared_screen(peer_id, cx); + }) + .log_err(); + } + }) + .into_any_element(), + ) + }, + )), ); this.child(pile.render(cx)) }, @@ -226,15 +217,10 @@ impl Render for CollabTitlebarItem { .child( IconButton::new("leave-call", ui::Icon::Exit) .style(ButtonStyle::Subtle) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().hang_up(cx).detach(); - }) - .log_err(); - } + .on_click(move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); }), ), ) @@ -252,15 +238,8 @@ impl Render for CollabTitlebarItem { ) .style(ButtonStyle::Subtle) .selected(is_muted) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_mute(cx); - }) - .log_err(); - } + .on_click(move |_, cx| { + crate::toggle_mute(&Default::default(), cx) }), ) .child( @@ -275,26 +254,15 @@ impl Render for CollabTitlebarItem { cx, ) }) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_deafen(cx); - }) - .log_err(); - } + .on_click(move |_, cx| { + crate::toggle_mute(&Default::default(), cx) }), ) .child( IconButton::new("screen-share", ui::Icon::Screen) .style(ButtonStyle::Subtle) .on_click(move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_screen_share(cx); - }) - .log_err(); + crate::toggle_screen_sharing(&Default::default(), cx) }), ) .pl_2(), diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index 57a33c6790868bcd97a597da5a68a2608d0a684a..efd3ff869225aced36002a3bdb4f1f5905579c5a 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -9,22 +9,21 @@ mod panel_settings; use std::{rc::Rc, sync::Arc}; +use call::{report_call_event_for_room, ActiveCall, Room}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::{ - point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, - WindowOptions, + actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds, + WindowKind, WindowOptions, }; pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; use settings::Settings; +use util::ResultExt; use workspace::AppState; -// actions!( -// collab, -// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] -// ); +actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall); pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); @@ -42,61 +41,61 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { // cx.add_global_action(toggle_deafen); } -// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { -// let call = ActiveCall::global(cx).read(cx); -// if let Some(room) = call.room().cloned() { -// let client = call.client(); -// let toggle_screen_sharing = room.update(cx, |room, cx| { -// if room.is_screen_sharing() { -// report_call_event_for_room( -// "disable screen share", -// room.id(), -// room.channel_id(), -// &client, -// cx, -// ); -// Task::ready(room.unshare_screen(cx)) -// } else { -// report_call_event_for_room( -// "enable screen share", -// room.id(), -// room.channel_id(), -// &client, -// cx, -// ); -// room.share_screen(cx) -// } -// }); -// toggle_screen_sharing.detach_and_log_err(cx); -// } -// } +pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { + let call = ActiveCall::global(cx).read(cx); + if let Some(room) = call.room().cloned() { + let client = call.client(); + let toggle_screen_sharing = room.update(cx, |room, cx| { + if room.is_screen_sharing() { + report_call_event_for_room( + "disable screen share", + room.id(), + room.channel_id(), + &client, + cx, + ); + Task::ready(room.unshare_screen(cx)) + } else { + report_call_event_for_room( + "enable screen share", + room.id(), + room.channel_id(), + &client, + cx, + ); + room.share_screen(cx) + } + }); + toggle_screen_sharing.detach_and_log_err(cx); + } +} -// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { -// let call = ActiveCall::global(cx).read(cx); -// if let Some(room) = call.room().cloned() { -// let client = call.client(); -// room.update(cx, |room, cx| { -// let operation = if room.is_muted(cx) { -// "enable microphone" -// } else { -// "disable microphone" -// }; -// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); +pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { + let call = ActiveCall::global(cx).read(cx); + if let Some(room) = call.room().cloned() { + let client = call.client(); + room.update(cx, |room, cx| { + let operation = if room.is_muted(cx) { + "enable microphone" + } else { + "disable microphone" + }; + report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); -// room.toggle_mute(cx) -// }) -// .map(|task| task.detach_and_log_err(cx)) -// .log_err(); -// } -// } + room.toggle_mute(cx) + }) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} -// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { -// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { -// room.update(cx, Room::toggle_deafen) -// .map(|task| task.detach_and_log_err(cx)) -// .log_err(); -// } -// } +pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + room.update(cx, Room::toggle_deafen) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} fn notification_window_options( screen: Rc, diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml index 2dd7c6468eed0655fc082feaae89674db6f88bc5..a06ac6e3e09f4634795665c4c35f569ed70ff8d0 100644 --- a/crates/workspace2/Cargo.toml +++ b/crates/workspace2/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] db = { path = "../db2", package = "db2" } +call = { path = "../call2", package = "call2" } client = { path = "../client2", package = "client2" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } @@ -36,7 +37,6 @@ theme = { path = "../theme2", package = "theme2" } util = { path = "../util" } ui = { package = "ui2", path = "../ui2" } -async-trait.workspace = true async-recursion = "1.0.0" itertools = "0.10" bincode = "1.2.1" diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index c98fac00c6773a85955b05c2114a2c7b59e6cec9..4d5d582e13b730a3bc4030ab0d792f7eaaa6f2af 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,5 +1,6 @@ use crate::{AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, bail, Result}; +use call::ActiveCall; use collections::HashMap; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -126,6 +127,7 @@ impl PaneGroup { &self, project: &Model, follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -135,6 +137,7 @@ impl PaneGroup { project, 0, follower_states, + active_call, active_pane, zoomed, app_state, @@ -196,6 +199,7 @@ impl Member { project: &Model, basis: usize, follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, diff --git a/crates/call2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs similarity index 94% rename from crates/call2/src/shared_screen.rs rename to crates/workspace2/src/shared_screen.rs index c38ebeac021d59c810fc27ff528ddc773f9642f4..c4bcb31958afcaf3e69b37ea116df7baa9a91f41 100644 --- a/crates/call2/src/shared_screen.rs +++ b/crates/workspace2/src/shared_screen.rs @@ -1,5 +1,9 @@ -use crate::participant::{Frame, RemoteVideoTrack}; +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; use client::{proto::PeerId, User}; use futures::StreamExt; use gpui::{ @@ -9,7 +13,6 @@ use gpui::{ }; use std::sync::{Arc, Weak}; use ui::{h_stack, Icon, IconElement}; -use workspace::{item::Item, ItemNavHistory, WorkspaceId}; pub enum Event { Close, @@ -56,7 +59,7 @@ impl SharedScreen { } impl EventEmitter for SharedScreen {} -impl EventEmitter for SharedScreen {} +impl EventEmitter for SharedScreen {} impl FocusableView for SharedScreen { fn focus_handle(&self, _: &AppContext) -> FocusHandle { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 66eea706709169c2a88eccfccf9a0a81ed274f38..ea796274bb6ebb64597da20d5a5d50b8cc3f913c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -10,15 +10,16 @@ mod persistence; pub mod searchable; // todo!() mod modal_layer; +mod shared_screen; mod status_bar; mod toolbar; mod workspace_settings; use anyhow::{anyhow, Context as _, Result}; -use async_trait::async_trait; +use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, User, UserStore, + Client, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; @@ -28,11 +29,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, - ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext, + actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext, + AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, + FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, + ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; @@ -52,6 +53,7 @@ use postage::stream::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; use settings::Settings; +use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ @@ -209,6 +211,7 @@ 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| { @@ -302,7 +305,6 @@ pub struct AppState { pub user_store: Model, pub workspace_store: Model, pub fs: Arc, - pub call_factory: CallFactory, pub build_window_options: fn(Option, Option, &mut AppContext) -> WindowOptions, pub node_runtime: Arc, @@ -321,69 +323,6 @@ struct Follower { peer_id: PeerId, } -#[cfg(any(test, feature = "test-support"))] -pub struct TestCallHandler; - -#[cfg(any(test, feature = "test-support"))] -impl CallHandler for TestCallHandler { - fn peer_state( - &mut self, - id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)> { - None - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option> { - None - } - - fn room_id(&self, cx: &AppContext) -> Option { - None - } - - fn hang_up(&self, cx: &mut AppContext) -> Task> { - Task::ready(Err(anyhow!("TestCallHandler should not be hanging up"))) - } - - fn active_project(&self, cx: &AppContext) -> Option> { - None - } - - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task> { - unimplemented!() - } - - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { - None - } - - fn is_muted(&self, cx: &AppContext) -> Option { - None - } - - fn toggle_mute(&self, cx: &mut AppContext) {} - - fn toggle_screen_share(&self, cx: &mut AppContext) {} - - fn toggle_deafen(&self, cx: &mut AppContext) {} - - fn is_deafened(&self, cx: &AppContext) -> Option { - None - } -} - impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { @@ -414,7 +353,6 @@ impl AppState { workspace_store, node_runtime: FakeNodeRuntime::new(), build_window_options: |_, _, _| Default::default(), - call_factory: |_| Box::new(TestCallHandler), }) } } @@ -471,40 +409,6 @@ pub enum Event { WorkspaceCreated(WeakView), } -#[async_trait(?Send)] -pub trait CallHandler { - fn peer_state( - &mut self, - id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)>; - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option>; - fn room_id(&self, cx: &AppContext) -> Option; - fn is_in_room(&self, cx: &mut ViewContext) -> bool { - self.room_id(cx).is_some() - } - fn hang_up(&self, cx: &mut AppContext) -> Task>; - fn active_project(&self, cx: &AppContext) -> Option>; - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task>; - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>>; - fn is_muted(&self, cx: &AppContext) -> Option; - fn is_deafened(&self, cx: &AppContext) -> Option; - fn toggle_mute(&self, cx: &mut AppContext); - fn toggle_deafen(&self, cx: &mut AppContext); - fn toggle_screen_share(&self, cx: &mut AppContext); -} - pub struct Workspace { window_self: WindowHandle, weak_self: WeakView, @@ -525,10 +429,10 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, - call_handler: Box, follower_states: HashMap, FollowerState>, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, + active_call: Option<(Model, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, @@ -556,7 +460,6 @@ struct FollowerState { enum WorkspaceBounds {} -type CallFactory = fn(&mut ViewContext) -> Box; impl Workspace { pub fn new( workspace_id: WorkspaceId, @@ -648,19 +551,9 @@ impl Workspace { mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); let _apply_leader_updates = cx.spawn(|this, mut cx| async move { while let Some((leader_id, update)) = leader_updates_rx.next().await { - let mut cx2 = cx.clone(); - let t = this.clone(); - - Workspace::process_leader_update(&this, leader_id, update, &mut cx) + Self::process_leader_update(&this, leader_id, update, &mut cx) .await .log_err(); - - // this.update(&mut cx, |this, cxx| { - // this.call_handler - // .process_leader_update(leader_id, update, cx2) - // })? - // .await - // .log_err(); } Ok(()) @@ -693,6 +586,14 @@ impl Workspace { // drag_and_drop.register_container(weak_handle.clone()); // }); + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); + active_call = Some((call, subscriptions)); + } + let subscriptions = vec![ cx.observe_window_activation(Self::on_window_activation_changed), cx.observe_window_bounds(move |_, cx| { @@ -769,8 +670,7 @@ impl Workspace { follower_states: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, - - call_handler: (app_state.call_factory)(cx), + active_call, database_id: workspace_id, app_state, _observe_current_user, @@ -1217,7 +1117,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { //todo!(saveing) - + let active_call = self.active_call().cloned(); let window = cx.window_handle(); cx.spawn(|this, mut cx| async move { @@ -1228,27 +1128,27 @@ impl Workspace { .count() })?; - if !quitting - && workspace_count == 1 - && this - .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx)) - .log_err() - .unwrap_or_default() - { - let answer = window.update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - &["Close window and hang up", "Cancel"], - ) - })?; + if let Some(active_call) = active_call { + if !quitting + && workspace_count == 1 + && active_call.read_with(&cx, |call, _| call.room().is_some())? + { + let answer = window.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))? - .await - .log_err(); + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(&mut cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } } } @@ -2032,7 +1932,7 @@ impl Workspace { pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { self.active_pane.update(cx, |pane, cx| { - pane.add_item(shared_screen, false, true, None, cx) + pane.add_item(Box::new(shared_screen), false, true, None, cx) }); } } @@ -2510,19 +2410,19 @@ impl Workspace { // } pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { - let follower_states = &mut self.follower_states; - let state = follower_states.remove(pane)?; + let state = self.follower_states.remove(pane)?; let leader_id = state.leader_id; for (_, item) in state.items_by_leader_view_id { item.set_leader_peer_id(None, cx); } - if follower_states + if self + .follower_states .values() .all(|state| state.leader_id != state.leader_id) { let project_id = self.project.read(cx).remote_id(); - let room_id = self.call_handler.room_id(cx)?; + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); self.app_state .client .send(proto::Unfollow { @@ -2878,9 +2778,8 @@ impl Workspace { } else { None }; - let room_id = self.call_handler.room_id(cx)?; self.app_state().workspace_store.update(cx, |store, cx| { - store.update_followers(project_id, room_id, update, cx) + store.update_followers(project_id, update, cx) }) } @@ -2888,12 +2787,31 @@ impl Workspace { self.follower_states.get(pane).map(|state| state.leader_id) } - pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); - let (leader_in_this_project, leader_in_this_app) = - self.call_handler.peer_state(leader_id, &self.project, cx)?; + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(leader_id)?; let mut items_to_activate = Vec::new(); + + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + call::ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); + } + call::ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + call::ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; + for (pane, state) in &self.follower_states { if state.leader_id != leader_id { continue; @@ -2914,7 +2832,7 @@ impl Workspace { } if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_activate.push((pane.clone(), shared_screen)); + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } @@ -2923,8 +2841,8 @@ impl Workspace { if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); } else { - pane.update(cx, |pane, mut cx| { - pane.add_item(item.boxed_clone(), false, false, None, &mut cx) + pane.update(cx, |pane, cx| { + pane.add_item(item.boxed_clone(), false, false, None, cx) }); } @@ -2941,21 +2859,20 @@ impl Workspace { peer_id: PeerId, pane: &View, cx: &mut ViewContext, - ) -> Option> { - self.call_handler.shared_screen_for_peer(peer_id, pane, cx) - // let call = self.active_call()?; - // let room = call.read(cx).room()?.read(cx); - // let participant = room.remote_participant_for_peer_id(peer_id)?; - // let track = participant.video_tracks.values().next()?.clone(); - // let user = participant.user.clone(); - - // for item in pane.read(cx).items_of_type::() { - // if item.read(cx).peer_id == peer_id { - // return Some(item); - // } - // } + ) -> Option> { + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(item); + } + } - // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) } pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { @@ -2984,6 +2901,25 @@ impl Workspace { } } + fn active_call(&self) -> Option<&Model> { + self.active_call.as_ref().map(|(call, _)| call) + } + + fn on_active_call_event( + &mut self, + _: Model, + event: &call::room::Event, + cx: &mut ViewContext, + ) { + match event { + call::room::Event::ParticipantLocationChanged { participant_id } + | call::room::Event::RemoteVideoTracksChanged { participant_id } => { + self.leader_updated(*participant_id, cx); + } + _ => {} + } + } + pub fn database_id(&self) -> WorkspaceId { self.database_id } @@ -3393,7 +3329,6 @@ impl Workspace { fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_| Box::new(TestCallHandler), }); let workspace = Self::new(0, project, app_state, cx); workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); @@ -3472,10 +3407,6 @@ impl Workspace { self.modal_layer .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) } - - pub fn call_state(&mut self) -> &mut dyn CallHandler { - &mut *self.call_handler - } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { @@ -3676,6 +3607,7 @@ impl Render for Workspace { .child(self.center.render( &self.project, &self.follower_states, + self.active_call(), &self.active_pane, self.zoomed.as_ref(), &self.app_state, @@ -3846,10 +3778,14 @@ impl WorkspaceStore { pub fn update_followers( &self, project_id: Option, - room_id: u64, update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { + if !cx.has_global::>() { + return None; + } + + let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); let follower_ids: Vec<_> = self .followers .iter() @@ -3885,17 +3821,9 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; + let active_project = ActiveCall::global(cx).read(cx).location().cloned(); + let mut response = proto::FollowResponse::default(); - let active_project = this - .workspaces - .iter() - .next() - .and_then(|workspace| { - workspace - .read_with(cx, |this, cx| this.call_handler.active_project(cx)) - .log_err() - }) - .flatten(); for workspace in &this.workspaces { workspace .update(cx, |workspace, cx| { @@ -4048,187 +3976,184 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } -// async fn join_channel_internal( -// channel_id: u64, -// app_state: &Arc, -// requesting_window: Option>, -// active_call: &ModelHandle, -// cx: &mut AsyncAppContext, -// ) -> Result { -// let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { -// let Some(room) = active_call.room().map(|room| room.read(cx)) else { -// return (false, None); -// }; - -// let already_in_channel = room.channel_id() == Some(channel_id); -// let should_prompt = room.is_sharing_project() -// && room.remote_participants().len() > 0 -// && !already_in_channel; -// let open_room = if already_in_channel { -// active_call.room().cloned() -// } else { -// None -// }; -// (should_prompt, open_room) -// }); - -// if let Some(room) = open_room { -// let task = room.update(cx, |room, cx| { -// if let Some((project, host)) = room.most_active_project(cx) { -// return Some(join_remote_project(project, host, app_state.clone(), cx)); -// } - -// None -// }); -// if let Some(task) = task { -// task.await?; -// } -// return anyhow::Ok(true); -// } +async fn join_channel_internal( + channel_id: u64, + app_state: &Arc, + requesting_window: Option>, + active_call: &Model, + cx: &mut AsyncAppContext, +) -> Result { + let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { + let Some(room) = active_call.room().map(|room| room.read(cx)) else { + return (false, None); + }; -// if should_prompt { -// if let Some(workspace) = requesting_window { -// if let Some(window) = workspace.update(cx, |cx| cx.window()) { -// let answer = window.prompt( -// PromptLevel::Warning, -// "Leaving this call will unshare your current project.\nDo you want to switch channels?", -// &["Yes, Join Channel", "Cancel"], -// cx, -// ); - -// if let Some(mut answer) = answer { -// if answer.next().await == Some(1) { -// return Ok(false); -// } -// } -// } else { -// return Ok(false); // unreachable!() hopefully -// } -// } else { -// return Ok(false); // unreachable!() hopefully -// } -// } + let already_in_channel = room.channel_id() == Some(channel_id); + let should_prompt = room.is_sharing_project() + && room.remote_participants().len() > 0 + && !already_in_channel; + let open_room = if already_in_channel { + active_call.room().cloned() + } else { + None + }; + (should_prompt, open_room) + })?; -// let client = cx.read(|cx| active_call.read(cx).client()); - -// let mut client_status = client.status(); - -// // this loop will terminate within client::CONNECTION_TIMEOUT seconds. -// 'outer: loop { -// let Some(status) = client_status.recv().await else { -// return Err(anyhow!("error connecting")); -// }; - -// match status { -// Status::Connecting -// | Status::Authenticating -// | Status::Reconnecting -// | Status::Reauthenticating => continue, -// Status::Connected { .. } => break 'outer, -// Status::SignedOut => return Err(anyhow!("not signed in")), -// Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), -// Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { -// return Err(anyhow!("zed is offline")) -// } -// } -// } + if let Some(room) = open_room { + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } -// let room = active_call -// .update(cx, |active_call, cx| { -// active_call.join_channel(channel_id, cx) -// }) -// .await?; - -// room.update(cx, |room, _| room.room_update_completed()) -// .await; - -// let task = room.update(cx, |room, cx| { -// if let Some((project, host)) = room.most_active_project(cx) { -// return Some(join_remote_project(project, host, app_state.clone(), cx)); -// } - -// None -// }); -// if let Some(task) = task { -// task.await?; -// return anyhow::Ok(true); -// } -// anyhow::Ok(false) -// } + None + })?; + if let Some(task) = task { + task.await?; + } + return anyhow::Ok(true); + } -// pub fn join_channel( -// channel_id: u64, -// app_state: Arc, -// requesting_window: Option>, -// cx: &mut AppContext, -// ) -> Task> { -// let active_call = ActiveCall::global(cx); -// cx.spawn(|mut cx| async move { -// let result = join_channel_internal( -// channel_id, -// &app_state, -// requesting_window, -// &active_call, -// &mut cx, -// ) -// .await; - -// // join channel succeeded, and opened a window -// if matches!(result, Ok(true)) { -// return anyhow::Ok(()); -// } - -// if requesting_window.is_some() { -// return anyhow::Ok(()); -// } - -// // find an existing workspace to focus and show call controls -// let mut active_window = activate_any_workspace_window(&mut cx); -// if active_window.is_none() { -// // no open workspaces, make one to show the error in (blergh) -// cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) -// .await; -// } - -// active_window = activate_any_workspace_window(&mut cx); -// if active_window.is_none() { -// return result.map(|_| ()); // unreachable!() assuming new_local always opens a window -// } - -// if let Err(err) = result { -// let prompt = active_window.unwrap().prompt( -// PromptLevel::Critical, -// &format!("Failed to join channel: {}", err), -// &["Ok"], -// &mut cx, -// ); -// if let Some(mut prompt) = prompt { -// prompt.next().await; -// } else { -// return Err(err); -// } -// } - -// // return ok, we showed the error to the user. -// return anyhow::Ok(()); -// }) -// } + if should_prompt { + if let Some(workspace) = requesting_window { + let answer = workspace.update(cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + ) + })?.await; -// pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { -// for window in cx.windows() { -// let found = window.update(cx, |cx| { -// let is_workspace = cx.root_view().clone().downcast::().is_some(); -// if is_workspace { -// cx.activate_window(); -// } -// is_workspace -// }); -// if found == Some(true) { -// return Some(window); -// } -// } -// None -// } + if answer == Ok(1) { + return Ok(false); + } + } else { + return Ok(false); // unreachable!() hopefully + } + } + + let client = cx.update(|cx| active_call.read(cx).client())?; + + let mut client_status = client.status(); + + // this loop will terminate within client::CONNECTION_TIMEOUT seconds. + 'outer: loop { + let Some(status) = client_status.recv().await else { + return Err(anyhow!("error connecting")); + }; + + match status { + Status::Connecting + | Status::Authenticating + | Status::Reconnecting + | Status::Reauthenticating => continue, + Status::Connected { .. } => break 'outer, + Status::SignedOut => return Err(anyhow!("not signed in")), + Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), + Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { + return Err(anyhow!("zed is offline")) + } + } + } + + let room = active_call + .update(cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + })? + .await?; + + let Some(room) = room else { + return anyhow::Ok(true); + }; + + room.update(cx, |room, _| room.room_update_completed())? + .await; + + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + })?; + if let Some(task) = task { + task.await?; + return anyhow::Ok(true); + } + anyhow::Ok(false) +} + +pub fn join_channel( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + cx: &mut AppContext, +) -> Task> { + let active_call = ActiveCall::global(cx); + cx.spawn(|mut cx| async move { + let result = join_channel_internal( + channel_id, + &app_state, + requesting_window, + &active_call, + &mut cx, + ) + .await; + + // join channel succeeded, and opened a window + if matches!(result, Ok(true)) { + return anyhow::Ok(()); + } + + if requesting_window.is_some() { + return anyhow::Ok(()); + } + + // find an existing workspace to focus and show call controls + let mut active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + // no open workspaces, make one to show the error in (blergh) + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))? + .await?; + } + + active_window = activate_any_workspace_window(&mut cx); + let Some(active_window) = active_window else { + return anyhow::Ok(()); + }; + + if let Err(err) = result { + active_window + .update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + ) + })? + .await + .ok(); + } + + // return ok, we showed the error to the user. + return anyhow::Ok(()); + }) +} + +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { + cx.update(|cx| { + for window in cx.windows() { + let is_workspace = window.downcast::().is_some(); + if is_workspace { + window.update(cx, |_, cx| cx.activate_window()).ok(); + return Some(window); + } + } + None + }) + .ok() + .flatten() +} #[allow(clippy::type_complexity)] pub fn open_paths( diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 4c7e914e37be6b96d5c241bc4eba0c354fe2eb15..5b641acfa0d4b78c46a25300d3db41767891fa25 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -191,7 +191,6 @@ fn main() { user_store: user_store.clone(), fs, build_window_options, - call_factory: call::Call::new, workspace_store, node_runtime, }); From 71a1125e88422a6a6c5be3107d2339fb0de0e54e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2023 17:06:08 -0800 Subject: [PATCH 19/48] Allow joining remote projects in zed2 Co-authored-by: Nathan --- crates/workspace2/src/workspace2.rs | 159 +++++++++++++--------------- 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index ea796274bb6ebb64597da20d5a5d50b8cc3f913c..3a9508c5bf87051bf22b3343dbbeaf854fdba61c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -4245,95 +4245,82 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { - todo!() - // let windows = cx.windows(); - // cx.spawn(|mut cx| async move { - // let existing_workspace = windows.into_iter().find_map(|window| { - // window.downcast::().and_then(|window| { - // window - // .update(&mut cx, |workspace, cx| { - // if workspace.project().read(cx).remote_id() == Some(project_id) { - // Some(cx.view().downgrade()) - // } else { - // None - // } - // }) - // .unwrap_or(None) - // }) - // }); + let windows = cx.windows(); + cx.spawn(|mut cx| async move { + let existing_workspace = windows.into_iter().find_map(|window| { + window.downcast::().and_then(|window| { + window + .update(&mut cx, |workspace, cx| { + if workspace.project().read(cx).remote_id() == Some(project_id) { + Some(window) + } else { + None + } + }) + .unwrap_or(None) + }) + }); - // let workspace = if let Some(existing_workspace) = existing_workspace { - // existing_workspace - // } else { - // let active_call = cx.update(ActiveCall::global); - // let room = active_call - // .read_with(&cx, |call, _| call.room().cloned()) - // .ok_or_else(|| anyhow!("not in a call"))?; - // let project = room - // .update(&mut cx, |room, cx| { - // room.join_project( - // project_id, - // app_state.languages.clone(), - // app_state.fs.clone(), - // cx, - // ) - // }) - // .await?; + let workspace = if let Some(existing_workspace) = existing_workspace { + existing_workspace + } else { + let active_call = cx.update(|cx| ActiveCall::global(cx))?; + let room = active_call + .read_with(&cx, |call, _| call.room().cloned())? + .ok_or_else(|| anyhow!("not in a call"))?; + let project = room + .update(&mut cx, |room, cx| { + room.join_project( + project_id, + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ) + })? + .await?; - // let window_bounds_override = window_bounds_env_override(&cx); - // let window = cx.add_window( - // (app_state.build_window_options)( - // window_bounds_override, - // None, - // cx.platform().as_ref(), - // ), - // |cx| Workspace::new(0, project, app_state.clone(), cx), - // ); - // let workspace = window.root(&cx).unwrap(); - // (app_state.initialize_workspace)( - // workspace.downgrade(), - // false, - // app_state.clone(), - // cx.clone(), - // ) - // .await - // .log_err(); - - // workspace.downgrade() - // }; - - // workspace.window().activate(&mut cx); - // cx.platform().activate(true); - - // workspace.update(&mut cx, |workspace, cx| { - // if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - // let follow_peer_id = room - // .read(cx) - // .remote_participants() - // .iter() - // .find(|(_, participant)| participant.user.id == follow_user_id) - // .map(|(_, p)| p.peer_id) - // .or_else(|| { - // // If we couldn't follow the given user, follow the host instead. - // let collaborator = workspace - // .project() - // .read(cx) - // .collaborators() - // .values() - // .find(|collaborator| collaborator.replica_id == 0)?; - // Some(collaborator.peer_id) - // }); - - // if let Some(follow_peer_id) = follow_peer_id { - // workspace - // .follow(follow_peer_id, cx) - // .map(|follow| follow.detach_and_log_err(cx)); - // } - // } - // })?; + let window_bounds_override = window_bounds_env_override(&cx); + cx.update(|cx| { + let options = (app_state.build_window_options)(window_bounds_override, None, cx); + cx.open_window(options, |cx| { + cx.build_view(|cx| Workspace::new(0, project, app_state.clone(), cx)) + }) + })? + }; + + workspace.update(&mut cx, |workspace, cx| { + cx.activate(true); + cx.activate_window(); - // anyhow::Ok(()) - // }) + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let follow_peer_id = room + .read(cx) + .remote_participants() + .iter() + .find(|(_, participant)| participant.user.id == follow_user_id) + .map(|(_, p)| p.peer_id) + .or_else(|| { + // If we couldn't follow the given user, follow the host instead. + let collaborator = workspace + .project() + .read(cx) + .collaborators() + .values() + .find(|collaborator| collaborator.replica_id == 0)?; + Some(collaborator.peer_id) + }); + + // todo!("uncomment following") + // if let Some(follow_peer_id) = follow_peer_id { + // workspace + // .follow(follow_peer_id, cx) + // .map(|follow| follow.detach_and_log_err(cx)); + // } + } + })?; + + anyhow::Ok(()) + }) } pub fn restart(_: &Restart, cx: &mut AppContext) { From 591dc9d82a4cd6b0835e2da9e5da3e782602d4cf Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 20:13:52 -0500 Subject: [PATCH 20/48] 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 eff3a72fb568f8709eaa7a1e1d90706d4cba63ea Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Dec 2023 17:51:53 -0800 Subject: [PATCH 21/48] Start work on following in zed2 Co-authored-by: Nathan --- crates/collab_ui2/src/collab_panel.rs | 7 +- crates/gpui2/src/window.rs | 7 + crates/workspace2/src/pane_group.rs | 103 +++++++-- crates/workspace2/src/workspace2.rs | 303 +++++++++++++------------- 4 files changed, 249 insertions(+), 171 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 9144298897561a449e7095a97d94fad0beaa43b3..ba740dddac17994f6e8612548373c251b287ab6f 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1165,12 +1165,11 @@ impl CollabPanel { div().into_any_element() }), ) - .when(!is_current_user, |this| { + .when_some(peer_id, |this, peer_id| { this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { - this.workspace.update(cx, |workspace, cx| { - // workspace.follow(peer_id, cx) - }); + this.workspace + .update(cx, |workspace, cx| workspace.follow(peer_id, cx)); })) }) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 5724f1e0701a2b960afb478fad0186649c29debd..b59f970793e91f88d2339651f21cd0582c826f9c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2708,6 +2708,7 @@ pub enum ElementId { Integer(usize), Name(SharedString), FocusHandle(FocusId), + NamedInteger(SharedString, usize), } impl ElementId { @@ -2757,3 +2758,9 @@ impl<'a> From<&'a FocusHandle> for ElementId { ElementId::FocusHandle(handle.id) } } + +impl From<(&'static str, EntityId)> for ElementId { + fn from((name, id): (&'static str, EntityId)) -> Self { + ElementId::NamedInteger(name.into(), id.as_u64() as usize) + } +} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index 4d5d582e13b730a3bc4030ab0d792f7eaaa6f2af..66465a4982895e3ed22b8b76d288464b1e54e100 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,19 +1,20 @@ use crate::{AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, bail, Result}; -use call::ActiveCall; +use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{ - point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext, + point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View, + ViewContext, }; use parking_lot::Mutex; use project::Project; use serde::Deserialize; use std::sync::Arc; -use ui::prelude::*; +use ui::{prelude::*, Button}; const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; @@ -207,19 +208,89 @@ impl Member { ) -> impl IntoElement { match self { Member::Pane(pane) => { - // todo!() - // let pane_element = if Some(pane.into()) == zoomed { - // None - // } else { - // Some(pane) - // }; - - div().size_full().child(pane.clone()).into_any() - - // Stack::new() - // .with_child(pane_element.contained().with_border(leader_border)) - // .with_children(leader_status_box) - // .into_any() + let leader = follower_states.get(pane).and_then(|state| { + let room = active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(state.leader_id) + }); + + let mut leader_border = None; + let mut leader_status_box = None; + if let Some(leader) = &leader { + let mut leader_color = cx + .theme() + .players() + .color_for_participant(leader.participant_index.0) + .cursor; + leader_color.fade_out(0.3); + leader_border = Some(leader_color); + + leader_status_box = match leader.location { + ParticipantLocation::SharedProject { + project_id: leader_project_id, + } => { + if Some(leader_project_id) == project.read(cx).remote_id() { + None + } else { + let leader_user = leader.user.clone(); + let leader_user_id = leader.user.id; + Some( + Button::new( + ("leader-status", pane.entity_id()), + format!( + "Follow {} to their active project", + leader_user.github_login, + ), + ) + .on_click(cx.listener( + move |this, _, cx| { + crate::join_remote_project( + leader_project_id, + leader_user_id, + this.app_state().clone(), + cx, + ) + .detach_and_log_err(cx); + }, + )), + ) + } + } + ParticipantLocation::UnsharedProject => Some(Button::new( + ("leader-status", pane.entity_id()), + format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ), + )), + ParticipantLocation::External => Some(Button::new( + ("leader-status", pane.entity_id()), + format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ), + )), + }; + } + + div() + .relative() + .size_full() + .child(pane.clone()) + .when_some(leader_border, |this, color| { + this.border_2().border_color(color) + }) + .when_some(leader_status_box, |this, status_box| { + this.child( + div() + .absolute() + .w_96() + .bottom_3() + .right_3() + .z_index(1) + .child(status_box), + ) + }) + .into_any() // let el = div() // .flex() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 3a9508c5bf87051bf22b3343dbbeaf854fdba61c..77d744b9fc9266dabe9b68990a3fe5f2ede38889 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2270,60 +2270,60 @@ impl Workspace { cx.notify(); } - // fn start_following( - // &mut self, - // leader_id: PeerId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let pane = self.active_pane().clone(); - - // self.last_leaders_by_pane - // .insert(pane.downgrade(), leader_id); - // self.unfollow(&pane, cx); - // self.follower_states.insert( - // pane.clone(), - // FollowerState { - // leader_id, - // active_view_id: None, - // items_by_leader_view_id: Default::default(), - // }, - // ); - // cx.notify(); - - // let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - // let project_id = self.project.read(cx).remote_id(); - // let request = self.app_state.client.request(proto::Follow { - // room_id, - // project_id, - // leader_id: Some(leader_id), - // }); + fn start_following( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + let pane = self.active_pane().clone(); + + self.last_leaders_by_pane + .insert(pane.downgrade(), leader_id); + self.unfollow(&pane, cx); + self.follower_states.insert( + pane.clone(), + FollowerState { + leader_id, + active_view_id: None, + items_by_leader_view_id: Default::default(), + }, + ); + cx.notify(); - // Some(cx.spawn(|this, mut cx| async move { - // let response = request.await?; - // this.update(&mut cx, |this, _| { - // let state = this - // .follower_states - // .get_mut(&pane) - // .ok_or_else(|| anyhow!("following interrupted"))?; - // state.active_view_id = if let Some(active_view_id) = response.active_view_id { - // Some(ViewId::from_proto(active_view_id)?) - // } else { - // None - // }; - // Ok::<_, anyhow::Error>(()) - // })??; - // Self::add_views_from_leader( - // this.clone(), - // leader_id, - // vec![pane], - // response.views, - // &mut cx, - // ) - // .await?; - // this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; - // Ok(()) - // })) - // } + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let project_id = self.project.read(cx).remote_id(); + let request = self.app_state.client.request(proto::Follow { + room_id, + project_id, + leader_id: Some(leader_id), + }); + + Some(cx.spawn(|this, mut cx| async move { + let response = request.await?; + this.update(&mut cx, |this, _| { + let state = this + .follower_states + .get_mut(&pane) + .ok_or_else(|| anyhow!("following interrupted"))?; + state.active_view_id = if let Some(active_view_id) = response.active_view_id { + Some(ViewId::from_proto(active_view_id)?) + } else { + None + }; + Ok::<_, anyhow::Error>(()) + })??; + Self::add_views_from_leader( + this.clone(), + leader_id, + vec![pane], + response.views, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; + Ok(()) + })) + } // pub fn follow_next_collaborator( // &mut self, @@ -2362,52 +2362,52 @@ impl Workspace { // self.follow(leader_id, cx) // } - // 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); + 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 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) - // } - // } - // }; + 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 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 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_view(pane); + return None; + } + } - // // Otherwise, follow. - // self.start_following(leader_id, cx) - // } + // Otherwise, follow. + self.start_following(leader_id, cx) + } pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { let state = self.follower_states.remove(pane)?; @@ -2557,57 +2557,55 @@ impl Workspace { } } - // // RPC handlers + // RPC handlers fn handle_follow( &mut self, - _follower_project_id: Option, - _cx: &mut ViewContext, + follower_project_id: Option, + cx: &mut ViewContext, ) -> proto::FollowResponse { - todo!() + let client = &self.app_state.client; + let project_id = self.project.read(cx).remote_id(); - // let client = &self.app_state.client; - // let project_id = self.project.read(cx).remote_id(); + let active_view_id = self.active_item(cx).and_then(|i| { + Some( + i.to_followable_item_handle(cx)? + .remote_id(client, cx)? + .to_proto(), + ) + }); - // let active_view_id = self.active_item(cx).and_then(|i| { - // Some( - // i.to_followable_item_handle(cx)? - // .remote_id(client, cx)? - // .to_proto(), - // ) - // }); + cx.notify(); - // cx.notify(); - - // self.last_active_view_id = active_view_id.clone(); - // proto::FollowResponse { - // active_view_id, - // views: self - // .panes() - // .iter() - // .flat_map(|pane| { - // let leader_id = self.leader_for_pane(pane); - // pane.read(cx).items().filter_map({ - // let cx = &cx; - // move |item| { - // let item = item.to_followable_item_handle(cx)?; - // if (project_id.is_none() || project_id != follower_project_id) - // && item.is_project_item(cx) - // { - // return None; - // } - // let id = item.remote_id(client, cx)?.to_proto(); - // let variant = item.to_state_proto(cx)?; - // Some(proto::View { - // id: Some(id), - // leader_id, - // variant: Some(variant), - // }) - // } - // }) - // }) - // .collect(), - // } + self.last_active_view_id = active_view_id.clone(); + proto::FollowResponse { + active_view_id, + views: self + .panes() + .iter() + .flat_map(|pane| { + let leader_id = self.leader_for_pane(pane); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let item = item.to_followable_item_handle(cx)?; + if (project_id.is_none() || project_id != follower_project_id) + && item.is_project_item(cx) + { + return None; + } + let id = item.remote_id(client, cx)?.to_proto(); + let variant = item.to_state_proto(cx)?; + Some(proto::View { + id: Some(id), + leader_id, + variant: Some(variant), + }) + } + }) + }) + .collect(), + } } fn handle_update_followers( @@ -2627,6 +2625,8 @@ 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, _| { @@ -3762,15 +3762,15 @@ impl Render for Workspace { // } impl WorkspaceStore { - pub fn new(client: Arc, _cx: &mut ModelContext) -> Self { + pub fn new(client: Arc, cx: &mut ModelContext) -> Self { Self { workspaces: Default::default(), followers: Default::default(), - _subscriptions: vec![], - // client.add_request_handler(cx.weak_model(), Self::handle_follow), - // client.add_message_handler(cx.weak_model(), Self::handle_unfollow), - // client.add_message_handler(cx.weak_model(), Self::handle_update_followers), - // ], + _subscriptions: vec![ + client.add_request_handler(cx.weak_model(), Self::handle_follow), + client.add_message_handler(cx.weak_model(), Self::handle_unfollow), + client.add_message_handler(cx.weak_model(), Self::handle_update_followers), + ], client, } } @@ -3875,11 +3875,13 @@ impl WorkspaceStore { this: Model, envelope: TypedEnvelope, _: Arc, - mut cx: AsyncWindowContext, + mut cx: AsyncAppContext, ) -> Result<()> { 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| { @@ -4310,12 +4312,11 @@ pub fn join_remote_project( Some(collaborator.peer_id) }); - // todo!("uncomment following") - // if let Some(follow_peer_id) = follow_peer_id { - // workspace - // .follow(follow_peer_id, cx) - // .map(|follow| follow.detach_and_log_err(cx)); - // } + if let Some(follow_peer_id) = follow_peer_id { + workspace + .follow(follow_peer_id, cx) + .map(|follow| follow.detach_and_log_err(cx)); + } } })?; From 1f6c69c7dc942b3f8323f64410fd83806cd12c98 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 4 Dec 2023 12:56:02 +0200 Subject: [PATCH 22/48] Allow opening buffers without a project entry --- crates/project/src/project.rs | 76 ++++++++++---------- crates/project/src/worktree.rs | 112 +++++++++++++++++++----------- crates/rpc/proto/zed.proto | 2 +- crates/rpc/src/rpc.rs | 2 +- crates/util/src/paths.rs | 9 ++- crates/workspace/src/pane.rs | 17 +++-- crates/workspace/src/workspace.rs | 5 +- crates/zed/src/zed.rs | 95 ++++++++++++++++++++++++- 8 files changed, 228 insertions(+), 90 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3802039a81be6972ab67f0bc689dd6793f96e755..e5189f7b47a44d6e101b9821904c79169301906e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1658,19 +1658,15 @@ impl Project { pub fn open_path( &mut self, - path: impl Into, + path: ProjectPath, cx: &mut ModelContext, - ) -> Task> { - let project_path = path.into(); - let task = self.open_buffer(project_path.clone(), cx); + ) -> Task, AnyModelHandle)>> { + let task = self.open_buffer(path.clone(), cx); cx.spawn_weak(|_, cx| async move { let buffer = task.await?; - let project_entry_id = buffer - .read_with(&cx, |buffer, cx| { - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) - }) - .with_context(|| format!("no project entry for {project_path:?}"))?; - + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + }); let buffer: &AnyModelHandle = &buffer; Ok((project_entry_id, buffer.clone())) }) @@ -1985,8 +1981,10 @@ impl Project { remote_id, ); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); + if let Some(entry_id) = file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } } @@ -2441,24 +2439,25 @@ impl Project { return None; }; - match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { - Some(_) => { - return None; - } - None => { - let remote_id = buffer.read(cx).remote_id(); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); - - self.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); + let remote_id = buffer.read(cx).remote_id(); + if let Some(entry_id) = file.entry_id { + match self.local_buffer_ids_by_entry_id.get(&entry_id) { + Some(_) => { + return None; + } + None => { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } - } + }; + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); } _ => {} } @@ -5777,7 +5776,7 @@ impl Project { ignored_paths_to_process.pop_front() { if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_path_excluded(&ignored_abs_path) + || snapshot.is_path_excluded(ignored_abs_path.clone()) { continue; } @@ -6208,10 +6207,13 @@ impl Project { return; } - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + let new_file = if let Some(entry) = old_file + .entry_id + .and_then(|entry_id| snapshot.entry_for_id(entry_id)) + { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6220,7 +6222,7 @@ impl Project { } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6250,10 +6252,12 @@ impl Project { ); } - if new_file.entry_id != *entry_id { + if new_file.entry_id != Some(*entry_id) { self.local_buffer_ids_by_entry_id.remove(entry_id); - self.local_buffer_ids_by_entry_id - .insert(new_file.entry_id, buffer_id); + if let Some(entry_id) = new_file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); + } } if new_file != *old_file { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d5a046ba0dc4303c0acc95219c4846fb34136cec..b992ae8849c8710a114746ef6ac21acb05400769 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -955,13 +955,16 @@ impl LocalWorktree { ) -> Task)>> { let path = Arc::from(path); let abs_path = self.absolutize(&path); + let is_excluded = self.is_path_excluded(abs_path.clone()); let fs = self.fs.clone(); - let entry = self.refresh_entry(path.clone(), None, cx); + let entry = if is_excluded { + None + } else { + Some(self.refresh_entry(path.clone(), None, cx)) + }; cx.spawn(|this, cx| async move { let text = fs.load(&abs_path).await?; - let entry = entry.await?; - let mut index_task = None; let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot()); if let Some(repo) = snapshot.repository_for_path(&path) { @@ -981,18 +984,46 @@ impl LocalWorktree { None }; - Ok(( - File { - entry_id: entry.id, - worktree: this, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }, - text, - diff_base, - )) + match entry { + Some(entry) => { + let entry = entry.await?; + Ok(( + File { + entry_id: Some(entry.id), + worktree: this, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + } + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!("Loading metadata for excluded file {abs_path:?}") + })? + .with_context(|| { + format!("Excluded file {abs_path:?} got removed during loading") + })?; + Ok(( + File { + entry_id: None, + worktree: this, + path, + mtime: metadata.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + } + } }) } @@ -1020,7 +1051,7 @@ impl LocalWorktree { if has_changed_file { let new_file = Arc::new(File { - entry_id: entry.id, + entry_id: Some(entry.id), worktree: handle, path: entry.path, mtime: entry.mtime, @@ -2226,10 +2257,20 @@ impl LocalSnapshot { paths } - pub fn is_path_excluded(&self, abs_path: &Path) -> bool { - self.file_scan_exclusions - .iter() - .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) + pub fn is_path_excluded(&self, mut path: PathBuf) -> bool { + loop { + if self + .file_scan_exclusions + .iter() + .any(|exclude_matcher| exclude_matcher.is_match(&path)) + { + return true; + } + if !path.pop() { + break; + } + } + false } } @@ -2458,8 +2499,7 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) - || snapshot.is_path_excluded(&git_dir_abs_path); + let git_dir_excluded = snapshot.is_path_excluded(git_dir_abs_path.clone()); if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { @@ -2666,7 +2706,7 @@ pub struct File { pub worktree: ModelHandle, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: ProjectEntryId, + pub(crate) entry_id: Option, pub(crate) is_local: bool, pub(crate) is_deleted: bool, } @@ -2735,7 +2775,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.id() as u64, - entry_id: self.entry_id.to_proto(), + entry_id: self.entry_id.map(|id| id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), is_deleted: self.is_deleted, @@ -2793,7 +2833,7 @@ impl File { worktree, path: entry.path.clone(), mtime: entry.mtime, - entry_id: entry.id, + entry_id: Some(entry.id), is_local: true, is_deleted: false, }) @@ -2818,7 +2858,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: ProjectEntryId::from_proto(proto.entry_id), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_deleted: proto.is_deleted, }) @@ -2836,7 +2876,7 @@ impl File { if self.is_deleted { None } else { - Some(self.entry_id) + self.entry_id } } } @@ -3338,16 +3378,7 @@ impl BackgroundScanner { return false; } - // FS events may come for files which parent directory is excluded, need to check ignore those. - let mut path_to_test = abs_path.clone(); - let mut excluded_file_event = snapshot.is_path_excluded(abs_path) - || snapshot.is_path_excluded(&relative_path); - while !excluded_file_event && path_to_test.pop() { - if snapshot.is_path_excluded(&path_to_test) { - excluded_file_event = true; - } - } - if excluded_file_event { + if snapshot.is_path_excluded(abs_path.clone()) { if !is_git_related { log::debug!("ignoring FS event for excluded path {relative_path:?}"); } @@ -3531,7 +3562,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(job.abs_path.to_path_buf()) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3603,7 +3634,10 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_path_excluded(&child_abs_path) { + if state + .snapshot + .is_path_excluded(child_abs_path.to_path_buf()) + { let relative_path = job.path.join(child_name); log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a6d27fa57d4a0a9a063f4f0a30b634207ef8ac63..5b8fd5b19991cd7d154ac26879a1484747a789a3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1357,7 +1357,7 @@ message User { message File { uint64 worktree_id = 1; - uint64 entry_id = 2; + optional uint64 entry_id = 2; string path = 3; Timestamp mtime = 4; bool is_deleted = 5; diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 6f35bf64bc23c116c67000eabc29be07f5d6da8c..da0880377fb7ef4e381587a08c4ac3e0a9a2e4dc 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 66; +pub const PROTOCOL_VERSION: u32 = 67; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index d0ba7957ec28c90aabacef8903a1544cd05e5a42..77f042a7b8bfbbbeead76ffe223a33517f8a6dc9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -218,10 +218,13 @@ impl PathMatcher { }) } + // TODO kb tests pub fn is_match>(&self, other: P) -> bool { - other.as_ref().starts_with(&self.maybe_path) - || self.glob.is_match(&other) - || self.check_with_end_separator(other.as_ref()) + let other_path = other.as_ref(); + other_path.starts_with(&self.maybe_path) + || other_path.file_name() == Some(self.maybe_path.as_os_str()) + || self.glob.is_match(other_path) + || self.check_with_end_separator(other_path) } fn check_with_end_separator(&self, path: &Path) -> bool { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ea70e4f7def8f35715bfbfdbda827a3b3c943d58..a50a109c83287997d87a71bcb4d396d43adb25aa 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -481,18 +481,21 @@ impl Pane { pub(crate) fn open_item( &mut self, - project_entry_id: ProjectEntryId, + project_entry_id: Option, focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { let mut existing_item = None; - for (index, item) in self.items.iter().enumerate() { - if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; + if let Some(project_entry_id) = project_entry_id { + for (index, item) in self.items.iter().enumerate() { + if item.is_singleton(cx) + && item.project_entry_ids(cx).as_slice() == [project_entry_id] + { + let item = item.boxed_clone(); + existing_item = Some((index, item)); + break; + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bea26e402ef41fea9b0acaa7d73a7459110be40a..89cc4b70066d420d31547516f591a6198916d338 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1549,6 +1549,7 @@ impl Workspace { let abs_path = abs_path.clone(); async move { let (worktree, project_path) = project_path?; + // TODO kb consider excluded files here? if fs.is_file(&abs_path).await { Some( this.update(&mut cx, |this, cx| { @@ -2129,13 +2130,13 @@ impl Workspace { }) } - pub(crate) fn load_path( + fn load_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task< Result<( - ProjectEntryId, + Option, impl 'static + FnOnce(&mut ViewContext) -> Box, )>, > { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d202c2aaed1b2af57afad12fe206e44ce42479ea..65ba2daad4a6d0fcb7e919d6b0087bd114eef9ea 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -763,7 +763,7 @@ mod tests { AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, }; use language::LanguageRegistry; - use project::{Project, ProjectPath}; + use project::{project_settings::ProjectSettings, Project, ProjectPath}; use serde_json::json; use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; use std::{ @@ -1308,6 +1308,99 @@ mod tests { }); } + #[gpui::test] + async fn test_opening_ignored_and_excluded_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = + Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + }); + }); + }); + // TODO kb also test external excluded dirs opening + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + ".gitignore": "ignored_dir\n", + ".git": { + "HEAD": "ref: refs/heads/main", + }, + "regular_dir": { + "file": "regular file contents", + }, + "ignored_dir": { + "file": "ignored file contents", + }, + "excluded_dir": { + "file": "excluded file contents", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + // dbg!(&entries); + + let (opened_workspace, new_items) = cx + .update(|cx| { + workspace::open_paths( + &[Path::new("/root/excluded_dir/file").to_path_buf()], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + // dbg!( + // &workspace, + // &opened_workspace, + // new_items + // .iter() + // .map(|i| i + // .as_ref() + // .expect("should be present") + // .as_ref() + // .expect("should not error")) + // .map(|i| cx.read(|cx| i.project_path(cx))) + // .collect::>() + // ); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + dbg!(&entries); + // #[rustfmt::skip] + // workspace.update(cx, |w, cx| { + // dbg!(w.open_paths(vec!["/root/regular_dir/file".into()], true, cx)); + // dbg!(w.open_paths(vec!["/root/ignored_dir/file".into()], true, cx)); + // dbg!(w.open_paths(vec!["/root/excluded_dir/file".into()], true, cx)); + // dbg!(w.open_paths(vec!["/root/excluded_dir/file".into()], false, cx)); + // + // }); + + // // Open the first entry + // let entry_1 = workspace + // .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + // .await + // .unwrap(); + // cx.read(|cx| { + // let pane = workspace.read(cx).active_pane().read(cx); + // assert_eq!( + // pane.active_item().unwrap().project_path(cx), + // Some(file1.clone()) + // ); + // assert_eq!(pane.items_len(), 1); + // }); + } + #[gpui::test] async fn test_save_conflicting_item(cx: &mut TestAppContext) { let app_state = init_test(cx); From f0ca7141b88c2f227b7697edaa3cfd75759b135f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 4 Dec 2023 17:30:46 +0200 Subject: [PATCH 23/48] Ignore excluded files on worktree entry refresh --- crates/collab/src/tests/integration_tests.rs | 27 ++-- .../random_project_collaboration_tests.rs | 1 - crates/project/src/project.rs | 121 ++++++++-------- crates/project/src/worktree.rs | 111 +++++++++------ crates/project/src/worktree_tests.rs | 30 ++-- crates/project_panel/src/project_panel.rs | 55 ++++---- crates/rpc/proto/zed.proto | 2 +- crates/terminal_view/src/terminal_view.rs | 1 + crates/util/src/paths.rs | 3 +- crates/workspace/src/workspace.rs | 1 - crates/zed/src/zed.rs | 129 +++++++++++------- 11 files changed, 268 insertions(+), 213 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index c5820b539526c94879edfd2a06412c7271ab3252..ad4c59e3773eb3de099d402ac736e6364daed688 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2981,11 +2981,10 @@ async fn test_fs_operations( let entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "c.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "c.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( @@ -3010,7 +3009,6 @@ async fn test_fs_operations( .update(cx_b, |project, cx| { project.rename_entry(entry.id, Path::new("d.txt"), cx) }) - .unwrap() .await .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3034,11 +3032,10 @@ async fn test_fs_operations( let dir_entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR"), true, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( @@ -3061,25 +3058,19 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/e.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/e.txt"), false, cx) }) .await .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) }) .await .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) }) .await .unwrap(); @@ -3120,9 +3111,7 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .copy_entry(entry.id, Path::new("f.txt"), cx) - .unwrap() + project.copy_entry(entry.id, Path::new("f.txt"), cx) }) .await .unwrap(); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 42a2b7927581f26a6d341ed9ed1d0683b43c89f6..f839333c95aedb94f55e24ce775acd23839e4a90 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest { ensure_project_shared(&project, client, cx).await; project .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .unwrap() .await?; } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e5189f7b47a44d6e101b9821904c79169301906e..fde5e71df324c7197d2d76aed012ddb7cfb95a83 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1121,20 +1121,22 @@ impl Project { project_path: impl Into, is_directory: bool, cx: &mut ModelContext, - ) -> Option>> { + ) -> Task>> { let project_path = project_path.into(); - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { + return Task::ready(Ok(None)); + }; if self.is_local() { - Some(worktree.update(cx, |worktree, cx| { + worktree.update(cx, |worktree, cx| { worktree .as_local_mut() .unwrap() .create_entry(project_path.path, is_directory, cx) - })) + }) } else { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::CreateProjectEntry { worktree_id: project_path.worktree_id.to_proto(), @@ -1143,19 +1145,20 @@ impl Project { is_directory, }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1164,8 +1167,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1178,7 +1183,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::CopyProjectEntry { project_id, @@ -1186,19 +1191,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1207,8 +1213,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1221,7 +1229,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::RenameProjectEntry { project_id, @@ -1229,19 +1237,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -6820,7 +6829,7 @@ impl Project { }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6844,11 +6853,10 @@ impl Project { .as_local_mut() .unwrap() .rename_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })? + }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6872,11 +6880,10 @@ impl Project { .as_local_mut() .unwrap() .copy_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })? + }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b992ae8849c8710a114746ef6ac21acb05400769..b77b8a3fba6e1f98130ab082364fb7cddcd3dd75 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -955,13 +955,8 @@ impl LocalWorktree { ) -> Task)>> { let path = Arc::from(path); let abs_path = self.absolutize(&path); - let is_excluded = self.is_path_excluded(abs_path.clone()); let fs = self.fs.clone(); - let entry = if is_excluded { - None - } else { - Some(self.refresh_entry(path.clone(), None, cx)) - }; + let entry = self.refresh_entry(path.clone(), None, cx); cx.spawn(|this, cx| async move { let text = fs.load(&abs_path).await?; @@ -984,22 +979,19 @@ impl LocalWorktree { None }; - match entry { - Some(entry) => { - let entry = entry.await?; - Ok(( - File { - entry_id: Some(entry.id), - worktree: this, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }, - text, - diff_base, - )) - } + match entry.await? { + Some(entry) => Ok(( + File { + entry_id: Some(entry.id), + worktree: this, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )), None => { let metadata = fs .metadata(&abs_path) @@ -1044,17 +1036,37 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); - let save = self.write_file(path, text, buffer.line_ending(), cx); + let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx); + let fs = Arc::clone(&self.fs); + let abs_path = self.absolutize(&path); cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; + let (entry_id, mtime, path) = match entry { + Some(entry) => (Some(entry.id), entry.mtime, entry.path), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!( + "Fetching metadata after saving the excluded buffer {abs_path:?}" + ) + })? + .with_context(|| { + format!("Excluded buffer {path:?} got removed during saving") + })?; + (None, metadata.mtime, path) + } + }; + if has_changed_file { let new_file = Arc::new(File { - entry_id: Some(entry.id), + entry_id, worktree: handle, - path: entry.path, - mtime: entry.mtime, + path, + mtime, is_local: true, is_deleted: false, }); @@ -1080,13 +1092,13 @@ impl LocalWorktree { project_id, buffer_id, version: serialize_version(&version), - mtime: Some(entry.mtime.into()), + mtime: Some(mtime.into()), fingerprint: serialize_fingerprint(fingerprint), })?; } buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); + buffer.did_save(version.clone(), fingerprint, mtime, cx); }); Ok(()) @@ -1111,7 +1123,7 @@ impl LocalWorktree { path: impl Into>, is_dir: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { let path = path.into(); let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); @@ -1128,7 +1140,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let (result, refreshes) = this.update(&mut cx, |this, cx| { - let mut refreshes = Vec::>>::new(); + let mut refreshes = Vec::new(); let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); for refresh_path in refresh_paths.ancestors() { if refresh_path == Path::new("") { @@ -1155,14 +1167,14 @@ impl LocalWorktree { }) } - pub fn write_file( + pub(crate) fn write_file( &self, path: impl Into>, text: Rope, line_ending: LineEnding, cx: &mut ModelContext, - ) -> Task> { - let path = path.into(); + ) -> Task>> { + let path: Arc = path.into(); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx @@ -1221,8 +1233,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1232,7 +1247,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { rename.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1240,7 +1255,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), Some(old_path), cx) }) .await - })) + }) } pub fn copy_entry( @@ -1248,8 +1263,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1264,7 +1282,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { copy.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1272,7 +1290,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), None, cx) }) .await - })) + }) } pub fn expand_entry( @@ -1308,7 +1326,10 @@ impl LocalWorktree { path: Arc, old_path: Option>, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { + if self.is_path_excluded(self.absolutize(&path)) { + return Task::ready(Ok(None)); + } let paths = if let Some(old_path) = old_path.as_ref() { vec![old_path.clone(), path.clone()] } else { @@ -1317,13 +1338,15 @@ impl LocalWorktree { let mut refresh = self.refresh_entries_for_paths(paths); cx.spawn_weak(move |this, mut cx| async move { refresh.recv().await; - this.upgrade(&cx) + let new_entry = this + .upgrade(&cx) .ok_or_else(|| anyhow!("worktree was dropped"))? .update(&mut cx, |this, _| { this.entry_for_path(path) .cloned() .ok_or_else(|| anyhow!("failed to read path after update")) - }) + })?; + Ok(Some(new_entry)) }) } diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index b4cf162d8f1e61dbf317b33917428ce79468bdea..35c1bb3ab1b52e1da94f191b5a735ddc8d7863ad 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1174,6 +1174,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .create_entry("a/e".as_ref(), true, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_dir()); @@ -1222,6 +1223,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1257,6 +1259,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1275,6 +1278,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/e.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1291,6 +1295,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("d/e/f/g.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1616,14 +1621,14 @@ fn randomly_mutate_worktree( entry.id.0, new_path ); - let task = worktree.rename_entry(entry.id, new_path, cx).unwrap(); + let task = worktree.rename_entry(entry.id, new_path, cx); cx.foreground().spawn(async move { - task.await?; + task.await?.unwrap(); Ok(()) }) } _ => { - let task = if entry.is_dir() { + if entry.is_dir() { let child_path = entry.path.join(random_filename(rng)); let is_dir = rng.gen_bool(0.3); log::info!( @@ -1631,15 +1636,20 @@ fn randomly_mutate_worktree( if is_dir { "dir" } else { "file" }, child_path, ); - worktree.create_entry(child_path, is_dir, cx) + let task = worktree.create_entry(child_path, is_dir, cx); + cx.foreground().spawn(async move { + task.await?; + Ok(()) + }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx) - }; - cx.foreground().spawn(async move { - task.await?; - Ok(()) - }) + let task = + worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + cx.foreground().spawn(async move { + task.await?; + Ok(()) + }) + } } } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 875d4d4f83736aa6c768b6dda21e915e889afc06..c37d38804151585ca99bdc909cddbbbf0633f141 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -621,7 +621,7 @@ impl ProjectPanel { edited_entry_id = NEW_ENTRY_ID; edit_task = self.project.update(cx, |project, cx| { project.create_entry((worktree_id, &new_path), is_dir, cx) - })?; + }); } else { let new_path = if let Some(parent) = entry.path.clone().parent() { parent.join(&filename) @@ -635,7 +635,7 @@ impl ProjectPanel { edited_entry_id = entry.id; edit_task = self.project.update(cx, |project, cx| { project.rename_entry(entry.id, new_path.as_path(), cx) - })?; + }); }; edit_state.processing_filename = Some(filename); @@ -648,21 +648,22 @@ impl ProjectPanel { cx.notify(); })?; - let new_entry = new_entry?; - this.update(&mut cx, |this, cx| { - if let Some(selection) = &mut this.selection { - if selection.entry_id == edited_entry_id { - selection.worktree_id = worktree_id; - selection.entry_id = new_entry.id; - this.expand_to_selection(cx); + if let Some(new_entry) = new_entry? { + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } } - } - this.update_visible_entries(None, cx); - if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); - } - cx.notify(); - })?; + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + } Ok(()) })) } @@ -935,15 +936,17 @@ impl ProjectPanel { } if clipboard_entry.is_cut() { - if let Some(task) = self.project.update(cx, |project, cx| { - project.rename_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx) - } - } else if let Some(task) = self.project.update(cx, |project, cx| { - project.copy_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx) + self.project + .update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) + } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) } } None @@ -1026,7 +1029,7 @@ impl ProjectPanel { let mut new_path = destination_path.to_path_buf(); new_path.push(entry_path.path.file_name()?); if new_path != entry_path.path.as_ref() { - let task = project.rename_entry(entry_to_move, new_path, cx)?; + let task = project.rename_entry(entry_to_move, new_path, cx); cx.foreground().spawn(task).detach_and_log_err(cx); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5b8fd5b19991cd7d154ac26879a1484747a789a3..611514aacb44f9445674f2dca0b947eda8088ee3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -430,7 +430,7 @@ message ExpandProjectEntryResponse { } message ProjectEntryResponse { - Entry entry = 1; + optional Entry entry = 1; uint64 worktree_scan_id = 2; } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5a13efd07a0da4b8490444b3322bfc4600b70e17..dda976b2cdf245d681b44493ce9e845f01e25291 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1132,6 +1132,7 @@ mod tests { }) }) .await + .unwrap() .unwrap(); (wt, entry) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 77f042a7b8bfbbbeead76ffe223a33517f8a6dc9..21d6ff74b237592b93a2cdd51a8132208f2878ff 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -218,7 +218,8 @@ impl PathMatcher { }) } - // TODO kb tests + // TODO kb tests for matching + // TODO kb add an integration test on excluded file opening pub fn is_match>(&self, other: P) -> bool { let other_path = other.as_ref(); other_path.starts_with(&self.maybe_path) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 89cc4b70066d420d31547516f591a6198916d338..455c27a57e1821f233a35a6eb6f9f3e223fa1bb0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1549,7 +1549,6 @@ impl Workspace { let abs_path = abs_path.clone(); async move { let (worktree, project_path) = project_path?; - // TODO kb consider excluded files here? if fs.is_file(&abs_path).await { Some( this.update(&mut cx, |this, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 65ba2daad4a6d0fcb7e919d6b0087bd114eef9ea..d0a526748327f5deace2de7e78b47affd8d147e0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -615,8 +615,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, dir_path), true, cx) }) - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } } @@ -625,8 +625,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, file_path), false, cx) }) - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } let editor = workspace @@ -1309,7 +1309,7 @@ mod tests { } #[gpui::test] - async fn test_opening_ignored_and_excluded_paths(cx: &mut TestAppContext) { + async fn test_opening_excluded_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { @@ -1319,7 +1319,6 @@ mod tests { }); }); }); - // TODO kb also test external excluded dirs opening app_state .fs .as_fake() @@ -1334,6 +1333,9 @@ mod tests { "file": "regular file contents", }, "ignored_dir": { + "ignored_subdir": { + "file": "ignored subfile contents", + }, "file": "ignored file contents", }, "excluded_dir": { @@ -1347,58 +1349,79 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - // dbg!(&entries); - + let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); + let paths_to_open = [ + Path::new("/root/excluded_dir/file").to_path_buf(), + Path::new("/root/.git/HEAD").to_path_buf(), + Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), + ]; let (opened_workspace, new_items) = cx - .update(|cx| { - workspace::open_paths( - &[Path::new("/root/excluded_dir/file").to_path_buf()], - &app_state, - None, - cx, - ) - }) + .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) .await .unwrap(); - // dbg!( - // &workspace, - // &opened_workspace, - // new_items - // .iter() - // .map(|i| i - // .as_ref() - // .expect("should be present") - // .as_ref() - // .expect("should not error")) - // .map(|i| cx.read(|cx| i.project_path(cx))) - // .collect::>() - // ); + + assert_eq!( + opened_workspace.id(), + workspace.id(), + "Excluded files in subfolders of a workspace root should be opened in the workspace" + ); + let mut opened_paths = cx.read(|cx| { + assert_eq!( + new_items.len(), + paths_to_open.len(), + "Expect to get the same number of opened items as submitted paths to open" + ); + new_items + .iter() + .zip(paths_to_open.iter()) + .map(|(i, path)| { + match i { + Some(Ok(i)) => { + Some(i.project_path(cx).map(|p| p.path.display().to_string())) + } + Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), + None => None, + } + .flatten() + }) + .collect::>() + }); + opened_paths.sort(); + assert_eq!( + opened_paths, + vec![ + None, + Some(".git/HEAD".to_string()), + Some("excluded_dir/file".to_string()), + ], + "Excluded files should get opened, excluded dir should not get opened" + ); let entries = cx.read(|cx| workspace.file_project_paths(cx)); - dbg!(&entries); - // #[rustfmt::skip] - // workspace.update(cx, |w, cx| { - // dbg!(w.open_paths(vec!["/root/regular_dir/file".into()], true, cx)); - // dbg!(w.open_paths(vec!["/root/ignored_dir/file".into()], true, cx)); - // dbg!(w.open_paths(vec!["/root/excluded_dir/file".into()], true, cx)); - // dbg!(w.open_paths(vec!["/root/excluded_dir/file".into()], false, cx)); - // - // }); - - // // Open the first entry - // let entry_1 = workspace - // .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) - // .await - // .unwrap(); - // cx.read(|cx| { - // let pane = workspace.read(cx).active_pane().read(cx); - // assert_eq!( - // pane.active_item().unwrap().project_path(cx), - // Some(file1.clone()) - // ); - // assert_eq!(pane.items_len(), 1); - // }); + assert_eq!( + initial_entries, entries, + "Workspace entries should not change after opening excluded files and directories paths" + ); + + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let mut opened_buffer_paths = pane + .items() + .map(|i| { + i.project_path(cx) + .expect("all excluded files that got open should have a path") + .path + .display() + .to_string() + }) + .collect::>(); + opened_buffer_paths.sort(); + assert_eq!( + opened_buffer_paths, + vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], + "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" + ); + }); } #[gpui::test] From 2c2c14a360493b4fad9086b436359949041e5ec2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 4 Dec 2023 23:00:01 +0200 Subject: [PATCH 24/48] Fix the regex matcher --- crates/project/src/project.rs | 11 ++++++----- crates/project/src/search.rs | 32 +++++++++++++++++++++++--------- crates/project/src/worktree.rs | 3 +-- crates/util/src/paths.rs | 12 +++++++++++- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fde5e71df324c7197d2d76aed012ddb7cfb95a83..b1432265cca2d770d47f8805016a536ca5bc0d25 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5784,11 +5784,6 @@ impl Project { while let Some(ignored_abs_path) = ignored_paths_to_process.pop_front() { - if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_path_excluded(ignored_abs_path.clone()) - { - continue; - } if let Some(fs_metadata) = fs .metadata(&ignored_abs_path) .await @@ -5816,6 +5811,12 @@ impl Project { } } } else if !fs_metadata.is_symlink { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot + .is_path_excluded(ignored_abs_path.clone()) + { + continue; + } let matches = if let Some(file) = fs .open_sync(&ignored_abs_path) .await diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index c673440326e82630bd34c8117665b3f3cc092b69..d664ba67ed07127d9b5c71ee8ae1ffc72e649b63 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -371,15 +371,29 @@ impl SearchQuery { pub fn file_matches(&self, file_path: Option<&Path>) -> bool { match file_path { Some(file_path) => { - !self - .files_to_exclude() - .iter() - .any(|exclude_glob| exclude_glob.is_match(file_path)) - && (self.files_to_include().is_empty() - || self - .files_to_include() - .iter() - .any(|include_glob| include_glob.is_match(file_path))) + let mut path = file_path.to_path_buf(); + let mut matches = false; + loop { + matches = !self + .files_to_exclude() + .iter() + .any(|exclude_glob| exclude_glob.is_match(&path)) + && (self.files_to_include().is_empty() + || self + .files_to_include() + .iter() + .any(|include_glob| include_glob.is_match(&path))); + if matches || !path.pop() { + break; + } + } + + let path_str = file_path.to_string_lossy(); + if path_str.contains("node_modules") && path_str.contains("prettier") { + dbg!(path_str, path, matches); + } + + matches } None => self.files_to_include().is_empty(), } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b77b8a3fba6e1f98130ab082364fb7cddcd3dd75..6855d59d6a86adc09ead2898c0afe42d26fd7613 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2290,10 +2290,9 @@ impl LocalSnapshot { return true; } if !path.pop() { - break; + return false; } } - false } } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 21d6ff74b237592b93a2cdd51a8132208f2878ff..44e78b9376fd4cbabf253727e159c70476b17f72 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -223,7 +223,7 @@ impl PathMatcher { pub fn is_match>(&self, other: P) -> bool { let other_path = other.as_ref(); other_path.starts_with(&self.maybe_path) - || other_path.file_name() == Some(self.maybe_path.as_os_str()) + || other_path.ends_with(&self.maybe_path) || self.glob.is_match(other_path) || self.check_with_end_separator(other_path) } @@ -422,4 +422,14 @@ mod tests { "Path matcher {path_matcher} should match {path:?}" ); } + + #[test] + fn project_search() { + let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); + let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); + assert!( + path_matcher.is_match(&path), + "Path matcher {path_matcher} should match {path:?}" + ); + } } From b43dc480a9a0754d49f8826a964e5365a79c4cdb Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Dec 2023 11:56:59 +0200 Subject: [PATCH 25/48] Integration tests for excluded files --- crates/collab/src/tests/following_tests.rs | 137 +++++++++++++++++++++ crates/project/src/search.rs | 2 +- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index a28f2ae87f0984241ca7df30fac0807d4e0fa31b..97509d730faf6f78cce728ce4e091985b297c430 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; +use project::project_settings::ProjectSettings; use rpc::proto::PeerId; use serde_json::json; +use settings::SettingsStore; use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::{test::TestPanel, DockPosition}, @@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces( }); } +#[gpui::test] +async fn test_following_into_excluded_file( + deterministic: Arc, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + 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; + for cx in [&mut cx_a, &mut cx_b] { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); + }); + }); + }); + } + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .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); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".git": { + "COMMIT_EDITMSG": "write your commit message here", + }, + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + 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; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let peer_id_a = client_a.peer_id().unwrap(); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); + + // Client A opens editors for a regular file and an excluded file. + let editor_for_regular = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_for_excluded_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client A updates their selections in those editors + editor_for_regular.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_for_excluded_a.update(cx_a, |editor, cx| { + editor.select_all(&Default::default(), cx); + editor.handle_input("new commit message", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![18..17]); + }); + + // When client B starts following client A, currently visible file is replicated + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), + Some((worktree_id, ".git/COMMIT_EDITMSG").into()) + ); + assert_eq!( + editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![18..17] + ); + + // Changes from B to the excluded file are replicated in A's editor + editor_for_excluded_b.update(cx_b, |editor, cx| { + editor.handle_input("\nCo-Authored-By: B ", cx); + }); + deterministic.run_until_parked(); + editor_for_excluded_a.update(cx_a, |editor, cx| { + assert_eq!( + editor.text(cx), + "new commit messag\nCo-Authored-By: B " + ); + }); +} + fn visible_push_notifications( cx: &mut TestAppContext, ) -> Vec> { diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index d664ba67ed07127d9b5c71ee8ae1ffc72e649b63..fb9c9199bd1c13df7fba6ac7798fbfed62b214c1 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -372,7 +372,7 @@ impl SearchQuery { match file_path { Some(file_path) => { let mut path = file_path.to_path_buf(); - let mut matches = false; + let mut matches; loop { matches = !self .files_to_exclude() From 92fbdb429c2bf18ff3dbcb813499a1ee25467fba Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Dec 2023 12:28:26 +0200 Subject: [PATCH 26/48] Add project search in gitignored test --- crates/project/src/project_tests.rs | 88 +++++++++++++++++++++++++++++ crates/project/src/search.rs | 28 ++++----- crates/util/src/paths.rs | 2 - 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5d061b868fb37dd730d09ea184fd3f7a91d447be..4fe6e1699b763cca151a7d8bc2e6a7b901c12c6b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4050,6 +4050,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex ); } +#[gpui::test] +async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index.txt": "index_key:index_value" + }, + "node_modules": { + "eslint": { + "index.ts": "const eslint_key = 'eslint value'", + "package.json": r#"{ "some_key": "some value" }"#, + }, + "prettier": { + "index.ts": "const prettier_key = 'prettier value'", + "package.json": r#"{ "other_key": "other value" }"#, + }, + }, + "package.json": r#"{ "main_key": "main value" }"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let query = "key"; + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([("package.json".to_string(), vec![8..11])]), + "Only one non-ignored file should have the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("package.json".to_string(), vec![8..11]), + ("target/index.txt".to_string(), vec![6..9]), + ( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + ), + ("node_modules/prettier/index.ts".to_string(), vec![15..18]), + ("node_modules/eslint/index.ts".to_string(), vec![13..16]), + ("node_modules/eslint/package.json".to_string(), vec![8..11]), + ]), + "Unrestricted search with ignored directories should find every file with the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + query, + false, + false, + true, + vec![PathMatcher::new("node_modules/prettier/**").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + )]), + "With search including ignored prettier directory and excluding TS files, only one file should be found" + ); +} + #[test] fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix("**/*.js"), ""); diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index fb9c9199bd1c13df7fba6ac7798fbfed62b214c1..bfbc537b27e92821a02e401ccf05a7cd013fb2b7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -372,28 +372,24 @@ impl SearchQuery { match file_path { Some(file_path) => { let mut path = file_path.to_path_buf(); - let mut matches; loop { - matches = !self + if self .files_to_exclude() .iter() .any(|exclude_glob| exclude_glob.is_match(&path)) - && (self.files_to_include().is_empty() - || self - .files_to_include() - .iter() - .any(|include_glob| include_glob.is_match(&path))); - if matches || !path.pop() { - break; + { + return false; + } else if self.files_to_include().is_empty() + || self + .files_to_include() + .iter() + .any(|include_glob| include_glob.is_match(&path)) + { + return true; + } else if !path.pop() { + return false; } } - - let path_str = file_path.to_string_lossy(); - if path_str.contains("node_modules") && path_str.contains("prettier") { - dbg!(path_str, path, matches); - } - - matches } None => self.files_to_include().is_empty(), } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 44e78b9376fd4cbabf253727e159c70476b17f72..19b244383fdf2ab24cfec9381a6f8ddb87f6e977 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -218,8 +218,6 @@ impl PathMatcher { }) } - // TODO kb tests for matching - // TODO kb add an integration test on excluded file opening pub fn is_match>(&self, other: P) -> bool { let other_path = other.as_ref(); other_path.starts_with(&self.maybe_path) From e5616bce9813fa16c27a8cbc3bc8d21cfc538c27 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Dec 2023 12:39:08 +0200 Subject: [PATCH 27/48] Fix the test --- crates/project/src/worktree_tests.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 35c1bb3ab1b52e1da94f191b5a735ddc8d7863ad..e8865873277ecab09e0414529c299559f2bce3c1 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1052,11 +1052,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { &[ ".git/HEAD", ".git/foo", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", ], - &["target", "node_modules"], + &["target"], &[ ".DS_Store", "src/.DS_Store", @@ -1106,6 +1107,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { ".git/HEAD", ".git/foo", ".git/new_file", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", @@ -1114,7 +1116,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "build_output/new_file", "test_output/new_file", ], - &["target", "node_modules", "test_output"], + &["target", "test_output"], &[ ".DS_Store", "src/.DS_Store", From af72772a72f46948566887ae550a0b5cf81394e8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 09:02:21 -0500 Subject: [PATCH 28/48] 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 29/48] 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 20352c51c1514fc9853b4a2e171d5c425601c893 Mon Sep 17 00:00:00 2001 From: Federico Dionisi Date: Tue, 5 Dec 2023 15:12:37 +0100 Subject: [PATCH 30/48] Fix panic opening the theme selector --- crates/theme_selector2/src/theme_selector.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index be55194e76ebdaedc0e72aa4a9f9d4d6314fb3eb..0d4c1e64667ecb98e0cadaf39d162dead1a2c46b 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -98,7 +98,7 @@ impl ThemeSelectorDelegate { let original_theme = cx.theme().clone(); let staff_mode = cx.is_staff(); - let registry = cx.global::>(); + let registry = cx.global::(); let theme_names = registry.list(staff_mode).collect::>(); //todo!(theme sorting) // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); @@ -126,7 +126,7 @@ impl ThemeSelectorDelegate { fn show_selected_theme(&mut self, cx: &mut ViewContext>) { if let Some(mat) = self.matches.get(self.selected_index) { - let registry = cx.global::>(); + let registry = cx.global::(); match registry.get(&mat.string) { Ok(theme) => { Self::set_theme(theme, cx); From 16b5d4b35cf784a5594668042971250c39125337 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Dec 2023 16:13:39 +0200 Subject: [PATCH 31/48] Port to gpui2 --- crates/collab2/src/tests/following_tests.rs | 425 +++- crates/collab2/src/tests/integration_tests.rs | 32 +- .../random_project_collaboration_tests.rs | 1 - crates/project/src/project.rs | 5 +- crates/project/src/worktree.rs | 15 +- crates/project2/src/project2.rs | 208 +- crates/project2/src/project_tests.rs | 88 + crates/project2/src/search.rs | 22 +- crates/project2/src/worktree.rs | 172 +- crates/project2/src/worktree_tests.rs | 36 +- crates/project_panel2/src/project_panel.rs | 55 +- crates/rpc2/proto/zed.proto | 4 +- crates/rpc2/src/rpc.rs | 2 +- crates/terminal_view2/src/terminal_view.rs | 1 + crates/workspace2/src/pane.rs | 17 +- crates/workspace2/src/workspace2.rs | 6 +- crates/zed2/src/zed2.rs | 1851 ++++++++++++++++- 17 files changed, 2585 insertions(+), 355 deletions(-) diff --git a/crates/collab2/src/tests/following_tests.rs b/crates/collab2/src/tests/following_tests.rs index 61d14c25c426cb93f4101aeb56ac2119d8efe0f7..5178df408f95b8809495836576c3f1c74159cf85 100644 --- a/crates/collab2/src/tests/following_tests.rs +++ b/crates/collab2/src/tests/following_tests.rs @@ -4,10 +4,12 @@ // use call::ActiveCall; // use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; // use editor::{Editor, ExcerptRange, MultiBuffer}; -// use gpui::{BackgroundExecutor, TestAppContext, View}; +// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext}; // use live_kit_client::MacOSDisplay; +// use project::project_settings::ProjectSettings; // use rpc::proto::PeerId; // use serde_json::json; +// use settings::SettingsStore; // use std::borrow::Cow; // use workspace::{ // dock::{test::TestPanel, DockPosition}, @@ -24,7 +26,7 @@ // cx_c: &mut TestAppContext, // cx_d: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// 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; @@ -71,12 +73,22 @@ // .unwrap(); // let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a); +// let workspace_a = window_a.root(cx_a).unwrap(); // let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b); +// let workspace_b = window_b.root(cx_b).unwrap(); + +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; +// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c); +// let cx_c = &mut cx_c; +// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d); +// let cx_d = &mut cx_d; // // Client A opens some editors. -// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); +// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); // let editor_a1 = workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -132,8 +144,8 @@ // .await // .unwrap(); -// cx_c.foreground().run_until_parked(); -// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { +// cx_c.executor().run_until_parked(); +// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -145,19 +157,19 @@ // Some((worktree_id, "2.txt").into()) // ); // assert_eq!( -// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), +// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), // vec![2..1] // ); // assert_eq!( -// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), +// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), // vec![3..2] // ); -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // let active_call_c = cx_c.read(ActiveCall::global); // let project_c = client_c.build_remote_project(project_id, cx_c).await; // let window_c = client_c.build_workspace(&project_c, cx_c); -// let workspace_c = window_c.root(cx_c); +// let workspace_c = window_c.root(cx_c).unwrap(); // active_call_c // .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) // .await @@ -172,10 +184,13 @@ // .await // .unwrap(); -// cx_d.foreground().run_until_parked(); +// cx_d.executor().run_until_parked(); // let active_call_d = cx_d.read(ActiveCall::global); // let project_d = client_d.build_remote_project(project_id, cx_d).await; -// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); +// let workspace_d = client_d +// .build_workspace(&project_d, cx_d) +// .root(cx_d) +// .unwrap(); // active_call_d // .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) // .await @@ -183,7 +198,7 @@ // drop(project_d); // // All clients see that clients B and C are following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -198,7 +213,7 @@ // }); // // All clients see that clients B is following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -216,7 +231,7 @@ // .unwrap(); // // All clients see that clients B and C are following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -240,7 +255,7 @@ // .unwrap(); // // All clients see that D is following C -// cx_d.foreground().run_until_parked(); +// cx_d.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -257,7 +272,7 @@ // cx_c.drop_last(workspace_c); // // Clients A and B see that client B is following A, and client C is not present in the followers. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -271,12 +286,15 @@ // workspace.activate_item(&editor_a1, cx) // }); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // // When client A opens a multibuffer, client B does so as well. -// let multibuffer_a = cx_a.add_model(|cx| { +// let multibuffer_a = cx_a.build_model(|cx| { // let buffer_a1 = project_a.update(cx, |project, cx| { // project // .get_open_buffer(&(worktree_id, "1.txt").into(), cx) @@ -308,12 +326,12 @@ // }); // let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { // let editor = -// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); +// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); // workspace.add_item(Box::new(editor.clone()), cx); // editor // }); // executor.run_until_parked(); -// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { +// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -321,8 +339,8 @@ // .unwrap() // }); // assert_eq!( -// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), -// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), +// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), +// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), // ); // // When client A navigates back and forth, client B does so as well. @@ -333,8 +351,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // workspace_a @@ -344,8 +365,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b2.item_id() +// ); // }); // workspace_a @@ -355,8 +379,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // // Changes to client A's editor are reflected on client B. @@ -364,20 +391,20 @@ // editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); // }); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| { +// editor_b1.update(cx_b, |editor, cx| { // assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); // }); // editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); +// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); // editor_a1.update(cx_a, |editor, cx| { // editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.set_scroll_position(vec2f(0., 100.), cx); +// editor.set_scroll_position(point(0., 100.), cx); // }); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| { +// editor_b1.update(cx_b, |editor, cx| { // assert_eq!(editor.selections.ranges(cx), &[3..3]); // }); @@ -390,11 +417,11 @@ // }); // executor.run_until_parked(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, cx| workspace +// workspace_b.update(cx_b, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// editor_b1.id() +// .item_id()), +// editor_b1.item_id() // ); // // Client A starts following client B. @@ -405,15 +432,15 @@ // .await // .unwrap(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), +// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), // Some(peer_id_b) // ); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// editor_a1.id() +// .item_id()), +// editor_a1.item_id() // ); // // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. @@ -432,7 +459,7 @@ // .await // .unwrap(); // executor.run_until_parked(); -// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { +// let shared_screen = workspace_a.update(cx_a, |workspace, cx| { // workspace // .active_item(cx) // .expect("no active item") @@ -446,8 +473,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) +// workspace_a.update(cx_a, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_a1.item_id() +// ) // }); // // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. @@ -455,26 +485,26 @@ // workspace.activate_item(&multibuffer_editor_b, cx) // }); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { +// workspace_a.update(cx_a, |workspace, cx| { // assert_eq!( -// workspace.active_item(cx).unwrap().id(), -// multibuffer_editor_a.id() +// workspace.active_item(cx).unwrap().item_id(), +// multibuffer_editor_a.item_id() // ) // }); // // Client B activates a panel, and the previously-opened screen-sharing item gets activated. -// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); +// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left)); // workspace_b.update(cx_b, |workspace, cx| { // workspace.add_panel(panel, cx); // workspace.toggle_panel_focus::(cx); // }); // executor.run_until_parked(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// shared_screen.id() +// .item_id()), +// shared_screen.item_id() // ); // // Toggling the focus back to the pane causes client A to return to the multibuffer. @@ -482,16 +512,16 @@ // workspace.toggle_panel_focus::(cx); // }); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { +// workspace_a.update(cx_a, |workspace, cx| { // assert_eq!( -// workspace.active_item(cx).unwrap().id(), -// multibuffer_editor_a.id() +// workspace.active_item(cx).unwrap().item_id(), +// multibuffer_editor_a.item_id() // ) // }); // // Client B activates an item that doesn't implement following, // // so the previously-opened screen-sharing item gets activated. -// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); +// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new()); // workspace_b.update(cx_b, |workspace, cx| { // workspace.active_pane().update(cx, |pane, cx| { // pane.add_item(Box::new(unfollowable_item), true, true, None, cx) @@ -499,18 +529,18 @@ // }); // executor.run_until_parked(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// shared_screen.id() +// .item_id()), +// shared_screen.item_id() // ); // // Following interrupts when client B disconnects. // client_b.disconnect(&cx_b.to_async()); // executor.advance_clock(RECONNECT_TIMEOUT); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), +// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), // None // ); // } @@ -521,7 +551,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// 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; // server @@ -560,13 +590,19 @@ // .await // .unwrap(); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); +// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); +// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let client_b_id = project_a.read_with(cx_a, |project, _| { +// let client_b_id = project_a.update(cx_a, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); @@ -584,7 +620,7 @@ // .await // .unwrap(); -// let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { +// let pane_paths = |pane: &View, cx: &mut TestAppContext| { // pane.update(cx, |pane, cx| { // pane.items() // .map(|item| { @@ -642,7 +678,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// 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; // server @@ -685,7 +721,10 @@ // .unwrap(); // // Client A opens a file. -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -696,7 +735,10 @@ // .unwrap(); // // Client B opens a different file. -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // workspace_b // .update(cx_b, |workspace, cx| { // workspace.open_path((worktree_id, "2.txt"), None, true, cx) @@ -1167,7 +1209,7 @@ // cx_b: &mut TestAppContext, // ) { // // 2 clients connect to a server. -// let mut server = TestServer::start(&executor).await; +// 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; // server @@ -1207,8 +1249,17 @@ // .await // .unwrap(); +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + // // Client A opens some editors. -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // let _editor_a1 = workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -1219,9 +1270,12 @@ // .unwrap(); // // Client B starts following client A. -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); -// let leader_id = project_b.read_with(cx_b, |project, _| { +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); +// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); +// let leader_id = project_b.update(cx_b, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); // workspace_b @@ -1231,10 +1285,10 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); -// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { +// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -1245,7 +1299,7 @@ // // When client B moves, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1256,14 +1310,14 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // // When client B edits, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1274,16 +1328,16 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // // When client B scrolls, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| { -// editor.set_scroll_position(vec2f(0., 3.), cx) +// editor.set_scroll_position(point(0., 3.), cx) // }); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1294,7 +1348,7 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); @@ -1303,13 +1357,13 @@ // workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) // }); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); @@ -1321,7 +1375,7 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); // } @@ -1332,7 +1386,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// 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; // server @@ -1345,20 +1399,26 @@ // client_a.fs().insert_tree("/a", json!({})).await; // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // 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); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // executor.run_until_parked(); -// let client_a_id = project_b.read_with(cx_b, |project, _| { +// let client_a_id = project_b.update(cx_b, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); -// let client_b_id = project_a.read_with(cx_a, |project, _| { +// let client_b_id = project_a.update(cx_a, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); @@ -1370,13 +1430,13 @@ // }); // futures::try_join!(a_follow_b, b_follow_a).unwrap(); -// workspace_a.read_with(cx_a, |workspace, _| { +// workspace_a.update(cx_a, |workspace, _| { // assert_eq!( // workspace.leader_for_pane(workspace.active_pane()), // Some(client_b_id) // ); // }); -// workspace_b.read_with(cx_b, |workspace, _| { +// workspace_b.update(cx_b, |workspace, _| { // assert_eq!( // workspace.leader_for_pane(workspace.active_pane()), // Some(client_a_id) @@ -1398,7 +1458,7 @@ // // b opens a different file in project 2, a follows b // // b opens a different file in project 1, a cannot follow b // // b shares the project, a joins the project and follows b -// let mut server = TestServer::start(&executor).await; +// 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; // cx_a.update(editor::init); @@ -1435,8 +1495,14 @@ // let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; // let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).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_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); // cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); @@ -1455,6 +1521,12 @@ // .await // .unwrap(); +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + // workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) @@ -1476,11 +1548,12 @@ // let workspace_b_project_a = cx_b // .windows() // .iter() -// .max_by_key(|window| window.id()) +// .max_by_key(|window| window.item_id()) // .unwrap() // .downcast::() // .unwrap() -// .root(cx_b); +// .root(cx_b) +// .unwrap(); // // assert that b is following a in project a in w.rs // workspace_b_project_a.update(cx_b, |workspace, cx| { @@ -1534,7 +1607,7 @@ // workspace.leader_for_pane(workspace.active_pane()) // ); // let item = workspace.active_pane().read(cx).active_item().unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); +// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into()); // }); // // b moves to y.rs in b's project, a is still following but can't yet see @@ -1578,11 +1651,12 @@ // let workspace_a_project_b = cx_a // .windows() // .iter() -// .max_by_key(|window| window.id()) +// .max_by_key(|window| window.item_id()) // .unwrap() // .downcast::() // .unwrap() -// .root(cx_a); +// .root(cx_a) +// .unwrap(); // workspace_a_project_b.update(cx_a, |workspace, cx| { // assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); @@ -1596,12 +1670,151 @@ // }); // } +// #[gpui::test] +// async fn test_following_into_excluded_file( +// executor: BackgroundExecutor, +// mut cx_a: &mut TestAppContext, +// mut 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; +// for cx in [&mut cx_a, &mut cx_b] { +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, |project_settings| { +// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); +// }); +// }); +// }); +// } +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .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); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// ".git": { +// "COMMIT_EDITMSG": "write your commit message here", +// }, +// "1.txt": "one\none\none", +// "2.txt": "two\ntwo\ntwo", +// "3.txt": "three\nthree\nthree", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); + +// 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; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let window_a = client_a.build_workspace(&project_a, cx_a); +// let workspace_a = window_a.root(cx_a).unwrap(); +// let peer_id_a = client_a.peer_id().unwrap(); +// let window_b = client_b.build_workspace(&project_b, cx_b); +// let workspace_b = window_b.root(cx_b).unwrap(); + +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + +// // Client A opens editors for a regular file and an excluded file. +// let editor_for_regular = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "1.txt"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let editor_for_excluded_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// // Client A updates their selections in those editors +// editor_for_regular.update(cx_a, |editor, cx| { +// editor.handle_input("a", cx); +// editor.handle_input("b", cx); +// editor.handle_input("c", cx); +// editor.select_left(&Default::default(), cx); +// assert_eq!(editor.selections.ranges(cx), vec![3..2]); +// }); +// editor_for_excluded_a.update(cx_a, |editor, cx| { +// editor.select_all(&Default::default(), cx); +// editor.handle_input("new commit message", cx); +// editor.select_left(&Default::default(), cx); +// assert_eq!(editor.selections.ranges(cx), vec![18..17]); +// }); + +// // When client B starts following client A, currently visible file is replicated +// workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.follow(peer_id_a, cx).unwrap() +// }) +// .await +// .unwrap(); + +// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert_eq!( +// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), +// Some((worktree_id, ".git/COMMIT_EDITMSG").into()) +// ); +// assert_eq!( +// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), +// vec![18..17] +// ); + +// // Changes from B to the excluded file are replicated in A's editor +// editor_for_excluded_b.update(cx_b, |editor, cx| { +// editor.handle_input("\nCo-Authored-By: B ", cx); +// }); +// executor.run_until_parked(); +// editor_for_excluded_a.update(cx_a, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "new commit messag\nCo-Authored-By: B " +// ); +// }); +// } + // fn visible_push_notifications( // cx: &mut TestAppContext, -// ) -> Vec> { +// ) -> Vec> { // let mut ret = Vec::new(); // for window in cx.windows() { -// window.read_with(cx, |window| { +// window.update(cx, |window| { // if let Some(handle) = window // .root_view() // .clone() @@ -1645,8 +1858,8 @@ // }) // } -// fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { -// workspace.read_with(cx, |workspace, cx| { +// fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { +// workspace.update(cx, |workspace, cx| { // let active_pane = workspace.active_pane(); // workspace // .panes() diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index 7104d36b8de51e5cda88531c3a80ff7400c047b3..823c8e9045eb02fe67fa605bc1cf2d21fb88a670 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -2781,11 +2781,10 @@ async fn test_fs_operations( let entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "c.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "c.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2812,8 +2811,8 @@ async fn test_fs_operations( .update(cx_b, |project, cx| { project.rename_entry(entry.id, Path::new("d.txt"), cx) }) - .unwrap() .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2838,11 +2837,10 @@ async fn test_fs_operations( let dir_entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR"), true, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2867,27 +2865,24 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/e.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/e.txt"), false, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2928,11 +2923,10 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .copy_entry(entry.id, Path::new("f.txt"), cx) - .unwrap() + project.copy_entry(entry.id, Path::new("f.txt"), cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { diff --git a/crates/collab2/src/tests/random_project_collaboration_tests.rs b/crates/collab2/src/tests/random_project_collaboration_tests.rs index 47b936a6117df1873702cb1937614548aa03d796..f4194b98e8adbf41742a5aa279d766cf09c2477d 100644 --- a/crates/collab2/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab2/src/tests/random_project_collaboration_tests.rs @@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest { ensure_project_shared(&project, client, cx).await; project .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .unwrap() .await?; } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b1432265cca2d770d47f8805016a536ca5bc0d25..2e779b71b2c4c2765c2c73745ac6ebd24db44bc9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5812,8 +5812,9 @@ impl Project { } } else if !fs_metadata.is_symlink { if !query.file_matches(Some(&ignored_abs_path)) - || snapshot - .is_path_excluded(ignored_abs_path.clone()) + || snapshot.is_path_excluded( + ignored_entry.path.to_path_buf(), + ) { continue; } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6855d59d6a86adc09ead2898c0afe42d26fd7613..c721d127add1344ee22df0a4224413c60c3fc9b2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1327,7 +1327,7 @@ impl LocalWorktree { old_path: Option>, cx: &mut ModelContext, ) -> Task>> { - if self.is_path_excluded(self.absolutize(&path)) { + if self.is_path_excluded(path.to_path_buf()) { return Task::ready(Ok(None)); } let paths = if let Some(old_path) = old_path.as_ref() { @@ -2521,7 +2521,7 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - let git_dir_excluded = snapshot.is_path_excluded(git_dir_abs_path.clone()); + let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf()); if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { @@ -3400,7 +3400,7 @@ impl BackgroundScanner { return false; } - if snapshot.is_path_excluded(abs_path.clone()) { + if snapshot.is_path_excluded(relative_path.to_path_buf()) { if !is_git_related { log::debug!("ignoring FS event for excluded path {relative_path:?}"); } @@ -3584,7 +3584,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_path_excluded(job.abs_path.to_path_buf()) { + if snapshot.is_path_excluded(job.path.to_path_buf()) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3656,11 +3656,8 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state - .snapshot - .is_path_excluded(child_abs_path.to_path_buf()) - { - let relative_path = job.path.join(child_name); + let relative_path = job.path.join(child_name); + if state.snapshot.is_path_excluded(relative_path.clone()) { log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); continue; diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 9750fe053dbb0e1ff63f27a170376c0bd7bc6af0..243f896b0f14035e399f382b4b68e2f16ffd7438 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -1151,20 +1151,22 @@ impl Project { project_path: impl Into, is_directory: bool, cx: &mut ModelContext, - ) -> Option>> { + ) -> Task>> { let project_path = project_path.into(); - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { + return Task::ready(Ok(None)); + }; if self.is_local() { - Some(worktree.update(cx, |worktree, cx| { + worktree.update(cx, |worktree, cx| { worktree .as_local_mut() .unwrap() .create_entry(project_path.path, is_directory, cx) - })) + }) } else { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::CreateProjectEntry { worktree_id: project_path.worktree_id.to_proto(), @@ -1173,19 +1175,20 @@ impl Project { is_directory, }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1194,8 +1197,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1208,7 +1213,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::CopyProjectEntry { project_id, @@ -1216,19 +1221,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1237,8 +1243,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1251,7 +1259,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::RenameProjectEntry { project_id, @@ -1259,19 +1267,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1688,18 +1697,15 @@ impl Project { pub fn open_path( &mut self, - path: impl Into, + path: ProjectPath, cx: &mut ModelContext, - ) -> Task> { - let project_path = path.into(); - let task = self.open_buffer(project_path.clone(), cx); - cx.spawn(move |_, mut cx| async move { + ) -> Task, AnyModel)>> { + let task = self.open_buffer(path.clone(), cx); + cx.spawn(move |_, cx| async move { let buffer = task.await?; - let project_entry_id = buffer - .update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) - })? - .with_context(|| format!("no project entry for {project_path:?}"))?; + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + })?; let buffer: &AnyModel = &buffer; Ok((project_entry_id, buffer.clone())) @@ -2018,8 +2024,10 @@ impl Project { remote_id, ); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); + if let Some(entry_id) = file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } } @@ -2474,24 +2482,25 @@ impl Project { return None; }; - match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { - Some(_) => { - return None; - } - None => { - let remote_id = buffer.read(cx).remote_id(); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); - - self.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); + let remote_id = buffer.read(cx).remote_id(); + if let Some(entry_id) = file.entry_id { + match self.local_buffer_ids_by_entry_id.get(&entry_id) { + Some(_) => { + return None; + } + None => { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } - } + }; + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); } _ => {} } @@ -5845,11 +5854,6 @@ impl Project { while let Some(ignored_abs_path) = ignored_paths_to_process.pop_front() { - if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_path_excluded(&ignored_abs_path) - { - continue; - } if let Some(fs_metadata) = fs .metadata(&ignored_abs_path) .await @@ -5877,6 +5881,13 @@ impl Project { } } } else if !fs_metadata.is_symlink { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_path_excluded( + ignored_entry.path.to_path_buf(), + ) + { + continue; + } let matches = if let Some(file) = fs .open_sync(&ignored_abs_path) .await @@ -6278,10 +6289,13 @@ impl Project { return; } - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + let new_file = if let Some(entry) = old_file + .entry_id + .and_then(|entry_id| snapshot.entry_for_id(entry_id)) + { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6290,7 +6304,7 @@ impl Project { } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6320,10 +6334,12 @@ impl Project { ); } - if new_file.entry_id != *entry_id { + if new_file.entry_id != Some(*entry_id) { self.local_buffer_ids_by_entry_id.remove(entry_id); - self.local_buffer_ids_by_entry_id - .insert(new_file.entry_id, buffer_id); + if let Some(entry_id) = new_file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); + } } if new_file != *old_file { @@ -6890,7 +6906,7 @@ impl Project { })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6914,11 +6930,10 @@ impl Project { .as_local_mut() .unwrap() .rename_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })?? + })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6942,11 +6957,10 @@ impl Project { .as_local_mut() .unwrap() .copy_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })?? + })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 4dfb8004e3e644309b89ee31d99f5ac07e05f4b3..8f41c75fb4de0415089c1ce66c30cb93278c079c 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -4182,6 +4182,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex ); } +#[gpui::test] +async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index.txt": "index_key:index_value" + }, + "node_modules": { + "eslint": { + "index.ts": "const eslint_key = 'eslint value'", + "package.json": r#"{ "some_key": "some value" }"#, + }, + "prettier": { + "index.ts": "const prettier_key = 'prettier value'", + "package.json": r#"{ "other_key": "other value" }"#, + }, + }, + "package.json": r#"{ "main_key": "main value" }"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let query = "key"; + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([("package.json".to_string(), vec![8..11])]), + "Only one non-ignored file should have the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("package.json".to_string(), vec![8..11]), + ("target/index.txt".to_string(), vec![6..9]), + ( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + ), + ("node_modules/prettier/index.ts".to_string(), vec![15..18]), + ("node_modules/eslint/index.ts".to_string(), vec![13..16]), + ("node_modules/eslint/package.json".to_string(), vec![8..11]), + ]), + "Unrestricted search with ignored directories should find every file with the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + query, + false, + false, + true, + vec![PathMatcher::new("node_modules/prettier/**").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + )]), + "With search including ignored prettier directory and excluding TS files, only one file should be found" + ); +} + #[test] fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix("**/*.js"), ""); diff --git a/crates/project2/src/search.rs b/crates/project2/src/search.rs index c673440326e82630bd34c8117665b3f3cc092b69..bfbc537b27e92821a02e401ccf05a7cd013fb2b7 100644 --- a/crates/project2/src/search.rs +++ b/crates/project2/src/search.rs @@ -371,15 +371,25 @@ impl SearchQuery { pub fn file_matches(&self, file_path: Option<&Path>) -> bool { match file_path { Some(file_path) => { - !self - .files_to_exclude() - .iter() - .any(|exclude_glob| exclude_glob.is_match(file_path)) - && (self.files_to_include().is_empty() + let mut path = file_path.to_path_buf(); + loop { + if self + .files_to_exclude() + .iter() + .any(|exclude_glob| exclude_glob.is_match(&path)) + { + return false; + } else if self.files_to_include().is_empty() || self .files_to_include() .iter() - .any(|include_glob| include_glob.is_match(file_path))) + .any(|include_glob| include_glob.is_match(&path)) + { + return true; + } else if !path.pop() { + return false; + } + } } None => self.files_to_include().is_empty(), } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index e424375220c1c7aa2292d9a4762d7581ea67222e..a5cb322cb5f52beb71c1b4200f58f098c2f6d88a 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -958,8 +958,6 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let entry = entry.await?; - let mut index_task = None; let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; if let Some(repo) = snapshot.repository_for_path(&path) { @@ -982,18 +980,43 @@ impl LocalWorktree { let worktree = this .upgrade() .ok_or_else(|| anyhow!("worktree was dropped"))?; - Ok(( - File { - entry_id: entry.id, - worktree, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }, - text, - diff_base, - )) + match entry.await? { + Some(entry) => Ok(( + File { + entry_id: Some(entry.id), + worktree, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!("Loading metadata for excluded file {abs_path:?}") + })? + .with_context(|| { + format!("Excluded file {abs_path:?} got removed during loading") + })?; + Ok(( + File { + entry_id: None, + worktree, + path, + mtime: metadata.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + } + } }) } @@ -1013,18 +1036,38 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); - let save = self.write_file(path, text, buffer.line_ending(), cx); + let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx); + let fs = Arc::clone(&self.fs); + let abs_path = self.absolutize(&path); cx.spawn(move |this, mut cx| async move { let entry = save.await?; let this = this.upgrade().context("worktree dropped")?; + let (entry_id, mtime, path) = match entry { + Some(entry) => (Some(entry.id), entry.mtime, entry.path), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!( + "Fetching metadata after saving the excluded buffer {abs_path:?}" + ) + })? + .with_context(|| { + format!("Excluded buffer {path:?} got removed during saving") + })?; + (None, metadata.mtime, path) + } + }; + if has_changed_file { let new_file = Arc::new(File { - entry_id: entry.id, + entry_id, worktree: this, - path: entry.path, - mtime: entry.mtime, + path, + mtime, is_local: true, is_deleted: false, }); @@ -1050,13 +1093,13 @@ impl LocalWorktree { project_id, buffer_id, version: serialize_version(&version), - mtime: Some(entry.mtime.into()), + mtime: Some(mtime.into()), fingerprint: serialize_fingerprint(fingerprint), })?; } buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); + buffer.did_save(version.clone(), fingerprint, mtime, cx); })?; Ok(()) @@ -1081,7 +1124,7 @@ impl LocalWorktree { path: impl Into>, is_dir: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { let path = path.into(); let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); @@ -1098,7 +1141,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let (result, refreshes) = this.update(&mut cx, |this, cx| { - let mut refreshes = Vec::>>::new(); + let mut refreshes = Vec::new(); let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); for refresh_path in refresh_paths.ancestors() { if refresh_path == Path::new("") { @@ -1125,14 +1168,14 @@ impl LocalWorktree { }) } - pub fn write_file( + pub(crate) fn write_file( &self, path: impl Into>, text: Rope, line_ending: LineEnding, cx: &mut ModelContext, - ) -> Task> { - let path = path.into(); + ) -> Task>> { + let path: Arc = path.into(); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx @@ -1191,8 +1234,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1202,7 +1248,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { rename.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1210,7 +1256,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), Some(old_path), cx) })? .await - })) + }) } pub fn copy_entry( @@ -1218,8 +1264,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1234,7 +1283,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { copy.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1242,7 +1291,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), None, cx) })? .await - })) + }) } pub fn expand_entry( @@ -1278,7 +1327,10 @@ impl LocalWorktree { path: Arc, old_path: Option>, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { + if self.is_path_excluded(path.to_path_buf()) { + return Task::ready(Ok(None)); + } let paths = if let Some(old_path) = old_path.as_ref() { vec![old_path.clone(), path.clone()] } else { @@ -1287,11 +1339,12 @@ impl LocalWorktree { let mut refresh = self.refresh_entries_for_paths(paths); cx.spawn(move |this, mut cx| async move { refresh.recv().await; - this.update(&mut cx, |this, _| { + let new_entry = this.update(&mut cx, |this, _| { this.entry_for_path(path) .cloned() .ok_or_else(|| anyhow!("failed to read path after update")) - })? + })??; + Ok(Some(new_entry)) }) } @@ -2222,10 +2275,19 @@ impl LocalSnapshot { paths } - pub fn is_path_excluded(&self, abs_path: &Path) -> bool { - self.file_scan_exclusions - .iter() - .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) + pub fn is_path_excluded(&self, mut path: PathBuf) -> bool { + loop { + if self + .file_scan_exclusions + .iter() + .any(|exclude_matcher| exclude_matcher.is_match(&path)) + { + return true; + } + if !path.pop() { + return false; + } + } } } @@ -2455,8 +2517,7 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) - || snapshot.is_path_excluded(&git_dir_abs_path); + let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf()); if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { @@ -2663,7 +2724,7 @@ pub struct File { pub worktree: Model, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: ProjectEntryId, + pub(crate) entry_id: Option, pub(crate) is_local: bool, pub(crate) is_deleted: bool, } @@ -2732,7 +2793,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.entity_id().as_u64(), - entry_id: self.entry_id.to_proto(), + entry_id: self.entry_id.map(|id| id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), is_deleted: self.is_deleted, @@ -2790,7 +2851,7 @@ impl File { worktree, path: entry.path.clone(), mtime: entry.mtime, - entry_id: entry.id, + entry_id: Some(entry.id), is_local: true, is_deleted: false, }) @@ -2815,7 +2876,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: ProjectEntryId::from_proto(proto.entry_id), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_deleted: proto.is_deleted, }) @@ -2833,7 +2894,7 @@ impl File { if self.is_deleted { None } else { - Some(self.entry_id) + self.entry_id } } } @@ -3329,16 +3390,7 @@ impl BackgroundScanner { return false; } - // FS events may come for files which parent directory is excluded, need to check ignore those. - let mut path_to_test = abs_path.clone(); - let mut excluded_file_event = snapshot.is_path_excluded(abs_path) - || snapshot.is_path_excluded(&relative_path); - while !excluded_file_event && path_to_test.pop() { - if snapshot.is_path_excluded(&path_to_test) { - excluded_file_event = true; - } - } - if excluded_file_event { + if snapshot.is_path_excluded(relative_path.to_path_buf()) { if !is_git_related { log::debug!("ignoring FS event for excluded path {relative_path:?}"); } @@ -3522,7 +3574,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(job.path.to_path_buf()) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3593,9 +3645,9 @@ impl BackgroundScanner { } { + let relative_path = job.path.join(child_name); let mut state = self.state.lock(); - if state.snapshot.is_path_excluded(&child_abs_path) { - let relative_path = job.path.join(child_name); + if state.snapshot.is_path_excluded(relative_path.clone()) { log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); continue; diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs index 501a5f736f1934807984d90357593058ef1cde68..fbf8b74d624672c6e62b2a98d96ee8764b500957 100644 --- a/crates/project2/src/worktree_tests.rs +++ b/crates/project2/src/worktree_tests.rs @@ -1055,11 +1055,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { &[ ".git/HEAD", ".git/foo", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", ], - &["target", "node_modules"], + &["target"], &[ ".DS_Store", "src/.DS_Store", @@ -1109,6 +1110,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { ".git/HEAD", ".git/foo", ".git/new_file", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", @@ -1117,7 +1119,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "build_output/new_file", "test_output/new_file", ], - &["target", "node_modules", "test_output"], + &["target", "test_output"], &[ ".DS_Store", "src/.DS_Store", @@ -1177,6 +1179,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .create_entry("a/e".as_ref(), true, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_dir()); @@ -1226,6 +1229,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1261,6 +1265,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1279,6 +1284,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/e.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1295,6 +1301,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("d/e/f/g.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1620,14 +1627,14 @@ fn randomly_mutate_worktree( entry.id.0, new_path ); - let task = worktree.rename_entry(entry.id, new_path, cx).unwrap(); + let task = worktree.rename_entry(entry.id, new_path, cx); cx.background_executor().spawn(async move { - task.await?; + task.await?.unwrap(); Ok(()) }) } _ => { - let task = if entry.is_dir() { + if entry.is_dir() { let child_path = entry.path.join(random_filename(rng)); let is_dir = rng.gen_bool(0.3); log::info!( @@ -1635,15 +1642,20 @@ fn randomly_mutate_worktree( if is_dir { "dir" } else { "file" }, child_path, ); - worktree.create_entry(child_path, is_dir, cx) + let task = worktree.create_entry(child_path, is_dir, cx); + cx.background_executor().spawn(async move { + task.await?; + Ok(()) + }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx) - }; - cx.background_executor().spawn(async move { - task.await?; - Ok(()) - }) + let task = + worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + cx.background_executor().spawn(async move { + task.await?; + Ok(()) + }) + } } } } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index ce039071cf830d3282dcc860fc6ce083576aafc1..81a7c779ca2380e62890f652ddd893660b6abc5f 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -610,7 +610,7 @@ impl ProjectPanel { edited_entry_id = NEW_ENTRY_ID; edit_task = self.project.update(cx, |project, cx| { project.create_entry((worktree_id, &new_path), is_dir, cx) - })?; + }); } else { let new_path = if let Some(parent) = entry.path.clone().parent() { parent.join(&filename) @@ -624,7 +624,7 @@ impl ProjectPanel { edited_entry_id = entry.id; edit_task = self.project.update(cx, |project, cx| { project.rename_entry(entry.id, new_path.as_path(), cx) - })?; + }); }; edit_state.processing_filename = Some(filename); @@ -637,21 +637,22 @@ impl ProjectPanel { cx.notify(); })?; - let new_entry = new_entry?; - this.update(&mut cx, |this, cx| { - if let Some(selection) = &mut this.selection { - if selection.entry_id == edited_entry_id { - selection.worktree_id = worktree_id; - selection.entry_id = new_entry.id; - this.expand_to_selection(cx); + if let Some(new_entry) = new_entry? { + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } } - } - this.update_visible_entries(None, cx); - if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); - } - cx.notify(); - })?; + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + } Ok(()) })) } @@ -931,15 +932,17 @@ impl ProjectPanel { } if clipboard_entry.is_cut() { - if let Some(task) = self.project.update(cx, |project, cx| { - project.rename_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx); - } - } else if let Some(task) = self.project.update(cx, |project, cx| { - project.copy_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx); + self.project + .update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) + } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) } Some(()) @@ -1025,7 +1028,7 @@ impl ProjectPanel { // let mut new_path = destination_path.to_path_buf(); // new_path.push(entry_path.path.file_name()?); // if new_path != entry_path.path.as_ref() { - // let task = project.rename_entry(entry_to_move, new_path, cx)?; + // let task = project.rename_entry(entry_to_move, new_path, cx); // cx.foreground_executor().spawn(task).detach_and_log_err(cx); // } diff --git a/crates/rpc2/proto/zed.proto b/crates/rpc2/proto/zed.proto index a6d27fa57d4a0a9a063f4f0a30b634207ef8ac63..611514aacb44f9445674f2dca0b947eda8088ee3 100644 --- a/crates/rpc2/proto/zed.proto +++ b/crates/rpc2/proto/zed.proto @@ -430,7 +430,7 @@ message ExpandProjectEntryResponse { } message ProjectEntryResponse { - Entry entry = 1; + optional Entry entry = 1; uint64 worktree_scan_id = 2; } @@ -1357,7 +1357,7 @@ message User { message File { uint64 worktree_id = 1; - uint64 entry_id = 2; + optional uint64 entry_id = 2; string path = 3; Timestamp mtime = 4; bool is_deleted = 5; diff --git a/crates/rpc2/src/rpc.rs b/crates/rpc2/src/rpc.rs index 6f35bf64bc23c116c67000eabc29be07f5d6da8c..da0880377fb7ef4e381587a08c4ac3e0a9a2e4dc 100644 --- a/crates/rpc2/src/rpc.rs +++ b/crates/rpc2/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 66; +pub const PROTOCOL_VERSION: u32 = 67; diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 4186a610bf0ae11cdb4bef306e2df2ef1b9b7925..e184fa68762b3480732c222f713069b517b8412b 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -1170,6 +1170,7 @@ mod tests { }) }) .await + .unwrap() .unwrap(); (wt, entry) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 855ce0e931e4c13b78d6312df1b1ad79fb4bd433..ca2c4c2161eda08c2edd137e84b4d357dc25cf4f 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -537,18 +537,21 @@ impl Pane { pub(crate) fn open_item( &mut self, - project_entry_id: ProjectEntryId, + project_entry_id: Option, focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { let mut existing_item = None; - for (index, item) in self.items.iter().enumerate() { - if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; + if let Some(project_entry_id) = project_entry_id { + for (index, item) in self.items.iter().enumerate() { + if item.is_singleton(cx) + && item.project_entry_ids(cx).as_slice() == [project_entry_id] + { + let item = item.boxed_clone(); + existing_item = Some((index, item)); + break; + } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 77d744b9fc9266dabe9b68990a3fe5f2ede38889..b8bfe803d93364ac609efe6e064315a2253d6538 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -10,7 +10,7 @@ mod persistence; pub mod searchable; // todo!() mod modal_layer; -mod shared_screen; +pub mod shared_screen; mod status_bar; mod toolbar; mod workspace_settings; @@ -1853,13 +1853,13 @@ impl Workspace { }) } - pub(crate) fn load_path( + fn load_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task< Result<( - ProjectEntryId, + Option, impl 'static + Send + FnOnce(&mut ViewContext) -> Box, )>, > { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 1b9f1cc719bc889377ba113a78b09a72d91656a1..abe8e7a86f19802f2ab8e7347ba87ba4b6a08050 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -582,8 +582,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, dir_path), true, cx) })? - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } } @@ -592,8 +592,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, file_path), false, cx) })? - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } let editor = workspace @@ -718,3 +718,1846 @@ fn open_bundled_file( }) .detach_and_log_err(cx); } + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use assets::Assets; +// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; +// use fs::{FakeFs, Fs}; +// use gpui::{ +// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle, +// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, +// }; +// use language::LanguageRegistry; +// use project::{project_settings::ProjectSettings, Project, ProjectPath}; +// use serde_json::json; +// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// }; +// use theme::{ThemeRegistry, ThemeSettings}; +// use workspace::{ +// item::{Item, ItemHandle}, +// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle, +// }; + +// #[gpui::test] +// async fn test_open_paths_action(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "aa": null, +// "ab": null, +// }, +// "b": { +// "ba": null, +// "bb": null, +// }, +// "c": { +// "ca": null, +// "cb": null, +// }, +// "d": { +// "da": null, +// "db": null, +// }, +// }), +// ) +// .await; + +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], +// &app_state, +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); + +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); +// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); +// workspace_1.update(cx, |workspace, cx| { +// assert_eq!(workspace.worktrees(cx).count(), 2); +// assert!(workspace.left_dock().read(cx).is_open()); +// assert!(workspace.active_pane().is_focused(cx)); +// }); + +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], +// &app_state, +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 2); + +// // Replace existing windows +// let window = cx.windows()[0].downcast::().unwrap(); +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], +// &app_state, +// Some(window), +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 2); +// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); +// workspace_1.update(cx, |workspace, cx| { +// assert_eq!( +// workspace +// .worktrees(cx) +// .map(|w| w.read(cx).abs_path()) +// .collect::>(), +// &[Path::new("/root/c").into(), Path::new("/root/d").into()] +// ); +// assert!(workspace.left_dock().read(cx).is_open()); +// assert!(workspace.active_pane().is_focused(cx)); +// }); +// } + +// #[gpui::test] +// async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({"a": "hey"})) +// .await; + +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); + +// // When opening the workspace, the window is not in a edited state. +// let window = cx.windows()[0].downcast::().unwrap(); +// let workspace = window.root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert!(!window.is_edited(cx)); + +// // Editing a buffer marks the window as edited. +// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); +// assert!(window.is_edited(cx)); + +// // Undoing the edit restores the window's edited state. +// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); +// assert!(!window.is_edited(cx)); + +// // Redoing the edit marks the window as edited again. +// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); +// assert!(window.is_edited(cx)); + +// // Closing the item restores the window's edited state. +// let close = pane.update(cx, |pane, cx| { +// drop(editor); +// pane.close_active_item(&Default::default(), cx).unwrap() +// }); +// executor.run_until_parked(); + +// window.simulate_prompt_answer(1, cx); +// close.await.unwrap(); +// assert!(!window.is_edited(cx)); + +// // Opening the buffer again doesn't impact the window's edited state. +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert!(!window.is_edited(cx)); + +// // Editing the buffer marks the window as edited. +// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); +// assert!(window.is_edited(cx)); + +// // Ensure closing the window via the mouse gets preempted due to the +// // buffer having unsaved changes. +// assert!(!window.simulate_close(cx)); +// executor.run_until_parked(); +// assert_eq!(cx.windows().len(), 1); + +// // The window is successfully closed after the user dismisses the prompt. +// window.simulate_prompt_answer(1, cx); +// executor.run_until_parked(); +// assert_eq!(cx.windows().len(), 0); +// } + +// #[gpui::test] +// async fn test_new_empty_workspace(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// cx.update(|cx| { +// open_new(&app_state, cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }) +// }) +// .await; + +// let window = cx +// .windows() +// .first() +// .unwrap() +// .downcast::() +// .unwrap(); +// let workspace = window.root(cx); + +// let editor = workspace.update(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(editor.text(cx).is_empty()); +// assert!(!editor.is_dirty(cx)); +// }); + +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name"); +// }); +// } + +// #[gpui::test] +// async fn test_open_entry(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1", +// "file2": "contents 2", +// "file3": "contents 3", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); + +// // Open the first entry +// let entry_1 = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file1.clone()) +// ); +// assert_eq!(pane.items_len(), 1); +// }); + +// // Open the second entry +// workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file2.clone()) +// ); +// assert_eq!(pane.items_len(), 2); +// }); + +// // Open the first entry again. The existing pane item is activated. +// let entry_1b = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); +// assert_eq!(entry_1.id(), entry_1b.id()); + +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file1.clone()) +// ); +// assert_eq!(pane.items_len(), 2); +// }); + +// // Split the pane with the first entry, then open the second entry again. +// workspace +// .update(cx, |w, cx| { +// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); +// w.open_path(file2.clone(), None, true, cx) +// }) +// .await +// .unwrap(); + +// workspace.read_with(cx, |w, cx| { +// assert_eq!( +// w.active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .project_path(cx), +// Some(file2.clone()) +// ); +// }); + +// // Open the third entry twice concurrently. Only one pane item is added. +// let (t1, t2) = workspace.update(cx, |w, cx| { +// ( +// w.open_path(file3.clone(), None, true, cx), +// w.open_path(file3.clone(), None, true, cx), +// ) +// }); +// t1.await.unwrap(); +// t2.await.unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file3.clone()) +// ); +// let pane_entries = pane +// .items() +// .map(|i| i.project_path(cx).unwrap()) +// .collect::>(); +// assert_eq!(pane_entries, &[file1, file2, file3]); +// }); +// } + +// #[gpui::test] +// async fn test_open_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/", +// json!({ +// "dir1": { +// "a.txt": "" +// }, +// "dir2": { +// "b.txt": "" +// }, +// "dir3": { +// "c.txt": "" +// }, +// "d.txt": "" +// }), +// ) +// .await; + +// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); +// let workspace = cx.windows()[0].downcast::().unwrap().root(cx); + +// #[track_caller] +// fn assert_project_panel_selection( +// workspace: &Workspace, +// expected_worktree_path: &Path, +// expected_entry_path: &Path, +// cx: &AppContext, +// ) { +// let project_panel = [ +// workspace.left_dock().read(cx).panel::(), +// workspace.right_dock().read(cx).panel::(), +// workspace.bottom_dock().read(cx).panel::(), +// ] +// .into_iter() +// .find_map(std::convert::identity) +// .expect("found no project panels") +// .read(cx); +// let (selected_worktree, selected_entry) = project_panel +// .selected_entry(cx) +// .expect("project panel should have a selected entry"); +// assert_eq!( +// selected_worktree.abs_path().as_ref(), +// expected_worktree_path, +// "Unexpected project panel selected worktree path" +// ); +// assert_eq!( +// selected_entry.path.as_ref(), +// expected_entry_path, +// "Unexpected project panel selected entry path" +// ); +// } + +// // Open a file within an existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir1/a.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "a.txt" +// ); +// }); + +// // Open a file outside of any existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir2/b.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "b.txt" +// ); +// }); + +// // Ensure opening a directory and one of its children only adds one worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "c.txt" +// ); +// }); + +// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/d.txt".into()], false, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); + +// let visible_worktree_roots = workspace +// .visible_worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// visible_worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); + +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "d.txt" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_opening_excluded_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, |project_settings| { +// project_settings.file_scan_exclusions = +// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); +// }); +// }); +// }); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// ".gitignore": "ignored_dir\n", +// ".git": { +// "HEAD": "ref: refs/heads/main", +// }, +// "regular_dir": { +// "file": "regular file contents", +// }, +// "ignored_dir": { +// "ignored_subdir": { +// "file": "ignored subfile contents", +// }, +// "file": "ignored file contents", +// }, +// "excluded_dir": { +// "file": "excluded file contents", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let paths_to_open = [ +// Path::new("/root/excluded_dir/file").to_path_buf(), +// Path::new("/root/.git/HEAD").to_path_buf(), +// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), +// ]; +// let (opened_workspace, new_items) = cx +// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) +// .await +// .unwrap(); + +// assert_eq!( +// opened_workspace.id(), +// workspace.id(), +// "Excluded files in subfolders of a workspace root should be opened in the workspace" +// ); +// let mut opened_paths = cx.read(|cx| { +// assert_eq!( +// new_items.len(), +// paths_to_open.len(), +// "Expect to get the same number of opened items as submitted paths to open" +// ); +// new_items +// .iter() +// .zip(paths_to_open.iter()) +// .map(|(i, path)| { +// match i { +// Some(Ok(i)) => { +// Some(i.project_path(cx).map(|p| p.path.display().to_string())) +// } +// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), +// None => None, +// } +// .flatten() +// }) +// .collect::>() +// }); +// opened_paths.sort(); +// assert_eq!( +// opened_paths, +// vec![ +// None, +// Some(".git/HEAD".to_string()), +// Some("excluded_dir/file".to_string()), +// ], +// "Excluded files should get opened, excluded dir should not get opened" +// ); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// assert_eq!( +// initial_entries, entries, +// "Workspace entries should not change after opening excluded files and directories paths" +// ); + +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// let mut opened_buffer_paths = pane +// .items() +// .map(|i| { +// i.project_path(cx) +// .expect("all excluded files that got open should have a path") +// .path +// .display() +// .to_string() +// }) +// .collect::>(); +// opened_buffer_paths.sort(); +// assert_eq!( +// opened_buffer_paths, +// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], +// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_save_conflicting_item(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "a.txt": "" })) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // Open a file within an existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx) +// }) +// .await; +// let editor = cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// let item = pane.active_item().unwrap(); +// item.downcast::().unwrap() +// }); + +// editor.update(cx, |editor, cx| editor.handle_input("x", cx)); +// app_state +// .fs +// .as_fake() +// .insert_file("/root/a.txt", "changed".to_string()) +// .await; +// editor +// .condition(cx, |editor, cx| editor.has_conflict(cx)) +// .await; +// cx.read(|cx| assert!(editor.is_dirty(cx))); + +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(0, cx); +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert!(!editor.has_conflict(cx)); +// }); +// } + +// #[gpui::test] +// async fn test_open_and_save_new_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(rust_lang())); +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); + +// // Create a new untitled buffer +// cx.dispatch_action(window.into(), NewFile); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "untitled"); +// assert!(Arc::ptr_eq( +// &editor.language_at(0, cx).unwrap(), +// &languages::PLAIN_TEXT +// )); +// editor.handle_input("hi", cx); +// assert!(editor.is_dirty(cx)); +// }); + +// // Save the buffer. This prompts for a filename. +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|parent_dir| { +// assert_eq!(parent_dir, Path::new("/root")); +// Some(parent_dir.join("the-new-name.rs")) +// }); +// cx.read(|cx| { +// assert!(editor.is_dirty(cx)); +// assert_eq!(editor.read(cx).title(cx), "untitled"); +// }); + +// // When the save completes, the buffer's title is updated and the language is assigned based +// // on the path. +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name.rs"); +// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust"); +// }); + +// // Edit the file and save it again. This time, there is no filename prompt. +// editor.update(cx, |editor, cx| { +// editor.handle_input(" there", cx); +// assert!(editor.is_dirty(cx)); +// }); +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// save_task.await.unwrap(); +// assert!(!cx.did_prompt_for_new_path()); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name.rs") +// }); + +// // Open the same newly-created file in another pane item. The new editor should reuse +// // the same buffer. +// cx.dispatch_action(window.into(), NewFile); +// workspace +// .update(cx, |workspace, cx| { +// workspace.split_and_clone( +// workspace.active_pane().clone(), +// SplitDirection::Right, +// cx, +// ); +// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) +// }) +// .await +// .unwrap(); +// let editor2 = workspace.update(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// cx.read(|cx| { +// assert_eq!( +// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), +// editor.read(cx).buffer().read(cx).as_singleton().unwrap() +// ); +// }) +// } + +// #[gpui::test] +// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + +// let project = Project::test(app_state.fs.clone(), [], cx).await; +// project.update(cx, |project, _| project.languages().add(rust_lang())); +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // Create a new untitled buffer +// cx.dispatch_action(window.into(), NewFile); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(Arc::ptr_eq( +// &editor.language_at(0, cx).unwrap(), +// &languages::PLAIN_TEXT +// )); +// editor.handle_input("hi", cx); +// assert!(editor.is_dirty(cx)); +// }); + +// // Save the buffer. This prompts for a filename. +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); +// save_task.await.unwrap(); +// // The buffer is not dirty anymore and the language is assigned based on the path. +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust") +// }); +// } + +// #[gpui::test] +// async fn test_pane_actions(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1", +// "file2": "contents 2", +// "file3": "contents 3", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); + +// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); + +// workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); + +// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| { +// let editor = pane_1.active_item().unwrap().downcast::().unwrap(); +// assert_eq!(editor.project_path(cx), Some(file1.clone())); +// let buffer = editor.update(cx, |editor, cx| { +// editor.insert("dirt", cx); +// editor.buffer().downgrade() +// }); +// (editor.downgrade(), buffer) +// }); + +// cx.dispatch_action(window.into(), pane::SplitRight); +// let editor_2 = cx.update(|cx| { +// let pane_2 = workspace.read(cx).active_pane().clone(); +// assert_ne!(pane_1, pane_2); + +// let pane2_item = pane_2.read(cx).active_item().unwrap(); +// assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); + +// pane2_item.downcast::().unwrap().downgrade() +// }); +// cx.dispatch_action( +// window.into(), +// workspace::CloseActiveItem { save_intent: None }, +// ); + +// cx.foreground().run_until_parked(); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.panes().len(), 1); +// assert_eq!(workspace.active_pane(), &pane_1); +// }); + +// cx.dispatch_action( +// window.into(), +// workspace::CloseActiveItem { save_intent: None }, +// ); +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(1, cx); +// cx.foreground().run_until_parked(); + +// workspace.read_with(cx, |workspace, cx| { +// assert_eq!(workspace.panes().len(), 1); +// assert!(workspace.active_item(cx).is_none()); +// }); + +// cx.assert_dropped(editor_1); +// cx.assert_dropped(editor_2); +// cx.assert_dropped(buffer); +// } + +// #[gpui::test] +// async fn test_navigation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1\n".repeat(20), +// "file2": "contents 2\n".repeat(20), +// "file3": "contents 3\n".repeat(20), +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); + +// let editor1 = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// editor1.update(cx, |editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { +// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)]) +// }); +// }); +// let editor2 = workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let editor3 = workspace +// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// editor3 +// .update(cx, |editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { +// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)]) +// }); +// editor.newline(&Default::default(), cx); +// editor.newline(&Default::default(), cx); +// editor.move_down(&Default::default(), cx); +// editor.move_down(&Default::default(), cx); +// editor.save(project.clone(), cx) +// }) +// .await +// .unwrap(); +// editor3.update(cx, |editor, cx| { +// editor.set_scroll_position(vec2f(0., 12.5), cx) +// }); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(16, 0), 12.5) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file2.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go back one more time and ensure we don't navigate past the first item in the history. +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file2.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go forward to an item that has been closed, ensuring it gets re-opened at the same +// // location. +// pane.update(cx, |pane, cx| { +// let editor3_id = editor3.id(); +// drop(editor3); +// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(16, 0), 12.5) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. +// pane.update(cx, |pane, cx| { +// let editor2_id = editor2.id(); +// drop(editor2); +// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// app_state +// .fs +// .remove_file(Path::new("/root/a/file2"), Default::default()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Modify file to collapse multiple nav history entries into the same location. +// // Ensure we don't visit the same location twice when navigating. +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]) +// }) +// }); + +// for _ in 0..5 { +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) +// }); +// }); +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)]) +// }) +// }); +// } + +// editor1.update(cx, |editor, cx| { +// editor.transact(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)]) +// }); +// editor.insert("", cx); +// }) +// }); + +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) +// }) +// }); +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(2, 0), 0.) +// ); +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(3, 0), 0.) +// ); + +// fn active_location( +// workspace: &ViewHandle, +// cx: &mut TestAppContext, +// ) -> (ProjectPath, DisplayPoint, f32) { +// workspace.update(cx, |workspace, cx| { +// let item = workspace.active_item(cx).unwrap(); +// let editor = item.downcast::().unwrap(); +// let (selections, scroll_position) = editor.update(cx, |editor, cx| { +// ( +// editor.selections.display_ranges(cx), +// editor.scroll_position(cx), +// ) +// }); +// ( +// item.project_path(cx).unwrap(), +// selections[0].start, +// scroll_position.y(), +// ) +// }) +// } +// } + +// #[gpui::test] +// async fn test_reopening_closed_items(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "", +// "file2": "", +// "file3": "", +// "file4": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); +// let file4 = entries[3].clone(); + +// let file1_item_id = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file2_item_id = workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file3_item_id = workspace +// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file4_item_id = workspace +// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// // Close all the pane items in some arbitrary order. +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), None); + +// // Reopen all the closed items, ensuring they are reopened in the same order +// // in which they were closed. +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// // Reopening past the last closed item is a no-op. +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// // Reopening closed items doesn't interfere with navigation history. +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// fn active_path( +// workspace: &ViewHandle, +// cx: &TestAppContext, +// ) -> Option { +// workspace.read_with(cx, |workspace, cx| { +// let item = workspace.active_item(cx)?; +// item.project_path(cx) +// }) +// } +// } + +// #[gpui::test] +// async fn test_base_keymap(cx: &mut gpui::TestAppContext) { +// struct TestView; + +// impl Entity for TestView { +// type Event = (); +// } + +// impl View for TestView { +// fn ui_name() -> &'static str { +// "TestView" +// } + +// fn render(&mut self, _: &mut ViewContext) -> AnyElement { +// Empty::new().into_any() +// } +// } + +// let executor = cx.background(); +// let fs = FakeFs::new(executor.clone()); + +// actions!(test, [A, B]); +// // From the Atom keymap +// actions!(workspace, [ActivatePreviousPane]); +// // From the JetBrains keymap +// actions!(pane, [ActivatePrevItem]); + +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "Atom" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::A" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init(Assets, cx); +// welcome::init(cx); + +// cx.add_global_action(|_: &A, _cx| {}); +// cx.add_global_action(|_: &B, _cx| {}); +// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); +// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + +// let settings_rx = watch_config_file( +// executor.clone(), +// fs.clone(), +// PathBuf::from("/settings.json"), +// ); +// let keymap_rx = +// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + +// handle_keymap_file_changes(keymap_rx, cx); +// handle_settings_file_changes(settings_rx, cx); +// }); + +// cx.foreground().run_until_parked(); + +// let window = cx.add_window(|_| TestView); + +// // Test loading the keymap base at all +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &A), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the users keymap, while retaining the base keymap +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::B" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &B), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the base, while retaining the users keymap +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "JetBrains" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &B), ("[", &ActivatePrevItem)], +// line!(), +// ); + +// #[track_caller] +// fn assert_key_bindings_for<'a>( +// window: AnyWindowHandle, +// cx: &TestAppContext, +// actions: Vec<(&'static str, &'a dyn Action)>, +// line: u32, +// ) { +// for (key, action) in actions { +// // assert that... +// assert!( +// cx.available_actions(window, 0) +// .into_iter() +// .any(|(_, bound_action, b)| { +// // action names match... +// bound_action.name() == action.name() +// && bound_action.namespace() == action.namespace() +// // and key strokes contain the given key +// && b.iter() +// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) +// }), +// "On {} Failed to find {} with key binding {}", +// line, +// action.name(), +// key +// ); +// } +// } +// } + +// #[gpui::test] +// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { +// struct TestView; + +// impl Entity for TestView { +// type Event = (); +// } + +// impl View for TestView { +// fn ui_name() -> &'static str { +// "TestView" +// } + +// fn render(&mut self, _: &mut ViewContext) -> AnyElement { +// Empty::new().into_any() +// } +// } + +// let executor = cx.background(); +// let fs = FakeFs::new(executor.clone()); + +// actions!(test, [A, B]); +// // From the Atom keymap +// actions!(workspace, [ActivatePreviousPane]); +// // From the JetBrains keymap +// actions!(pane, [ActivatePrevItem]); + +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "Atom" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::A" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init(Assets, cx); +// welcome::init(cx); + +// cx.add_global_action(|_: &A, _cx| {}); +// cx.add_global_action(|_: &B, _cx| {}); +// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); +// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + +// let settings_rx = watch_config_file( +// executor.clone(), +// fs.clone(), +// PathBuf::from("/settings.json"), +// ); +// let keymap_rx = +// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + +// handle_keymap_file_changes(keymap_rx, cx); +// handle_settings_file_changes(settings_rx, cx); +// }); + +// cx.foreground().run_until_parked(); + +// let window = cx.add_window(|_| TestView); + +// // Test loading the keymap base at all +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &A), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test disabling the key binding for the base keymap +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": null +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the base, while retaining the users keymap +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "JetBrains" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!()); + +// #[track_caller] +// fn assert_key_bindings_for<'a>( +// window: AnyWindowHandle, +// cx: &TestAppContext, +// actions: Vec<(&'static str, &'a dyn Action)>, +// line: u32, +// ) { +// for (key, action) in actions { +// // assert that... +// assert!( +// cx.available_actions(window, 0) +// .into_iter() +// .any(|(_, bound_action, b)| { +// // action names match... +// bound_action.name() == action.name() +// && bound_action.namespace() == action.namespace() +// // and key strokes contain the given key +// && b.iter() +// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) +// }), +// "On {} Failed to find {} with key binding {}", +// line, +// action.name(), +// key +// ); +// } +// } +// } + +// #[gpui::test] +// fn test_bundled_settings_and_themes(cx: &mut AppContext) { +// cx.platform() +// .fonts() +// .add_fonts(&[ +// Assets +// .load("fonts/zed-sans/zed-sans-extended.ttf") +// .unwrap() +// .to_vec() +// .into(), +// Assets +// .load("fonts/zed-mono/zed-mono-extended.ttf") +// .unwrap() +// .to_vec() +// .into(), +// Assets +// .load("fonts/plex/IBMPlexSans-Regular.ttf") +// .unwrap() +// .to_vec() +// .into(), +// ]) +// .unwrap(); +// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); +// let mut settings = SettingsStore::default(); +// settings +// .set_default_settings(&settings::default_settings(), cx) +// .unwrap(); +// cx.set_global(settings); +// theme::init(Assets, cx); + +// let mut has_default_theme = false; +// for theme_name in themes.list(false).map(|meta| meta.name) { +// let theme = themes.get(&theme_name).unwrap(); +// assert_eq!(theme.meta.name, theme_name); +// if theme.meta.name == settings::get::(cx).theme.meta.name { +// has_default_theme = true; +// } +// } +// assert!(has_default_theme); +// } + +// #[gpui::test] +// fn test_bundled_languages(cx: &mut AppContext) { +// cx.set_global(SettingsStore::test(cx)); +// let mut languages = LanguageRegistry::test(); +// languages.set_executor(cx.background().clone()); +// let languages = Arc::new(languages); +// let node_runtime = node_runtime::FakeNodeRuntime::new(); +// languages::init(languages.clone(), node_runtime, cx); +// for name in languages.language_names() { +// languages.language_for_name(&name); +// } +// cx.foreground().run_until_parked(); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let mut app_state = AppState::test(cx); +// let state = Arc::get_mut(&mut app_state).unwrap(); +// state.initialize_workspace = initialize_workspace; +// state.build_window_options = build_window_options; +// theme::init((), cx); +// audio::init((), cx); +// channel::init(&app_state.client, app_state.user_store.clone(), cx); +// call::init(app_state.client.clone(), app_state.user_store.clone(), cx); +// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// language::init(cx); +// editor::init(cx); +// project_panel::init_settings(cx); +// collab_ui::init(&app_state, cx); +// pane::init(cx); +// project_panel::init((), cx); +// terminal_view::init(cx); +// assistant::init(cx); +// app_state +// }) +// } + +// fn rust_lang() -> Arc { +// Arc::new(language::Language::new( +// language::LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )) +// } +// } From 09db455db25ef222abf74b5d45cf4138117ef334 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 15:38:36 +0100 Subject: [PATCH 32/48] Port `semantic_index` to gpui2 Co-Authored-By: Julia Risley --- Cargo.lock | 51 + Cargo.toml | 3 +- crates/ai2/src/auth.rs | 2 +- crates/ai2/src/providers/open_ai/embedding.rs | 4 +- crates/gpui2/src/app/entity_map.rs | 19 +- crates/semantic_index2/Cargo.toml | 69 + crates/semantic_index2/README.md | 20 + crates/semantic_index2/eval/gpt-engineer.json | 114 ++ crates/semantic_index2/eval/tree-sitter.json | 104 + crates/semantic_index2/src/db.rs | 603 ++++++ crates/semantic_index2/src/embedding_queue.rs | 169 ++ crates/semantic_index2/src/parsing.rs | 414 ++++ crates/semantic_index2/src/semantic_index.rs | 1280 +++++++++++++ .../src/semantic_index_settings.rs | 28 + .../src/semantic_index_tests.rs | 1697 +++++++++++++++++ crates/workspace2/src/workspace2.rs | 2 - 16 files changed, 4569 insertions(+), 10 deletions(-) create mode 100644 crates/semantic_index2/Cargo.toml create mode 100644 crates/semantic_index2/README.md create mode 100644 crates/semantic_index2/eval/gpt-engineer.json create mode 100644 crates/semantic_index2/eval/tree-sitter.json create mode 100644 crates/semantic_index2/src/db.rs create mode 100644 crates/semantic_index2/src/embedding_queue.rs create mode 100644 crates/semantic_index2/src/parsing.rs create mode 100644 crates/semantic_index2/src/semantic_index.rs create mode 100644 crates/semantic_index2/src/semantic_index_settings.rs create mode 100644 crates/semantic_index2/src/semantic_index_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6121ec9718ff2db2e0130e614ffe4a9076cc5d95..39683c9fc11b41700a5b5123287444979ae87bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8232,6 +8232,57 @@ dependencies = [ "workspace", ] +[[package]] +name = "semantic_index2" +version = "0.1.0" +dependencies = [ + "ai2", + "anyhow", + "async-trait", + "client2", + "collections", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "globset", + "gpui2", + "language2", + "lazy_static", + "log", + "ndarray", + "node_runtime", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "postage", + "pretty_assertions", + "project2", + "rand 0.8.5", + "rpc2", + "rusqlite", + "rust-embed", + "schemars", + "serde", + "serde_json", + "settings2", + "sha1", + "smol", + "tempdir", + "tiktoken-rs", + "tree-sitter", + "tree-sitter-cpp", + "tree-sitter-elixir", + "tree-sitter-json 0.20.0", + "tree-sitter-lua", + "tree-sitter-php", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-toml", + "tree-sitter-typescript", + "unindent", + "util", + "workspace2", +] + [[package]] name = "semver" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 3658ffad297f2c9d4fb3bf5eb6b03ede591d37e6..610a4dc11e03cc24c86db033d7b5b95c25ab64ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,8 @@ members = [ "crates/rpc2", "crates/search", "crates/search2", + "crates/semantic_index", + "crates/semantic_index2", "crates/settings", "crates/settings2", "crates/snippet", @@ -114,7 +116,6 @@ members = [ "crates/theme_selector2", "crates/ui2", "crates/util", - "crates/semantic_index", "crates/story", "crates/vim", "crates/vcs_menu", diff --git a/crates/ai2/src/auth.rs b/crates/ai2/src/auth.rs index baa1fe7b83299ef66db4ecf0d0403b1ac92dc5bc..1ea49bd615999a7f0318d3e205d3f86cee9c64a8 100644 --- a/crates/ai2/src/auth.rs +++ b/crates/ai2/src/auth.rs @@ -7,7 +7,7 @@ pub enum ProviderCredential { NotNeeded, } -pub trait CredentialProvider { +pub trait CredentialProvider: Send + Sync { fn has_credentials(&self) -> bool; fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); diff --git a/crates/ai2/src/providers/open_ai/embedding.rs b/crates/ai2/src/providers/open_ai/embedding.rs index 8f62c8dc0d440675146cdcae6f1e4a803de15cec..d5fe4e8c5842709c587b9898862e0a2461461ed2 100644 --- a/crates/ai2/src/providers/open_ai/embedding.rs +++ b/crates/ai2/src/providers/open_ai/embedding.rs @@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider { model: OpenAILanguageModel, credential: Arc>, pub client: Arc, - pub executor: Arc, + pub executor: BackgroundExecutor, rate_limit_count_rx: watch::Receiver>, rate_limit_count_tx: Arc>>>, } @@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage { } impl OpenAIEmbeddingProvider { - pub fn new(client: Arc, executor: Arc) -> Self { + pub fn new(client: Arc, executor: BackgroundExecutor) -> Self { let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index a34582f4f4024c5eeab0363d45896be7eaa2ee95..99d8542eba89cde981f8cbf966d298ec1d8af2af 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -482,10 +482,6 @@ impl WeakModel { /// Update the entity referenced by this model with the given function if /// the referenced entity still exists. Returns an error if the entity has /// been released. - /// - /// The update function receives a context appropriate for its environment. - /// When updating in an `AppContext`, it receives a `ModelContext`. - /// When updating an a `WindowContext`, it receives a `ViewContext`. pub fn update( &self, cx: &mut C, @@ -501,6 +497,21 @@ impl WeakModel { .map(|this| cx.update_model(&this, update)), ) } + + /// Reads the entity referenced by this model with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result + where + C: Context, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .ok_or_else(|| anyhow!("entity release")) + .map(|this| cx.read_model(&this, read)), + ) + } } impl Hash for WeakModel { diff --git a/crates/semantic_index2/Cargo.toml b/crates/semantic_index2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..65ffb05ca5c8bc9f2d06a864f4c22acec3f34594 --- /dev/null +++ b/crates/semantic_index2/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "semantic_index2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/semantic_index.rs" +doctest = false + +[dependencies] +ai = { package = "ai2", path = "../ai2" } +collections = { path = "../collections" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } +rpc = { package = "rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } +anyhow.workspace = true +postage.workspace = true +futures.workspace = true +ordered-float.workspace = true +smol.workspace = true +rusqlite.workspace = true +log.workspace = true +tree-sitter.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_json.workspace = true +async-trait.workspace = true +tiktoken-rs.workspace = true +parking_lot.workspace = true +rand.workspace = true +schemars.workspace = true +globset.workspace = true +sha1 = "0.10.5" +ndarray = { version = "0.15.0" } + +[dev-dependencies] +ai = { package = "ai2", path = "../ai2", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"]} +rust-embed = { version = "8.0", features = ["include-exclude"] } +client = { package = "client2", path = "../client2" } +node_runtime = { path = "../node_runtime"} + +pretty_assertions.workspace = true +rand.workspace = true +unindent.workspace = true +tempdir.workspace = true +ctor.workspace = true +env_logger.workspace = true + +tree-sitter-typescript.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-toml.workspace = true +tree-sitter-cpp.workspace = true +tree-sitter-elixir.workspace = true +tree-sitter-lua.workspace = true +tree-sitter-ruby.workspace = true +tree-sitter-php.workspace = true diff --git a/crates/semantic_index2/README.md b/crates/semantic_index2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..85f83af121ed96a51ac84165c19cda3cd8aff7d4 --- /dev/null +++ b/crates/semantic_index2/README.md @@ -0,0 +1,20 @@ + +# Semantic Index + +## Evaluation + +### Metrics + +nDCG@k: +- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return. +- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?" + +MRR@k: +- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list." + +MAP@k: +- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list. + +Resources: +- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg) +- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0) diff --git a/crates/semantic_index2/eval/gpt-engineer.json b/crates/semantic_index2/eval/gpt-engineer.json new file mode 100644 index 0000000000000000000000000000000000000000..d008cc65d13b0c6a718beb57ece2393bb999029c --- /dev/null +++ b/crates/semantic_index2/eval/gpt-engineer.json @@ -0,0 +1,114 @@ +{ + "repo": "https://github.com/AntonOsika/gpt-engineer.git", + "commit": "7735a6445bae3611c62f521e6464c67c957f87c2", + "assertions": [ + { + "query": "How do I contribute to this project?", + "matches": [ + ".github/CONTRIBUTING.md:1", + "ROADMAP.md:48" + ] + }, + { + "query": "What version of the openai package is active?", + "matches": [ + "pyproject.toml:14" + ] + }, + { + "query": "Ask user for clarification", + "matches": [ + "gpt_engineer/steps.py:69" + ] + }, + { + "query": "generate tests for python code", + "matches": [ + "gpt_engineer/steps.py:153" + ] + }, + { + "query": "get item from database based on key", + "matches": [ + "gpt_engineer/db.py:42", + "gpt_engineer/db.py:68" + ] + }, + { + "query": "prompt user to select files", + "matches": [ + "gpt_engineer/file_selector.py:171", + "gpt_engineer/file_selector.py:306", + "gpt_engineer/file_selector.py:289", + "gpt_engineer/file_selector.py:234" + ] + }, + { + "query": "send to rudderstack", + "matches": [ + "gpt_engineer/collect.py:11", + "gpt_engineer/collect.py:38" + ] + }, + { + "query": "parse code blocks from chat messages", + "matches": [ + "gpt_engineer/chat_to_files.py:10", + "docs/intro/chat_parsing.md:1" + ] + }, + { + "query": "how do I use the docker cli?", + "matches": [ + "docker/README.md:1" + ] + }, + { + "query": "ask the user if the code ran successfully?", + "matches": [ + "gpt_engineer/learning.py:54" + ] + }, + { + "query": "how is consent granted by the user?", + "matches": [ + "gpt_engineer/learning.py:107", + "gpt_engineer/learning.py:130", + "gpt_engineer/learning.py:152" + ] + }, + { + "query": "what are all the different steps the agent can take?", + "matches": [ + "docs/intro/steps_module.md:1", + "gpt_engineer/steps.py:391" + ] + }, + { + "query": "ask the user for clarification?", + "matches": [ + "gpt_engineer/steps.py:69" + ] + }, + { + "query": "what models are available?", + "matches": [ + "gpt_engineer/ai.py:315", + "gpt_engineer/ai.py:341", + "docs/open-models.md:1" + ] + }, + { + "query": "what is the current focus of the project?", + "matches": [ + "ROADMAP.md:11" + ] + }, + { + "query": "does the agent know how to fix code?", + "matches": [ + "gpt_engineer/steps.py:367" + ] + } + ] +} diff --git a/crates/semantic_index2/eval/tree-sitter.json b/crates/semantic_index2/eval/tree-sitter.json new file mode 100644 index 0000000000000000000000000000000000000000..d3dcc86937d723e8a00e2b2bfc91e86163c1aac3 --- /dev/null +++ b/crates/semantic_index2/eval/tree-sitter.json @@ -0,0 +1,104 @@ +{ + "repo": "https://github.com/tree-sitter/tree-sitter.git", + "commit": "46af27796a76c72d8466627d499f2bca4af958ee", + "assertions": [ + { + "query": "What attributes are available for the tags configuration struct?", + "matches": [ + "tags/src/lib.rs:24" + ] + }, + { + "query": "create a new tag configuration", + "matches": [ + "tags/src/lib.rs:119" + ] + }, + { + "query": "generate tags based on config", + "matches": [ + "tags/src/lib.rs:261" + ] + }, + { + "query": "match on ts quantifier in rust", + "matches": [ + "lib/binding_rust/lib.rs:139" + ] + }, + { + "query": "cli command to generate tags", + "matches": [ + "cli/src/tags.rs:10" + ] + }, + { + "query": "what version of the tree-sitter-tags package is active?", + "matches": [ + "tags/Cargo.toml:4" + ] + }, + { + "query": "Insert a new parse state", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:153" + ] + }, + { + "query": "Handle conflict when numerous actions occur on the same symbol", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:363", + "cli/src/generate/build_tables/build_parse_table.rs:442" + ] + }, + { + "query": "Match based on associativity of actions", + "matches": [ + "cri/src/generate/build_tables/build_parse_table.rs:542" + ] + }, + { + "query": "Format token set display", + "matches": [ + "cli/src/generate/build_tables/item.rs:246" + ] + }, + { + "query": "extract choices from rule", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:124" + ] + }, + { + "query": "How do we identify if a symbol is being used?", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:175" + ] + }, + { + "query": "How do we launch the playground?", + "matches": [ + "cli/src/playground.rs:46" + ] + }, + { + "query": "How do we test treesitter query matches in rust?", + "matches": [ + "cli/src/query_testing.rs:152", + "cli/src/tests/query_test.rs:781", + "cli/src/tests/query_test.rs:2163", + "cli/src/tests/query_test.rs:3781", + "cli/src/tests/query_test.rs:887" + ] + }, + { + "query": "What does the CLI do?", + "matches": [ + "cli/README.md:10", + "cli/loader/README.md:3", + "docs/section-5-implementation.md:14", + "docs/section-5-implementation.md:18" + ] + } + ] +} diff --git a/crates/semantic_index2/src/db.rs b/crates/semantic_index2/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..f34baeaaae1373f4186e604689ea66df2094925d --- /dev/null +++ b/crates/semantic_index2/src/db.rs @@ -0,0 +1,603 @@ +use crate::{ + parsing::{Span, SpanDigest}, + SEMANTIC_INDEX_VERSION, +}; +use ai::embedding::Embedding; +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +use futures::channel::oneshot; +use gpui::BackgroundExecutor; +use ndarray::{Array1, Array2}; +use ordered_float::OrderedFloat; +use project::Fs; +use rpc::proto::Timestamp; +use rusqlite::params; +use rusqlite::types::Value; +use std::{ + future::Future, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::SystemTime, +}; +use util::{paths::PathMatcher, TryFutureExt}; + +pub fn argsort(data: &[T]) -> Vec { + let mut indices = (0..data.len()).collect::>(); + indices.sort_by_key(|&i| &data[i]); + indices.reverse(); + indices +} + +#[derive(Debug)] +pub struct FileRecord { + pub id: usize, + pub relative_path: String, + pub mtime: Timestamp, +} + +#[derive(Clone)] +pub struct VectorDatabase { + path: Arc, + transactions: + smol::channel::Sender>, +} + +impl VectorDatabase { + pub async fn new( + fs: Arc, + path: Arc, + executor: BackgroundExecutor, + ) -> Result { + if let Some(db_directory) = path.parent() { + fs.create_dir(db_directory).await?; + } + + let (transactions_tx, transactions_rx) = smol::channel::unbounded::< + Box, + >(); + executor + .spawn({ + let path = path.clone(); + async move { + let mut connection = rusqlite::Connection::open(&path)?; + + connection.pragma_update(None, "journal_mode", "wal")?; + connection.pragma_update(None, "synchronous", "normal")?; + connection.pragma_update(None, "cache_size", 1000000)?; + connection.pragma_update(None, "temp_store", "MEMORY")?; + + while let Ok(transaction) = transactions_rx.recv().await { + transaction(&mut connection); + } + + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + let this = Self { + transactions: transactions_tx, + path, + }; + this.initialize_database().await?; + Ok(this) + } + + pub fn path(&self) -> &Arc { + &self.path + } + + fn transact(&self, f: F) -> impl Future> + where + F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result, + T: 'static + Send, + { + let (tx, rx) = oneshot::channel(); + let transactions = self.transactions.clone(); + async move { + if transactions + .send(Box::new(|connection| { + let result = connection + .transaction() + .map_err(|err| anyhow!(err)) + .and_then(|transaction| { + let result = f(&transaction)?; + transaction.commit()?; + Ok(result) + }); + let _ = tx.send(result); + })) + .await + .is_err() + { + return Err(anyhow!("connection was dropped"))?; + } + rx.await? + } + } + + fn initialize_database(&self) -> impl Future> { + self.transact(|db| { + rusqlite::vtab::array::load_module(&db)?; + + // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped + let version_query = db.prepare("SELECT version from semantic_index_config"); + let version = version_query + .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?))); + if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) { + log::trace!("vector database schema up to date"); + return Ok(()); + } + + log::trace!("vector database schema out of date. updating..."); + // We renamed the `documents` table to `spans`, so we want to drop + // `documents` without recreating it if it exists. + db.execute("DROP TABLE IF EXISTS documents", []) + .context("failed to drop 'documents' table")?; + db.execute("DROP TABLE IF EXISTS spans", []) + .context("failed to drop 'spans' table")?; + db.execute("DROP TABLE IF EXISTS files", []) + .context("failed to drop 'files' table")?; + db.execute("DROP TABLE IF EXISTS worktrees", []) + .context("failed to drop 'worktrees' table")?; + db.execute("DROP TABLE IF EXISTS semantic_index_config", []) + .context("failed to drop 'semantic_index_config' table")?; + + // Initialize Vector Databasing Tables + db.execute( + "CREATE TABLE semantic_index_config ( + version INTEGER NOT NULL + )", + [], + )?; + + db.execute( + "INSERT INTO semantic_index_config (version) VALUES (?1)", + params![SEMANTIC_INDEX_VERSION], + )?; + + db.execute( + "CREATE TABLE worktrees ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + absolute_path VARCHAR NOT NULL + ); + CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path); + ", + [], + )?; + + db.execute( + "CREATE TABLE files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + worktree_id INTEGER NOT NULL, + relative_path VARCHAR NOT NULL, + mtime_seconds INTEGER NOT NULL, + mtime_nanos INTEGER NOT NULL, + FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE + )", + [], + )?; + + db.execute( + "CREATE UNIQUE INDEX files_worktree_id_and_relative_path ON files (worktree_id, relative_path)", + [], + )?; + + db.execute( + "CREATE TABLE spans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id INTEGER NOT NULL, + start_byte INTEGER NOT NULL, + end_byte INTEGER NOT NULL, + name VARCHAR NOT NULL, + embedding BLOB NOT NULL, + digest BLOB NOT NULL, + FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE + )", + [], + )?; + db.execute( + "CREATE INDEX spans_digest ON spans (digest)", + [], + )?; + + log::trace!("vector database initialized with updated schema."); + Ok(()) + }) + } + + pub fn delete_file( + &self, + worktree_id: i64, + delete_path: Arc, + ) -> impl Future> { + self.transact(move |db| { + db.execute( + "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2", + params![worktree_id, delete_path.to_str()], + )?; + Ok(()) + }) + } + + pub fn insert_file( + &self, + worktree_id: i64, + path: Arc, + mtime: SystemTime, + spans: Vec, + ) -> impl Future> { + self.transact(move |db| { + // Return the existing ID, if both the file and mtime match + let mtime = Timestamp::from(mtime); + + db.execute( + " + REPLACE INTO files + (worktree_id, relative_path, mtime_seconds, mtime_nanos) + VALUES (?1, ?2, ?3, ?4) + ", + params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos], + )?; + + let file_id = db.last_insert_rowid(); + + let mut query = db.prepare( + " + INSERT INTO spans + (file_id, start_byte, end_byte, name, embedding, digest) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ", + )?; + + for span in spans { + query.execute(params![ + file_id, + span.range.start.to_string(), + span.range.end.to_string(), + span.name, + span.embedding, + span.digest + ])?; + } + + Ok(()) + }) + } + + pub fn worktree_previously_indexed( + &self, + worktree_root_path: &Path, + ) -> impl Future> { + let worktree_root_path = worktree_root_path.to_string_lossy().into_owned(); + self.transact(move |db| { + let mut worktree_query = + db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; + let worktree_id = worktree_query + .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?)); + + if worktree_id.is_ok() { + return Ok(true); + } else { + return Ok(false); + } + }) + } + + pub fn embeddings_for_digests( + &self, + digests: Vec, + ) -> impl Future>> { + self.transact(move |db| { + let mut query = db.prepare( + " + SELECT digest, embedding + FROM spans + WHERE digest IN rarray(?) + ", + )?; + let mut embeddings_by_digest = HashMap::default(); + let digests = Rc::new( + digests + .into_iter() + .map(|p| Value::Blob(p.0.to_vec())) + .collect::>(), + ); + let rows = query.query_map(params![digests], |row| { + Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) + })?; + + for row in rows { + if let Ok(row) = row { + embeddings_by_digest.insert(row.0, row.1); + } + } + + Ok(embeddings_by_digest) + }) + } + + pub fn embeddings_for_files( + &self, + worktree_id_file_paths: HashMap>>, + ) -> impl Future>> { + self.transact(move |db| { + let mut query = db.prepare( + " + SELECT digest, embedding + FROM spans + LEFT JOIN files ON files.id = spans.file_id + WHERE files.worktree_id = ? AND files.relative_path IN rarray(?) + ", + )?; + let mut embeddings_by_digest = HashMap::default(); + for (worktree_id, file_paths) in worktree_id_file_paths { + let file_paths = Rc::new( + file_paths + .into_iter() + .map(|p| Value::Text(p.to_string_lossy().into_owned())) + .collect::>(), + ); + let rows = query.query_map(params![worktree_id, file_paths], |row| { + Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) + })?; + + for row in rows { + if let Ok(row) = row { + embeddings_by_digest.insert(row.0, row.1); + } + } + } + + Ok(embeddings_by_digest) + }) + } + + pub fn find_or_create_worktree( + &self, + worktree_root_path: Arc, + ) -> impl Future> { + self.transact(move |db| { + let mut worktree_query = + db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; + let worktree_id = worktree_query + .query_row(params![worktree_root_path.to_string_lossy()], |row| { + Ok(row.get::<_, i64>(0)?) + }); + + if worktree_id.is_ok() { + return Ok(worktree_id?); + } + + // If worktree_id is Err, insert new worktree + db.execute( + "INSERT into worktrees (absolute_path) VALUES (?1)", + params![worktree_root_path.to_string_lossy()], + )?; + Ok(db.last_insert_rowid()) + }) + } + + pub fn get_file_mtimes( + &self, + worktree_id: i64, + ) -> impl Future>> { + self.transact(move |db| { + let mut statement = db.prepare( + " + SELECT relative_path, mtime_seconds, mtime_nanos + FROM files + WHERE worktree_id = ?1 + ORDER BY relative_path", + )?; + let mut result: HashMap = HashMap::default(); + for row in statement.query_map(params![worktree_id], |row| { + Ok(( + row.get::<_, String>(0)?.into(), + Timestamp { + seconds: row.get(1)?, + nanos: row.get(2)?, + } + .into(), + )) + })? { + let row = row?; + result.insert(row.0, row.1); + } + Ok(result) + }) + } + + pub fn top_k_search( + &self, + query_embedding: &Embedding, + limit: usize, + file_ids: &[i64], + ) -> impl Future)>>> { + let file_ids = file_ids.to_vec(); + let query = query_embedding.clone().0; + let query = Array1::from_vec(query); + self.transact(move |db| { + let mut query_statement = db.prepare( + " + SELECT + id, embedding + FROM + spans + WHERE + file_id IN rarray(?) + ", + )?; + + let deserialized_rows = query_statement + .query_map(params![ids_to_sql(&file_ids)], |row| { + Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?)) + })? + .filter_map(|row| row.ok()) + .collect::>(); + + if deserialized_rows.len() == 0 { + return Ok(Vec::new()); + } + + // Get Length of Embeddings Returned + let embedding_len = deserialized_rows[0].1 .0.len(); + + let batch_n = 1000; + let mut batches = Vec::new(); + let mut batch_ids = Vec::new(); + let mut batch_embeddings: Vec = Vec::new(); + deserialized_rows.iter().for_each(|(id, embedding)| { + batch_ids.push(id); + batch_embeddings.extend(&embedding.0); + + if batch_ids.len() == batch_n { + let embeddings = std::mem::take(&mut batch_embeddings); + let ids = std::mem::take(&mut batch_ids); + let array = + Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings); + match array { + Ok(array) => { + batches.push((ids, array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + } + }); + + if batch_ids.len() > 0 { + let array = Array2::from_shape_vec( + (batch_ids.len(), embedding_len), + batch_embeddings.clone(), + ); + match array { + Ok(array) => { + batches.push((batch_ids.clone(), array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + } + + let mut ids: Vec = Vec::new(); + let mut results = Vec::new(); + for (batch_ids, array) in batches { + let scores = array + .dot(&query.t()) + .to_vec() + .iter() + .map(|score| OrderedFloat(*score)) + .collect::>>(); + results.extend(scores); + ids.extend(batch_ids); + } + + let sorted_idx = argsort(&results); + let mut sorted_results = Vec::new(); + let last_idx = limit.min(sorted_idx.len()); + for idx in &sorted_idx[0..last_idx] { + sorted_results.push((ids[*idx] as i64, results[*idx])) + } + + Ok(sorted_results) + }) + } + + pub fn retrieve_included_file_ids( + &self, + worktree_ids: &[i64], + includes: &[PathMatcher], + excludes: &[PathMatcher], + ) -> impl Future>> { + let worktree_ids = worktree_ids.to_vec(); + let includes = includes.to_vec(); + let excludes = excludes.to_vec(); + self.transact(move |db| { + let mut file_query = db.prepare( + " + SELECT + id, relative_path + FROM + files + WHERE + worktree_id IN rarray(?) + ", + )?; + + let mut file_ids = Vec::::new(); + let mut rows = file_query.query([ids_to_sql(&worktree_ids)])?; + + while let Some(row) = rows.next()? { + let file_id = row.get(0)?; + let relative_path = row.get_ref(1)?.as_str()?; + let included = + includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path)); + let excluded = excludes.iter().any(|glob| glob.is_match(relative_path)); + if included && !excluded { + file_ids.push(file_id); + } + } + + anyhow::Ok(file_ids) + }) + } + + pub fn spans_for_ids( + &self, + ids: &[i64], + ) -> impl Future)>>> { + let ids = ids.to_vec(); + self.transact(move |db| { + let mut statement = db.prepare( + " + SELECT + spans.id, + files.worktree_id, + files.relative_path, + spans.start_byte, + spans.end_byte + FROM + spans, files + WHERE + spans.file_id = files.id AND + spans.id in rarray(?) + ", + )?; + + let result_iter = statement.query_map(params![ids_to_sql(&ids)], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, i64>(1)?, + row.get::<_, String>(2)?.into(), + row.get(3)?..row.get(4)?, + )) + })?; + + let mut values_by_id = HashMap::)>::default(); + for row in result_iter { + let (id, worktree_id, path, range) = row?; + values_by_id.insert(id, (worktree_id, path, range)); + } + + let mut results = Vec::with_capacity(ids.len()); + for id in &ids { + let value = values_by_id + .remove(id) + .ok_or(anyhow!("missing span id {}", id))?; + results.push(value); + } + + Ok(results) + }) + } +} + +fn ids_to_sql(ids: &[i64]) -> Rc> { + Rc::new( + ids.iter() + .copied() + .map(|v| rusqlite::types::Value::from(v)) + .collect::>(), + ) +} diff --git a/crates/semantic_index2/src/embedding_queue.rs b/crates/semantic_index2/src/embedding_queue.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2371a1196b59834c0d5fcc034f3b0a364e4d38b --- /dev/null +++ b/crates/semantic_index2/src/embedding_queue.rs @@ -0,0 +1,169 @@ +use crate::{parsing::Span, JobHandle}; +use ai::embedding::EmbeddingProvider; +use gpui::BackgroundExecutor; +use parking_lot::Mutex; +use smol::channel; +use std::{mem, ops::Range, path::Path, sync::Arc, time::SystemTime}; + +#[derive(Clone)] +pub struct FileToEmbed { + pub worktree_id: i64, + pub path: Arc, + pub mtime: SystemTime, + pub spans: Vec, + pub job_handle: JobHandle, +} + +impl std::fmt::Debug for FileToEmbed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FileToEmbed") + .field("worktree_id", &self.worktree_id) + .field("path", &self.path) + .field("mtime", &self.mtime) + .field("spans", &self.spans) + .finish_non_exhaustive() + } +} + +impl PartialEq for FileToEmbed { + fn eq(&self, other: &Self) -> bool { + self.worktree_id == other.worktree_id + && self.path == other.path + && self.mtime == other.mtime + && self.spans == other.spans + } +} + +pub struct EmbeddingQueue { + embedding_provider: Arc, + pending_batch: Vec, + executor: BackgroundExecutor, + pending_batch_token_count: usize, + finished_files_tx: channel::Sender, + finished_files_rx: channel::Receiver, +} + +#[derive(Clone)] +pub struct FileFragmentToEmbed { + file: Arc>, + span_range: Range, +} + +impl EmbeddingQueue { + pub fn new( + embedding_provider: Arc, + executor: BackgroundExecutor, + ) -> Self { + let (finished_files_tx, finished_files_rx) = channel::unbounded(); + Self { + embedding_provider, + executor, + pending_batch: Vec::new(), + pending_batch_token_count: 0, + finished_files_tx, + finished_files_rx, + } + } + + pub fn push(&mut self, file: FileToEmbed) { + if file.spans.is_empty() { + self.finished_files_tx.try_send(file).unwrap(); + return; + } + + let file = Arc::new(Mutex::new(file)); + + self.pending_batch.push(FileFragmentToEmbed { + file: file.clone(), + span_range: 0..0, + }); + + let mut fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range; + for (ix, span) in file.lock().spans.iter().enumerate() { + let span_token_count = if span.embedding.is_none() { + span.token_count + } else { + 0 + }; + + let next_token_count = self.pending_batch_token_count + span_token_count; + if next_token_count > self.embedding_provider.max_tokens_per_batch() { + let range_end = fragment_range.end; + self.flush(); + self.pending_batch.push(FileFragmentToEmbed { + file: file.clone(), + span_range: range_end..range_end, + }); + fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range; + } + + fragment_range.end = ix + 1; + self.pending_batch_token_count += span_token_count; + } + } + + pub fn flush(&mut self) { + let batch = mem::take(&mut self.pending_batch); + self.pending_batch_token_count = 0; + if batch.is_empty() { + return; + } + + let finished_files_tx = self.finished_files_tx.clone(); + let embedding_provider = self.embedding_provider.clone(); + + self.executor + .spawn(async move { + let mut spans = Vec::new(); + for fragment in &batch { + let file = fragment.file.lock(); + spans.extend( + file.spans[fragment.span_range.clone()] + .iter() + .filter(|d| d.embedding.is_none()) + .map(|d| d.content.clone()), + ); + } + + // If spans is 0, just send the fragment to the finished files if its the last one. + if spans.is_empty() { + for fragment in batch.clone() { + if let Some(file) = Arc::into_inner(fragment.file) { + finished_files_tx.try_send(file.into_inner()).unwrap(); + } + } + return; + }; + + match embedding_provider.embed_batch(spans).await { + Ok(embeddings) => { + let mut embeddings = embeddings.into_iter(); + for fragment in batch { + for span in &mut fragment.file.lock().spans[fragment.span_range.clone()] + .iter_mut() + .filter(|d| d.embedding.is_none()) + { + if let Some(embedding) = embeddings.next() { + span.embedding = Some(embedding); + } else { + log::error!("number of embeddings != number of documents"); + } + } + + if let Some(file) = Arc::into_inner(fragment.file) { + finished_files_tx.try_send(file.into_inner()).unwrap(); + } + } + } + Err(error) => { + log::error!("{:?}", error); + } + } + }) + .detach(); + } + + pub fn finished_files(&self) -> channel::Receiver { + self.finished_files_rx.clone() + } +} diff --git a/crates/semantic_index2/src/parsing.rs b/crates/semantic_index2/src/parsing.rs new file mode 100644 index 0000000000000000000000000000000000000000..cb15ca453b2c0640739bd44a95482ca527b8d91b --- /dev/null +++ b/crates/semantic_index2/src/parsing.rs @@ -0,0 +1,414 @@ +use ai::{ + embedding::{Embedding, EmbeddingProvider}, + models::TruncationDirection, +}; +use anyhow::{anyhow, Result}; +use language::{Grammar, Language}; +use rusqlite::{ + types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}, + ToSql, +}; +use sha1::{Digest, Sha1}; +use std::{ + borrow::Cow, + cmp::{self, Reverse}, + collections::HashSet, + ops::Range, + path::Path, + sync::Arc, +}; +use tree_sitter::{Parser, QueryCursor}; + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct SpanDigest(pub [u8; 20]); + +impl FromSql for SpanDigest { + fn column_result(value: ValueRef) -> FromSqlResult { + let blob = value.as_blob()?; + let bytes = + blob.try_into() + .map_err(|_| rusqlite::types::FromSqlError::InvalidBlobSize { + expected_size: 20, + blob_size: blob.len(), + })?; + return Ok(SpanDigest(bytes)); + } +} + +impl ToSql for SpanDigest { + fn to_sql(&self) -> rusqlite::Result { + self.0.to_sql() + } +} + +impl From<&'_ str> for SpanDigest { + fn from(value: &'_ str) -> Self { + let mut sha1 = Sha1::new(); + sha1.update(value); + Self(sha1.finalize().into()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Span { + pub name: String, + pub range: Range, + pub content: String, + pub embedding: Option, + pub digest: SpanDigest, + pub token_count: usize, +} + +const CODE_CONTEXT_TEMPLATE: &str = + "The below code snippet is from file ''\n\n```\n\n```"; +const ENTIRE_FILE_TEMPLATE: &str = + "The below snippet is from file ''\n\n```\n\n```"; +const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file ''\n\n"; +pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[ + "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme", +]; + +pub struct CodeContextRetriever { + pub parser: Parser, + pub cursor: QueryCursor, + pub embedding_provider: Arc, +} + +// Every match has an item, this represents the fundamental treesitter symbol and anchors the search +// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication. +// If there are preceeding comments, we track this with a context capture +// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture +// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture +#[derive(Debug, Clone)] +pub struct CodeContextMatch { + pub start_col: usize, + pub item_range: Option>, + pub name_range: Option>, + pub context_ranges: Vec>, + pub collapse_ranges: Vec>, +} + +impl CodeContextRetriever { + pub fn new(embedding_provider: Arc) -> Self { + Self { + parser: Parser::new(), + cursor: QueryCursor::new(), + embedding_provider, + } + } + + fn parse_entire_file( + &self, + relative_path: Option<&Path>, + language_name: Arc, + content: &str, + ) -> Result> { + let document_span = ENTIRE_FILE_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", language_name.as_ref()) + .replace("", &content); + let digest = SpanDigest::from(document_span.as_str()); + let model = self.embedding_provider.base_model(); + let document_span = model.truncate( + &document_span, + model.capacity()?, + ai::models::TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_span)?; + + Ok(vec![Span { + range: 0..content.len(), + content: document_span, + embedding: Default::default(), + name: language_name.to_string(), + digest, + token_count, + }]) + } + + fn parse_markdown_file( + &self, + relative_path: Option<&Path>, + content: &str, + ) -> Result> { + let document_span = MARKDOWN_CONTEXT_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", &content); + let digest = SpanDigest::from(document_span.as_str()); + + let model = self.embedding_provider.base_model(); + let document_span = model.truncate( + &document_span, + model.capacity()?, + ai::models::TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_span)?; + + Ok(vec![Span { + range: 0..content.len(), + content: document_span, + embedding: None, + name: "Markdown".to_string(), + digest, + token_count, + }]) + } + + fn get_matches_in_file( + &mut self, + content: &str, + grammar: &Arc, + ) -> Result> { + let embedding_config = grammar + .embedding_config + .as_ref() + .ok_or_else(|| anyhow!("no embedding queries"))?; + self.parser.set_language(grammar.ts_language).unwrap(); + + let tree = self + .parser + .parse(&content, None) + .ok_or_else(|| anyhow!("parsing failed"))?; + + let mut captures: Vec = Vec::new(); + let mut collapse_ranges: Vec> = Vec::new(); + let mut keep_ranges: Vec> = Vec::new(); + for mat in self.cursor.matches( + &embedding_config.query, + tree.root_node(), + content.as_bytes(), + ) { + let mut start_col = 0; + let mut item_range: Option> = None; + let mut name_range: Option> = None; + let mut context_ranges: Vec> = Vec::new(); + collapse_ranges.clear(); + keep_ranges.clear(); + for capture in mat.captures { + if capture.index == embedding_config.item_capture_ix { + item_range = Some(capture.node.byte_range()); + start_col = capture.node.start_position().column; + } else if Some(capture.index) == embedding_config.name_capture_ix { + name_range = Some(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.context_capture_ix { + context_ranges.push(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.collapse_capture_ix { + collapse_ranges.push(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.keep_capture_ix { + keep_ranges.push(capture.node.byte_range()); + } + } + + captures.push(CodeContextMatch { + start_col, + item_range, + name_range, + context_ranges, + collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges), + }); + } + Ok(captures) + } + + pub fn parse_file_with_template( + &mut self, + relative_path: Option<&Path>, + content: &str, + language: Arc, + ) -> Result> { + let language_name = language.name(); + + if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) { + return self.parse_entire_file(relative_path, language_name, &content); + } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) { + return self.parse_markdown_file(relative_path, &content); + } + + let mut spans = self.parse_file(content, language)?; + for span in &mut spans { + let document_content = CODE_CONTEXT_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", language_name.as_ref()) + .replace("item", &span.content); + + let model = self.embedding_provider.base_model(); + let document_content = model.truncate( + &document_content, + model.capacity()?, + TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_content)?; + + span.content = document_content; + span.token_count = token_count; + } + Ok(spans) + } + + pub fn parse_file(&mut self, content: &str, language: Arc) -> Result> { + let grammar = language + .grammar() + .ok_or_else(|| anyhow!("no grammar for language"))?; + + // Iterate through query matches + let matches = self.get_matches_in_file(content, grammar)?; + + let language_scope = language.default_scope(); + let placeholder = language_scope.collapsed_placeholder(); + + let mut spans = Vec::new(); + let mut collapsed_ranges_within = Vec::new(); + let mut parsed_name_ranges = HashSet::new(); + for (i, context_match) in matches.iter().enumerate() { + // Items which are collapsible but not embeddable have no item range + let item_range = if let Some(item_range) = context_match.item_range.clone() { + item_range + } else { + continue; + }; + + // Checks for deduplication + let name; + if let Some(name_range) = context_match.name_range.clone() { + name = content + .get(name_range.clone()) + .map_or(String::new(), |s| s.to_string()); + if parsed_name_ranges.contains(&name_range) { + continue; + } + parsed_name_ranges.insert(name_range); + } else { + name = String::new(); + } + + collapsed_ranges_within.clear(); + 'outer: for remaining_match in &matches[(i + 1)..] { + for collapsed_range in &remaining_match.collapse_ranges { + if item_range.start <= collapsed_range.start + && item_range.end >= collapsed_range.end + { + collapsed_ranges_within.push(collapsed_range.clone()); + } else { + break 'outer; + } + } + } + + collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end))); + + let mut span_content = String::new(); + for context_range in &context_match.context_ranges { + add_content_from_range( + &mut span_content, + content, + context_range.clone(), + context_match.start_col, + ); + span_content.push_str("\n"); + } + + let mut offset = item_range.start; + for collapsed_range in &collapsed_ranges_within { + if collapsed_range.start > offset { + add_content_from_range( + &mut span_content, + content, + offset..collapsed_range.start, + context_match.start_col, + ); + offset = collapsed_range.start; + } + + if collapsed_range.end > offset { + span_content.push_str(placeholder); + offset = collapsed_range.end; + } + } + + if offset < item_range.end { + add_content_from_range( + &mut span_content, + content, + offset..item_range.end, + context_match.start_col, + ); + } + + let sha1 = SpanDigest::from(span_content.as_str()); + spans.push(Span { + name, + content: span_content, + range: item_range.clone(), + embedding: None, + digest: sha1, + token_count: 0, + }) + } + + return Ok(spans); + } +} + +pub(crate) fn subtract_ranges( + ranges: &[Range], + ranges_to_subtract: &[Range], +) -> Vec> { + let mut result = Vec::new(); + + let mut ranges_to_subtract = ranges_to_subtract.iter().peekable(); + + for range in ranges { + let mut offset = range.start; + + while offset < range.end { + if let Some(range_to_subtract) = ranges_to_subtract.peek() { + if offset < range_to_subtract.start { + let next_offset = cmp::min(range_to_subtract.start, range.end); + result.push(offset..next_offset); + offset = next_offset; + } else { + let next_offset = cmp::min(range_to_subtract.end, range.end); + offset = next_offset; + } + + if offset >= range_to_subtract.end { + ranges_to_subtract.next(); + } + } else { + result.push(offset..range.end); + offset = range.end; + } + } + } + + result +} + +fn add_content_from_range( + output: &mut String, + content: &str, + range: Range, + start_col: usize, +) { + for mut line in content.get(range.clone()).unwrap_or("").lines() { + for _ in 0..start_col { + if line.starts_with(' ') { + line = &line[1..]; + } else { + break; + } + } + output.push_str(line); + output.push('\n'); + } + output.pop(); +} diff --git a/crates/semantic_index2/src/semantic_index.rs b/crates/semantic_index2/src/semantic_index.rs new file mode 100644 index 0000000000000000000000000000000000000000..0b207b0bf68b3c504050d62c7b60a99d5dbb5804 --- /dev/null +++ b/crates/semantic_index2/src/semantic_index.rs @@ -0,0 +1,1280 @@ +mod db; +mod embedding_queue; +mod parsing; +pub mod semantic_index_settings; + +#[cfg(test)] +mod semantic_index_tests; + +use crate::semantic_index_settings::SemanticIndexSettings; +use ai::embedding::{Embedding, EmbeddingProvider}; +use ai::providers::open_ai::OpenAIEmbeddingProvider; +use anyhow::{anyhow, Context as _, Result}; +use collections::{BTreeMap, HashMap, HashSet}; +use db::VectorDatabase; +use embedding_queue::{EmbeddingQueue, FileToEmbed}; +use futures::{future, FutureExt, StreamExt}; +use gpui::{ + AppContext, AsyncAppContext, BorrowWindow, Context, Model, ModelContext, Task, ViewContext, + WeakModel, +}; +use language::{Anchor, Bias, Buffer, Language, LanguageRegistry}; +use lazy_static::lazy_static; +use ordered_float::OrderedFloat; +use parking_lot::Mutex; +use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; +use postage::watch; +use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId}; +use settings::Settings; +use smol::channel; +use std::{ + cmp::Reverse, + env, + future::Future, + mem, + ops::Range, + path::{Path, PathBuf}, + sync::{Arc, Weak}, + time::{Duration, Instant, SystemTime}, +}; +use util::paths::PathMatcher; +use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt}; +use workspace::Workspace; + +const SEMANTIC_INDEX_VERSION: usize = 11; +const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60); +const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250); + +lazy_static! { + static ref OPENAI_API_KEY: Option = env::var("OPENAI_API_KEY").ok(); +} + +pub fn init( + fs: Arc, + http_client: Arc, + language_registry: Arc, + cx: &mut AppContext, +) { + SemanticIndexSettings::register(cx); + + let db_file_path = EMBEDDINGS_DIR + .join(Path::new(RELEASE_CHANNEL_NAME.as_str())) + .join("embeddings_db"); + + cx.observe_new_views( + |workspace: &mut Workspace, cx: &mut ViewContext| { + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; + let project = workspace.project().clone(); + + if project.read(cx).is_local() { + cx.app_mut() + .spawn(|mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + })? + .await?; + if previously_indexed { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx))? + .await?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }, + ) + .detach(); + + cx.spawn(move |cx| async move { + let semantic_index = SemanticIndex::new( + fs, + db_file_path, + Arc::new(OpenAIEmbeddingProvider::new( + http_client, + cx.background_executor().clone(), + )), + language_registry, + cx.clone(), + ) + .await?; + + cx.update(|cx| cx.set_global(semantic_index.clone()))?; + + anyhow::Ok(()) + }) + .detach(); +} + +#[derive(Copy, Clone, Debug)] +pub enum SemanticIndexStatus { + NotAuthenticated, + NotIndexed, + Indexed, + Indexing { + remaining_files: usize, + rate_limit_expiry: Option, + }, +} + +pub struct SemanticIndex { + fs: Arc, + db: VectorDatabase, + embedding_provider: Arc, + language_registry: Arc, + parsing_files_tx: channel::Sender<(Arc>, PendingFile)>, + _embedding_task: Task<()>, + _parsing_files_tasks: Vec>, + projects: HashMap, ProjectState>, +} + +struct ProjectState { + worktrees: HashMap, + pending_file_count_rx: watch::Receiver, + pending_file_count_tx: Arc>>, + pending_index: usize, + _subscription: gpui::Subscription, + _observe_pending_file_count: Task<()>, +} + +enum WorktreeState { + Registering(RegisteringWorktreeState), + Registered(RegisteredWorktreeState), +} + +impl WorktreeState { + fn is_registered(&self) -> bool { + matches!(self, Self::Registered(_)) + } + + fn paths_changed( + &mut self, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + worktree: &Worktree, + ) { + let changed_paths = match self { + Self::Registering(state) => &mut state.changed_paths, + Self::Registered(state) => &mut state.changed_paths, + }; + + for (path, entry_id, change) in changes.iter() { + let Some(entry) = worktree.entry_for_id(*entry_id) else { + continue; + }; + if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() { + continue; + } + changed_paths.insert( + path.clone(), + ChangedPathInfo { + mtime: entry.mtime, + is_deleted: *change == PathChange::Removed, + }, + ); + } + } +} + +struct RegisteringWorktreeState { + changed_paths: BTreeMap, ChangedPathInfo>, + done_rx: watch::Receiver>, + _registration: Task<()>, +} + +impl RegisteringWorktreeState { + fn done(&self) -> impl Future { + let mut done_rx = self.done_rx.clone(); + async move { + while let Some(result) = done_rx.next().await { + if result.is_some() { + break; + } + } + } + } +} + +struct RegisteredWorktreeState { + db_id: i64, + changed_paths: BTreeMap, ChangedPathInfo>, +} + +struct ChangedPathInfo { + mtime: SystemTime, + is_deleted: bool, +} + +#[derive(Clone)] +pub struct JobHandle { + /// The outer Arc is here to count the clones of a JobHandle instance; + /// when the last handle to a given job is dropped, we decrement a counter (just once). + tx: Arc>>>, +} + +impl JobHandle { + fn new(tx: &Arc>>) -> Self { + *tx.lock().borrow_mut() += 1; + Self { + tx: Arc::new(Arc::downgrade(&tx)), + } + } +} + +impl ProjectState { + fn new(subscription: gpui::Subscription, cx: &mut ModelContext) -> Self { + let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0); + let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx)); + Self { + worktrees: Default::default(), + pending_file_count_rx: pending_file_count_rx.clone(), + pending_file_count_tx, + pending_index: 0, + _subscription: subscription, + _observe_pending_file_count: cx.spawn({ + let mut pending_file_count_rx = pending_file_count_rx.clone(); + |this, mut cx| async move { + while let Some(_) = pending_file_count_rx.next().await { + if this.update(&mut cx, |_, cx| cx.notify()).is_err() { + break; + } + } + } + }), + } + } + + fn worktree_id_for_db_id(&self, id: i64) -> Option { + self.worktrees + .iter() + .find_map(|(worktree_id, worktree_state)| match worktree_state { + WorktreeState::Registered(state) if state.db_id == id => Some(*worktree_id), + _ => None, + }) + } +} + +#[derive(Clone)] +pub struct PendingFile { + worktree_db_id: i64, + relative_path: Arc, + absolute_path: PathBuf, + language: Option>, + modified_time: SystemTime, + job_handle: JobHandle, +} + +#[derive(Clone)] +pub struct SearchResult { + pub buffer: Model, + pub range: Range, + pub similarity: OrderedFloat, +} + +impl SemanticIndex { + pub fn global(cx: &mut AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } + } + + pub fn authenticate(&mut self, cx: &mut AppContext) -> bool { + if !self.embedding_provider.has_credentials() { + self.embedding_provider.retrieve_credentials(cx); + } else { + return true; + } + + self.embedding_provider.has_credentials() + } + + pub fn is_authenticated(&self) -> bool { + self.embedding_provider.has_credentials() + } + + pub fn enabled(cx: &AppContext) -> bool { + SemanticIndexSettings::get_global(cx).enabled + } + + pub fn status(&self, project: &Model) -> SemanticIndexStatus { + if !self.is_authenticated() { + return SemanticIndexStatus::NotAuthenticated; + } + + if let Some(project_state) = self.projects.get(&project.downgrade()) { + if project_state + .worktrees + .values() + .all(|worktree| worktree.is_registered()) + && project_state.pending_index == 0 + { + SemanticIndexStatus::Indexed + } else { + SemanticIndexStatus::Indexing { + remaining_files: project_state.pending_file_count_rx.borrow().clone(), + rate_limit_expiry: self.embedding_provider.rate_limit_expiration(), + } + } + } else { + SemanticIndexStatus::NotIndexed + } + } + + pub async fn new( + fs: Arc, + database_path: PathBuf, + embedding_provider: Arc, + language_registry: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let t0 = Instant::now(); + let database_path = Arc::from(database_path); + let db = VectorDatabase::new(fs.clone(), database_path, cx.background_executor().clone()) + .await?; + + log::trace!( + "db initialization took {:?} milliseconds", + t0.elapsed().as_millis() + ); + + cx.build_model(|cx| { + let t0 = Instant::now(); + let embedding_queue = + EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor().clone()); + let _embedding_task = cx.background_executor().spawn({ + let embedded_files = embedding_queue.finished_files(); + let db = db.clone(); + async move { + while let Ok(file) = embedded_files.recv().await { + db.insert_file(file.worktree_id, file.path, file.mtime, file.spans) + .await + .log_err(); + } + } + }); + + // Parse files into embeddable spans. + let (parsing_files_tx, parsing_files_rx) = + channel::unbounded::<(Arc>, PendingFile)>(); + let embedding_queue = Arc::new(Mutex::new(embedding_queue)); + let mut _parsing_files_tasks = Vec::new(); + for _ in 0..cx.background_executor().num_cpus() { + let fs = fs.clone(); + let mut parsing_files_rx = parsing_files_rx.clone(); + let embedding_provider = embedding_provider.clone(); + let embedding_queue = embedding_queue.clone(); + let background = cx.background_executor().clone(); + _parsing_files_tasks.push(cx.background_executor().spawn(async move { + let mut retriever = CodeContextRetriever::new(embedding_provider.clone()); + loop { + let mut timer = background.timer(EMBEDDING_QUEUE_FLUSH_TIMEOUT).fuse(); + let mut next_file_to_parse = parsing_files_rx.next().fuse(); + futures::select_biased! { + next_file_to_parse = next_file_to_parse => { + if let Some((embeddings_for_digest, pending_file)) = next_file_to_parse { + Self::parse_file( + &fs, + pending_file, + &mut retriever, + &embedding_queue, + &embeddings_for_digest, + ) + .await + } else { + break; + } + }, + _ = timer => { + embedding_queue.lock().flush(); + } + } + } + })); + } + + log::trace!( + "semantic index task initialization took {:?} milliseconds", + t0.elapsed().as_millis() + ); + Self { + fs, + db, + embedding_provider, + language_registry, + parsing_files_tx, + _embedding_task, + _parsing_files_tasks, + projects: Default::default(), + } + }) + } + + async fn parse_file( + fs: &Arc, + pending_file: PendingFile, + retriever: &mut CodeContextRetriever, + embedding_queue: &Arc>, + embeddings_for_digest: &HashMap, + ) { + let Some(language) = pending_file.language else { + return; + }; + + if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() { + if let Some(mut spans) = retriever + .parse_file_with_template(Some(&pending_file.relative_path), &content, language) + .log_err() + { + log::trace!( + "parsed path {:?}: {} spans", + pending_file.relative_path, + spans.len() + ); + + for span in &mut spans { + if let Some(embedding) = embeddings_for_digest.get(&span.digest) { + span.embedding = Some(embedding.to_owned()); + } + } + + embedding_queue.lock().push(FileToEmbed { + worktree_id: pending_file.worktree_db_id, + path: pending_file.relative_path, + mtime: pending_file.modified_time, + job_handle: pending_file.job_handle, + spans, + }); + } + } + } + + pub fn project_previously_indexed( + &mut self, + project: &Model, + cx: &mut ModelContext, + ) -> Task> { + let worktrees_indexed_previously = project + .read(cx) + .worktrees() + .map(|worktree| { + self.db + .worktree_previously_indexed(&worktree.read(cx).abs_path()) + }) + .collect::>(); + cx.spawn(|_, _cx| async move { + let worktree_indexed_previously = + futures::future::join_all(worktrees_indexed_previously).await; + + Ok(worktree_indexed_previously + .iter() + .filter(|worktree| worktree.is_ok()) + .all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned()))) + }) + } + + fn project_entries_changed( + &mut self, + project: Model, + worktree_id: WorktreeId, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + cx: &mut ModelContext, + ) { + let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else { + return; + }; + let project = project.downgrade(); + let Some(project_state) = self.projects.get_mut(&project) else { + return; + }; + + let worktree = worktree.read(cx); + let worktree_state = + if let Some(worktree_state) = project_state.worktrees.get_mut(&worktree_id) { + worktree_state + } else { + return; + }; + worktree_state.paths_changed(changes, worktree); + if let WorktreeState::Registered(_) = worktree_state { + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(BACKGROUND_INDEXING_DELAY) + .await; + if let Some((this, project)) = this.upgrade().zip(project.upgrade()) { + this.update(&mut cx, |this, cx| { + this.index_project(project, cx).detach_and_log_err(cx) + })?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + + fn register_worktree( + &mut self, + project: Model, + worktree: Model, + cx: &mut ModelContext, + ) { + let project = project.downgrade(); + let project_state = if let Some(project_state) = self.projects.get_mut(&project) { + project_state + } else { + return; + }; + let worktree = if let Some(worktree) = worktree.read(cx).as_local() { + worktree + } else { + return; + }; + let worktree_abs_path = worktree.abs_path().clone(); + let scan_complete = worktree.scan_complete(); + let worktree_id = worktree.id(); + let db = self.db.clone(); + let language_registry = self.language_registry.clone(); + let (mut done_tx, done_rx) = watch::channel(); + let registration = cx.spawn(|this, mut cx| { + async move { + let register = async { + scan_complete.await; + let db_id = db.find_or_create_worktree(worktree_abs_path).await?; + let mut file_mtimes = db.get_file_mtimes(db_id).await?; + let worktree = if let Some(project) = project.upgrade() { + project + .read_with(&cx, |project, cx| project.worktree_for_id(worktree_id, cx)) + .ok() + .flatten() + .context("worktree not found")? + } else { + return anyhow::Ok(()); + }; + let worktree = worktree.read_with(&cx, |worktree, _| worktree.snapshot())?; + let mut changed_paths = cx + .background_executor() + .spawn(async move { + let mut changed_paths = BTreeMap::new(); + for file in worktree.files(false, 0) { + let absolute_path = worktree.absolutize(&file.path); + + if file.is_external || file.is_ignored || file.is_symlink { + continue; + } + + if let Ok(language) = language_registry + .language_for_file(&absolute_path, None) + .await + { + // Test if file is valid parseable file + if !PARSEABLE_ENTIRE_FILE_TYPES + .contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + + let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); + let already_stored = stored_mtime + .map_or(false, |existing_mtime| { + existing_mtime == file.mtime + }); + + if !already_stored { + changed_paths.insert( + file.path.clone(), + ChangedPathInfo { + mtime: file.mtime, + is_deleted: false, + }, + ); + } + } + } + + // Clean up entries from database that are no longer in the worktree. + for (path, mtime) in file_mtimes { + changed_paths.insert( + path.into(), + ChangedPathInfo { + mtime, + is_deleted: true, + }, + ); + } + + anyhow::Ok(changed_paths) + }) + .await?; + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project) + .context("project not registered")?; + let project = project.upgrade().context("project was dropped")?; + + if let Some(WorktreeState::Registering(state)) = + project_state.worktrees.remove(&worktree_id) + { + changed_paths.extend(state.changed_paths); + } + project_state.worktrees.insert( + worktree_id, + WorktreeState::Registered(RegisteredWorktreeState { + db_id, + changed_paths, + }), + ); + this.index_project(project, cx).detach_and_log_err(cx); + + anyhow::Ok(()) + })??; + + anyhow::Ok(()) + }; + + if register.await.log_err().is_none() { + // Stop tracking this worktree if the registration failed. + this.update(&mut cx, |this, _| { + this.projects.get_mut(&project).map(|project_state| { + project_state.worktrees.remove(&worktree_id); + }); + }) + .ok(); + } + + *done_tx.borrow_mut() = Some(()); + } + }); + project_state.worktrees.insert( + worktree_id, + WorktreeState::Registering(RegisteringWorktreeState { + changed_paths: Default::default(), + done_rx, + _registration: registration, + }), + ); + } + + fn project_worktrees_changed(&mut self, project: Model, cx: &mut ModelContext) { + let project_state = if let Some(project_state) = self.projects.get_mut(&project.downgrade()) + { + project_state + } else { + return; + }; + + let mut worktrees = project + .read(cx) + .worktrees() + .filter(|worktree| worktree.read(cx).is_local()) + .collect::>(); + let worktree_ids = worktrees + .iter() + .map(|worktree| worktree.read(cx).id()) + .collect::>(); + + // Remove worktrees that are no longer present + project_state + .worktrees + .retain(|worktree_id, _| worktree_ids.contains(worktree_id)); + + // Register new worktrees + worktrees.retain(|worktree| { + let worktree_id = worktree.read(cx).id(); + !project_state.worktrees.contains_key(&worktree_id) + }); + for worktree in worktrees { + self.register_worktree(project.clone(), worktree, cx); + } + } + + pub fn pending_file_count(&self, project: &Model) -> Option> { + Some( + self.projects + .get(&project.downgrade())? + .pending_file_count_rx + .clone(), + ) + } + + pub fn search_project( + &mut self, + project: Model, + query: String, + limit: usize, + includes: Vec, + excludes: Vec, + cx: &mut ModelContext, + ) -> Task>> { + if query.is_empty() { + return Task::ready(Ok(Vec::new())); + } + + let index = self.index_project(project.clone(), cx); + let embedding_provider = self.embedding_provider.clone(); + + cx.spawn(|this, mut cx| async move { + index.await?; + let t0 = Instant::now(); + + let query = embedding_provider + .embed_batch(vec![query]) + .await? + .pop() + .context("could not embed query")?; + log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis()); + + let search_start = Instant::now(); + let modified_buffer_results = this.update(&mut cx, |this, cx| { + this.search_modified_buffers( + &project, + query.clone(), + limit, + &includes, + &excludes, + cx, + ) + })?; + let file_results = this.update(&mut cx, |this, cx| { + this.search_files(project, query, limit, includes, excludes, cx) + })?; + let (modified_buffer_results, file_results) = + futures::join!(modified_buffer_results, file_results); + + // Weave together the results from modified buffers and files. + let mut results = Vec::new(); + let mut modified_buffers = HashSet::default(); + for result in modified_buffer_results.log_err().unwrap_or_default() { + modified_buffers.insert(result.buffer.clone()); + results.push(result); + } + for result in file_results.log_err().unwrap_or_default() { + if !modified_buffers.contains(&result.buffer) { + results.push(result); + } + } + results.sort_by_key(|result| Reverse(result.similarity)); + results.truncate(limit); + log::trace!("Semantic search took {:?}", search_start.elapsed()); + Ok(results) + }) + } + + pub fn search_files( + &mut self, + project: Model, + query: Embedding, + limit: usize, + includes: Vec, + excludes: Vec, + cx: &mut ModelContext, + ) -> Task>> { + let db_path = self.db.path().clone(); + let fs = self.fs.clone(); + cx.spawn(|this, mut cx| async move { + let database = VectorDatabase::new( + fs.clone(), + db_path.clone(), + cx.background_executor().clone(), + ) + .await?; + + let worktree_db_ids = this.read_with(&cx, |this, _| { + let project_state = this + .projects + .get(&project.downgrade()) + .context("project was not indexed")?; + let worktree_db_ids = project_state + .worktrees + .values() + .filter_map(|worktree| { + if let WorktreeState::Registered(worktree) = worktree { + Some(worktree.db_id) + } else { + None + } + }) + .collect::>(); + anyhow::Ok(worktree_db_ids) + })??; + + let file_ids = database + .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes) + .await?; + + let batch_n = cx.background_executor().num_cpus(); + let ids_len = file_ids.clone().len(); + let minimum_batch_size = 50; + + let batch_size = { + let size = ids_len / batch_n; + if size < minimum_batch_size { + minimum_batch_size + } else { + size + } + }; + + let mut batch_results = Vec::new(); + for batch in file_ids.chunks(batch_size) { + let batch = batch.into_iter().map(|v| *v).collect::>(); + let limit = limit.clone(); + let fs = fs.clone(); + let db_path = db_path.clone(); + let query = query.clone(); + if let Some(db) = + VectorDatabase::new(fs, db_path.clone(), cx.background_executor().clone()) + .await + .log_err() + { + batch_results.push(async move { + db.top_k_search(&query, limit, batch.as_slice()).await + }); + } + } + + let batch_results = futures::future::join_all(batch_results).await; + + let mut results = Vec::new(); + for batch_result in batch_results { + if batch_result.is_ok() { + for (id, similarity) in batch_result.unwrap() { + let ix = match results + .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) + { + Ok(ix) => ix, + Err(ix) => ix, + }; + + results.insert(ix, (id, similarity)); + results.truncate(limit); + } + } + } + + let ids = results.iter().map(|(id, _)| *id).collect::>(); + let scores = results + .into_iter() + .map(|(_, score)| score) + .collect::>(); + let spans = database.spans_for_ids(ids.as_slice()).await?; + + let mut tasks = Vec::new(); + let mut ranges = Vec::new(); + let weak_project = project.downgrade(); + project.update(&mut cx, |project, cx| { + let this = this.upgrade().context("index was dropped")?; + for (worktree_db_id, file_path, byte_range) in spans { + let project_state = + if let Some(state) = this.read(cx).projects.get(&weak_project) { + state + } else { + return Err(anyhow!("project not added")); + }; + if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) { + tasks.push(project.open_buffer((worktree_id, file_path), cx)); + ranges.push(byte_range); + } + } + + Ok(()) + })??; + + let buffers = futures::future::join_all(tasks).await; + Ok(buffers + .into_iter() + .zip(ranges) + .zip(scores) + .filter_map(|((buffer, range), similarity)| { + let buffer = buffer.log_err()?; + let range = buffer + .read_with(&cx, |buffer, _| { + let start = buffer.clip_offset(range.start, Bias::Left); + let end = buffer.clip_offset(range.end, Bias::Right); + buffer.anchor_before(start)..buffer.anchor_after(end) + }) + .log_err()?; + Some(SearchResult { + buffer, + range, + similarity, + }) + }) + .collect()) + }) + } + + fn search_modified_buffers( + &self, + project: &Model, + query: Embedding, + limit: usize, + includes: &[PathMatcher], + excludes: &[PathMatcher], + cx: &mut ModelContext, + ) -> Task>> { + let modified_buffers = project + .read(cx) + .opened_buffers() + .into_iter() + .filter_map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| { + excludes.iter().any(|matcher| matcher.is_match(&path)) + }); + + let included = if includes.len() == 0 { + true + } else { + snapshot.resolve_file_path(cx, false).map_or(false, |path| { + includes.iter().any(|matcher| matcher.is_match(&path)) + }) + }; + + if buffer.is_dirty() && !excluded && included { + Some((buffer_handle, snapshot)) + } else { + None + } + }) + .collect::>(); + + let embedding_provider = self.embedding_provider.clone(); + let fs = self.fs.clone(); + let db_path = self.db.path().clone(); + let background = cx.background_executor().clone(); + cx.background_executor().spawn(async move { + let db = VectorDatabase::new(fs, db_path.clone(), background).await?; + let mut results = Vec::::new(); + + let mut retriever = CodeContextRetriever::new(embedding_provider.clone()); + for (buffer, snapshot) in modified_buffers { + let language = snapshot + .language_at(0) + .cloned() + .unwrap_or_else(|| language::PLAIN_TEXT.clone()); + let mut spans = retriever + .parse_file_with_template(None, &snapshot.text(), language) + .log_err() + .unwrap_or_default(); + if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) + .await + .log_err() + .is_some() + { + for span in spans { + let similarity = span.embedding.unwrap().similarity(&query); + let ix = match results + .binary_search_by_key(&Reverse(similarity), |result| { + Reverse(result.similarity) + }) { + Ok(ix) => ix, + Err(ix) => ix, + }; + + let range = { + let start = snapshot.clip_offset(span.range.start, Bias::Left); + let end = snapshot.clip_offset(span.range.end, Bias::Right); + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }; + + results.insert( + ix, + SearchResult { + buffer: buffer.clone(), + range, + similarity, + }, + ); + results.truncate(limit); + } + } + } + + Ok(results) + }) + } + + pub fn index_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Task> { + if !self.is_authenticated() { + if !self.authenticate(cx) { + return Task::ready(Err(anyhow!("user is not authenticated"))); + } + } + + if !self.projects.contains_key(&project.downgrade()) { + let subscription = cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => { + this.project_worktrees_changed(project.clone(), cx); + } + project::Event::WorktreeUpdatedEntries(worktree_id, changes) => { + this.project_entries_changed(project, *worktree_id, changes.clone(), cx); + } + _ => {} + }); + let project_state = ProjectState::new(subscription, cx); + self.projects.insert(project.downgrade(), project_state); + self.project_worktrees_changed(project.clone(), cx); + } + let project_state = self.projects.get_mut(&project.downgrade()).unwrap(); + project_state.pending_index += 1; + cx.notify(); + + let mut pending_file_count_rx = project_state.pending_file_count_rx.clone(); + let db = self.db.clone(); + let language_registry = self.language_registry.clone(); + let parsing_files_tx = self.parsing_files_tx.clone(); + let worktree_registration = self.wait_for_worktree_registration(&project, cx); + + cx.spawn(|this, mut cx| async move { + worktree_registration.await?; + + let mut pending_files = Vec::new(); + let mut files_to_delete = Vec::new(); + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project.downgrade()) + .context("project was dropped")?; + let pending_file_count_tx = &project_state.pending_file_count_tx; + + project_state + .worktrees + .retain(|worktree_id, worktree_state| { + let worktree = if let Some(worktree) = + project.read(cx).worktree_for_id(*worktree_id, cx) + { + worktree + } else { + return false; + }; + let worktree_state = + if let WorktreeState::Registered(worktree_state) = worktree_state { + worktree_state + } else { + return true; + }; + + worktree_state.changed_paths.retain(|path, info| { + if info.is_deleted { + files_to_delete.push((worktree_state.db_id, path.clone())); + } else { + let absolute_path = worktree.read(cx).absolutize(path); + let job_handle = JobHandle::new(pending_file_count_tx); + pending_files.push(PendingFile { + absolute_path, + relative_path: path.clone(), + language: None, + job_handle, + modified_time: info.mtime, + worktree_db_id: worktree_state.db_id, + }); + } + + false + }); + true + }); + + anyhow::Ok(()) + })??; + + cx.background_executor() + .spawn(async move { + for (worktree_db_id, path) in files_to_delete { + db.delete_file(worktree_db_id, path).await.log_err(); + } + + let embeddings_for_digest = { + let mut files = HashMap::default(); + for pending_file in &pending_files { + files + .entry(pending_file.worktree_db_id) + .or_insert(Vec::new()) + .push(pending_file.relative_path.clone()); + } + Arc::new( + db.embeddings_for_files(files) + .await + .log_err() + .unwrap_or_default(), + ) + }; + + for mut pending_file in pending_files { + if let Ok(language) = language_registry + .language_for_file(&pending_file.relative_path, None) + .await + { + if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + pending_file.language = Some(language); + } + parsing_files_tx + .try_send((embeddings_for_digest.clone(), pending_file)) + .ok(); + } + + // Wait until we're done indexing. + while let Some(count) = pending_file_count_rx.next().await { + if count == 0 { + break; + } + } + }) + .await; + + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project.downgrade()) + .context("project was dropped")?; + project_state.pending_index -= 1; + cx.notify(); + anyhow::Ok(()) + })??; + + Ok(()) + }) + } + + fn wait_for_worktree_registration( + &self, + project: &Model, + cx: &mut ModelContext, + ) -> Task> { + let project = project.downgrade(); + cx.spawn(|this, cx| async move { + loop { + let mut pending_worktrees = Vec::new(); + this.upgrade() + .context("semantic index dropped")? + .read_with(&cx, |this, _| { + if let Some(project) = this.projects.get(&project) { + for worktree in project.worktrees.values() { + if let WorktreeState::Registering(worktree) = worktree { + pending_worktrees.push(worktree.done()); + } + } + } + })?; + + if pending_worktrees.is_empty() { + break; + } else { + future::join_all(pending_worktrees).await; + } + } + Ok(()) + }) + } + + async fn embed_spans( + spans: &mut [Span], + embedding_provider: &dyn EmbeddingProvider, + db: &VectorDatabase, + ) -> Result<()> { + let mut batch = Vec::new(); + let mut batch_tokens = 0; + let mut embeddings = Vec::new(); + + let digests = spans + .iter() + .map(|span| span.digest.clone()) + .collect::>(); + let embeddings_for_digests = db + .embeddings_for_digests(digests) + .await + .log_err() + .unwrap_or_default(); + + for span in &*spans { + if embeddings_for_digests.contains_key(&span.digest) { + continue; + }; + + if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + embeddings.extend(batch_embeddings); + batch_tokens = 0; + } + + batch_tokens += span.token_count; + batch.push(span.content.clone()); + } + + if !batch.is_empty() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + + embeddings.extend(batch_embeddings); + } + + let mut embeddings = embeddings.into_iter(); + for span in spans { + let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) { + Some(embedding.clone()) + } else { + embeddings.next() + }; + let embedding = embedding.context("failed to embed spans")?; + span.embedding = Some(embedding); + } + Ok(()) + } +} + +impl Drop for JobHandle { + fn drop(&mut self) { + if let Some(inner) = Arc::get_mut(&mut self.tx) { + // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not) + if let Some(tx) = inner.upgrade() { + let mut tx = tx.lock(); + *tx.borrow_mut() -= 1; + } + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[test] + fn test_job_handle() { + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let tx = Arc::new(Mutex::new(job_count_tx)); + let job_handle = JobHandle::new(&tx); + + assert_eq!(1, *job_count_rx.borrow()); + let new_job_handle = job_handle.clone(); + assert_eq!(1, *job_count_rx.borrow()); + drop(job_handle); + assert_eq!(1, *job_count_rx.borrow()); + drop(new_job_handle); + assert_eq!(0, *job_count_rx.borrow()); + } +} diff --git a/crates/semantic_index2/src/semantic_index_settings.rs b/crates/semantic_index2/src/semantic_index_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..306a38fa9c2ec52f5a69d27898cc9fccc1af956c --- /dev/null +++ b/crates/semantic_index2/src/semantic_index_settings.rs @@ -0,0 +1,28 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Deserialize, Debug)] +pub struct SemanticIndexSettings { + pub enabled: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct SemanticIndexSettingsContent { + pub enabled: Option, +} + +impl Settings for SemanticIndexSettings { + const KEY: Option<&'static str> = Some("semantic_index"); + + type FileContent = SemanticIndexSettingsContent; + + 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/semantic_index2/src/semantic_index_tests.rs b/crates/semantic_index2/src/semantic_index_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..ced08f4cbc30a991bfad0577af24f96c8ff81d8b --- /dev/null +++ b/crates/semantic_index2/src/semantic_index_tests.rs @@ -0,0 +1,1697 @@ +use crate::{ + embedding_queue::EmbeddingQueue, + parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest}, + semantic_index_settings::SemanticIndexSettings, + FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT, +}; +use ai::test::FakeEmbeddingProvider; + +use gpui::{Task, TestAppContext}; +use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; +use parking_lot::Mutex; +use pretty_assertions::assert_eq; +use project::{project_settings::ProjectSettings, FakeFs, Fs, Project}; +use rand::{rngs::StdRng, Rng}; +use serde_json::json; +use settings::{Settings, SettingsStore}; +use std::{path::Path, sync::Arc, time::SystemTime}; +use unindent::Unindent; +use util::{paths::PathMatcher, RandomCharIter}; + +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +#[gpui::test] +async fn test_semantic_index(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/the-root", + json!({ + "src": { + "file1.rs": " + fn aaa() { + println!(\"aaaaaaaaaaaa!\"); + } + + fn zzzzz() { + println!(\"SLEEPING\"); + } + ".unindent(), + "file2.rs": " + fn bbb() { + println!(\"bbbbbbbbbbbbb!\"); + } + struct pqpqpqp {} + ".unindent(), + "file3.toml": " + ZZZZZZZZZZZZZZZZZZ = 5 + ".unindent(), + } + }), + ) + .await; + + let languages = Arc::new(LanguageRegistry::new(Task::ready(()))); + let rust_language = rust_lang(); + let toml_language = toml_lang(); + languages.add(rust_language); + languages.add(toml_language); + + let db_dir = tempdir::TempDir::new("vector-store").unwrap(); + let db_path = db_dir.path().join("db.sqlite"); + + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let semantic_index = SemanticIndex::new( + fs.clone(), + db_path, + embedding_provider.clone(), + languages, + cx.to_async(), + ) + .await + .unwrap(); + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + let search_results = semantic_index.update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + vec![], + vec![], + cx, + ) + }); + let pending_file_count = + semantic_index.read_with(cx, |index, _| index.pending_file_count(&project).unwrap()); + cx.background_executor.run_until_parked(); + assert_eq!(*pending_file_count.borrow(), 3); + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + assert_eq!(*pending_file_count.borrow(), 0); + + let search_results = search_results.await.unwrap(); + assert_search_results( + &search_results, + &[ + (Path::new("src/file1.rs").into(), 0), + (Path::new("src/file2.rs").into(), 0), + (Path::new("src/file3.toml").into(), 0), + (Path::new("src/file1.rs").into(), 45), + (Path::new("src/file2.rs").into(), 45), + ], + cx, + ); + + // Test Include Files Functonality + let include_files = vec![PathMatcher::new("*.rs").unwrap()]; + let exclude_files = vec![PathMatcher::new("*.rs").unwrap()]; + let rust_only_search_results = semantic_index + .update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + include_files, + vec![], + cx, + ) + }) + .await + .unwrap(); + + assert_search_results( + &rust_only_search_results, + &[ + (Path::new("src/file1.rs").into(), 0), + (Path::new("src/file2.rs").into(), 0), + (Path::new("src/file1.rs").into(), 45), + (Path::new("src/file2.rs").into(), 45), + ], + cx, + ); + + let no_rust_search_results = semantic_index + .update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + vec![], + exclude_files, + cx, + ) + }) + .await + .unwrap(); + + assert_search_results( + &no_rust_search_results, + &[(Path::new("src/file3.toml").into(), 0)], + cx, + ); + + fs.save( + "/the-root/src/file2.rs".as_ref(), + &" + fn dddd() { println!(\"ddddd!\"); } + struct pqpqpqp {} + " + .unindent() + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + + let prev_embedding_count = embedding_provider.embedding_count(); + let index = semantic_index.update(cx, |store, cx| store.index_project(project.clone(), cx)); + cx.background_executor.run_until_parked(); + assert_eq!(*pending_file_count.borrow(), 1); + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + assert_eq!(*pending_file_count.borrow(), 0); + index.await.unwrap(); + + assert_eq!( + embedding_provider.embedding_count() - prev_embedding_count, + 1 + ); +} + +#[gpui::test(iterations = 10)] +async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) { + let (outstanding_job_count, _) = postage::watch::channel_with(0); + let outstanding_job_count = Arc::new(Mutex::new(outstanding_job_count)); + + let files = (1..=3) + .map(|file_ix| FileToEmbed { + worktree_id: 5, + path: Path::new(&format!("path-{file_ix}")).into(), + mtime: SystemTime::now(), + spans: (0..rng.gen_range(4..22)) + .map(|document_ix| { + let content_len = rng.gen_range(10..100); + let content = RandomCharIter::new(&mut rng) + .with_simple_text() + .take(content_len) + .collect::(); + let digest = SpanDigest::from(content.as_str()); + Span { + range: 0..10, + embedding: None, + name: format!("document {document_ix}"), + content, + digest, + token_count: rng.gen_range(10..30), + } + }) + .collect(), + job_handle: JobHandle::new(&outstanding_job_count), + }) + .collect::>(); + + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + + let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor.clone()); + for file in &files { + queue.push(file.clone()); + } + queue.flush(); + + cx.background_executor.run_until_parked(); + let finished_files = queue.finished_files(); + let mut embedded_files: Vec<_> = files + .iter() + .map(|_| finished_files.try_recv().expect("no finished file")) + .collect(); + + let expected_files: Vec<_> = files + .iter() + .map(|file| { + let mut file = file.clone(); + for doc in &mut file.spans { + doc.embedding = Some(embedding_provider.embed_sync(doc.content.as_ref())); + } + file + }) + .collect(); + + embedded_files.sort_by_key(|f| f.path.clone()); + + assert_eq!(embedded_files, expected_files); +} + +#[track_caller] +fn assert_search_results( + actual: &[SearchResult], + expected: &[(Arc, usize)], + cx: &TestAppContext, +) { + let actual = actual + .iter() + .map(|search_result| { + search_result.buffer.read_with(cx, |buffer, _cx| { + ( + buffer.file().unwrap().path().clone(), + search_result.range.start.to_offset(buffer), + ) + }) + }) + .collect::>(); + assert_eq!(actual, expected); +} + +#[gpui::test] +async fn test_code_context_retrieval_rust() { + let language = rust_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /// A doc comment + /// that spans multiple lines + #[gpui::test] + fn a() { + b + } + + impl C for D { + } + + impl E { + // This is also a preceding comment + pub fn function_1() -> Option<()> { + unimplemented!(); + } + + // This is a preceding comment + fn function_2() -> Result<()> { + unimplemented!(); + } + } + + #[derive(Clone)] + struct D { + name: String + } + " + .unindent(); + + let documents = retriever.parse_file(&text, language).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /// A doc comment + /// that spans multiple lines + #[gpui::test] + fn a() { + b + }" + .unindent(), + text.find("fn a").unwrap(), + ), + ( + " + impl C for D { + }" + .unindent(), + text.find("impl C").unwrap(), + ), + ( + " + impl E { + // This is also a preceding comment + pub fn function_1() -> Option<()> { /* ... */ } + + // This is a preceding comment + fn function_2() -> Result<()> { /* ... */ } + }" + .unindent(), + text.find("impl E").unwrap(), + ), + ( + " + // This is also a preceding comment + pub fn function_1() -> Option<()> { + unimplemented!(); + }" + .unindent(), + text.find("pub fn function_1").unwrap(), + ), + ( + " + // This is a preceding comment + fn function_2() -> Result<()> { + unimplemented!(); + }" + .unindent(), + text.find("fn function_2").unwrap(), + ), + ( + " + #[derive(Clone)] + struct D { + name: String + }" + .unindent(), + text.find("struct D").unwrap(), + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_json() { + let language = json_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + { + "array": [1, 2, 3, 4], + "string": "abcdefg", + "nested_object": { + "array_2": [5, 6, 7, 8], + "string_2": "hijklmnop", + "boolean": true, + "none": null + } + } + "# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + { + "array": [], + "string": "", + "nested_object": { + "array_2": [], + "string_2": "", + "boolean": true, + "none": null + } + }"# + .unindent(), + text.find("{").unwrap(), + )], + ); + + let text = r#" + [ + { + "name": "somebody", + "age": 42 + }, + { + "name": "somebody else", + "age": 43 + } + ] + "# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + [{ + "name": "", + "age": 42 + }]"# + .unindent(), + text.find("[").unwrap(), + )], + ); +} + +fn assert_documents_eq( + documents: &[Span], + expected_contents_and_start_offsets: &[(String, usize)], +) { + assert_eq!( + documents + .iter() + .map(|document| (document.content.clone(), document.range.start)) + .collect::>(), + expected_contents_and_start_offsets + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_javascript() { + let language = js_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /* globals importScripts, backend */ + function _authorize() {} + + /** + * Sometimes the frontend build is way faster than backend. + */ + export async function authorizeBank() { + _authorize(pushModal, upgradingAccountId, {}); + } + + export class SettingsPage { + /* This is a test setting */ + constructor(page) { + this.page = page; + } + } + + /* This is a test comment */ + class TestClass {} + + /* Schema for editor_events in Clickhouse. */ + export interface ClickhouseEditorEvent { + installation_id: string + operation: string + } + " + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /* globals importScripts, backend */ + function _authorize() {}" + .unindent(), + 37, + ), + ( + " + /** + * Sometimes the frontend build is way faster than backend. + */ + export async function authorizeBank() { + _authorize(pushModal, upgradingAccountId, {}); + }" + .unindent(), + 131, + ), + ( + " + export class SettingsPage { + /* This is a test setting */ + constructor(page) { + this.page = page; + } + }" + .unindent(), + 225, + ), + ( + " + /* This is a test setting */ + constructor(page) { + this.page = page; + }" + .unindent(), + 290, + ), + ( + " + /* This is a test comment */ + class TestClass {}" + .unindent(), + 374, + ), + ( + " + /* Schema for editor_events in Clickhouse. */ + export interface ClickhouseEditorEvent { + installation_id: string + operation: string + }" + .unindent(), + 440, + ), + ], + ) +} + +#[gpui::test] +async fn test_code_context_retrieval_lua() { + let language = lua_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end + end + "#.unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + (r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + --[ ... ]-- + --[ ... ]-- + end + end"#.unindent(), + 114), + (r#" + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end"#.unindent(), 809), + ] + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_elixir() { + let language = elixir_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + defmodule File.Stream do + @moduledoc """ + Defines a `File.Stream` struct returned by `File.stream!/3`. + + The following fields are public: + + * `path` - the file path + * `modes` - the file modes + * `raw` - a boolean indicating if bin functions should be used + * `line_or_bytes` - if reading should read lines or a given number of bytes + * `node` - the node the file belongs to + + """ + + defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil + + @type t :: %__MODULE__{} + + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + defmodule File.Stream do + @moduledoc """ + Defines a `File.Stream` struct returned by `File.stream!/3`. + + The following fields are public: + + * `path` - the file path + * `modes` - the file modes + * `raw` - a boolean indicating if bin functions should be used + * `line_or_bytes` - if reading should read lines or a given number of bytes + * `node` - the node the file belongs to + + """ + + defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil + + @type t :: %__MODULE__{} + + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"# + .unindent(), + 0, + ),(r#" + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"#.unindent(), 574)], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_cpp() { + let language = cpp_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /** + * @brief Main function + * @returns 0 on exit + */ + int main() { return 0; } + + /** + * This is a test comment + */ + class MyClass { // The class + public: // Access specifier + int myNum; // Attribute (int variable) + string myString; // Attribute (string variable) + }; + + // This is a test comment + enum Color { red, green, blue }; + + /** This is a preceding block comment + * This is the second line + */ + struct { // Structure declaration + int myNum; // Member (int variable) + string myString; // Member (string variable) + } myStructure; + + /** + * @brief Matrix class. + */ + template ::value || std::is_floating_point::value, + bool>::type> + class Matrix2 { + std::vector> _mat; + + public: + /** + * @brief Constructor + * @tparam Integer ensuring integers are being evaluated and not other + * data types. + * @param size denoting the size of Matrix as size x size + */ + template ::value, + Integer>::type> + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + } + }" + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /** + * @brief Main function + * @returns 0 on exit + */ + int main() { return 0; }" + .unindent(), + 54, + ), + ( + " + /** + * This is a test comment + */ + class MyClass { // The class + public: // Access specifier + int myNum; // Attribute (int variable) + string myString; // Attribute (string variable) + }" + .unindent(), + 112, + ), + ( + " + // This is a test comment + enum Color { red, green, blue }" + .unindent(), + 322, + ), + ( + " + /** This is a preceding block comment + * This is the second line + */ + struct { // Structure declaration + int myNum; // Member (int variable) + string myString; // Member (string variable) + } myStructure;" + .unindent(), + 425, + ), + ( + " + /** + * @brief Matrix class. + */ + template ::value || std::is_floating_point::value, + bool>::type> + class Matrix2 { + std::vector> _mat; + + public: + /** + * @brief Constructor + * @tparam Integer ensuring integers are being evaluated and not other + * data types. + * @param size denoting the size of Matrix as size x size + */ + template ::value, + Integer>::type> + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + } + }" + .unindent(), + 612, + ), + ( + " + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + }" + .unindent(), + 1226, + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_ruby() { + let language = ruby_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end + + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end + end + + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + @name, @legs = name, legs + end + + def <=>(other) + legs <=> other.legs + end + end + + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + # ... + end + + def challenge_passed? + # ... + end + end"# + .unindent(), + 558, + ), + ( + r#" + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end"# + .unindent(), + 663, + ), + ( + r#" + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end"# + .unindent(), + 1254, + ), + ( + r#" + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + # ... + end + + def <=>(other) + # ... + end + end"# + .unindent(), + 1363, + ), + ( + r#" + def initialize(name, legs) + @name, @legs = name, legs + end"# + .unindent(), + 1427, + ), + ( + r#" + def <=>(other) + legs <=> other.legs + end"# + .unindent(), + 1501, + ), + ( + r#" + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(), + 1591, + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_php() { + let language = php_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + } + + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + } + } + + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + } + + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + } + + ?>"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + /* + This is a multiple-lines comment block + that spans over multiple + lines + */ + function functionName() { + echo "Hello world!"; + }"# + .unindent(), + 123, + ), + ( + r#" + trait HasAchievements + { + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + {/* ... */} + + public function achievements(): BelongsToMany + {/* ... */} + }"# + .unindent(), + 177, + ), + (r#" + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + { + if ($progress > 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + }"#.unindent(), 245), + (r#" + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + }"#.unindent(), 902), + (r#" + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + }"#.unindent(), + 1146), + (r#" + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + }"#.unindent(), 1265) + ], + ); +} + +fn js_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Javascript".into(), + path_suffixes: vec!["js".into()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_embedding_query( + &r#" + + ( + (comment)* @context + . + [ + (export_statement + (function_declaration + "async"? @name + "function" @name + name: (_) @name)) + (function_declaration + "async"? @name + "function" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (class_declaration + "class" @name + name: (_) @name)) + (class_declaration + "class" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (interface_declaration + "interface" @name + name: (_) @name)) + (interface_declaration + "interface" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (enum_declaration + "enum" @name + name: (_) @name)) + (enum_declaration + "enum" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + (method_definition + [ + "get" + "set" + "async" + "*" + "static" + ]* @name + name: (_) @name) @item + ) + + "# + .unindent(), + ) + .unwrap(), + ) +} + +fn rust_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + collapsed_placeholder: " /* ... */ ".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 + ) + + (attribute_item) @collapse + (use_declaration) @collapse + "#, + ) + .unwrap(), + ) +} + +fn json_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".into()], + ..Default::default() + }, + Some(tree_sitter_json::language()), + ) + .with_embedding_query( + r#" + (document) @item + + (array + "[" @keep + . + (object)? @keep + "]" @keep) @collapse + + (pair value: (string + "\"" @keep + "\"" @keep) @collapse) + "#, + ) + .unwrap(), + ) +} + +fn toml_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + path_suffixes: vec!["toml".into()], + ..Default::default() + }, + Some(tree_sitter_toml::language()), + )) +} + +fn cpp_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "CPP".into(), + path_suffixes: vec!["cpp".into()], + ..Default::default() + }, + Some(tree_sitter_cpp::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + (function_definition + (type_qualifier)? @name + type: (_)? @name + declarator: [ + (function_declarator + declarator: (_) @name) + (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name)) + (pointer_declarator + "*" @name + declarator: (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name))) + (reference_declarator + ["&" "&&"] @name + (function_declarator + declarator: (_) @name)) + ] + (type_qualifier)? @name) @item + ) + + ( + (comment)* @context + . + (template_declaration + (class_specifier + "class" @name + name: (_) @name) + ) @item + ) + + ( + (comment)* @context + . + (class_specifier + "class" @name + name: (_) @name) @item + ) + + ( + (comment)* @context + . + (enum_specifier + "enum" @name + name: (_) @name) @item + ) + + ( + (comment)* @context + . + (declaration + type: (struct_specifier + "struct" @name) + declarator: (_) @name) @item + ) + + "#, + ) + .unwrap(), + ) +} + +fn lua_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Lua".into(), + path_suffixes: vec!["lua".into()], + collapsed_placeholder: "--[ ... ]--".to_string(), + ..Default::default() + }, + Some(tree_sitter_lua::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + (function_declaration + "function" @name + name: (_) @name + (comment)* @collapse + body: (block) @collapse + ) @item + ) + "#, + ) + .unwrap(), + ) +} + +fn php_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "PHP".into(), + path_suffixes: vec!["php".into()], + collapsed_placeholder: "/* ... */".into(), + ..Default::default() + }, + Some(tree_sitter_php::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (function_definition + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (trait_declaration + "trait" @name + name: (_) @name) + + (method_declaration + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (interface_declaration + "interface" @name + name: (_) @name + ) + + (enum_declaration + "enum" @name + name: (_) @name + ) + + ] @item + ) + "#, + ) + .unwrap(), + ) +} + +fn ruby_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Ruby".into(), + path_suffixes: vec!["rb".into()], + collapsed_placeholder: "# ...".to_string(), + ..Default::default() + }, + Some(tree_sitter_ruby::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (module + "module" @name + name: (_) @name) + (method + "def" @name + name: (_) @name + body: (body_statement) @collapse) + (class + "class" @name + name: (_) @name) + (singleton_method + "def" @name + object: (_) @name + "." @name + name: (_) @name + body: (body_statement) @collapse) + ] @item + ) + "#, + ) + .unwrap(), + ) +} + +fn elixir_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Elixir".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + Some(tree_sitter_elixir::language()), + ) + .with_embedding_query( + r#" + ( + (unary_operator + operator: "@" + operand: (call + target: (identifier) @unary + (#match? @unary "^(doc)$")) + ) @context + . + (call + target: (identifier) @name + (arguments + [ + (identifier) @name + (call + target: (identifier) @name) + (binary_operator + left: (call + target: (identifier) @name) + operator: "when") + ]) + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + ) + + (call + target: (identifier) @name + (arguments (alias) @name) + (#any-match? @name "^(defmodule|defprotocol)$")) @item + "#, + ) + .unwrap(), + ) +} + +#[gpui::test] +fn test_subtract_ranges() { + // collapsed_ranges: Vec>, keep_ranges: Vec> + + assert_eq!( + subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]), + vec![1..4, 10..21] + ); + + assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]); +} + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + SemanticIndexSettings::register(cx); + ProjectSettings::register(cx); + }); +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 77d744b9fc9266dabe9b68990a3fe5f2ede38889..5741fa4a94cf79202f7f9b4a5694c1058be59dd5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3942,8 +3942,6 @@ impl std::fmt::Debug for OpenPaths { } } -pub struct WorkspaceCreated(pub WeakView); - pub fn activate_workspace_for_project( cx: &mut AppContext, predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static, From 7c5df51d2eda06b16ec3f1326c7ab4126416e3ab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 10:11:18 -0500 Subject: [PATCH 33/48] 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 34/48] 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 22997305384b9e293cbcdeadd16bece4f346fd00 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 10:59:15 -0500 Subject: [PATCH 35/48] Fix an issue with the text in the theme selector editor not showing (#3501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issues where the text—both placeholder and user-entered—would not show up in the editor in the theme selector. It seems to be the min width on the wrapper element that fixes this. Release Notes: - N/A --- crates/theme_selector2/src/theme_selector.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index 0d4c1e64667ecb98e0cadaf39d162dead1a2c46b..582ce43a88ddc8c197617693adbf0e05ab7b11e3 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View, - ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString, + View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Theme, ThemeRegistry, ThemeSettings}; -use ui::{prelude::*, ListItem}; +use ui::{prelude::*, v_stack, ListItem}; use util::ResultExt; use workspace::{ui::HighlightedLabel, Workspace}; @@ -65,10 +65,10 @@ impl FocusableView for ThemeSelector { } impl Render for ThemeSelector { - type Element = View>; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - self.picker.clone() + v_stack().min_w_96().child(self.picker.clone()) } } 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 36/48] 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 37/48] 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 dccdcd322101461fbdd83d83bf1736a3bd01b47c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 12:41:54 -0500 Subject: [PATCH 38/48] 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 39/48] 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 38d41acf9bfd76274ea93679f906e9e4fc320ea9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:29:19 -0800 Subject: [PATCH 40/48] 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 41/48] 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 42/48] 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 43/48] 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 44/48] 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 45/48] 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 863222edc5524e864cae584d918854bb4708d217 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 12:57:23 -0800 Subject: [PATCH 46/48] 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 47/48] 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 48/48] 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(); + }), + ) } }