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,
};
use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
ToOffsetUtf16,
};
use project::{
lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
@ -580,6 +581,15 @@ struct BufferOffset(usize);
pub trait Addon: 'static {
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;
}

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(
h_flex()
.cursor_pointer()

View file

@ -14,8 +14,9 @@ use git::repository::RepoPath;
use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, ToggleStaged};
use gpui::*;
use language::Buffer;
use language::{Buffer, File};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use multi_buffer::ExcerptInfo;
use panel::PanelHeader;
use project::git::{GitEvent, Repository};
use project::{Fs, Project, ProjectPath};
@ -1007,19 +1008,19 @@ impl GitPanel {
};
if status_entry.status.is_created() {
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;
}
} else {
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;
}
}
}
}
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() {
if pending.repo_paths.contains(&entry.repo_path) {
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(
&self,
has_write_access: bool,
@ -1473,14 +1517,6 @@ impl GitPanel {
.map(|name| name.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 selected = self.selected_entry == Some(ix);
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 mut is_staged = pending
.or_else(|| entry.is_staged)
.map(ToggleState::from)
.unwrap_or(ToggleState::Indeterminate);
let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
if !self.has_staged_changes() && !entry.status.is_created() {
is_staged = ToggleState::Selected;
@ -1597,6 +1630,16 @@ impl GitPanel {
)
.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 {
@ -1734,6 +1777,28 @@ impl EventEmitter<Event> 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 {
fn persistent_name() -> &'static str {
"GitPanel"

View file

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

View file

@ -15,6 +15,7 @@ use gpui::{
use language::{Buffer, LanguageRegistry};
use rpc::{proto, AnyProtoClient};
use settings::WorktreeId;
use std::path::Path;
use std::sync::Arc;
use text::BufferId;
use util::{maybe, ResultExt};
@ -341,10 +342,18 @@ impl Repository {
}
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;
}
self.repository_entry.relativize(&path.path).log_err()
self.repository_entry.relativize(path).log_err()
}
pub fn open_commit_buffer(

View file

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