diff --git a/Cargo.lock b/Cargo.lock index 5f04ebbe3e..a885c50968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5245,6 +5245,7 @@ dependencies = [ "git", "gpui", "menu", + "picker", "project", "schemars", "serde", @@ -5256,7 +5257,6 @@ dependencies = [ "util", "windows 0.58.0", "workspace", - "worktree", ] [[package]] @@ -7045,7 +7045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -9511,9 +9511,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -13372,6 +13372,7 @@ dependencies = [ "client", "collections", "feature_flags", + "git_ui", "gpui", "http_client", "notifications", @@ -15264,7 +15265,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 89e9c44574..e7481808e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -489,6 +489,7 @@ strum = { version = "0.26.0", features = ["derive"] } subtle = "2.5.0" sys-locale = "0.3.1" sysinfo = "0.31.0" +take-until = "0.2.0" tempfile = "3.9.0" thiserror = "1.0.29" tiktoken-rs = "0.6.0" diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 7223600d6f..acc7987d80 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -31,7 +31,7 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -worktree.workspace = true +picker.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2a6c76f9ac..a68bb3092e 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -7,13 +7,13 @@ use editor::scroll::ScrollbarAutoHide; use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar}; use futures::channel::mpsc; use futures::StreamExt as _; -use git::repository::{GitRepository, RepoPath}; +use git::repository::RepoPath; use git::status::FileStatus; use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll}; use gpui::*; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::git::GitState; -use project::{Fs, Project, ProjectPath, WorktreeId}; +use project::git::RepositoryHandle; +use project::{Fs, Project, ProjectPath}; use serde::{Deserialize, Serialize}; use settings::Settings as _; use std::sync::atomic::{AtomicBool, Ordering}; @@ -22,14 +22,13 @@ use theme::ThemeSettings; use ui::{ prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip, }; -use util::{maybe, ResultExt, TryFutureExt}; +use util::{ResultExt, TryFutureExt}; use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::Toast; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use worktree::RepositoryEntry; actions!( git_panel, @@ -80,7 +79,6 @@ pub struct GitListEntry { } pub struct GitPanel { - weak_workspace: WeakView, current_modifiers: Modifiers, focus_handle: FocusHandle, fs: Arc, @@ -88,6 +86,7 @@ pub struct GitPanel { pending_serialization: Task>, workspace: WeakView, project: Model, + active_repository: Option, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, selected_entry: Option, @@ -97,46 +96,46 @@ pub struct GitPanel { visible_entries: Vec, all_staged: Option, width: Option, - reveal_in_editor: Task<()>, err_sender: mpsc::Sender, } -fn first_worktree_repository( - project: &Model, - worktree_id: WorktreeId, - cx: &mut AppContext, -) -> Option<(RepositoryEntry, Arc)> { - project - .read(cx) - .worktree_for_id(worktree_id, cx) - .and_then(|worktree| { - let snapshot = worktree.read(cx).snapshot(); - let repo = snapshot.repositories().iter().next()?.clone(); - let git_repo = worktree - .read(cx) - .as_local()? - .get_local_repo(&repo)? - .repo() - .clone(); - Some((repo, git_repo)) - }) -} +fn commit_message_editor( + active_repository: Option<&RepositoryHandle>, + cx: &mut ViewContext<'_, Editor>, +) -> Editor { + let theme = ThemeSettings::get_global(cx); -fn first_repository_in_project( - project: &Model, - cx: &mut AppContext, -) -> Option<(WorktreeId, RepositoryEntry, Arc)> { - project.read(cx).worktrees(cx).next().and_then(|worktree| { - let snapshot = worktree.read(cx).snapshot(); - let repo = snapshot.repositories().iter().next()?.clone(); - let git_repo = worktree - .read(cx) - .as_local()? - .get_local_repo(&repo)? - .repo() - .clone(); - Some((snapshot.id(), repo, git_repo)) - }) + let mut text_style = cx.text_style(); + let refinement = TextStyleRefinement { + font_family: Some(theme.buffer_font.family.clone()), + font_features: Some(FontFeatures::disable_ligatures()), + font_size: Some(px(12.).into()), + color: Some(cx.theme().colors().editor_foreground), + background_color: Some(gpui::transparent_black()), + ..Default::default() + }; + text_style.refine(&refinement); + + let mut commit_editor = if let Some(active_repository) = active_repository.as_ref() { + let buffer = + cx.new_model(|cx| MultiBuffer::singleton(active_repository.commit_message(), cx)); + Editor::new( + EditorMode::AutoHeight { max_lines: 10 }, + buffer, + None, + false, + cx, + ) + } else { + Editor::auto_height(10, cx) + }; + commit_editor.set_use_autoclose(false); + commit_editor.set_show_gutter(false, cx); + commit_editor.set_show_wrap_guides(false, cx); + commit_editor.set_show_indent_guides(false, cx); + commit_editor.set_text_style_refinement(refinement); + commit_editor.set_placeholder_text("Enter commit message", cx); + commit_editor } impl GitPanel { @@ -150,8 +149,8 @@ impl GitPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let fs = workspace.app_state().fs.clone(); let project = workspace.project().clone(); - let weak_workspace = cx.view().downgrade(); let git_state = project.read(cx).git_state().cloned(); + let active_repository = project.read(cx).active_repository(cx); let (err_sender, mut err_receiver) = mpsc::channel(1); let workspace = cx.view().downgrade(); @@ -162,143 +161,12 @@ impl GitPanel { this.hide_scrollbar(cx); }) .detach(); - cx.subscribe(&project, { - let git_state = git_state.clone(); - move |this, project, event, cx| { - use project::Event; - let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| { - let snapshot = worktree.read(cx).snapshot(); - snapshot.id() - }); - let first_repo_in_project = first_repository_in_project(&project, cx); - - let Some(git_state) = git_state.clone() else { - return; - }; - git_state.update(cx, |git_state, _| { - match event { - project::Event::WorktreeRemoved(id) => { - let Some((worktree_id, _, _)) = - git_state.active_repository.as_ref() - else { - return; - }; - if worktree_id == id { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeOrderChanged => { - // activate the new first worktree if the first was moved - let Some(first_id) = first_worktree_id else { - return; - }; - if !git_state - .active_repository - .as_ref() - .is_some_and(|(id, _, _)| id == &first_id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - Event::WorktreeAdded(_) => { - let Some(first_id) = first_worktree_id else { - return; - }; - if !git_state - .active_repository - .as_ref() - .is_some_and(|(id, _, _)| id == &first_id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeUpdatedEntries(id, _) => { - if git_state - .active_repository - .as_ref() - .is_some_and(|(active_id, _, _)| active_id == id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeUpdatedGitRepositories(_) => { - let Some(first) = first_repo_in_project else { - return; - }; - git_state.active_repository = Some(first); - this.schedule_update(); - } - project::Event::Closed => { - this.reveal_in_editor = Task::ready(()); - this.visible_entries.clear(); - } - _ => {} - }; - }); - } - }) - .detach(); - - let commit_editor = cx.new_view(|cx| { - let theme = ThemeSettings::get_global(cx); - - let mut text_style = cx.text_style(); - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(px(12.).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - text_style.refine(&refinement); - - let mut commit_editor = if let Some(git_state) = git_state.as_ref() { - let buffer = cx.new_model(|cx| { - MultiBuffer::singleton(git_state.read(cx).commit_message.clone(), cx) - }); - // TODO should we attach the project? - Editor::new( - EditorMode::AutoHeight { max_lines: 10 }, - buffer, - None, - false, - cx, - ) - } else { - Editor::auto_height(10, cx) - }; - commit_editor.set_use_autoclose(false); - commit_editor.set_show_gutter(false, cx); - commit_editor.set_show_wrap_guides(false, cx); - commit_editor.set_show_indent_guides(false, cx); - commit_editor.set_text_style_refinement(refinement); - commit_editor.set_placeholder_text("Enter commit message", cx); - commit_editor - }); + let commit_editor = + cx.new_view(|cx| commit_message_editor(active_repository.as_ref(), cx)); let scroll_handle = UniformListScrollHandle::new(); - let mut visible_worktrees = project.read(cx).visible_worktrees(cx); - let first_worktree = visible_worktrees.next(); - drop(visible_worktrees); - if let Some(first_worktree) = first_worktree { - let snapshot = first_worktree.read(cx).snapshot(); - - if let Some(((repo, git_repo), git_state)) = - first_worktree_repository(&project, snapshot.id(), cx).zip(git_state) - { - git_state.update(cx, |git_state, _| { - git_state.activate_repository(snapshot.id(), repo, git_repo); - }); - } - }; - let rebuild_requested = Arc::new(AtomicBool::new(false)); let flag = rebuild_requested.clone(); let handle = cx.view().downgrade(); @@ -309,6 +177,9 @@ impl GitPanel { if let Some(this) = handle.upgrade() { this.update(&mut cx, |this, cx| { this.update_visible_entries(cx); + let active_repository = this.active_repository.as_ref(); + this.commit_editor = + cx.new_view(|cx| commit_message_editor(active_repository, cx)); }) .ok(); } @@ -318,24 +189,33 @@ impl GitPanel { }) .detach(); + if let Some(git_state) = git_state { + cx.subscribe(&git_state, move |this, git_state, event, cx| match event { + project::git::Event::RepositoriesUpdated => { + this.active_repository = git_state.read(cx).active_repository(); + this.schedule_update(); + } + }) + .detach(); + } + let mut git_panel = Self { - weak_workspace, focus_handle: cx.focus_handle(), - fs, pending_serialization: Task::ready(None), visible_entries: Vec::new(), all_staged: None, current_modifiers: cx.modifiers(), width: Some(px(360.)), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), - scroll_handle, selected_entry: None, show_scrollbar: false, hide_scrollbar_task: None, + active_repository, + scroll_handle, + fs, rebuild_requested, commit_editor, project, - reveal_in_editor: Task::ready(()), err_sender, workspace, }; @@ -380,19 +260,6 @@ impl GitPanel { git_panel } - fn git_state(&self, cx: &AppContext) -> Option> { - self.project.read(cx).git_state().cloned() - } - - fn active_repository<'a>( - &self, - cx: &'a AppContext, - ) -> Option<&'a (WorktreeId, RepositoryEntry, Arc)> { - let git_state = self.git_state(cx)?; - let active_repository = git_state.read(cx).active_repository.as_ref()?; - Some(active_repository) - } - fn serialize(&mut self, cx: &mut ViewContext) { // TODO: we can store stage status here let width = self.width; @@ -595,7 +462,13 @@ impl GitPanel { } fn select_first_entry_if_none(&mut self, cx: &mut ViewContext) { - if !self.no_entries(cx) && self.selected_entry.is_none() { + let have_entries = self + .active_repository + .as_ref() + .map_or(false, |active_repository| { + active_repository.entry_count() > 0 + }); + if have_entries && self.selected_entry.is_none() { self.selected_entry = Some(0); self.scroll_to_selected_entry(cx); cx.notify(); @@ -624,16 +497,15 @@ impl GitPanel { } fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext) { - let Some(git_state) = self.git_state(cx) else { + let Some(active_repository) = self.active_repository.as_ref() else { return; }; - let result = git_state.update(cx, |git_state, _| { - if entry.status.is_staged().unwrap_or(false) { - git_state.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone()) - } else { - git_state.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone()) - } - }); + let result = if entry.status.is_staged().unwrap_or(false) { + active_repository + .unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone()) + } else { + active_repository.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone()) + }; if let Err(e) = result { self.show_err_toast("toggle staged error", e, cx); } @@ -647,26 +519,24 @@ impl GitPanel { } fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext) { - let Some((worktree_id, path)) = maybe!({ - let git_state = self.git_state(cx)?; - let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?; - let path = repo.work_directory.unrelativize(&entry.repo_path)?; - Some((*id, path)) - }) else { + let Some(active_repository) = self.active_repository.as_ref() else { + return; + }; + let Some(path) = active_repository.unrelativize(&entry.repo_path) else { return; }; - let path = (worktree_id, path).into(); let path_exists = self.project.update(cx, |project, cx| { project.entry_for_path(&path, cx).is_some() }); if !path_exists { return; } + // TODO maybe move all of this into project? cx.emit(Event::OpenedEntry { path }); } fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext) { - let Some(git_state) = self.git_state(cx) else { + let Some(active_repository) = self.active_repository.as_ref() else { return; }; for entry in &mut self.visible_entries { @@ -674,20 +544,20 @@ impl GitPanel { } self.all_staged = Some(true); - if let Err(e) = git_state.read(cx).stage_all(self.err_sender.clone()) { + if let Err(e) = active_repository.stage_all(self.err_sender.clone()) { self.show_err_toast("stage all error", e, cx); }; } fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext) { - let Some(git_state) = self.git_state(cx) else { + let Some(active_repository) = self.active_repository.as_ref() else { return; }; for entry in &mut self.visible_entries { entry.is_staged = Some(false); } self.all_staged = Some(false); - if let Err(e) = git_state.read(cx).unstage_all(self.err_sender.clone()) { + if let Err(e) = active_repository.unstage_all(self.err_sender.clone()) { self.show_err_toast("unstage all error", e, cx); }; } @@ -699,12 +569,10 @@ impl GitPanel { /// Commit all staged changes fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext) { - let Some(git_state) = self.git_state(cx) else { + let Some(active_repository) = self.active_repository.as_ref() else { return; }; - if let Err(e) = git_state.update(cx, |git_state, cx| { - git_state.commit(self.err_sender.clone(), cx) - }) { + if let Err(e) = active_repository.commit(self.err_sender.clone(), cx) { self.show_err_toast("commit error", e, cx); }; self.commit_editor @@ -713,12 +581,10 @@ impl GitPanel { /// Commit all changes, regardless of whether they are staged or not fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext) { - let Some(git_state) = self.git_state(cx) else { + let Some(active_repository) = self.active_repository.as_ref() else { return; }; - if let Err(e) = git_state.update(cx, |git_state, cx| { - git_state.commit_all(self.err_sender.clone(), cx) - }) { + if let Err(e) = active_repository.commit_all(self.err_sender.clone(), cx) { self.show_err_toast("commit all error", e, cx); }; self.commit_editor @@ -790,11 +656,6 @@ impl GitPanel { }); } - fn no_entries(&self, cx: &mut ViewContext) -> bool { - self.git_state(cx) - .map_or(true, |git_state| git_state.read(cx).entry_count() == 0) - } - fn for_each_visible_entry( &self, range: Range, @@ -832,11 +693,10 @@ impl GitPanel { self.rebuild_requested.store(true, Ordering::Relaxed); } - #[track_caller] fn update_visible_entries(&mut self, cx: &mut ViewContext) { self.visible_entries.clear(); - let Some((_, repo, _)) = self.active_repository(cx) else { + let Some(repo) = self.active_repository.as_ref() else { // Just clear entries if no repository is active. cx.notify(); return; @@ -882,7 +742,7 @@ impl GitPanel { let entry = GitListEntry { depth, display_name, - repo_path: entry.repo_path, + repo_path: entry.repo_path.clone(), status: entry.status, is_staged, }; @@ -901,7 +761,7 @@ impl GitPanel { } fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext) { - let Some(workspace) = self.weak_workspace.upgrade() else { + let Some(workspace) = self.workspace.upgrade() else { return; }; let notif_id = NotificationId::Named(id.into()); @@ -942,8 +802,9 @@ impl GitPanel { pub fn render_panel_header(&self, cx: &mut ViewContext) -> impl IntoElement { let focus_handle = self.focus_handle(cx).clone(); let entry_count = self - .git_state(cx) - .map_or(0, |git_state| git_state.read(cx).entry_count()); + .active_repository + .as_ref() + .map_or(0, RepositoryHandle::entry_count); let changes_string = match entry_count { 0 => "No changes".to_string(), @@ -965,7 +826,7 @@ impl GitPanel { .child( Checkbox::new( "all-changes", - if self.no_entries(cx) { + if entry_count == 0 { ToggleState::Selected } else { self.all_staged @@ -1056,13 +917,15 @@ impl GitPanel { pub fn render_commit_editor(&self, cx: &ViewContext) -> impl IntoElement { let editor = self.commit_editor.clone(); let editor_focus_handle = editor.read(cx).focus_handle(cx).clone(); - let (can_commit, can_commit_all) = self.git_state(cx).map_or((false, false), |git_state| { - let git_state = git_state.read(cx); - ( - git_state.can_commit(false, cx), - git_state.can_commit(true, cx), - ) - }); + let (can_commit, can_commit_all) = self.active_repository.as_ref().map_or_else( + || (false, false), + |active_repository| { + ( + active_repository.can_commit(false, cx), + active_repository.can_commit(true, cx), + ) + }, + ); let focus_handle_1 = self.focus_handle(cx).clone(); let focus_handle_2 = self.focus_handle(cx).clone(); @@ -1316,15 +1179,17 @@ impl GitPanel { ToggleState::Indeterminate => None, }; let repo_path = repo_path.clone(); - let Some(git_state) = this.git_state(cx) else { + let Some(active_repository) = this.active_repository.as_ref() else { return; }; - let result = git_state.update(cx, |git_state, _| match toggle { - ToggleState::Selected | ToggleState::Indeterminate => git_state - .stage_entries(vec![repo_path], this.err_sender.clone()), - ToggleState::Unselected => git_state + let result = match toggle { + ToggleState::Selected | ToggleState::Indeterminate => { + active_repository + .stage_entries(vec![repo_path], this.err_sender.clone()) + } + ToggleState::Unselected => active_repository .unstage_entries(vec![repo_path], this.err_sender.clone()), - }); + }; if let Err(e) = result { this.show_err_toast("toggle staged error", e, cx); } @@ -1373,6 +1238,12 @@ impl GitPanel { impl Render for GitPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let project = self.project.read(cx); + let has_entries = self + .active_repository + .as_ref() + .map_or(false, |active_repository| { + active_repository.entry_count() > 0 + }); let has_co_authors = self .workspace .upgrade() @@ -1437,7 +1308,7 @@ impl Render for GitPanel { .bg(ElevationIndex::Surface.bg(cx)) .child(self.render_panel_header(cx)) .child(self.render_divider(cx)) - .child(if !self.no_entries(cx) { + .child(if has_entries { self.render_entries(cx).into_any_element() } else { self.render_empty_state(cx).into_any_element() diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 5bbd1b4585..5cb341ed45 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -6,6 +6,7 @@ use ui::{Color, Icon, IconName, IntoElement}; pub mod git_panel; mod git_panel_settings; +pub mod repository_selector; pub fn init(cx: &mut AppContext) { GitPanelSettings::register(cx); diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs new file mode 100644 index 0000000000..04d15d57cc --- /dev/null +++ b/crates/git_ui/src/repository_selector.rs @@ -0,0 +1,232 @@ +use gpui::{ + AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + Subscription, Task, View, WeakModel, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use project::{ + git::{GitState, RepositoryHandle}, + Project, +}; +use std::sync::Arc; +use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; + +pub struct RepositorySelector { + picker: View>, + /// The task used to update the picker's matches when there is a change to + /// the repository list. + update_matches_task: Option>, + _subscriptions: Vec, +} + +impl RepositorySelector { + pub fn new(project: Model, cx: &mut ViewContext) -> Self { + let git_state = project.read(cx).git_state().cloned(); + let all_repositories = git_state + .as_ref() + .map_or(vec![], |git_state| git_state.read(cx).all_repositories()); + let filtered_repositories = all_repositories.clone(); + let delegate = RepositorySelectorDelegate { + project: project.downgrade(), + repository_selector: cx.view().downgrade(), + repository_entries: all_repositories, + filtered_repositories, + selected_index: 0, + }; + + let picker = + cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()))); + + let _subscriptions = if let Some(git_state) = git_state { + vec![cx.subscribe(&git_state, Self::handle_project_git_event)] + } else { + Vec::new() + }; + + RepositorySelector { + picker, + update_matches_task: None, + _subscriptions, + } + } + + fn handle_project_git_event( + &mut self, + git_state: Model, + _event: &project::git::Event, + cx: &mut ViewContext, + ) { + // TODO handle events individually + let task = self.picker.update(cx, |this, cx| { + let query = this.query(cx); + this.delegate.repository_entries = git_state.read(cx).all_repositories(); + this.delegate.update_matches(query, cx) + }); + self.update_matches_task = Some(task); + } +} + +impl EventEmitter for RepositorySelector {} + +impl FocusableView for RepositorySelector { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for RepositorySelector { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + self.picker.clone() + } +} + +#[derive(IntoElement)] +pub struct RepositorySelectorPopoverMenu +where + T: PopoverTrigger, +{ + repository_selector: View, + trigger: T, + handle: Option>, +} + +impl RepositorySelectorPopoverMenu { + pub fn new(repository_selector: View, trigger: T) -> Self { + Self { + repository_selector, + trigger, + handle: None, + } + } + + pub fn with_handle(mut self, handle: PopoverMenuHandle) -> Self { + self.handle = Some(handle); + self + } +} + +impl RenderOnce for RepositorySelectorPopoverMenu { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + let repository_selector = self.repository_selector.clone(); + + PopoverMenu::new("repository-switcher") + .menu(move |_cx| Some(repository_selector.clone())) + .trigger(self.trigger) + .attach(gpui::Corner::BottomLeft) + .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) + } +} + +pub struct RepositorySelectorDelegate { + project: WeakModel, + repository_selector: WeakView, + repository_entries: Vec, + filtered_repositories: Vec, + selected_index: usize, +} + +impl RepositorySelectorDelegate { + pub fn update_repository_entries(&mut self, all_repositories: Vec) { + self.repository_entries = all_repositories.clone(); + self.filtered_repositories = all_repositories; + self.selected_index = 0; + } +} + +impl PickerDelegate for RepositorySelectorDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.filtered_repositories.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Select a repository...".into() + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let all_repositories = self.repository_entries.clone(); + + cx.spawn(|this, mut cx| async move { + let filtered_repositories = cx + .background_executor() + .spawn(async move { + if query.is_empty() { + all_repositories + } else { + all_repositories + .into_iter() + .filter(|_repo_info| { + // TODO: Implement repository filtering logic + true + }) + .collect() + } + }) + .await; + + this.update(&mut cx, |this, cx| { + this.delegate.filtered_repositories = filtered_repositories; + this.delegate.set_selected_index(0, cx); + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else { + return; + }; + selected_repo.activate(cx); + self.dismissed(cx); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.repository_selector + .update(cx, |_this, cx| cx.emit(DismissEvent)) + .ok(); + } + + fn render_header(&self, _cx: &mut ViewContext>) -> Option { + // TODO: Implement header rendering if needed + None + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let project = self.project.upgrade()?; + let repo_info = self.filtered_repositories.get(ix)?; + let display_name = repo_info.display_name(project.read(cx), cx); + // TODO: Implement repository item rendering + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(display_name)), + ) + } + + fn render_footer(&self, cx: &mut ViewContext>) -> Option { + // TODO: Implement footer rendering if needed + Some( + div() + .text_ui_sm(cx) + .child("Temporary location for repo selector") + .into_any_element(), + ) + } +} diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index d699061bad..5ad6fb55bb 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -1,25 +1,55 @@ -use anyhow::{anyhow, Context as _}; +use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; +use crate::{Project, ProjectPath}; +use anyhow::anyhow; use futures::channel::mpsc; use futures::{SinkExt as _, StreamExt as _}; use git::{ repository::{GitRepository, RepoPath}, status::{GitSummary, TrackedSummary}, }; -use gpui::{AppContext, Context as _, Model}; +use gpui::{ + AppContext, Context as _, EventEmitter, Model, ModelContext, SharedString, Subscription, + WeakModel, +}; use language::{Buffer, LanguageRegistry}; use settings::WorktreeId; use std::sync::Arc; use text::Rope; -use worktree::RepositoryEntry; +use util::maybe; +use worktree::{RepositoryEntry, StatusEntry}; pub struct GitState { - pub commit_message: Model, - - /// When a git repository is selected, this is used to track which repository's changes - /// are currently being viewed or modified in the UI. - pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc)>, - + repositories: Vec, + active_index: Option, update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender)>, + languages: Arc, + _subscription: Subscription, +} + +#[derive(Clone)] +pub struct RepositoryHandle { + git_state: WeakModel, + worktree_id: WorktreeId, + repository_entry: RepositoryEntry, + git_repo: Arc, + commit_message: Model, + update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender)>, +} + +impl PartialEq for RepositoryHandle { + fn eq(&self, other: &Self) -> bool { + self.worktree_id == other.worktree_id + && self.repository_entry.work_directory_id() + == other.repository_entry.work_directory_id() + } +} + +impl Eq for RepositoryHandle {} + +impl PartialEq for RepositoryHandle { + fn eq(&self, other: &RepositoryEntry) -> bool { + self.repository_entry.work_directory_id() == other.work_directory_id() + } } enum Message { @@ -29,11 +59,21 @@ enum Message { Unstage(Arc, Vec), } +pub enum Event { + RepositoriesUpdated, +} + +impl EventEmitter for GitState {} + impl GitState { - pub fn new(languages: Arc, cx: &mut AppContext) -> Self { + pub fn new( + worktree_store: &Model, + languages: Arc, + cx: &mut ModelContext<'_, Self>, + ) -> Self { let (update_sender, mut update_receiver) = mpsc::unbounded::<(Message, mpsc::Sender)>(); - cx.spawn(|cx| async move { + cx.spawn(|_, cx| async move { while let Some((msg, mut err_sender)) = update_receiver.next().await { let result = cx .background_executor() @@ -57,39 +97,147 @@ impl GitState { }) .detach(); - let commit_message = cx.new_model(|cx| Buffer::local("", cx)); - let markdown = languages.language_for_name("Markdown"); - cx.spawn({ - let commit_message = commit_message.clone(); - |mut cx| async move { - let markdown = markdown.await.context("failed to load Markdown language")?; - commit_message.update(&mut cx, |commit_message, cx| { - commit_message.set_language(Some(markdown), cx) - }) - } - }) - .detach_and_log_err(cx); + let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event); GitState { - commit_message, - active_repository: None, + languages, + repositories: vec![], + active_index: None, update_sender, + _subscription, } } - pub fn activate_repository( - &mut self, - worktree_id: WorktreeId, - active_repository: RepositoryEntry, - git_repo: Arc, - ) { - self.active_repository = Some((worktree_id, active_repository, git_repo)); + pub fn active_repository(&self) -> Option { + self.active_index + .map(|index| self.repositories[index].clone()) } - pub fn active_repository( - &self, - ) -> Option<&(WorktreeId, RepositoryEntry, Arc)> { - self.active_repository.as_ref() + fn on_worktree_store_event( + &mut self, + worktree_store: Model, + _event: &WorktreeStoreEvent, + cx: &mut ModelContext<'_, Self>, + ) { + // TODO inspect the event + + let mut new_repositories = Vec::new(); + let mut new_active_index = None; + let this = cx.weak_model(); + + worktree_store.update(cx, |worktree_store, cx| { + for worktree in worktree_store.worktrees() { + worktree.update(cx, |worktree, cx| { + let snapshot = worktree.snapshot(); + let Some(local) = worktree.as_local() else { + return; + }; + for repo in snapshot.repositories().iter() { + let Some(local_repo) = local.get_local_repo(repo) else { + continue; + }; + let existing = self + .repositories + .iter() + .enumerate() + .find(|(_, existing_handle)| existing_handle == &repo); + let handle = if let Some((index, handle)) = existing { + if self.active_index == Some(index) { + new_active_index = Some(new_repositories.len()); + } + // Update the statuses but keep everything else. + let mut existing_handle = handle.clone(); + existing_handle.repository_entry = repo.clone(); + existing_handle + } else { + let commit_message = cx.new_model(|cx| Buffer::local("", cx)); + cx.spawn({ + let commit_message = commit_message.downgrade(); + let languages = self.languages.clone(); + |_, mut cx| async move { + let markdown = languages.language_for_name("Markdown").await?; + commit_message.update(&mut cx, |commit_message, cx| { + commit_message.set_language(Some(markdown), cx); + })?; + anyhow::Ok(()) + } + }) + .detach_and_log_err(cx); + RepositoryHandle { + git_state: this.clone(), + worktree_id: worktree.id(), + repository_entry: repo.clone(), + git_repo: local_repo.repo().clone(), + commit_message, + update_sender: self.update_sender.clone(), + } + }; + new_repositories.push(handle); + } + }) + } + }); + + if new_active_index == None && new_repositories.len() > 0 { + new_active_index = Some(0); + } + + self.repositories = new_repositories; + self.active_index = new_active_index; + + cx.emit(Event::RepositoriesUpdated); + } + + pub fn all_repositories(&self) -> Vec { + self.repositories.clone() + } +} + +impl RepositoryHandle { + pub fn display_name(&self, project: &Project, cx: &AppContext) -> SharedString { + maybe!({ + let path = self.unrelativize(&"".into())?; + Some( + project + .absolute_path(&path, cx)? + .file_name()? + .to_string_lossy() + .to_string() + .into(), + ) + }) + .unwrap_or("".into()) + } + + pub fn activate(&self, cx: &mut AppContext) { + let Some(git_state) = self.git_state.upgrade() else { + return; + }; + git_state.update(cx, |git_state, cx| { + let Some((index, _)) = git_state + .repositories + .iter() + .enumerate() + .find(|(_, handle)| handle == &self) + else { + return; + }; + git_state.active_index = Some(index); + cx.emit(Event::RepositoriesUpdated); + }); + } + + pub fn status(&self) -> impl '_ + Iterator { + self.repository_entry.status() + } + + pub fn unrelativize(&self, path: &RepoPath) -> Option { + let path = self.repository_entry.unrelativize(path)?; + Some((self.worktree_id, path).into()) + } + + pub fn commit_message(&self) -> Model { + self.commit_message.clone() } pub fn stage_entries( @@ -100,11 +248,8 @@ impl GitState { if entries.is_empty() { return Ok(()); } - let Some((_, _, git_repo)) = self.active_repository.as_ref() else { - return Err(anyhow!("No active repository")); - }; self.update_sender - .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender)) + .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender)) .map_err(|_| anyhow!("Failed to submit stage operation"))?; Ok(()) } @@ -117,20 +262,15 @@ impl GitState { if entries.is_empty() { return Ok(()); } - let Some((_, _, git_repo)) = self.active_repository.as_ref() else { - return Err(anyhow!("No active repository")); - }; self.update_sender - .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender)) + .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender)) .map_err(|_| anyhow!("Failed to submit unstage operation"))?; Ok(()) } pub fn stage_all(&self, err_sender: mpsc::Sender) -> anyhow::Result<()> { - let Some((_, entry, _)) = self.active_repository.as_ref() else { - return Err(anyhow!("No active repository")); - }; - let to_stage = entry + let to_stage = self + .repository_entry .status() .filter(|entry| !entry.status.is_staged().unwrap_or(false)) .map(|entry| entry.repo_path.clone()) @@ -140,10 +280,8 @@ impl GitState { } pub fn unstage_all(&self, err_sender: mpsc::Sender) -> anyhow::Result<()> { - let Some((_, entry, _)) = self.active_repository.as_ref() else { - return Err(anyhow!("No active repository")); - }; - let to_unstage = entry + let to_unstage = self + .repository_entry .status() .filter(|entry| entry.status.is_staged().unwrap_or(true)) .map(|entry| entry.repo_path.clone()) @@ -155,23 +293,15 @@ impl GitState { /// Get a count of all entries in the active repository, including /// untracked files. pub fn entry_count(&self) -> usize { - self.active_repository - .as_ref() - .map_or(0, |(_, entry, _)| entry.status_len()) + self.repository_entry.status_len() } fn have_changes(&self) -> bool { - let Some((_, entry, _)) = self.active_repository.as_ref() else { - return false; - }; - entry.status_summary() != GitSummary::UNCHANGED + self.repository_entry.status_summary() != GitSummary::UNCHANGED } fn have_staged_changes(&self) -> bool { - let Some((_, entry, _)) = self.active_repository.as_ref() else { - return false; - }; - entry.status_summary().index != TrackedSummary::UNCHANGED + self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED } pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool { @@ -185,36 +315,33 @@ impl GitState { } pub fn commit( - &mut self, + &self, err_sender: mpsc::Sender, - cx: &AppContext, + cx: &mut AppContext, ) -> anyhow::Result<()> { if !self.can_commit(false, cx) { return Err(anyhow!("Unable to commit")); } - let Some((_, _, git_repo)) = self.active_repository() else { - return Err(anyhow!("No active repository")); - }; - let git_repo = git_repo.clone(); let message = self.commit_message.read(cx).as_rope().clone(); self.update_sender - .unbounded_send((Message::Commit(git_repo, message), err_sender)) + .unbounded_send((Message::Commit(self.git_repo.clone(), message), err_sender)) .map_err(|_| anyhow!("Failed to submit commit operation"))?; + self.commit_message.update(cx, |commit_message, cx| { + commit_message.set_text("", cx); + }); Ok(()) } pub fn commit_all( - &mut self, + &self, err_sender: mpsc::Sender, - cx: &AppContext, + cx: &mut AppContext, ) -> anyhow::Result<()> { if !self.can_commit(true, cx) { return Err(anyhow!("Unable to commit")); } - let Some((_, entry, git_repo)) = self.active_repository.as_ref() else { - return Err(anyhow!("No active repository")); - }; - let to_stage = entry + let to_stage = self + .repository_entry .status() .filter(|entry| !entry.status.is_staged().unwrap_or(false)) .map(|entry| entry.repo_path.clone()) @@ -222,10 +349,13 @@ impl GitState { let message = self.commit_message.read(cx).as_rope().clone(); self.update_sender .unbounded_send(( - Message::StageAndCommit(git_repo.clone(), message, to_stage), + Message::StageAndCommit(self.git_repo.clone(), message, to_stage), err_sender, )) .map_err(|_| anyhow!("Failed to submit commit operation"))?; + self.commit_message.update(cx, |commit_message, cx| { + commit_message.set_text("", cx); + }); Ok(()) } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index cbd93ad12a..ad24c94a45 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3128,12 +3128,15 @@ impl LspStore { }) .detach() } - WorktreeStoreEvent::WorktreeReleased(..) => {} WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx), - WorktreeStoreEvent::WorktreeOrderChanged => {} WorktreeStoreEvent::WorktreeUpdateSent(worktree) => { worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree)); } + WorktreeStoreEvent::WorktreeReleased(..) + | WorktreeStoreEvent::WorktreeOrderChanged + | WorktreeStoreEvent::WorktreeUpdatedEntries(..) + | WorktreeStoreEvent::WorktreeUpdatedGitRepositories(..) + | WorktreeStoreEvent::WorktreeDeletedEntry(..) => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 51afd8d614..78965f64b5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -22,6 +22,7 @@ mod project_tests; mod direnv; mod environment; pub use environment::EnvironmentErrorMessage; +use git::RepositoryHandle; pub mod search_history; mod yarn; @@ -691,7 +692,8 @@ impl Project { ) }); - let git_state = Some(cx.new_model(|cx| GitState::new(languages.clone(), cx))); + let git_state = + Some(cx.new_model(|cx| GitState::new(&worktree_store, languages.clone(), cx))); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); @@ -2324,6 +2326,18 @@ impl Project { } WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged), WorktreeStoreEvent::WorktreeUpdateSent(_) => {} + WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, changes) => { + self.client() + .telemetry() + .report_discovered_project_events(*worktree_id, changes); + cx.emit(Event::WorktreeUpdatedEntries(*worktree_id, changes.clone())) + } + WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id) => { + cx.emit(Event::WorktreeUpdatedGitRepositories(*worktree_id)) + } + WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, id) => { + cx.emit(Event::DeletedEntry(*worktree_id, *id)) + } } } @@ -2335,27 +2349,6 @@ impl Project { } } cx.observe(worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(worktree, |project, worktree, event, cx| { - let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); - match event { - worktree::Event::UpdatedEntries(changes) => { - cx.emit(Event::WorktreeUpdatedEntries( - worktree.read(cx).id(), - changes.clone(), - )); - - project - .client() - .telemetry() - .report_discovered_project_events(worktree_id, changes); - } - worktree::Event::UpdatedGitRepositories(_) => { - cx.emit(Event::WorktreeUpdatedGitRepositories(worktree_id)); - } - worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(worktree_id, *id)), - } - }) - .detach(); cx.notify(); } @@ -4169,6 +4162,17 @@ impl Project { pub fn git_state(&self) -> Option<&Model> { self.git_state.as_ref() } + + pub fn active_repository(&self, cx: &AppContext) -> Option { + self.git_state() + .and_then(|git_state| git_state.read(cx).active_repository()) + } + + pub fn all_repositories(&self, cx: &AppContext) -> Vec { + self.git_state() + .map(|git_state| git_state.read(cx).all_repositories()) + .unwrap_or_default() + } } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index e7b1510e2d..3595ed06d6 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -25,7 +25,7 @@ use smol::{ }; use text::ReplicaId; use util::{paths::SanitizedPath, ResultExt}; -use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings}; +use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings}; use crate::{search::SearchQuery, ProjectPath}; @@ -63,6 +63,9 @@ pub enum WorktreeStoreEvent { WorktreeReleased(EntityId, WorktreeId), WorktreeOrderChanged, WorktreeUpdateSent(Model), + WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), + WorktreeUpdatedGitRepositories(WorktreeId), + WorktreeDeletedEntry(WorktreeId, ProjectEntryId), } impl EventEmitter for WorktreeStore {} @@ -364,6 +367,26 @@ impl WorktreeStore { self.send_project_updates(cx); let handle_id = worktree.entity_id(); + cx.subscribe(worktree, |_, worktree, event, cx| { + let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); + match event { + worktree::Event::UpdatedEntries(changes) => { + cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries( + worktree.read(cx).id(), + changes.clone(), + )); + } + worktree::Event::UpdatedGitRepositories(_) => { + cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories( + worktree_id, + )); + } + worktree::Event::DeletedEntry(id) => { + cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id)) + } + } + }) + .detach(); cx.observe_release(worktree, move |this, worktree, cx| { cx.emit(WorktreeStoreEvent::WorktreeReleased( handle_id, diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 3d1ee71e10..a07c2e4b64 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -47,6 +47,7 @@ util.workspace = true telemetry.workspace = true workspace.workspace = true zed_actions.workspace = true +git_ui.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index cab85f6f19..03442dd5b7 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -16,6 +16,8 @@ use auto_update::AutoUpdateStatus; use call::ActiveCall; use client::{Client, UserStore}; use feature_flags::{FeatureFlagAppExt, ZedPro}; +use git_ui::repository_selector::RepositorySelector; +use git_ui::repository_selector::RepositorySelectorPopoverMenu; use gpui::{ actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement, Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful, @@ -98,6 +100,7 @@ pub struct TitleBar { platform_style: PlatformStyle, content: Stateful
, children: SmallVec<[AnyElement; 2]>, + repository_selector: View, project: Model, user_store: Model, client: Arc, @@ -181,6 +184,7 @@ impl Render for TitleBar { title_bar .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) + .children(self.render_current_repository(cx)) .children(self.render_project_branch(cx)) }) }) @@ -290,6 +294,7 @@ impl TitleBar { content: div().id(id.into()), children: SmallVec::new(), application_menu, + repository_selector: cx.new_view(|cx| RepositorySelector::new(project.clone(), cx)), workspace: workspace.weak_handle(), should_move: false, project, @@ -474,6 +479,39 @@ impl TitleBar { })) } + // NOTE: Not sure we want to keep this in the titlebar, but for while we are working on Git it is helpful in the short term + pub fn render_current_repository( + &self, + cx: &mut ViewContext, + ) -> Option { + // TODO what to render if no active repository? + let active_repository = self.project.read(cx).active_repository(cx)?; + let display_name = active_repository.display_name(self.project.read(cx), cx); + Some(RepositorySelectorPopoverMenu::new( + self.repository_selector.clone(), + ButtonLike::new("active-repository") + .style(ButtonStyle::Subtle) + .child( + h_flex().w_full().gap_0p5().child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child( + h_flex() + .gap_1() + .child( + Label::new(display_name) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + ), + ), + ), + )) + } + pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { let entry = { let mut names_and_branches = diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 1873b810fe..213e2fc0d4 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -32,7 +32,7 @@ rust-embed.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true -take-until = "0.2.0" +take-until.workspace = true tempfile = { workspace = true, optional = true } unicase.workspace = true