diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 02f4b474b5..6e62260255 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1285,7 +1285,7 @@ impl Editor { let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { - get_unstaged_changes_for_buffers( + get_uncommitted_changes_for_buffer( &project, buffer.read(cx).all_buffers(), buffer.clone(), @@ -13657,7 +13657,7 @@ impl Editor { let buffer_id = buffer.read(cx).remote_id(); if self.buffer.read(cx).change_set_for(buffer_id).is_none() { if let Some(project) = &self.project { - get_unstaged_changes_for_buffers( + get_uncommitted_changes_for_buffer( project, [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, buffers: impl IntoIterator>, buffer: Entity, @@ -14422,7 +14422,7 @@ fn get_unstaged_changes_for_buffers( let mut tasks = Vec::new(); project.update(cx, |project, cx| { 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 { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 35942cce25..fae15adf46 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5619,13 +5619,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) { let base_text = r#" impl A { - // this is an unstaged comment + // this is an uncommitted comment fn b() { c(); } - // this is another unstaged comment + // this is another uncommitted comment fn d() { // e @@ -5668,13 +5668,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) { cx.assert_state_with_diff( " ˇimpl A { - - // this is an unstaged comment + - // this is an uncommitted comment fn b() { c(); } - - // this is another unstaged comment + - // this is another uncommitted comment - fn d() { // e @@ -5691,13 +5691,13 @@ async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) { let expected_display_text = " impl A { - // this is an unstaged comment + // this is an uncommitted comment fn b() { ⋯ } - // this is another unstaged comment + // this is another uncommitted comment fn d() { ⋯ diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 6246ec14fb..c51dc0d6a6 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -290,7 +290,7 @@ impl EditorTestContext { editor.project.as_ref().unwrap().read(cx).fs().as_fake() }); 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"), &[(path.into(), diff_base.to_string())], ); diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a377740152..efedb0d461 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -265,13 +265,13 @@ impl GitRepository for RealGitRepository { .to_path_buf(); 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) .args(["update-index", "--add", "--remove", "--"]) .args(paths.iter().map(|p| p.as_ref())) .status()?; - if !cmd.success() { - return Err(anyhow!("Failed to stage paths: {cmd}")); + if !status.success() { + return Err(anyhow!("Failed to stage paths: {status}")); } } Ok(()) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 531267f391..1e7ce96cef 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -12,13 +12,11 @@ use editor::scroll::ScrollbarAutoHide; use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar}; use git::repository::RepoPath; use git::status::FileStatus; -use git::{ - CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll, COMMIT_MESSAGE, -}; +use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE}; use gpui::*; use language::{Buffer, BufferId}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::git::{GitRepo, RepositoryHandle}; +use project::git::{GitEvent, GitRepo, RepositoryHandle}; use project::{CreateOptions, Fs, Project, ProjectPath}; use rpc::proto; use serde::{Deserialize, Serialize}; @@ -43,7 +41,6 @@ actions!( Close, ToggleFocus, OpenMenu, - OpenSelected, FocusEditor, FocusChanges, FillCoAuthors, @@ -76,17 +73,17 @@ struct SerializedGitPanel { width: Option, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Section { Changed, - New, + Created, } impl Section { pub fn contains(&self, status: FileStatus) -> bool { match self { 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)] struct GitHeaderEntry { header: Section, - all_staged: ToggleState, } impl GitHeaderEntry { @@ -104,7 +100,7 @@ impl GitHeaderEntry { pub fn title(&self) -> &'static str { match self.header { Section::Changed => "Changed", - Section::New => "New", + Section::Created => "New", } } } @@ -126,11 +122,18 @@ impl GitListEntry { #[derive(Debug, PartialEq, Eq, Clone)] pub struct GitStatusEntry { - depth: usize, - display_name: String, - repo_path: RepoPath, - status: FileStatus, - is_staged: Option, + pub(crate) depth: usize, + pub(crate) display_name: String, + pub(crate) repo_path: RepoPath, + pub(crate) status: FileStatus, + pub(crate) is_staged: Option, +} + +pub struct PendingOperation { + finished: bool, + will_become_staged: bool, + repo_paths: HashSet, + op_id: usize, } pub struct GitPanel { @@ -152,9 +155,11 @@ pub struct GitPanel { entries: Vec, entries_by_path: collections::HashMap, width: Option, - pending: HashMap, + pending: Vec, commit_task: Task>, commit_pending: bool, + can_commit: bool, + can_commit_all: bool, } fn commit_message_buffer( @@ -287,9 +292,12 @@ impl GitPanel { &git_state, window, 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.schedule_update(window, cx); + this.schedule_update(true, window, cx); } }, ) @@ -303,7 +311,7 @@ impl GitPanel { pending_serialization: Task::ready(None), entries: Vec::new(), entries_by_path: HashMap::default(), - pending: HashMap::default(), + pending: Vec::new(), current_modifiers: window.modifiers(), width: Some(px(360.)), scrollbar_state: ScrollbarState::new(scroll_handle.clone()) @@ -321,8 +329,10 @@ impl GitPanel { commit_editor, project, 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 }); @@ -617,7 +627,7 @@ impl GitPanel { } } GitListEntry::Header(section) => { - let goal_staged_state = !section.all_staged.selected(); + let goal_staged_state = !self.header_state(section.header).selected(); let entries = self .entries .iter() @@ -629,12 +639,17 @@ impl GitPanel { .map(|status_entry| status_entry.repo_path) .collect::>(); - (!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({ let repo_paths = repo_paths.clone(); @@ -647,9 +662,9 @@ impl GitPanel { }; this.update(&mut cx, |this, cx| { - for repo_path in repo_paths { - if this.pending.get(&repo_path) == Some(&stage) { - this.pending.remove(&repo_path); + for pending in this.pending.iter_mut() { + if pending.op_id == op_id { + pending.finished = true } } result @@ -696,67 +711,6 @@ impl GitPanel { cx.emit(Event::OpenedEntry { path }); } - fn stage_all(&mut self, _: &git::StageAll, _window: &mut Window, cx: &mut Context) { - 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) { - 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) { - // TODO: Implement discard all - println!("Discard all triggered"); - } - /// Commit all staged changes fn commit_changes( &mut self, @@ -768,7 +722,7 @@ impl GitPanel { let Some(active_repository) = self.active_repository.clone() else { return; }; - if !active_repository.can_commit(false) { + if !self.can_commit { return; } if self.commit_editor.read(cx).is_empty(cx) { @@ -811,7 +765,7 @@ impl GitPanel { let Some(active_repository) = self.active_repository.clone() else { return; }; - if !active_repository.can_commit(true) { + if !self.can_commit_all { return; } 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) { + fn schedule_update( + &mut self, + clear_pending: bool, + window: &mut Window, + cx: &mut Context, + ) { let project = self.project.clone(); let handle = cx.entity().downgrade(); self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move { @@ -957,6 +916,9 @@ impl GitPanel { git_panel .update_in(&mut cx, |git_panel, window, cx| { git_panel.update_visible_entries(cx); + if clear_pending { + git_panel.clear_pending(); + } git_panel.commit_editor = 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.entries.clear(); self.entries_by_path.clear(); @@ -980,12 +946,11 @@ impl GitPanel { // First pass - collect all paths let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path)); - // Second pass - create entries with proper depth calculation - let mut new_any_staged = false; - let mut new_all_staged = true; - let mut changed_any_staged = false; - let mut changed_all_staged = true; + let mut has_changed_checked_boxes = false; + let mut has_changed = false; + let mut has_added_checked_boxes = false; + // Second pass - create entries with proper depth calculation for entry in repo.status() { let (depth, difference) = 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_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 { // Show partial path for deeply nested files entry @@ -1030,8 +986,15 @@ impl GitPanel { }; if is_new { + if entry.is_staged != Some(false) { + has_added_checked_boxes = true + } new_entries.push(entry); } else { + has_changed = true; + if entry.is_staged != Some(false) { + has_changed_checked_boxes = true + } changed_entries.push(entry); } } @@ -1041,11 +1004,8 @@ impl GitPanel { new_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path)); 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 { header: Section::Changed, - all_staged: toggle_state, })); self.entries.extend( changed_entries @@ -1054,10 +1014,8 @@ impl GitPanel { ); } 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 { - header: Section::New, - all_staged: toggle_state, + header: Section::Created, })); self.entries .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.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); 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) { let Some(workspace) = self.workspace.upgrade() else { return; @@ -1089,7 +1080,6 @@ impl GitPanel { } } -// GitPanel –– Render impl GitPanel { pub fn panel_button( &self, @@ -1199,21 +1189,13 @@ impl GitPanel { pub fn render_commit_editor( &self, name_and_email: Option<(SharedString, SharedString)>, - can_commit: bool, cx: &Context, ) -> impl IntoElement { 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 (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_2 = self.focus_handle(cx).clone(); @@ -1466,7 +1448,7 @@ impl GitPanel { has_write_access: bool, cx: &Context, ) -> 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) .fill() .elevation(ElevationIndex::Surface); @@ -1510,7 +1492,14 @@ 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.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 selected = self.selected_entry == Some(ix); let status_style = GitPanelSettings::get_global(cx).status_style; @@ -1559,13 +1548,19 @@ impl GitPanel { window, cx, ); + cx.stop_propagation(); }) }); let start_slot = h_flex() + .id(("start-slot", ix)) .gap(DynamicSpacing::Base04.rems(cx)) .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()); @@ -1581,27 +1576,14 @@ impl GitPanel { .toggle_state(selected) .disabled(!has_write_access) .on_click({ - let repo_path = entry.repo_path.clone(); + let entry = entry.clone(); cx.listener(move |this, _, window, cx| { this.selected_entry = Some(ix); - window.dispatch_action(Box::new(OpenSelected), cx); - cx.notify(); let Some(workspace) = this.workspace.upgrade() else { 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| { - 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.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| { git_panel .on_action({ @@ -1764,7 +1735,7 @@ impl Render for GitPanel { self.render_empty_state(cx).into_any_element() }) .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)) } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 8d8b025562..789dc8c21d 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,8 +1,4 @@ -use std::{ - any::{Any, TypeId}, - path::Path, - sync::Arc, -}; +use std::any::{Any, TypeId}; use anyhow::Result; use collections::HashSet; @@ -14,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, }; 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 theme::ActiveTheme; use ui::prelude::*; @@ -25,7 +21,7 @@ use workspace::{ ItemNavHistory, ToolbarItemLocation, Workspace, }; -use crate::git_panel::GitPanel; +use crate::git_panel::{GitPanel, GitStatusEntry}; actions!(git, [Diff]); @@ -37,18 +33,21 @@ pub(crate) struct ProjectDiff { workspace: WeakEntity, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, - pending_scroll: Option>, + pending_scroll: Option, _task: Task>, _subscription: Subscription, } struct DiffBuffer { - abs_path: Arc, + path_key: PathKey, buffer: Entity, change_set: Entity, } +const CHANGED_NAMESPACE: &'static str = "0"; +const ADDED_NAMESPACE: &'static str = "1"; + impl ProjectDiff { pub(crate) fn register( _: &mut Workspace, @@ -72,7 +71,7 @@ impl ProjectDiff { pub fn deploy_at( workspace: &mut Workspace, - path: Option>, + entry: Option, window: &mut Window, cx: &mut Context, ) { @@ -92,9 +91,9 @@ impl ProjectDiff { ); project_diff }; - if let Some(path) = path { + if let Some(entry) = entry { 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( &git_state, window, - move |this, _git_state, event, _window, _cx| match event { - project::git::Event::RepositoriesUpdated => { - *this.update_needed.borrow_mut() = (); - } + move |this, _git_state, _event, _window, _cx| { + *this.update_needed.borrow_mut() = (); }, ); @@ -155,15 +152,39 @@ impl ProjectDiff { } } - pub fn scroll_to(&mut self, path: Arc, window: &mut Window, cx: &mut Context) { - if let Some(position) = self.multibuffer.read(cx).location_for_path(&path, cx) { + pub fn scroll_to( + &mut self, + entry: GitStatusEntry, + window: &mut Window, + cx: &mut Context, + ) { + 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) { + if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }) }) } 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 { 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 .project .update(cx, |project, cx| project.open_buffer(project_path, cx)); @@ -235,11 +261,11 @@ impl ProjectDiff { let buffer = load_buffer.await?; let changes = project .update(&mut cx, |project, cx| { - project.open_unstaged_changes(buffer.clone(), cx) + project.open_uncommitted_changes(buffer.clone(), cx) })? .await?; Ok(DiffBuffer { - abs_path, + path_key, buffer, change_set: changes, }) @@ -259,7 +285,7 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - let abs_path = diff_buffer.abs_path; + let path_key = diff_buffer.path_key; let buffer = diff_buffer.buffer; let change_set = diff_buffer.change_set; @@ -272,15 +298,15 @@ impl ProjectDiff { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( - abs_path.clone(), + path_key.clone(), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); }); - if self.pending_scroll.as_ref() == Some(&abs_path) { - self.scroll_to(abs_path, window, cx); + if self.pending_scroll.as_ref() == Some(&path_key) { + self.scroll_to_path(path_key, window, cx); } } diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index 6ec2dab6c6..9c7f5f4e07 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -49,7 +49,7 @@ impl RepositorySelector { fn handle_project_git_event( &mut self, git_state: &Entity, - _event: &project::git::Event, + _event: &project::git::GitEvent, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index cc8afcb234..b523bbb921 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -67,7 +67,7 @@ pub struct MultiBuffer { /// Contains the state of the buffers being edited buffers: RefCell>, // only used by consumers using `set_excerpts_for_buffer` - buffers_by_path: BTreeMap, Vec>, + buffers_by_path: BTreeMap>, diff_bases: HashMap, all_diff_hunks_expanded: bool, 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; type ExcerptOffset = TypedOffset; type ExcerptPoint = TypedPoint; @@ -1395,7 +1404,7 @@ impl MultiBuffer { anchor_ranges } - pub fn location_for_path(&self, path: &Arc, cx: &App) -> Option { + pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { let excerpt_id = self.buffers_by_path.get(path)?.first()?; let snapshot = self.snapshot(cx); let excerpt = snapshot.excerpt(*excerpt_id)?; @@ -1408,7 +1417,7 @@ impl MultiBuffer { pub fn set_excerpts_for_path( &mut self, - path: Arc, + path: PathKey, buffer: Entity, ranges: Vec>, context_line_count: u32, @@ -1517,11 +1526,11 @@ impl MultiBuffer { } } - pub fn paths(&self) -> impl Iterator> + '_ { + pub fn paths(&self) -> impl Iterator + '_ { self.buffers_by_path.keys().cloned() } - pub fn remove_excerpts_for_path(&mut self, path: Arc, cx: &mut Context) { + pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { if let Some(to_remove) = self.buffers_by_path.remove(&path) { self.remove_excerpts(to_remove, cx) } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 61094a1b4f..2e13fa4558 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -6,7 +6,7 @@ use language::{Buffer, Rope}; use parking_lot::RwLock; use rand::prelude::*; use settings::SettingsStore; -use std::{env, path::PathBuf}; +use std::env; use util::test::sample_text; #[ctor::ctor] @@ -1596,7 +1596,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) { cx, ) }); - let path1: Arc = Arc::from(PathBuf::from("path1")); + let path1: PathKey = PathKey::namespaced("0", Path::new("/")); let buf2 = cx.new(|cx| { Buffer::local( indoc! { @@ -1615,7 +1615,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) { cx, ) }); - let path2: Arc = Arc::from(PathBuf::from("path2")); + let path2 = PathKey::namespaced("x", Path::new("/")); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index bbfaa1e478..10bc83da7a 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -149,37 +149,32 @@ impl BufferChangeSetState { ) -> oneshot::Receiver<()> { match diff_bases_change { DiffBasesChange::SetIndex(index) => { - self.index_text = index.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); + let mut index = index.unwrap_or_default(); + text::LineEnding::normalize(&mut index); + self.index_text = Some(Arc::new(index)); self.index_changed = true; } DiffBasesChange::SetHead(head) => { - self.head_text = head.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); + let mut head = head.unwrap_or_default(); + text::LineEnding::normalize(&mut head); + self.head_text = Some(Arc::new(head)); self.head_changed = true; } - DiffBasesChange::SetBoth(mut text) => { - if let Some(text) = text.as_mut() { - text::LineEnding::normalize(text); - } - self.head_text = text.map(Arc::new); + DiffBasesChange::SetBoth(text) => { + let mut text = text.unwrap_or_default(); + text::LineEnding::normalize(&mut text); + self.head_text = Some(Arc::new(text)); self.index_text = self.head_text.clone(); self.head_changed = true; self.index_changed = true; } DiffBasesChange::SetEach { index, head } => { - self.index_text = index.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); - self.head_text = head.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); + let mut index = index.unwrap_or_default(); + text::LineEnding::normalize(&mut index); + let mut head = head.unwrap_or_default(); + text::LineEnding::normalize(&mut head); + self.index_text = Some(Arc::new(index)); + self.head_text = Some(Arc::new(head)); self.head_changed = true; self.index_changed = true; } diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index af86d1d14b..90dff1ed93 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -69,11 +69,13 @@ enum Message { Unstage(GitRepo, Vec), } -pub enum Event { - RepositoriesUpdated, +pub enum GitEvent { + ActiveRepositoryChanged, + FileSystemUpdated, + GitStateUpdated, } -impl EventEmitter for GitState {} +impl EventEmitter for GitState {} impl GitState { pub fn new( @@ -103,7 +105,7 @@ impl GitState { fn on_worktree_store_event( &mut self, worktree_store: Entity, - _event: &WorktreeStoreEvent, + event: &WorktreeStoreEvent, cx: &mut Context<'_, Self>, ) { // TODO inspect the event @@ -172,7 +174,14 @@ impl GitState { self.repositories = new_repositories; 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 { @@ -314,7 +323,7 @@ impl RepositoryHandle { return; }; git_state.active_index = Some(index); - cx.emit(Event::RepositoriesUpdated); + cx.emit(GitEvent::ActiveRepositoryChanged); }); }