Add staged checkboxes to multibuffer headers (#24308)

Co-authored-by: Mikayla <mikayla@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Conrad Irwin 2025-02-05 18:32:07 -07:00 committed by GitHub
parent 0671be215f
commit 5d1c56829a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 151 additions and 37 deletions

View file

@ -124,7 +124,8 @@ pub use multi_buffer::{
ToOffset, ToPoint, ToOffset, ToPoint,
}; };
use multi_buffer::{ use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
ToOffsetUtf16,
}; };
use project::{ use project::{
lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
@ -580,6 +581,15 @@ struct BufferOffset(usize);
pub trait Addon: 'static { pub trait Addon: 'static {
fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} fn extend_key_context(&self, _: &mut KeyContext, _: &App) {}
fn render_buffer_header_controls(
&self,
_: &ExcerptInfo,
_: &Window,
_: &App,
) -> Option<AnyElement> {
None
}
fn to_any(&self) -> &dyn std::any::Any; fn to_any(&self) -> &dyn std::any::Any;
} }

View file

@ -2633,6 +2633,16 @@ impl EditorElement {
), ),
) )
}) })
.children(
self.editor
.read(cx)
.addons
.values()
.filter_map(|addon| {
addon.render_buffer_header_controls(for_excerpt, window, cx)
})
.take(1),
)
.child( .child(
h_flex() h_flex()
.cursor_pointer() .cursor_pointer()

View file

@ -14,8 +14,9 @@ use git::repository::RepoPath;
use git::status::FileStatus; use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, ToggleStaged}; use git::{CommitAllChanges, CommitChanges, ToggleStaged};
use gpui::*; use gpui::*;
use language::Buffer; use language::{Buffer, File};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use multi_buffer::ExcerptInfo;
use panel::PanelHeader; use panel::PanelHeader;
use project::git::{GitEvent, Repository}; use project::git::{GitEvent, Repository};
use project::{Fs, Project, ProjectPath}; use project::{Fs, Project, ProjectPath};
@ -1007,19 +1008,19 @@ impl GitPanel {
}; };
if status_entry.status.is_created() { if status_entry.status.is_created() {
self.new_count += 1; self.new_count += 1;
if self.entry_appears_staged(status_entry) != Some(false) { if self.entry_is_staged(status_entry) != Some(false) {
self.new_staged_count += 1; self.new_staged_count += 1;
} }
} else { } else {
self.tracked_count += 1; self.tracked_count += 1;
if self.entry_appears_staged(status_entry) != Some(false) { if self.entry_is_staged(status_entry) != Some(false) {
self.tracked_staged_count += 1; self.tracked_staged_count += 1;
} }
} }
} }
} }
fn entry_appears_staged(&self, entry: &GitStatusEntry) -> Option<bool> { fn entry_is_staged(&self, entry: &GitStatusEntry) -> Option<bool> {
for pending in self.pending.iter().rev() { for pending in self.pending.iter().rev() {
if pending.repo_paths.contains(&entry.repo_path) { if pending.repo_paths.contains(&entry.repo_path) {
return Some(pending.will_become_staged); return Some(pending.will_become_staged);
@ -1301,6 +1302,49 @@ impl GitPanel {
) )
} }
pub fn render_buffer_header_controls(
&self,
entity: &Entity<Self>,
file: &Arc<dyn File>,
_: &Window,
cx: &App,
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
let ix = self.entries_by_path.get(&repo_path)?;
let entry = self.entries.get(*ix)?;
let is_staged = self.entry_is_staged(entry.status_entry()?);
let checkbox = Checkbox::new("stage-file", is_staged.into())
.disabled(!self.has_write_access(cx))
.fill()
.elevation(ElevationIndex::Surface)
.on_click({
let entry = entry.clone();
let git_panel = entity.downgrade();
move |_, window, cx| {
git_panel
.update(cx, |this, cx| {
this.toggle_staged_for_entry(&entry, window, cx);
cx.stop_propagation();
})
.ok();
}
});
Some(
h_flex()
.id("start-slot")
.child(checkbox)
.child(git_status_icon(entry.status_entry()?.status, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
cx.stop_propagation();
})
.into_any_element(),
)
}
fn render_entries( fn render_entries(
&self, &self,
has_write_access: bool, has_write_access: bool,
@ -1473,14 +1517,6 @@ impl GitPanel {
.map(|name| name.to_string_lossy().into_owned()) .map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned()); .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned());
let pending = self.pending.iter().rev().find_map(|pending| {
if pending.repo_paths.contains(&entry.repo_path) {
Some(pending.will_become_staged)
} else {
None
}
});
let repo_path = entry.repo_path.clone(); let repo_path = entry.repo_path.clone();
let selected = self.selected_entry == Some(ix); let selected = self.selected_entry == Some(ix);
let status_style = GitPanelSettings::get_global(cx).status_style; let status_style = GitPanelSettings::get_global(cx).status_style;
@ -1512,10 +1548,7 @@ impl GitPanel {
let id: ElementId = ElementId::Name(format!("entry_{}", display_name).into()); let id: ElementId = ElementId::Name(format!("entry_{}", display_name).into());
let mut is_staged = pending let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
.or_else(|| entry.is_staged)
.map(ToggleState::from)
.unwrap_or(ToggleState::Indeterminate);
if !self.has_staged_changes() && !entry.status.is_created() { if !self.has_staged_changes() && !entry.status.is_created() {
is_staged = ToggleState::Selected; is_staged = ToggleState::Selected;
@ -1597,6 +1630,16 @@ impl GitPanel {
) )
.into_any_element() .into_any_element()
} }
fn has_write_access(&self, cx: &App) -> bool {
let room = self
.workspace
.upgrade()
.and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned());
room.as_ref()
.map_or(true, |room| room.read(cx).local_participant().can_write())
}
} }
impl Render for GitPanel { impl Render for GitPanel {
@ -1734,6 +1777,28 @@ impl EventEmitter<Event> for GitPanel {}
impl EventEmitter<PanelEvent> for GitPanel {} impl EventEmitter<PanelEvent> for GitPanel {}
pub(crate) struct GitPanelAddon {
pub(crate) git_panel: Entity<GitPanel>,
}
impl editor::Addon for GitPanelAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn render_buffer_header_controls(
&self,
excerpt_info: &ExcerptInfo,
window: &Window,
cx: &App,
) -> Option<AnyElement> {
let file = excerpt_info.buffer.file()?;
let git_panel = self.git_panel.read(cx);
git_panel.render_buffer_header_controls(&self.git_panel, &file, window, cx)
}
}
impl Panel for GitPanel { impl Panel for GitPanel {
fn persistent_name() -> &'static str { fn persistent_name() -> &'static str {
"GitPanel" "GitPanel"

View file

@ -21,7 +21,7 @@ use workspace::{
ItemNavHistory, ToolbarItemLocation, Workspace, ItemNavHistory, ToolbarItemLocation, Workspace,
}; };
use crate::git_panel::{GitPanel, GitStatusEntry}; use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
actions!(git, [Diff]); actions!(git, [Diff]);
@ -29,6 +29,7 @@ pub(crate) struct ProjectDiff {
multibuffer: Entity<MultiBuffer>, multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>, editor: Entity<Editor>,
project: Entity<Project>, project: Entity<Project>,
git_panel: Entity<GitPanel>,
git_state: Entity<GitState>, git_state: Entity<GitState>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -79,9 +80,16 @@ impl ProjectDiff {
workspace.activate_item(&existing, true, true, window, cx); workspace.activate_item(&existing, true, true, window, cx);
existing existing
} else { } else {
let workspace_handle = cx.entity().downgrade(); let workspace_handle = cx.entity();
let project_diff = let project_diff = cx.new(|cx| {
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx)); Self::new(
workspace.project().clone(),
workspace_handle,
workspace.panel::<GitPanel>(cx).unwrap(),
window,
cx,
)
});
workspace.add_item_to_active_pane( workspace.add_item_to_active_pane(
Box::new(project_diff.clone()), Box::new(project_diff.clone()),
None, None,
@ -100,7 +108,8 @@ impl ProjectDiff {
fn new( fn new(
project: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: Entity<Workspace>,
git_panel: Entity<GitPanel>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -116,6 +125,9 @@ impl ProjectDiff {
cx, cx,
); );
diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
git_panel: git_panel.clone(),
});
diff_display_editor diff_display_editor
}); });
cx.subscribe_in(&editor, window, Self::handle_editor_event) cx.subscribe_in(&editor, window, Self::handle_editor_event)
@ -141,7 +153,8 @@ impl ProjectDiff {
Self { Self {
project, project,
git_state: git_state.clone(), git_state: git_state.clone(),
workspace, git_panel: git_panel.clone(),
workspace: workspace.downgrade(),
focus_handle, focus_handle,
editor, editor,
multibuffer, multibuffer,
@ -423,9 +436,16 @@ impl Item for ProjectDiff {
where where
Self: Sized, Self: Sized,
{ {
Some( let workspace = self.workspace.upgrade()?;
cx.new(|cx| ProjectDiff::new(self.project.clone(), self.workspace.clone(), window, cx)), Some(cx.new(|cx| {
ProjectDiff::new(
self.project.clone(),
workspace,
self.git_panel.clone(),
window,
cx,
) )
}))
} }
fn is_dirty(&self, cx: &App) -> bool { fn is_dirty(&self, cx: &App) -> bool {

View file

@ -15,6 +15,7 @@ use gpui::{
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageRegistry};
use rpc::{proto, AnyProtoClient}; use rpc::{proto, AnyProtoClient};
use settings::WorktreeId; use settings::WorktreeId;
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use text::BufferId; use text::BufferId;
use util::{maybe, ResultExt}; use util::{maybe, ResultExt};
@ -341,10 +342,18 @@ impl Repository {
} }
pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> { pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
if path.worktree_id != self.worktree_id { self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
}
pub fn worktree_id_path_to_repo_path(
&self,
worktree_id: WorktreeId,
path: &Path,
) -> Option<RepoPath> {
if worktree_id != self.worktree_id {
return None; return None;
} }
self.repository_entry.relativize(&path.path).log_err() self.repository_entry.relativize(path).log_err()
} }
pub fn open_commit_buffer( pub fn open_commit_buffer(

View file

@ -58,12 +58,12 @@ impl From<bool> for ToggleState {
} }
} }
// impl From<Option<bool>> for ToggleState { impl From<Option<bool>> for ToggleState {
// fn from(selected: Option<bool>) -> Self { fn from(selected: Option<bool>) -> Self {
// match selected { match selected {
// Some(true) => Self::Selected, Some(true) => Self::Selected,
// Some(false) => Self::Unselected, Some(false) => Self::Unselected,
// None => Self::Unselected, None => Self::Indeterminate,
// } }
// } }
// } }