Git improvements (#24238)

- **Base diffs on uncommitted changes**
- **Show added files in project diff view**
- **Fix git panel optimism**

Release Notes:

- Git: update diffs to be relative to HEAD instead of the index; to pave
the way for showing which hunks are staged

---------

Co-authored-by: Cole <cole@zed.dev>
This commit is contained in:
Conrad Irwin 2025-02-04 23:09:41 -07:00 committed by GitHub
parent 22b7042b9e
commit 0963401a8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 241 additions and 231 deletions

View file

@ -1285,7 +1285,7 @@ impl Editor {
let mut code_action_providers = Vec::new(); let mut code_action_providers = Vec::new();
if let Some(project) = project.clone() { if let Some(project) = project.clone() {
get_unstaged_changes_for_buffers( get_uncommitted_changes_for_buffer(
&project, &project,
buffer.read(cx).all_buffers(), buffer.read(cx).all_buffers(),
buffer.clone(), buffer.clone(),
@ -13657,7 +13657,7 @@ impl Editor {
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
if self.buffer.read(cx).change_set_for(buffer_id).is_none() { if self.buffer.read(cx).change_set_for(buffer_id).is_none() {
if let Some(project) = &self.project { if let Some(project) = &self.project {
get_unstaged_changes_for_buffers( get_uncommitted_changes_for_buffer(
project, project,
[buffer.clone()], [buffer.clone()],
self.buffer.clone(), self.buffer.clone(),
@ -14413,7 +14413,7 @@ impl Editor {
} }
} }
fn get_unstaged_changes_for_buffers( fn get_uncommitted_changes_for_buffer(
project: &Entity<Project>, project: &Entity<Project>,
buffers: impl IntoIterator<Item = Entity<Buffer>>, buffers: impl IntoIterator<Item = Entity<Buffer>>,
buffer: Entity<MultiBuffer>, buffer: Entity<MultiBuffer>,
@ -14422,7 +14422,7 @@ fn get_unstaged_changes_for_buffers(
let mut tasks = Vec::new(); let mut tasks = Vec::new();
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
for buffer in buffers { for buffer in buffers {
tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) tasks.push(project.open_uncommitted_changes(buffer.clone(), cx))
} }
}); });
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {

View file

@ -5619,13 +5619,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
let base_text = r#" let base_text = r#"
impl A { impl A {
// this is an unstaged comment // this is an uncommitted comment
fn b() { fn b() {
c(); c();
} }
// this is another unstaged comment // this is another uncommitted comment
fn d() { fn d() {
// e // e
@ -5668,13 +5668,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
cx.assert_state_with_diff( cx.assert_state_with_diff(
" "
ˇimpl A { ˇimpl A {
- // this is an unstaged comment - // this is an uncommitted comment
fn b() { fn b() {
c(); c();
} }
- // this is another unstaged comment - // this is another uncommitted comment
- -
fn d() { fn d() {
// e // e
@ -5691,13 +5691,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
let expected_display_text = " let expected_display_text = "
impl A { impl A {
// this is an unstaged comment // this is an uncommitted comment
fn b() { fn b() {
} }
// this is another unstaged comment // this is another uncommitted comment
fn d() { fn d() {

View file

@ -290,7 +290,7 @@ impl EditorTestContext {
editor.project.as_ref().unwrap().read(cx).fs().as_fake() editor.project.as_ref().unwrap().read(cx).fs().as_fake()
}); });
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo( fs.set_head_for_repo(
&Self::root_path().join(".git"), &Self::root_path().join(".git"),
&[(path.into(), diff_base.to_string())], &[(path.into(), diff_base.to_string())],
); );

View file

@ -265,13 +265,13 @@ impl GitRepository for RealGitRepository {
.to_path_buf(); .to_path_buf();
if !paths.is_empty() { if !paths.is_empty() {
let cmd = new_std_command(&self.git_binary_path) let status = new_std_command(&self.git_binary_path)
.current_dir(&working_directory) .current_dir(&working_directory)
.args(["update-index", "--add", "--remove", "--"]) .args(["update-index", "--add", "--remove", "--"])
.args(paths.iter().map(|p| p.as_ref())) .args(paths.iter().map(|p| p.as_ref()))
.status()?; .status()?;
if !cmd.success() { if !status.success() {
return Err(anyhow!("Failed to stage paths: {cmd}")); return Err(anyhow!("Failed to stage paths: {status}"));
} }
} }
Ok(()) Ok(())

View file

@ -12,13 +12,11 @@ use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar}; use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
use git::repository::RepoPath; use git::repository::RepoPath;
use git::status::FileStatus; use git::status::FileStatus;
use git::{ use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE};
CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll, COMMIT_MESSAGE,
};
use gpui::*; use gpui::*;
use language::{Buffer, BufferId}; use language::{Buffer, BufferId};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use project::git::{GitRepo, RepositoryHandle}; use project::git::{GitEvent, GitRepo, RepositoryHandle};
use project::{CreateOptions, Fs, Project, ProjectPath}; use project::{CreateOptions, Fs, Project, ProjectPath};
use rpc::proto; use rpc::proto;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -43,7 +41,6 @@ actions!(
Close, Close,
ToggleFocus, ToggleFocus,
OpenMenu, OpenMenu,
OpenSelected,
FocusEditor, FocusEditor,
FocusChanges, FocusChanges,
FillCoAuthors, FillCoAuthors,
@ -76,17 +73,17 @@ struct SerializedGitPanel {
width: Option<Pixels>, width: Option<Pixels>,
} }
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Section { enum Section {
Changed, Changed,
New, Created,
} }
impl Section { impl Section {
pub fn contains(&self, status: FileStatus) -> bool { pub fn contains(&self, status: FileStatus) -> bool {
match self { match self {
Section::Changed => !status.is_created(), Section::Changed => !status.is_created(),
Section::New => status.is_created(), Section::Created => status.is_created(),
} }
} }
} }
@ -94,7 +91,6 @@ impl Section {
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
struct GitHeaderEntry { struct GitHeaderEntry {
header: Section, header: Section,
all_staged: ToggleState,
} }
impl GitHeaderEntry { impl GitHeaderEntry {
@ -104,7 +100,7 @@ impl GitHeaderEntry {
pub fn title(&self) -> &'static str { pub fn title(&self) -> &'static str {
match self.header { match self.header {
Section::Changed => "Changed", Section::Changed => "Changed",
Section::New => "New", Section::Created => "New",
} }
} }
} }
@ -126,11 +122,18 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry { pub struct GitStatusEntry {
depth: usize, pub(crate) depth: usize,
display_name: String, pub(crate) display_name: String,
repo_path: RepoPath, pub(crate) repo_path: RepoPath,
status: FileStatus, pub(crate) status: FileStatus,
is_staged: Option<bool>, pub(crate) is_staged: Option<bool>,
}
pub struct PendingOperation {
finished: bool,
will_become_staged: bool,
repo_paths: HashSet<RepoPath>,
op_id: usize,
} }
pub struct GitPanel { pub struct GitPanel {
@ -152,9 +155,11 @@ pub struct GitPanel {
entries: Vec<GitListEntry>, entries: Vec<GitListEntry>,
entries_by_path: collections::HashMap<RepoPath, usize>, entries_by_path: collections::HashMap<RepoPath, usize>,
width: Option<Pixels>, width: Option<Pixels>,
pending: HashMap<RepoPath, bool>, pending: Vec<PendingOperation>,
commit_task: Task<Result<()>>, commit_task: Task<Result<()>>,
commit_pending: bool, commit_pending: bool,
can_commit: bool,
can_commit_all: bool,
} }
fn commit_message_buffer( fn commit_message_buffer(
@ -287,9 +292,12 @@ impl GitPanel {
&git_state, &git_state,
window, window,
move |this, git_state, event, window, cx| match event { move |this, git_state, event, window, cx| match event {
project::git::Event::RepositoriesUpdated => { GitEvent::FileSystemUpdated => {
this.schedule_update(false, window, cx);
}
GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
this.active_repository = git_state.read(cx).active_repository(); this.active_repository = git_state.read(cx).active_repository();
this.schedule_update(window, cx); this.schedule_update(true, window, cx);
} }
}, },
) )
@ -303,7 +311,7 @@ impl GitPanel {
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
entries: Vec::new(), entries: Vec::new(),
entries_by_path: HashMap::default(), entries_by_path: HashMap::default(),
pending: HashMap::default(), pending: Vec::new(),
current_modifiers: window.modifiers(), current_modifiers: window.modifiers(),
width: Some(px(360.)), width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()) scrollbar_state: ScrollbarState::new(scroll_handle.clone())
@ -321,8 +329,10 @@ impl GitPanel {
commit_editor, commit_editor,
project, project,
workspace, workspace,
can_commit: false,
can_commit_all: false,
}; };
git_panel.schedule_update(window, cx); git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
git_panel git_panel
}); });
@ -617,7 +627,7 @@ impl GitPanel {
} }
} }
GitListEntry::Header(section) => { GitListEntry::Header(section) => {
let goal_staged_state = !section.all_staged.selected(); let goal_staged_state = !self.header_state(section.header).selected();
let entries = self let entries = self
.entries .entries
.iter() .iter()
@ -629,12 +639,17 @@ impl GitPanel {
.map(|status_entry| status_entry.repo_path) .map(|status_entry| status_entry.repo_path)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
(!section.all_staged.selected(), entries) (goal_staged_state, entries)
} }
}; };
for repo_path in repo_paths.iter() {
self.pending.insert(repo_path.clone(), stage); let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
} self.pending.push(PendingOperation {
op_id,
will_become_staged: stage,
repo_paths: repo_paths.iter().cloned().collect(),
finished: false,
});
cx.spawn({ cx.spawn({
let repo_paths = repo_paths.clone(); let repo_paths = repo_paths.clone();
@ -647,9 +662,9 @@ impl GitPanel {
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
for repo_path in repo_paths { for pending in this.pending.iter_mut() {
if this.pending.get(&repo_path) == Some(&stage) { if pending.op_id == op_id {
this.pending.remove(&repo_path); pending.finished = true
} }
} }
result result
@ -696,67 +711,6 @@ impl GitPanel {
cx.emit(Event::OpenedEntry { path }); cx.emit(Event::OpenedEntry { path });
} }
fn stage_all(&mut self, _: &git::StageAll, _window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.as_ref().cloned() else {
return;
};
let mut pending_paths = Vec::new();
for entry in self.entries.iter() {
if let Some(status_entry) = entry.status_entry() {
self.pending.insert(status_entry.repo_path.clone(), true);
pending_paths.push(status_entry.repo_path.clone());
}
}
cx.spawn(|this, mut cx| async move {
if let Err(e) = active_repository.stage_all().await {
this.update(&mut cx, |this, cx| {
this.show_err_toast(e, cx);
})
.ok();
};
this.update(&mut cx, |this, _cx| {
for repo_path in pending_paths {
this.pending.remove(&repo_path);
}
})
})
.detach();
}
fn unstage_all(&mut self, _: &git::UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.as_ref().cloned() else {
return;
};
let mut pending_paths = Vec::new();
for entry in self.entries.iter() {
if let Some(status_entry) = entry.status_entry() {
self.pending.insert(status_entry.repo_path.clone(), false);
pending_paths.push(status_entry.repo_path.clone());
}
}
cx.spawn(|this, mut cx| async move {
if let Err(e) = active_repository.unstage_all().await {
this.update(&mut cx, |this, cx| {
this.show_err_toast(e, cx);
})
.ok();
};
this.update(&mut cx, |this, _cx| {
for repo_path in pending_paths {
this.pending.remove(&repo_path);
}
})
})
.detach();
}
fn discard_all(&mut self, _: &git::RevertAll, _window: &mut Window, _cx: &mut Context<Self>) {
// TODO: Implement discard all
println!("Discard all triggered");
}
/// Commit all staged changes /// Commit all staged changes
fn commit_changes( fn commit_changes(
&mut self, &mut self,
@ -768,7 +722,7 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.clone() else { let Some(active_repository) = self.active_repository.clone() else {
return; return;
}; };
if !active_repository.can_commit(false) { if !self.can_commit {
return; return;
} }
if self.commit_editor.read(cx).is_empty(cx) { if self.commit_editor.read(cx).is_empty(cx) {
@ -811,7 +765,7 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.clone() else { let Some(active_repository) = self.active_repository.clone() else {
return; return;
}; };
if !active_repository.can_commit(true) { if !self.can_commit_all {
return; return;
} }
if self.commit_editor.read(cx).is_empty(cx) { if self.commit_editor.read(cx).is_empty(cx) {
@ -926,7 +880,12 @@ impl GitPanel {
}); });
} }
fn schedule_update(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn schedule_update(
&mut self,
clear_pending: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let project = self.project.clone(); let project = self.project.clone();
let handle = cx.entity().downgrade(); let handle = cx.entity().downgrade();
self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move { self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
@ -957,6 +916,9 @@ impl GitPanel {
git_panel git_panel
.update_in(&mut cx, |git_panel, window, cx| { .update_in(&mut cx, |git_panel, window, cx| {
git_panel.update_visible_entries(cx); git_panel.update_visible_entries(cx);
if clear_pending {
git_panel.clear_pending();
}
git_panel.commit_editor = git_panel.commit_editor =
cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx)); cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
}) })
@ -965,6 +927,10 @@ impl GitPanel {
}); });
} }
fn clear_pending(&mut self) {
self.pending.retain(|v| !v.finished)
}
fn update_visible_entries(&mut self, cx: &mut Context<Self>) { fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
self.entries.clear(); self.entries.clear();
self.entries_by_path.clear(); self.entries_by_path.clear();
@ -980,12 +946,11 @@ impl GitPanel {
// First pass - collect all paths // First pass - collect all paths
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path)); let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
// Second pass - create entries with proper depth calculation let mut has_changed_checked_boxes = false;
let mut new_any_staged = false; let mut has_changed = false;
let mut new_all_staged = true; let mut has_added_checked_boxes = false;
let mut changed_any_staged = false;
let mut changed_all_staged = true;
// Second pass - create entries with proper depth calculation
for entry in repo.status() { for entry in repo.status() {
let (depth, difference) = let (depth, difference) =
Self::calculate_depth_and_difference(&entry.repo_path, &path_set); Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
@ -993,15 +958,6 @@ impl GitPanel {
let is_new = entry.status.is_created(); let is_new = entry.status.is_created();
let is_staged = entry.status.is_staged(); let is_staged = entry.status.is_staged();
let new_is_staged = is_staged.unwrap_or(false);
if is_new {
new_any_staged |= new_is_staged;
new_all_staged &= new_is_staged;
} else {
changed_any_staged |= new_is_staged;
changed_all_staged &= new_is_staged;
}
let display_name = if difference > 1 { let display_name = if difference > 1 {
// Show partial path for deeply nested files // Show partial path for deeply nested files
entry entry
@ -1030,8 +986,15 @@ impl GitPanel {
}; };
if is_new { if is_new {
if entry.is_staged != Some(false) {
has_added_checked_boxes = true
}
new_entries.push(entry); new_entries.push(entry);
} else { } else {
has_changed = true;
if entry.is_staged != Some(false) {
has_changed_checked_boxes = true
}
changed_entries.push(entry); changed_entries.push(entry);
} }
} }
@ -1041,11 +1004,8 @@ impl GitPanel {
new_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path)); new_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
if changed_entries.len() > 0 { if changed_entries.len() > 0 {
let toggle_state =
ToggleState::from_any_and_all(changed_any_staged, changed_all_staged);
self.entries.push(GitListEntry::Header(GitHeaderEntry { self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Changed, header: Section::Changed,
all_staged: toggle_state,
})); }));
self.entries.extend( self.entries.extend(
changed_entries changed_entries
@ -1054,10 +1014,8 @@ impl GitPanel {
); );
} }
if new_entries.len() > 0 { if new_entries.len() > 0 {
let toggle_state = ToggleState::from_any_and_all(new_any_staged, new_all_staged);
self.entries.push(GitListEntry::Header(GitHeaderEntry { self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::New, header: Section::Created,
all_staged: toggle_state,
})); }));
self.entries self.entries
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry)); .extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
@ -1068,12 +1026,45 @@ impl GitPanel {
self.entries_by_path.insert(status_entry.repo_path, ix); self.entries_by_path.insert(status_entry.repo_path, ix);
} }
} }
self.can_commit = has_changed_checked_boxes || has_added_checked_boxes;
self.can_commit_all = has_changed || has_added_checked_boxes;
self.select_first_entry_if_none(cx); self.select_first_entry_if_none(cx);
cx.notify(); cx.notify();
} }
fn header_state(&self, header_type: Section) -> ToggleState {
let mut count = 0;
let mut staged_count = 0;
'outer: for entry in &self.entries {
let Some(entry) = entry.status_entry() else {
continue;
};
if entry.status.is_created() != (header_type == Section::Created) {
continue;
}
count += 1;
for pending in self.pending.iter().rev() {
if pending.repo_paths.contains(&entry.repo_path) {
if pending.will_become_staged {
staged_count += 1;
}
continue 'outer;
}
}
staged_count += entry.status.is_staged().unwrap_or(false) as usize;
}
if staged_count == 0 {
ToggleState::Unselected
} else if count == staged_count {
ToggleState::Selected
} else {
ToggleState::Indeterminate
}
}
fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) { fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else { let Some(workspace) = self.workspace.upgrade() else {
return; return;
@ -1089,7 +1080,6 @@ impl GitPanel {
} }
} }
// GitPanel Render
impl GitPanel { impl GitPanel {
pub fn panel_button( pub fn panel_button(
&self, &self,
@ -1199,21 +1189,13 @@ impl GitPanel {
pub fn render_commit_editor( pub fn render_commit_editor(
&self, &self,
name_and_email: Option<(SharedString, SharedString)>, name_and_email: Option<(SharedString, SharedString)>,
can_commit: bool,
cx: &Context<Self>, cx: &Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let editor = self.commit_editor.clone(); let editor = self.commit_editor.clone();
let can_commit = can_commit && !editor.read(cx).is_empty(cx); let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
let can_commit_all =
!self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone(); let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let (can_commit, can_commit_all) =
self.active_repository
.as_ref()
.map_or((false, false), |active_repository| {
(
can_commit && active_repository.can_commit(false),
can_commit && active_repository.can_commit(true),
)
});
let focus_handle_1 = self.focus_handle(cx).clone(); let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone(); let focus_handle_2 = self.focus_handle(cx).clone();
@ -1466,7 +1448,7 @@ impl GitPanel {
has_write_access: bool, has_write_access: bool,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let checkbox = Checkbox::new(header.title(), header.all_staged) let checkbox = Checkbox::new(header.title(), self.header_state(header.header))
.disabled(!has_write_access) .disabled(!has_write_access)
.fill() .fill()
.elevation(ElevationIndex::Surface); .elevation(ElevationIndex::Surface);
@ -1510,7 +1492,14 @@ 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.get(&entry.repo_path).copied(); 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;
@ -1559,13 +1548,19 @@ impl GitPanel {
window, window,
cx, cx,
); );
cx.stop_propagation();
}) })
}); });
let start_slot = h_flex() let start_slot = h_flex()
.id(("start-slot", ix))
.gap(DynamicSpacing::Base04.rems(cx)) .gap(DynamicSpacing::Base04.rems(cx))
.child(checkbox) .child(checkbox)
.child(git_status_icon(status, cx)); .child(git_status_icon(status, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
cx.stop_propagation();
});
let id = ElementId::Name(format!("entry_{}", display_name).into()); let id = ElementId::Name(format!("entry_{}", display_name).into());
@ -1581,27 +1576,14 @@ impl GitPanel {
.toggle_state(selected) .toggle_state(selected)
.disabled(!has_write_access) .disabled(!has_write_access)
.on_click({ .on_click({
let repo_path = entry.repo_path.clone(); let entry = entry.clone();
cx.listener(move |this, _, window, cx| { cx.listener(move |this, _, window, cx| {
this.selected_entry = Some(ix); this.selected_entry = Some(ix);
window.dispatch_action(Box::new(OpenSelected), cx);
cx.notify();
let Some(workspace) = this.workspace.upgrade() else { let Some(workspace) = this.workspace.upgrade() else {
return; return;
}; };
let Some(git_repo) = this.active_repository.as_ref() else {
return;
};
let Some(path) = git_repo
.repo_path_to_project_path(&repo_path)
.and_then(|project_path| {
this.project.read(cx).absolute_path(&project_path, cx)
})
else {
return;
};
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(path.into()), window, cx); ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
}) })
}) })
}) })
@ -1691,17 +1673,6 @@ impl Render for GitPanel {
this.on_action(cx.listener(|this, &ToggleStaged, window, cx| { this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
this.toggle_staged_for_selected(&ToggleStaged, window, cx) this.toggle_staged_for_selected(&ToggleStaged, window, cx)
})) }))
.on_action(
cx.listener(|this, &StageAll, window, cx| {
this.stage_all(&StageAll, window, cx)
}),
)
.on_action(cx.listener(|this, &UnstageAll, window, cx| {
this.unstage_all(&UnstageAll, window, cx)
}))
.on_action(cx.listener(|this, &RevertAll, window, cx| {
this.discard_all(&RevertAll, window, cx)
}))
.when(can_commit, |git_panel| { .when(can_commit, |git_panel| {
git_panel git_panel
.on_action({ .on_action({
@ -1764,7 +1735,7 @@ impl Render for GitPanel {
self.render_empty_state(cx).into_any_element() self.render_empty_state(cx).into_any_element()
}) })
.child(self.render_divider(cx)) .child(self.render_divider(cx))
.child(self.render_commit_editor(name_and_email, can_commit, cx)) .child(self.render_commit_editor(name_and_email, cx))
} }
} }

View file

@ -1,8 +1,4 @@
use std::{ use std::any::{Any, TypeId};
any::{Any, TypeId},
path::Path,
sync::Arc,
};
use anyhow::Result; use anyhow::Result;
use collections::HashSet; use collections::HashSet;
@ -14,7 +10,7 @@ use gpui::{
FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
}; };
use language::{Anchor, Buffer, Capability, OffsetRangeExt}; use language::{Anchor, Buffer, Capability, OffsetRangeExt};
use multi_buffer::MultiBuffer; use multi_buffer::{MultiBuffer, PathKey};
use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath}; use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::prelude::*; use ui::prelude::*;
@ -25,7 +21,7 @@ use workspace::{
ItemNavHistory, ToolbarItemLocation, Workspace, ItemNavHistory, ToolbarItemLocation, Workspace,
}; };
use crate::git_panel::GitPanel; use crate::git_panel::{GitPanel, GitStatusEntry};
actions!(git, [Diff]); actions!(git, [Diff]);
@ -37,18 +33,21 @@ pub(crate) struct ProjectDiff {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>, update_needed: postage::watch::Sender<()>,
pending_scroll: Option<Arc<Path>>, pending_scroll: Option<PathKey>,
_task: Task<Result<()>>, _task: Task<Result<()>>,
_subscription: Subscription, _subscription: Subscription,
} }
struct DiffBuffer { struct DiffBuffer {
abs_path: Arc<Path>, path_key: PathKey,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
change_set: Entity<BufferChangeSet>, change_set: Entity<BufferChangeSet>,
} }
const CHANGED_NAMESPACE: &'static str = "0";
const ADDED_NAMESPACE: &'static str = "1";
impl ProjectDiff { impl ProjectDiff {
pub(crate) fn register( pub(crate) fn register(
_: &mut Workspace, _: &mut Workspace,
@ -72,7 +71,7 @@ impl ProjectDiff {
pub fn deploy_at( pub fn deploy_at(
workspace: &mut Workspace, workspace: &mut Workspace,
path: Option<Arc<Path>>, entry: Option<GitStatusEntry>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
@ -92,9 +91,9 @@ impl ProjectDiff {
); );
project_diff project_diff
}; };
if let Some(path) = path { if let Some(entry) = entry {
project_diff.update(cx, |project_diff, cx| { project_diff.update(cx, |project_diff, cx| {
project_diff.scroll_to(path, window, cx); project_diff.scroll_to(entry, window, cx);
}) })
} }
} }
@ -126,10 +125,8 @@ impl ProjectDiff {
let git_state_subscription = cx.subscribe_in( let git_state_subscription = cx.subscribe_in(
&git_state, &git_state,
window, window,
move |this, _git_state, event, _window, _cx| match event { move |this, _git_state, _event, _window, _cx| {
project::git::Event::RepositoriesUpdated => { *this.update_needed.borrow_mut() = ();
*this.update_needed.borrow_mut() = ();
}
}, },
); );
@ -155,15 +152,39 @@ impl ProjectDiff {
} }
} }
pub fn scroll_to(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) { pub fn scroll_to(
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path, cx) { &mut self,
entry: GitStatusEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(git_repo) = self.git_state.read(cx).active_repository() else {
return;
};
let Some(path) = git_repo
.repo_path_to_project_path(&entry.repo_path)
.and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx))
else {
return;
};
let path_key = if entry.status.is_created() {
PathKey::namespaced(ADDED_NAMESPACE, &path)
} else {
PathKey::namespaced(CHANGED_NAMESPACE, &path)
};
self.scroll_to_path(path_key, window, cx)
}
fn scroll_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
s.select_ranges([position..position]); s.select_ranges([position..position]);
}) })
}) })
} else { } else {
self.pending_scroll = Some(path); self.pending_scroll = Some(path_key);
} }
} }
@ -223,9 +244,14 @@ impl ProjectDiff {
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
continue; continue;
}; };
let abs_path = Arc::from(abs_path); // Craft some artificial paths so that created entries will appear last.
let path_key = if entry.status.is_created() {
PathKey::namespaced(ADDED_NAMESPACE, &abs_path)
} else {
PathKey::namespaced(CHANGED_NAMESPACE, &abs_path)
};
previous_paths.remove(&abs_path); previous_paths.remove(&path_key);
let load_buffer = self let load_buffer = self
.project .project
.update(cx, |project, cx| project.open_buffer(project_path, cx)); .update(cx, |project, cx| project.open_buffer(project_path, cx));
@ -235,11 +261,11 @@ impl ProjectDiff {
let buffer = load_buffer.await?; let buffer = load_buffer.await?;
let changes = project let changes = project
.update(&mut cx, |project, cx| { .update(&mut cx, |project, cx| {
project.open_unstaged_changes(buffer.clone(), cx) project.open_uncommitted_changes(buffer.clone(), cx)
})? })?
.await?; .await?;
Ok(DiffBuffer { Ok(DiffBuffer {
abs_path, path_key,
buffer, buffer,
change_set: changes, change_set: changes,
}) })
@ -259,7 +285,7 @@ impl ProjectDiff {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let abs_path = diff_buffer.abs_path; let path_key = diff_buffer.path_key;
let buffer = diff_buffer.buffer; let buffer = diff_buffer.buffer;
let change_set = diff_buffer.change_set; let change_set = diff_buffer.change_set;
@ -272,15 +298,15 @@ impl ProjectDiff {
self.multibuffer.update(cx, |multibuffer, cx| { self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path( multibuffer.set_excerpts_for_path(
abs_path.clone(), path_key.clone(),
buffer, buffer,
diff_hunk_ranges, diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT, editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx, cx,
); );
}); });
if self.pending_scroll.as_ref() == Some(&abs_path) { if self.pending_scroll.as_ref() == Some(&path_key) {
self.scroll_to(abs_path, window, cx); self.scroll_to_path(path_key, window, cx);
} }
} }

