diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 837d50899c..b69161b7e5 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -663,11 +663,13 @@ impl std::fmt::Debug for BufferDiff { } } +#[derive(Clone, Debug)] pub enum BufferDiffEvent { DiffChanged { changed_range: Option>, }, LanguageChanged, + HunksStagedOrUnstaged(Option), } impl EventEmitter for BufferDiff {} @@ -762,6 +764,17 @@ impl BufferDiff { self.secondary_diff.clone() } + pub fn clear_pending_hunks(&mut self, cx: &mut Context) { + if let Some(secondary_diff) = &self.secondary_diff { + secondary_diff.update(cx, |diff, _| { + diff.inner.pending_hunks.clear(); + }); + cx.emit(BufferDiffEvent::DiffChanged { + changed_range: Some(Anchor::MIN..Anchor::MAX), + }); + } + } + pub fn stage_or_unstage_hunks( &mut self, stage: bool, @@ -784,6 +797,9 @@ impl BufferDiff { } }); } + cx.emit(BufferDiffEvent::HunksStagedOrUnstaged( + new_index_text.clone(), + )); if let Some((first, last)) = hunks.first().zip(hunks.last()) { let changed_range = first.buffer_range.start..last.buffer_range.end; cx.emit(BufferDiffEvent::DiffChanged { @@ -900,6 +916,14 @@ impl BufferDiff { } } + pub fn hunks<'a>( + &'a self, + buffer_snapshot: &'a text::BufferSnapshot, + cx: &'a App, + ) -> impl 'a + Iterator { + self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx) + } + pub fn hunks_intersecting_range<'a>( &'a self, range: Range, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 18439c4fc2..6b686c6f6f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7843,7 +7843,7 @@ impl Editor { for hunk in &hunks { self.prepare_restore_change(&mut revert_changes, hunk, cx); } - self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), window, cx); + self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx); } drop(chunk_by); if !revert_changes.is_empty() { @@ -13657,13 +13657,13 @@ impl Editor { pub fn toggle_staged_selected_diff_hunks( &mut self, _: &::git::ToggleStaged, - window: &mut Window, + _: &mut Window, cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); - self.stage_or_unstage_diff_hunks(stage, &ranges, window, cx); + self.stage_or_unstage_diff_hunks(stage, ranges, cx); } pub fn stage_and_next( @@ -13687,16 +13687,53 @@ impl Editor { pub fn stage_or_unstage_diff_hunks( &mut self, stage: bool, - ranges: &[Range], - window: &mut Window, + ranges: Vec>, cx: &mut Context, ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let chunk_by = self - .diff_hunks_in_ranges(&ranges, &snapshot) - .chunk_by(|hunk| hunk.buffer_id); - for (buffer_id, hunks) in &chunk_by { - self.do_stage_or_unstage(stage, buffer_id, hunks, window, cx); + let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); + cx.spawn(|this, mut cx| async move { + task.await?; + this.update(&mut cx, |this, cx| { + let snapshot = this.buffer.read(cx).snapshot(cx); + let chunk_by = this + .diff_hunks_in_ranges(&ranges, &snapshot) + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + this.do_stage_or_unstage(stage, buffer_id, hunks, cx); + } + }) + }) + .detach_and_log_err(cx); + } + + fn save_buffers_for_ranges_if_needed( + &mut self, + ranges: &[Range], + cx: &mut Context<'_, Editor>, + ) -> Task> { + let multibuffer = self.buffer.read(cx); + let snapshot = multibuffer.read(cx); + let buffer_ids: HashSet<_> = ranges + .iter() + .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) + .collect(); + drop(snapshot); + + let mut buffers = HashSet::default(); + for buffer_id in buffer_ids { + if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { + let buffer = buffer_entity.read(cx); + if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() + { + buffers.insert(buffer_entity); + } + } + } + + if let Some(project) = &self.project { + project.update(cx, |project, cx| project.save_buffers(buffers, cx)) + } else { + Task::ready(Ok(())) } } @@ -13709,7 +13746,7 @@ impl Editor { let ranges = self.selections.disjoint_anchor_ranges().collect::>(); if ranges.iter().any(|range| range.start != range.end) { - self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx); + self.stage_or_unstage_diff_hunks(stage, ranges, cx); return; } @@ -13728,7 +13765,7 @@ impl Editor { if run_twice { self.go_to_next_hunk(&GoToHunk, window, cx); } - self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx); + self.stage_or_unstage_diff_hunks(stage, ranges, cx); self.go_to_next_hunk(&GoToHunk, window, cx); } @@ -13737,31 +13774,16 @@ impl Editor { stage: bool, buffer_id: BufferId, hunks: impl Iterator, - window: &mut Window, cx: &mut App, - ) { - let Some(project) = self.project.as_ref() else { - return; - }; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { - return; - }; - let Some(diff) = self.buffer.read(cx).diff_for(buffer_id) else { - return; - }; + ) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; + let diff = self.buffer.read(cx).diff_for(buffer_id)?; let buffer_snapshot = buffer.read(cx).snapshot(); let file_exists = buffer_snapshot .file() .is_some_and(|file| file.disk_state().exists()); - let Some((repo, path)) = project - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - else { - log::debug!("no git repo for buffer id"); - return; - }; - - let new_index_text = diff.update(cx, |diff, cx| { + diff.update(cx, |diff, cx| { diff.stage_or_unstage_hunks( stage, &hunks @@ -13777,20 +13799,7 @@ impl Editor { cx, ) }); - - if file_exists { - let buffer_store = project.read(cx).buffer_store().clone(); - buffer_store - .update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx)) - .detach_and_log_err(cx); - } - - let recv = repo - .read(cx) - .set_index_text(&path, new_index_text.map(|rope| rope.to_string())); - - cx.background_spawn(async move { recv.await? }) - .detach_and_notify_err(window, cx); + None } pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { @@ -16305,7 +16314,7 @@ fn get_uncommitted_diff_for_buffer( } }); cx.spawn(|mut cx| async move { - let diffs = futures::future::join_all(tasks).await; + let diffs = future::join_all(tasks).await; buffer .update(&mut cx, |buffer, cx| { for diff in diffs.into_iter().flatten() { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 68754baef8..e868b75430 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -77,7 +77,7 @@ use ui::{ POPOVER_Y_PADDING, }; use unicode_segmentation::UnicodeSegmentation; -use util::{debug_panic, maybe, RangeExt, ResultExt}; +use util::{debug_panic, RangeExt, ResultExt}; use workspace::{item::Item, notifications::NotifyTaskExt}; const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; @@ -2676,24 +2676,21 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Div { - let file_status = maybe!({ - let project = self.editor.read(cx).project.as_ref()?.read(cx); - let (repo, path) = - project.repository_and_path_for_buffer_id(for_excerpt.buffer_id, cx)?; - let status = repo.read(cx).repository_entry.status_for_path(&path)?; - Some(status.status) - }) - .filter(|_| { - self.editor - .read(cx) - .buffer - .read(cx) - .all_diff_hunks_expanded() - }); - - let include_root = self - .editor + let editor = self.editor.read(cx); + let file_status = editor + .buffer .read(cx) + .all_diff_hunks_expanded() + .then(|| { + editor + .project + .as_ref()? + .read(cx) + .status_for_buffer_id(for_excerpt.buffer_id, cx) + }) + .flatten(); + + let include_root = editor .project .as_ref() .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) @@ -2705,7 +2702,7 @@ impl EditorElement { let parent_path = path.as_ref().and_then(|path| { Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR) }); - let focus_handle = self.editor.focus_handle(cx); + let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); div() @@ -2778,8 +2775,7 @@ impl EditorElement { ) }) .children( - self.editor - .read(cx) + editor .addons .values() .filter_map(|addon| { @@ -8822,12 +8818,11 @@ fn diff_hunk_controls( }) .on_click({ let editor = editor.clone(); - move |_event, window, cx| { + move |_event, _window, cx| { editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks( true, - &[hunk_range.start..hunk_range.start], - window, + vec![hunk_range.start..hunk_range.start], cx, ); }); @@ -8850,12 +8845,11 @@ fn diff_hunk_controls( }) .on_click({ let editor = editor.clone(); - move |_event, window, cx| { + move |_event, _window, cx| { editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks( false, - &[hunk_range.start..hunk_range.start], - window, + vec![hunk_range.start..hunk_range.start], cx, ); }); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index b38521756a..f881726da3 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1448,6 +1448,12 @@ impl FakeFs { }); } + pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option) { + self.with_git_state(dot_git, true, |state| { + state.simulated_index_write_error_message = message; + }); + } + pub fn paths(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 3c7af56f97..b1185c6856 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -862,6 +862,7 @@ pub struct FakeGitRepositoryState { pub statuses: HashMap, pub current_branch_name: Option, pub branches: HashSet, + pub simulated_index_write_error_message: Option, } impl FakeGitRepository { @@ -881,6 +882,7 @@ impl FakeGitRepositoryState { statuses: Default::default(), current_branch_name: Default::default(), branches: Default::default(), + simulated_index_write_error_message: None, } } } @@ -900,6 +902,9 @@ impl GitRepository for FakeGitRepository { fn set_index_text(&self, path: &RepoPath, content: Option) -> anyhow::Result<()> { let mut state = self.state.lock(); + if let Some(message) = state.simulated_index_write_error_message.clone() { + return Err(anyhow::anyhow!(message)); + } if let Some(content) = content { state.index_contents.insert(path.clone(), content); } else { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 98b88e6125..fe16099d0d 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -307,6 +307,13 @@ impl GitPanel { this.active_repository = git_store.read(cx).active_repository(); this.schedule_update(true, window, cx); } + GitEvent::IndexWriteError(error) => { + this.workspace + .update(cx, |workspace, cx| { + workspace.show_error(error, cx); + }) + .ok(); + } }, ) .detach(); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 7deccdeb63..bb38d9fb8e 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -19,7 +19,10 @@ use gpui::{ }; use language::{Anchor, Buffer, Capability, OffsetRangeExt}; use multi_buffer::{MultiBuffer, PathKey}; -use project::{git::GitStore, Project, ProjectPath}; +use project::{ + git::{GitEvent, GitStore}, + Project, ProjectPath, +}; use std::any::{Any, TypeId}; use theme::ActiveTheme; use ui::{prelude::*, vertical_divider, Tooltip}; @@ -141,8 +144,13 @@ impl ProjectDiff { let git_store_subscription = cx.subscribe_in( &git_store, window, - move |this, _git_store, _event, _window, _cx| { - *this.update_needed.borrow_mut() = (); + move |this, _git_store, event, _window, _cx| match event { + GitEvent::ActiveRepositoryChanged + | GitEvent::FileSystemUpdated + | GitEvent::GitStateUpdated => { + *this.update_needed.borrow_mut() = (); + } + _ => {} }, ); @@ -1017,9 +1025,6 @@ mod tests { editor.update_in(cx, |editor, window, cx| { editor.git_restore(&Default::default(), window, cx); }); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.statuses = HashMap::default(); - }); cx.run_until_parked(); assert_state_with_diff(&editor, cx, &"ˇ".unindent()); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e282ca2dd7..dbc15cc1f8 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -250,6 +250,7 @@ impl DiffState { } } BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), + _ => {} }), diff, } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index ab2ae7b5e5..0ee4f0c8ea 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -339,6 +339,7 @@ enum OpenBuffer { pub enum BufferStoreEvent { BufferAdded(Entity), + BufferDiffAdded(Entity), BufferDropped(BufferId), BufferChangedFilePath { buffer: Entity, @@ -1522,11 +1523,12 @@ impl BufferStore { if let Some(OpenBuffer::Complete { diff_state, .. }) = this.opened_buffers.get_mut(&buffer_id) { + let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); + cx.emit(BufferStoreEvent::BufferDiffAdded(diff.clone())); diff_state.update(cx, |diff_state, cx| { diff_state.language = language; diff_state.language_registry = language_registry; - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); match kind { DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()), DiffKind::Uncommitted => { diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index fcd3bb9562..efdf80c5ee 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -1,24 +1,38 @@ -use crate::buffer_store::BufferStore; -use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; -use crate::{Project, ProjectPath}; +use crate::{ + buffer_store::{BufferStore, BufferStoreEvent}, + worktree_store::{WorktreeStore, WorktreeStoreEvent}, + Project, ProjectItem, ProjectPath, +}; use anyhow::{Context as _, Result}; +use buffer_diff::BufferDiffEvent; use client::ProjectId; -use futures::channel::{mpsc, oneshot}; -use futures::StreamExt as _; -use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode}; -use git::repository::{GitRepository, RepoPath}; +use futures::{ + channel::{mpsc, oneshot}, + StreamExt as _, +}; +use git::{ + repository::{ + Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath, + ResetMode, + }, + status::FileStatus, +}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity, }; use language::{Buffer, LanguageRegistry}; -use rpc::proto::{git_reset, ToProto}; -use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use rpc::{ + proto::{self, git_reset, ToProto}, + AnyProtoClient, TypedEnvelope, +}; use settings::WorktreeId; -use std::collections::VecDeque; -use std::future::Future; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::{ + collections::VecDeque, + future::Future, + path::{Path, PathBuf}, + sync::Arc, +}; use text::BufferId; use util::{maybe, ResultExt}; use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory}; @@ -30,7 +44,7 @@ pub struct GitStore { repositories: Vec>, active_index: Option, update_sender: mpsc::UnboundedSender, - _subscription: Subscription, + _subscriptions: [Subscription; 2], } pub struct Repository { @@ -54,10 +68,12 @@ pub enum GitRepo { }, } +#[derive(Debug)] pub enum GitEvent { ActiveRepositoryChanged, FileSystemUpdated, GitStateUpdated, + IndexWriteError(anyhow::Error), } struct GitJob { @@ -81,7 +97,10 @@ impl GitStore { cx: &mut Context<'_, Self>, ) -> Self { let update_sender = Self::spawn_git_worker(cx); - let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event); + let _subscriptions = [ + cx.subscribe(worktree_store, Self::on_worktree_store_event), + cx.subscribe(&buffer_store, Self::on_buffer_store_event), + ]; GitStore { project_id, @@ -90,7 +109,7 @@ impl GitStore { repositories: Vec::new(), active_index: None, update_sender, - _subscription, + _subscriptions, } } @@ -227,10 +246,82 @@ impl GitStore { } } + fn on_buffer_store_event( + &mut self, + _: Entity, + event: &BufferStoreEvent, + cx: &mut Context<'_, Self>, + ) { + if let BufferStoreEvent::BufferDiffAdded(diff) = event { + cx.subscribe(diff, Self::on_buffer_diff_event).detach(); + } + } + + fn on_buffer_diff_event( + this: &mut GitStore, + diff: Entity, + event: &BufferDiffEvent, + cx: &mut Context<'_, GitStore>, + ) { + if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event { + let buffer_id = diff.read(cx).buffer_id; + if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) { + let recv = repo + .read(cx) + .set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string())); + let diff = diff.downgrade(); + cx.spawn(|this, mut cx| async move { + if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await + { + if let Err(error) = result { + diff.update(&mut cx, |diff, cx| { + diff.clear_pending_hunks(cx); + }) + .ok(); + this.update(&mut cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error))) + .ok(); + } + } + }) + .detach(); + } + } + } + pub fn all_repositories(&self) -> Vec> { self.repositories.clone() } + pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { + let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?; + let status = repo.read(cx).repository_entry.status_for_path(&path)?; + Some(status.status) + } + + fn repository_and_path_for_buffer_id( + &self, + buffer_id: BufferId, + cx: &App, + ) -> Option<(Entity, RepoPath)> { + let buffer = self.buffer_store.read(cx).get(buffer_id)?; + let path = buffer.read(cx).project_path(cx)?; + let mut result: Option<(Entity, RepoPath)> = None; + for repo_handle in &self.repositories { + let repo = repo_handle.read(cx); + if repo.worktree_id == path.worktree_id { + if let Ok(relative_path) = repo.repository_entry.relativize(&path.path) { + if result + .as_ref() + .is_none_or(|(result, _)| !repo.contains_sub_repo(result, cx)) + { + result = Some((repo_handle.clone(), relative_path)) + } + } + } + } + result + } + fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender { let (job_tx, mut job_rx) = mpsc::unbounded::(); @@ -658,9 +749,8 @@ impl GitStore { cx: &mut AsyncApp, ) -> Result> { this.update(cx, |this, cx| { - let repository_handle = this - .all_repositories() - .into_iter() + this.repositories + .iter() .find(|repository_handle| { repository_handle.read(cx).worktree_id == worktree_id && repository_handle @@ -669,8 +759,8 @@ impl GitStore { .work_directory_id() == work_directory_id }) - .context("missing repository handle")?; - anyhow::Ok(repository_handle) + .context("missing repository handle") + .cloned() })? } } @@ -1297,7 +1387,7 @@ impl Repository { }) } - pub fn set_index_text( + fn set_index_text( &self, path: &RepoPath, content: Option, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3c3dc7b2db..0a73c90d18 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3184,7 +3184,7 @@ impl LspStore { } } } - BufferStoreEvent::BufferDropped(_) => {} + _ => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9fc4b4f481..b998d4986c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -46,11 +46,7 @@ use futures::{ pub use image_store::{ImageItem, ImageStore}; use image_store::{ImageItemEvent, ImageStoreEvent}; -use ::git::{ - blame::Blame, - repository::{GitRepository, RepoPath}, - status::FileStatus, -}; +use ::git::{blame::Blame, repository::GitRepository, status::FileStatus}; use gpui::{ AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla, SharedString, Task, WeakEntity, Window, @@ -2276,7 +2272,6 @@ impl Project { BufferStoreEvent::BufferAdded(buffer) => { self.register_buffer(buffer, cx).log_err(); } - BufferStoreEvent::BufferChangedFilePath { .. } => {} BufferStoreEvent::BufferDropped(buffer_id) => { if let Some(ref ssh_client) = self.ssh_client { ssh_client @@ -2289,6 +2284,7 @@ impl Project { .log_err(); } } + _ => {} } } @@ -4336,35 +4332,8 @@ impl Project { self.git_store.read(cx).all_repositories() } - pub fn repository_and_path_for_buffer_id( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Option<(Entity, RepoPath)> { - let path = self - .buffer_for_id(buffer_id, cx)? - .read(cx) - .project_path(cx)?; - - let mut found: Option<(Entity, RepoPath)> = None; - for repo_handle in self.git_store.read(cx).all_repositories() { - let repo = repo_handle.read(cx); - if repo.worktree_id != path.worktree_id { - continue; - } - let Ok(relative_path) = repo.repository_entry.relativize(&path.path) else { - continue; - }; - if found - .as_ref() - .is_some_and(|(found, _)| repo.contains_sub_repo(found, cx)) - { - continue; - } - found = Some((repo_handle.clone(), relative_path)) - } - - found + pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { + self.git_store.read(cx).status_for_buffer_id(buffer_id, cx) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index e4e2335d3d..ddaba39747 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,5 +1,7 @@ use crate::{task_inventory::TaskContexts, Event, *}; -use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; +use buffer_diff::{ + assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, +}; use fs::FakeFs; use futures::{future, StreamExt}; use gpui::{App, SemanticVersion, UpdateGlobal}; @@ -5786,7 +5788,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + unstaged_diff.hunks(&snapshot, cx), &snapshot, &unstaged_diff.base_text_string().unwrap(), &[ @@ -6008,6 +6010,271 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { + use DiffHunkSecondaryStatus::*; + init_test(cx); + + let committed_contents = r#" + zero + one + two + three + four + five + "# + .unindent(); + let file_contents = r#" + one + TWO + three + FOUR + five + "# + .unindent(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "file.txt": file_contents.clone() + }), + ) + .await; + + fs.set_head_for_repo( + "/dir/.git".as_ref(), + &[("file.txt".into(), committed_contents.clone())], + ); + fs.set_index_for_repo( + "/dir/.git".as_ref(), + &[("file.txt".into(), committed_contents.clone())], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/file.txt", cx) + }) + .await + .unwrap(); + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + let uncommitted_diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + let mut diff_events = cx.events(&uncommitted_diff); + + // The hunks are initially unstaged. + uncommitted_diff.read_with(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(HasSecondaryHunk), + ), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(HasSecondaryHunk), + ), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(HasSecondaryHunk), + ), + ], + ); + }); + + // Stage a hunk. It appears as optimistically staged. + uncommitted_diff.update(cx, |diff, cx| { + let range = + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0)); + let hunks = diff + .hunks_intersecting_range(range, &snapshot, cx) + .collect::>(); + diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx); + + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(HasSecondaryHunk), + ), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(SecondaryHunkRemovalPending), + ), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(HasSecondaryHunk), + ), + ], + ); + }); + + // The diff emits a change event for the range of the staged hunk. + assert!(matches!( + diff_events.next().await.unwrap(), + BufferDiffEvent::HunksStagedOrUnstaged(_) + )); + let event = diff_events.next().await.unwrap(); + if let BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + } = event + { + let changed_range = changed_range.to_point(&snapshot); + assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0)); + } else { + panic!("Unexpected event {event:?}"); + } + + // When the write to the index completes, it appears as staged. + cx.run_until_parked(); + uncommitted_diff.update(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(HasSecondaryHunk), + ), + (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(HasSecondaryHunk), + ), + ], + ); + }); + + // The diff emits a change event for the changed index text. + let event = diff_events.next().await.unwrap(); + if let BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + } = event + { + let changed_range = changed_range.to_point(&snapshot); + assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0)); + } else { + panic!("Unexpected event {event:?}"); + } + + // Simulate a problem writing to the git index. + fs.set_error_message_for_index_write( + "/dir/.git".as_ref(), + Some("failed to write git index".into()), + ); + + // Stage another hunk. + uncommitted_diff.update(cx, |diff, cx| { + let range = + snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0)); + let hunks = diff + .hunks_intersecting_range(range, &snapshot, cx) + .collect::>(); + diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx); + + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(HasSecondaryHunk), + ), + (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(SecondaryHunkRemovalPending), + ), + ], + ); + }); + assert!(matches!( + diff_events.next().await.unwrap(), + BufferDiffEvent::HunksStagedOrUnstaged(_) + )); + let event = diff_events.next().await.unwrap(); + if let BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + } = event + { + let changed_range = changed_range.to_point(&snapshot); + assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0)); + } else { + panic!("Unexpected event {event:?}"); + } + + // When the write fails, the hunk returns to being unstaged. + cx.run_until_parked(); + uncommitted_diff.update(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(HasSecondaryHunk), + ), + (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(HasSecondaryHunk), + ), + ], + ); + }); + + let event = diff_events.next().await.unwrap(); + if let BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + } = event + { + let changed_range = changed_range.to_point(&snapshot); + assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0)); + } else { + panic!("Unexpected event {event:?}"); + } +} + #[gpui::test] async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -6065,7 +6332,7 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + uncommitted_diff.hunks(&snapshot, cx), &snapshot, &uncommitted_diff.base_text_string().unwrap(), &[( diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 28d4f08771..83e37a7e18 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -70,6 +70,10 @@ impl TreeMap { self.0.insert_or_replace(MapEntry { key, value }, &()); } + pub fn clear(&mut self) { + self.0 = SumTree::default(); + } + pub fn remove(&mut self, key: &K) -> Option { let mut removed = None; let mut cursor = self.0.cursor::>(&()); @@ -157,6 +161,14 @@ impl TreeMap { self.0.iter().map(|entry| &entry.value) } + pub fn first(&self) -> Option<(&K, &V)> { + self.0.first().map(|entry| (&entry.key, &entry.value)) + } + + pub fn last(&self) -> Option<(&K, &V)> { + self.0.last().map(|entry| (&entry.key, &entry.value)) + } + pub fn insert_tree(&mut self, other: TreeMap) { let edits = other .iter()