Detailed changes
@@ -663,11 +663,13 @@ impl std::fmt::Debug for BufferDiff {
}
}
+#[derive(Clone, Debug)]
pub enum BufferDiffEvent {
DiffChanged {
changed_range: Option<Range<text::Anchor>>,
},
LanguageChanged,
+ HunksStagedOrUnstaged(Option<Rope>),
}
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
@@ -762,6 +764,17 @@ impl BufferDiff {
self.secondary_diff.clone()
}
+ pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
+ 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<Item = DiffHunk> {
+ self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
+ }
+
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<text::Anchor>,
@@ -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<Self>,
) {
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<Anchor>],
- window: &mut Window,
+ ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) {
- 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<Anchor>],
+ cx: &mut Context<'_, Editor>,
+ ) -> Task<Result<()>> {
+ 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::<Vec<_>>();
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<Item = MultiBufferDiffHunk>,
- 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<Self>) {
@@ -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() {
@@ -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,
);
});
@@ -1448,6 +1448,12 @@ impl FakeFs {
});
}
+ pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
+ self.with_git_state(dot_git, true, |state| {
+ state.simulated_index_write_error_message = message;
+ });
+ }
+
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -862,6 +862,7 @@ pub struct FakeGitRepositoryState {
pub statuses: HashMap<RepoPath, FileStatus>,
pub current_branch_name: Option<String>,
pub branches: HashSet<String>,
+ pub simulated_index_write_error_message: Option<String>,
}
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<String>) -> 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 {
@@ -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();
@@ -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());
@@ -250,6 +250,7 @@ impl DiffState {
}
}
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
+ _ => {}
}),
diff,
}
@@ -339,6 +339,7 @@ enum OpenBuffer {
pub enum BufferStoreEvent {
BufferAdded(Entity<Buffer>),
+ BufferDiffAdded(Entity<BufferDiff>),
BufferDropped(BufferId),
BufferChangedFilePath {
buffer: Entity<Buffer>,
@@ -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 => {
@@ -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<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<GitJob>,
- _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<BufferStore>,
+ 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<buffer_diff::BufferDiff>,
+ 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<Entity<Repository>> {
self.repositories.clone()
}
+ pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
+ 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<Repository>, RepoPath)> {
+ let buffer = self.buffer_store.read(cx).get(buffer_id)?;
+ let path = buffer.read(cx).project_path(cx)?;
+ let mut result: Option<(Entity<Repository>, 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<GitJob> {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
@@ -658,9 +749,8 @@ impl GitStore {
cx: &mut AsyncApp,
) -> Result<Entity<Repository>> {
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<String>,
@@ -3184,7 +3184,7 @@ impl LspStore {
}
}
}
- BufferStoreEvent::BufferDropped(_) => {}
+ _ => {}
}
}
@@ -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<Repository>, RepoPath)> {
- let path = self
- .buffer_for_id(buffer_id, cx)?
- .read(cx)
- .project_path(cx)?;
-
- let mut found: Option<(Entity<Repository>, 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<FileStatus> {
+ self.git_store.read(cx).status_for_buffer_id(buffer_id, cx)
}
}
@@ -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::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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(),
&[(
@@ -70,6 +70,10 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
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<V> {
let mut removed = None;
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(&());
@@ -157,6 +161,14 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
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<K, V>) {
let edits = other
.iter()