use anyhow::Context as _;use editor::Editor;use fuzzy::StringMatchCandidate; use collections::HashSet;use git::repository::Branch;use gpui::http_client::Url;use gpui::{ Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,};use picker::{Picker, PickerDelegate, PickerEditorPosition};use project::git_store::Repository;use project::project_settings::ProjectSettings;use settings::Settings;use std::sync::Arc;use time::OffsetDateTime;use ui::{ Divider, HighlightedLabel, KeyBinding, ListHeader, ListItem, ListItemSpacing, Tooltip, prelude::*,};use ui_input::ErasedEditor;use util::ResultExt;use workspace::notifications::DetachAndPromptErr;use workspace::{ModalView, Workspace}; use crate::{branch_picker, git_panel::show_error_toast, resolve_active_repository}; actions!( branch_picker, [ /// Deletes the selected git branch or remote. DeleteBranch, /// Filter the list of remotes FilterRemotes ]); pub fn checkout_branch( workspace: &mut Workspace, _: &zed_actions::git::CheckoutBranch, window: &mut Window, cx: &mut Context<Workspace>,) { open(workspace, &zed_actions::git::Branch, window, cx);} pub fn switch( workspace: &mut Workspace, _: &zed_actions::git::Switch, window: &mut Window, cx: &mut Context<Workspace>,) { open(workspace, &zed_actions::git::Branch, window, cx);} pub fn open( workspace: &mut Workspace, _: &zed_actions::git::Branch, window: &mut Window, cx: &mut Context<Workspace>,) { let workspace_handle = workspace.weak_handle(); let repository = resolve_active_repository(workspace, cx); workspace.toggle_modal(window, cx, |window, cx| { BranchList::new( workspace_handle, repository, BranchListStyle::Modal, rems(34.), window, cx, ) })} pub fn popover( workspace: WeakEntity<Workspace>, modal_style: bool, repository: Option<Entity<Repository>>, window: &mut Window, cx: &mut App,) -> Entity<BranchList> { let (style, width) = if modal_style { (BranchListStyle::Modal, rems(34.)) } else { (BranchListStyle::Popover, rems(20.)) }; cx.new(|cx| { let list = BranchList::new(workspace, repository, style, width, window, cx); list.focus_handle(cx).focus(window, cx); list })} pub fn create_embedded( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, width: Rems, window: &mut Window, cx: &mut Context<BranchList>,) -> BranchList { BranchList::new_embedded(workspace, repository, width, window, cx)} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]enum BranchListStyle { Modal, Popover,} pub struct BranchList { width: Rems, pub picker: Entity<Picker<BranchListDelegate>>, picker_focus_handle: FocusHandle, _subscription: Option<Subscription>, embedded: bool,} impl BranchList { fn new( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, style: BranchListStyle, width: Rems, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let mut this = Self::new_inner(workspace, repository, style, width, false, window, cx); this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { cx.emit(DismissEvent); })); this } fn new_inner( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, style: BranchListStyle, width: Rems, embedded: bool, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); let default_branch_request = repository.clone().map(|repository| { repository.update(cx, |repository, _| repository.default_branch(false)) }); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request .context("No active repository")? .await??; let default_branch = default_branch_request .context("No active repository")? .await .map(Result::ok) .ok() .flatten() .flatten(); let all_branches = cx .background_spawn(async move { let remote_upstreams: HashSet<_> = all_branches .iter() .filter_map(|branch| { branch .upstream .as_ref() .filter(|upstream| upstream.is_remote()) .map(|upstream| upstream.ref_name.clone()) }) .collect(); all_branches.retain(|branch| !remote_upstreams.contains(&branch.ref_name)); all_branches.sort_by_key(|branch| { ( !branch.is_head, // Current branch (is_head=true) comes first branch .most_recent_commit .as_ref() .map(|commit| 0 - commit.commit_timestamp), ) }); all_branches }) .await; let _ = this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) }); anyhow::Ok(()) }) .detach_and_log_err(cx); let delegate = BranchListDelegate::new(workspace, repository, style, cx); let picker = cx.new(|cx| { Picker::uniform_list(delegate, window, cx) .show_scrollbar(true) .modal(!embedded) }); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); Self { picker, picker_focus_handle, width, _subscription: None, embedded, } } fn new_embedded( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, width: Rems, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let mut this = Self::new_inner( workspace, repository, BranchListStyle::Modal, width, true, window, cx, ); this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { cx.emit(DismissEvent); })); this } pub fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, _: &mut Window, cx: &mut Context<Self>, ) { self.picker .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers) } pub fn handle_delete( &mut self, _: &branch_picker::DeleteBranch, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { picker .delegate .delete_at(picker.delegate.selected_index, window, cx) }) } pub fn handle_filter( &mut self, _: &branch_picker::FilterRemotes, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { picker.delegate.branch_filter = picker.delegate.branch_filter.invert(); picker.update_matches(picker.query(cx), window, cx); picker.refresh_placeholder(window, cx); cx.notify(); }); }}impl ModalView for BranchList {}impl EventEmitter<DismissEvent> for BranchList {} impl Focusable for BranchList { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.picker_focus_handle.clone() }} impl Render for BranchList { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .key_context("GitBranchSelector") .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_delete)) .on_action(cx.listener(Self::handle_filter)) .child(self.picker.clone()) .when(!self.embedded, |this| { this.on_mouse_down_out({ cx.listener(move |this, _, window, cx| { this.picker.update(cx, |this, cx| { this.cancel(&Default::default(), window, cx); }) }) }) }) }} #[derive(Debug, Clone, PartialEq)]enum Entry { Branch { branch: Branch, positions: Vec<usize>, }, NewUrl { url: String, }, NewBranch { name: String, }, NewRemoteName { name: String, url: SharedString, },} impl Entry { fn as_branch(&self) -> Option<&Branch> { match self { Entry::Branch { branch, .. } => Some(branch), _ => None, } } fn name(&self) -> &str { match self { Entry::Branch { branch, .. } => branch.name(), Entry::NewUrl { url, .. } => url.as_str(), Entry::NewBranch { name, .. } => name.as_str(), Entry::NewRemoteName { name, .. } => name.as_str(), } } #[cfg(test)] fn is_new_url(&self) -> bool { matches!(self, Self::NewUrl { .. }) } #[cfg(test)] fn is_new_branch(&self) -> bool { matches!(self, Self::NewBranch { .. }) }} #[derive(Clone, Copy, PartialEq)]enum BranchFilter { /// Show both local and remote branches. All, /// Only show remote branches. Remote,} impl BranchFilter { fn invert(&self) -> Self { match self { BranchFilter::All => BranchFilter::Remote, BranchFilter::Remote => BranchFilter::All, } }} pub struct BranchListDelegate { workspace: WeakEntity<Workspace>, matches: Vec<Entry>, all_branches: Option<Vec<Branch>>, default_branch: Option<SharedString>, repo: Option<Entity<Repository>>, style: BranchListStyle, selected_index: usize, last_query: String, modifiers: Modifiers, branch_filter: BranchFilter, state: PickerState, focus_handle: FocusHandle,} #[derive(Debug)]enum PickerState { /// When we display list of branches/remotes List, /// When we set an url to create a new remote NewRemote, /// When we confirm the new remote url (after NewRemote) CreateRemote(SharedString), /// When we set a new branch to create NewBranch,} impl BranchListDelegate { fn new( workspace: WeakEntity<Workspace>, repo: Option<Entity<Repository>>, style: BranchListStyle, cx: &mut Context<BranchList>, ) -> Self { Self { workspace, matches: vec![], repo, style, all_branches: None, default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), branch_filter: BranchFilter::All, state: PickerState::List, focus_handle: cx.focus_handle(), } } fn create_branch( &self, from_branch: Option<SharedString>, new_branch_name: SharedString, window: &mut Window, cx: &mut Context<Picker<Self>>, ) { let Some(repo) = self.repo.clone() else { return; }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); let base_branch = from_branch.map(|b| b.to_string()); cx.spawn(async move |_, cx| { repo.update(cx, |repo, _| { repo.create_branch(new_branch_name, base_branch) }) .await??; Ok(()) }) .detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| { Some(e.to_string()) }); cx.emit(DismissEvent); } fn create_remote( &self, remote_name: String, remote_url: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) { let Some(repo) = self.repo.clone() else { return; }; let receiver = repo.update(cx, |repo, _| repo.create_remote(remote_name, remote_url)); cx.background_spawn(async move { receiver.await? }) .detach_and_prompt_err("Failed to create remote", window, cx, |e, _, _cx| { Some(e.to_string()) }); cx.emit(DismissEvent); } fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) { let Some(entry) = self.matches.get(idx).cloned() else { return; }; let Some(repo) = self.repo.clone() else { return; }; let workspace = self.workspace.clone(); cx.spawn_in(window, async move |picker, cx| { let mut is_remote = false; let result = match &entry { Entry::Branch { branch, .. } => match branch.remote_name() { Some(remote_name) => { is_remote = true; repo.update(cx, |repo, _| repo.remove_remote(remote_name.to_string())) .await? } None => { repo.update(cx, |repo, _| repo.delete_branch(branch.name().to_string())) .await? }Lines 1–500 of 2009. Load more
use anyhow::Context as _; use editor::Editor; use fuzzy::StringMatchCandidate; use collections::HashSet; use git::repository::Branch; use gpui::http_client::Url; use gpui::{ Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate, PickerEditorPosition}; use project::git_store::Repository; use project::project_settings::ProjectSettings; use settings::Settings; use std::sync::Arc; use time::OffsetDateTime; use ui::{ Divider, HighlightedLabel, KeyBinding, ListHeader, ListItem, ListItemSpacing, Tooltip, prelude::*, }; use ui_input::ErasedEditor; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; use crate::{branch_picker, git_panel::show_error_toast, resolve_active_repository}; actions!( branch_picker, [ /// Deletes the selected git branch or remote. DeleteBranch, /// Filter the list of remotes FilterRemotes ] ); pub fn checkout_branch( workspace: &mut Workspace, _: &zed_actions::git::CheckoutBranch, window: &mut Window, cx: &mut Context<Workspace>, ) { open(workspace, &zed_actions::git::Branch, window, cx); } pub fn switch( workspace: &mut Workspace, _: &zed_actions::git::Switch, window: &mut Window, cx: &mut Context<Workspace>, ) { open(workspace, &zed_actions::git::Branch, window, cx); } pub fn open( workspace: &mut Workspace, _: &zed_actions::git::Branch, window: &mut Window, cx: &mut Context<Workspace>, ) { let workspace_handle = workspace.weak_handle(); let repository = resolve_active_repository(workspace, cx); workspace.toggle_modal(window, cx, |window, cx| { BranchList::new( workspace_handle, repository, BranchListStyle::Modal, rems(34.), window, cx, ) }) } pub fn popover( workspace: WeakEntity<Workspace>, modal_style: bool, repository: Option<Entity<Repository>>, window: &mut Window, cx: &mut App, ) -> Entity<BranchList> { let (style, width) = if modal_style { (BranchListStyle::Modal, rems(34.)) } else { (BranchListStyle::Popover, rems(20.)) }; cx.new(|cx| { let list = BranchList::new(workspace, repository, style, width, window, cx); list.focus_handle(cx).focus(window, cx); list }) } pub fn create_embedded( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, width: Rems, window: &mut Window, cx: &mut Context<BranchList>, ) -> BranchList { BranchList::new_embedded(workspace, repository, width, window, cx) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum BranchListStyle { Modal, Popover, } pub struct BranchList { width: Rems, pub picker: Entity<Picker<BranchListDelegate>>, picker_focus_handle: FocusHandle, _subscription: Option<Subscription>, embedded: bool, } impl BranchList { fn new( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, style: BranchListStyle, width: Rems, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let mut this = Self::new_inner(workspace, repository, style, width, false, window, cx); this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { cx.emit(DismissEvent); })); this } fn new_inner( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, style: BranchListStyle, width: Rems, embedded: bool, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); let default_branch_request = repository.clone().map(|repository| { repository.update(cx, |repository, _| repository.default_branch(false)) }); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request .context("No active repository")? .await??; let default_branch = default_branch_request .context("No active repository")? .await .map(Result::ok) .ok() .flatten() .flatten(); let all_branches = cx .background_spawn(async move { let remote_upstreams: HashSet<_> = all_branches .iter() .filter_map(|branch| { branch .upstream .as_ref() .filter(|upstream| upstream.is_remote()) .map(|upstream| upstream.ref_name.clone()) }) .collect(); all_branches.retain(|branch| !remote_upstreams.contains(&branch.ref_name)); all_branches.sort_by_key(|branch| { ( !branch.is_head, // Current branch (is_head=true) comes first branch .most_recent_commit .as_ref() .map(|commit| 0 - commit.commit_timestamp), ) }); all_branches }) .await; let _ = this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) }); anyhow::Ok(()) }) .detach_and_log_err(cx); let delegate = BranchListDelegate::new(workspace, repository, style, cx); let picker = cx.new(|cx| { Picker::uniform_list(delegate, window, cx) .show_scrollbar(true) .modal(!embedded) }); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); Self { picker, picker_focus_handle, width, _subscription: None, embedded, } } fn new_embedded( workspace: WeakEntity<Workspace>, repository: Option<Entity<Repository>>, width: Rems, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let mut this = Self::new_inner( workspace, repository, BranchListStyle::Modal, width, true, window, cx, ); this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { cx.emit(DismissEvent); })); this } pub fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, _: &mut Window, cx: &mut Context<Self>, ) { self.picker .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers) } pub fn handle_delete( &mut self, _: &branch_picker::DeleteBranch, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { picker .delegate .delete_at(picker.delegate.selected_index, window, cx) }) } pub fn handle_filter( &mut self, _: &branch_picker::FilterRemotes, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { picker.delegate.branch_filter = picker.delegate.branch_filter.invert(); picker.update_matches(picker.query(cx), window, cx); picker.refresh_placeholder(window, cx); cx.notify(); }); } } impl ModalView for BranchList {} impl EventEmitter<DismissEvent> for BranchList {} impl Focusable for BranchList { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.picker_focus_handle.clone() } } impl Render for BranchList { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .key_context("GitBranchSelector") .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_delete)) .on_action(cx.listener(Self::handle_filter)) .child(self.picker.clone()) .when(!self.embedded, |this| { this.on_mouse_down_out({ cx.listener(move |this, _, window, cx| { this.picker.update(cx, |this, cx| { this.cancel(&Default::default(), window, cx); }) }) }) }) } } #[derive(Debug, Clone, PartialEq)] enum Entry { Branch { branch: Branch, positions: Vec<usize>, }, NewUrl { url: String, }, NewBranch { name: String, }, NewRemoteName { name: String, url: SharedString, }, } impl Entry { fn as_branch(&self) -> Option<&Branch> { match self { Entry::Branch { branch, .. } => Some(branch), _ => None, } } fn name(&self) -> &str { match self { Entry::Branch { branch, .. } => branch.name(), Entry::NewUrl { url, .. } => url.as_str(), Entry::NewBranch { name, .. } => name.as_str(), Entry::NewRemoteName { name, .. } => name.as_str(), } } #[cfg(test)] fn is_new_url(&self) -> bool { matches!(self, Self::NewUrl { .. }) } #[cfg(test)] fn is_new_branch(&self) -> bool { matches!(self, Self::NewBranch { .. }) } } #[derive(Clone, Copy, PartialEq)] enum BranchFilter { /// Show both local and remote branches. All, /// Only show remote branches. Remote, } impl BranchFilter { fn invert(&self) -> Self { match self { BranchFilter::All => BranchFilter::Remote, BranchFilter::Remote => BranchFilter::All, } } } pub struct BranchListDelegate { workspace: WeakEntity<Workspace>, matches: Vec<Entry>, all_branches: Option<Vec<Branch>>, default_branch: Option<SharedString>, repo: Option<Entity<Repository>>, style: BranchListStyle, selected_index: usize, last_query: String, modifiers: Modifiers, branch_filter: BranchFilter, state: PickerState, focus_handle: FocusHandle, } #[derive(Debug)] enum PickerState { /// When we display list of branches/remotes List, /// When we set an url to create a new remote NewRemote, /// When we confirm the new remote url (after NewRemote) CreateRemote(SharedString), /// When we set a new branch to create NewBranch, } impl BranchListDelegate { fn new( workspace: WeakEntity<Workspace>, repo: Option<Entity<Repository>>, style: BranchListStyle, cx: &mut Context<BranchList>, ) -> Self { Self { workspace, matches: vec![], repo, style, all_branches: None, default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), branch_filter: BranchFilter::All, state: PickerState::List, focus_handle: cx.focus_handle(), } } fn create_branch( &self, from_branch: Option<SharedString>, new_branch_name: SharedString, window: &mut Window, cx: &mut Context<Picker<Self>>, ) { let Some(repo) = self.repo.clone() else { return; }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); let base_branch = from_branch.map(|b| b.to_string()); cx.spawn(async move |_, cx| { repo.update(cx, |repo, _| { repo.create_branch(new_branch_name, base_branch) }) .await??; Ok(()) }) .detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| { Some(e.to_string()) }); cx.emit(DismissEvent); } fn create_remote( &self, remote_name: String, remote_url: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) { let Some(repo) = self.repo.clone() else { return; }; let receiver = repo.update(cx, |repo, _| repo.create_remote(remote_name, remote_url)); cx.background_spawn(async move { receiver.await? }) .detach_and_prompt_err("Failed to create remote", window, cx, |e, _, _cx| { Some(e.to_string()) }); cx.emit(DismissEvent); } fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) { let Some(entry) = self.matches.get(idx).cloned() else { return; }; let Some(repo) = self.repo.clone() else { return; }; let workspace = self.workspace.clone(); cx.spawn_in(window, async move |picker, cx| { let mut is_remote = false; let result = match &entry { Entry::Branch { branch, .. } => match branch.remote_name() { Some(remote_name) => { is_remote = true; repo.update(cx, |repo, _| repo.remove_remote(remote_name.to_string())) .await? } None => { repo.update(cx, |repo, _| repo.delete_branch(branch.name().to_string())) .await? }