View file

@ -49,7 +49,7 @@ impl RepositorySelector {
fn handle_project_git_event( fn handle_project_git_event(
&mut self, &mut self,
git_state: &Entity<GitState>, git_state: &Entity<GitState>,
_event: &project::git::Event, _event: &project::git::GitEvent,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {

View file

@ -67,7 +67,7 @@ pub struct MultiBuffer {
/// Contains the state of the buffers being edited /// Contains the state of the buffers being edited
buffers: RefCell<HashMap<BufferId, BufferState>>, buffers: RefCell<HashMap<BufferId, BufferState>>,
// only used by consumers using `set_excerpts_for_buffer` // only used by consumers using `set_excerpts_for_buffer`
buffers_by_path: BTreeMap<Arc<Path>, Vec<ExcerptId>>, buffers_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
diff_bases: HashMap<BufferId, ChangeSetState>, diff_bases: HashMap<BufferId, ChangeSetState>,
all_diff_hunks_expanded: bool, all_diff_hunks_expanded: bool,
subscriptions: Topic, subscriptions: Topic,
@ -143,6 +143,15 @@ impl MultiBufferDiffHunk {
} }
} }
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
pub struct PathKey(String);
impl PathKey {
pub fn namespaced(namespace: &str, path: &Path) -> Self {
Self(format!("{}/{}", namespace, path.to_string_lossy()))
}
}
pub type MultiBufferPoint = Point; pub type MultiBufferPoint = Point;
type ExcerptOffset = TypedOffset<Excerpt>; type ExcerptOffset = TypedOffset<Excerpt>;
type ExcerptPoint = TypedPoint<Excerpt>; type ExcerptPoint = TypedPoint<Excerpt>;
@ -1395,7 +1404,7 @@ impl MultiBuffer {
anchor_ranges anchor_ranges
} }
pub fn location_for_path(&self, path: &Arc<Path>, cx: &App) -> Option<Anchor> { pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
let excerpt_id = self.buffers_by_path.get(path)?.first()?; let excerpt_id = self.buffers_by_path.get(path)?.first()?;
let snapshot = self.snapshot(cx); let snapshot = self.snapshot(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?; let excerpt = snapshot.excerpt(*excerpt_id)?;
@ -1408,7 +1417,7 @@ impl MultiBuffer {
pub fn set_excerpts_for_path( pub fn set_excerpts_for_path(
&mut self, &mut self,
path: Arc<Path>, path: PathKey,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
ranges: Vec<Range<Point>>, ranges: Vec<Range<Point>>,
context_line_count: u32, context_line_count: u32,
@ -1517,11 +1526,11 @@ impl MultiBuffer {
} }
} }
pub fn paths(&self) -> impl Iterator<Item = Arc<Path>> + '_ { pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
self.buffers_by_path.keys().cloned() self.buffers_by_path.keys().cloned()
} }
pub fn remove_excerpts_for_path(&mut self, path: Arc<Path>, cx: &mut Context<Self>) { pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
if let Some(to_remove) = self.buffers_by_path.remove(&path) { if let Some(to_remove) = self.buffers_by_path.remove(&path) {
self.remove_excerpts(to_remove, cx) self.remove_excerpts(to_remove, cx)
} }

View file

@ -6,7 +6,7 @@ use language::{Buffer, Rope};
use parking_lot::RwLock; use parking_lot::RwLock;
use rand::prelude::*; use rand::prelude::*;
use settings::SettingsStore; use settings::SettingsStore;
use std::{env, path::PathBuf}; use std::env;
use util::test::sample_text; use util::test::sample_text;
#[ctor::ctor] #[ctor::ctor]
@ -1596,7 +1596,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
cx, cx,
) )
}); });
let path1: Arc<Path> = Arc::from(PathBuf::from("path1")); let path1: PathKey = PathKey::namespaced("0", Path::new("/"));
let buf2 = cx.new(|cx| { let buf2 = cx.new(|cx| {
Buffer::local( Buffer::local(
indoc! { indoc! {
@ -1615,7 +1615,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
cx, cx,
) )
}); });
let path2: Arc<Path> = Arc::from(PathBuf::from("path2")); let path2 = PathKey::namespaced("x", Path::new("/"));
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| { multibuffer.update(cx, |multibuffer, cx| {

View file

@ -149,37 +149,32 @@ impl BufferChangeSetState {
) -> oneshot::Receiver<()> { ) -> oneshot::Receiver<()> {
match diff_bases_change { match diff_bases_change {
DiffBasesChange::SetIndex(index) => { DiffBasesChange::SetIndex(index) => {
self.index_text = index.map(|mut text| { let mut index = index.unwrap_or_default();
text::LineEnding::normalize(&mut text); text::LineEnding::normalize(&mut index);
Arc::new(text) self.index_text = Some(Arc::new(index));
});
self.index_changed = true; self.index_changed = true;
} }
DiffBasesChange::SetHead(head) => { DiffBasesChange::SetHead(head) => {
self.head_text = head.map(|mut text| { let mut head = head.unwrap_or_default();
text::LineEnding::normalize(&mut text); text::LineEnding::normalize(&mut head);
Arc::new(text) self.head_text = Some(Arc::new(head));
});
self.head_changed = true; self.head_changed = true;
} }
DiffBasesChange::SetBoth(mut text) => { DiffBasesChange::SetBoth(text) => {
if let Some(text) = text.as_mut() { let mut text = text.unwrap_or_default();
text::LineEnding::normalize(text); text::LineEnding::normalize(&mut text);
} self.head_text = Some(Arc::new(text));
self.head_text = text.map(Arc::new);
self.index_text = self.head_text.clone(); self.index_text = self.head_text.clone();
self.head_changed = true; self.head_changed = true;
self.index_changed = true; self.index_changed = true;
} }
DiffBasesChange::SetEach { index, head } => { DiffBasesChange::SetEach { index, head } => {
self.index_text = index.map(|mut text| { let mut index = index.unwrap_or_default();
text::LineEnding::normalize(&mut text); text::LineEnding::normalize(&mut index);
Arc::new(text) let mut head = head.unwrap_or_default();
}); text::LineEnding::normalize(&mut head);
self.head_text = head.map(|mut text| { self.index_text = Some(Arc::new(index));
text::LineEnding::normalize(&mut text); self.head_text = Some(Arc::new(head));
Arc::new(text)
});
self.head_changed = true; self.head_changed = true;
self.index_changed = true; self.index_changed = true;
} }

View file

@ -69,11 +69,13 @@ enum Message {
Unstage(GitRepo, Vec<RepoPath>), Unstage(GitRepo, Vec<RepoPath>),
} }
pub enum Event { pub enum GitEvent {
RepositoriesUpdated, ActiveRepositoryChanged,
FileSystemUpdated,
GitStateUpdated,
} }
impl EventEmitter<Event> for GitState {} impl EventEmitter<GitEvent> for GitState {}
impl GitState { impl GitState {
pub fn new( pub fn new(
@ -103,7 +105,7 @@ impl GitState {
fn on_worktree_store_event( fn on_worktree_store_event(
&mut self, &mut self,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
_event: &WorktreeStoreEvent, event: &WorktreeStoreEvent,
cx: &mut Context<'_, Self>, cx: &mut Context<'_, Self>,
) { ) {
// TODO inspect the event // TODO inspect the event
@ -172,7 +174,14 @@ impl GitState {
self.repositories = new_repositories; self.repositories = new_repositories;
self.active_index = new_active_index; self.active_index = new_active_index;
cx.emit(Event::RepositoriesUpdated); match event {
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
cx.emit(GitEvent::GitStateUpdated);
}
_ => {
cx.emit(GitEvent::FileSystemUpdated);
}
}
} }
pub fn all_repositories(&self) -> Vec<RepositoryHandle> { pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
@ -314,7 +323,7 @@ impl RepositoryHandle {
return; return;
}; };
git_state.active_index = Some(index); git_state.active_index = Some(index);
cx.emit(Event::RepositoriesUpdated); cx.emit(GitEvent::ActiveRepositoryChanged);
}); });
} }