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();
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<Project>,
buffers: impl IntoIterator<Item = Entity<Buffer>>,
buffer: Entity<MultiBuffer>,
@ -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 {

View file

@ -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() {

View file

@ -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())],
);

View file

@ -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(())

View file

@ -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<Pixels>,
}
#[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<bool>,
pub(crate) depth: usize,
pub(crate) display_name: String,
pub(crate) repo_path: RepoPath,
pub(crate) status: FileStatus,
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 {
@ -152,9 +155,11 @@ pub struct GitPanel {
entries: Vec<GitListEntry>,
entries_by_path: collections::HashMap<RepoPath, usize>,
width: Option<Pixels>,
pending: HashMap<RepoPath, bool>,
pending: Vec<PendingOperation>,
commit_task: Task<Result<()>>,
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::<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({
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<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
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<Self>) {
fn schedule_update(
&mut self,
clear_pending: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
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>) {
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<Self>,
) -> 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<Self>,
) -> 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))
}
}

View file

@ -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<Workspace>,
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<Arc<Path>>,
pending_scroll: Option<PathKey>,
_task: Task<Result<()>>,
_subscription: Subscription,
}
struct DiffBuffer {
abs_path: Arc<Path>,
path_key: PathKey,
buffer: Entity<Buffer>,
change_set: Entity<BufferChangeSet>,
}
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<Arc<Path>>,
entry: Option<GitStatusEntry>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@ -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<Path>, window: &mut Window, cx: &mut Context<Self>) {
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<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| {
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<Self>,
) {
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);
}
}

View file

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

View file

@ -67,7 +67,7 @@ pub struct MultiBuffer {
/// Contains the state of the buffers being edited
buffers: RefCell<HashMap<BufferId, BufferState>>,
// 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>,
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<Excerpt>;
type ExcerptPoint = TypedPoint<Excerpt>;
@ -1395,7 +1404,7 @@ impl MultiBuffer {
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 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>,
path: PathKey,
buffer: Entity<Buffer>,
ranges: Vec<Range<Point>>,
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()
}
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) {
self.remove_excerpts(to_remove, cx)
}

View file

@ -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<Path> = 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<Path> = 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| {

View file

@ -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;
}

View file

@ -69,11 +69,13 @@ enum Message {
Unstage(GitRepo, Vec<RepoPath>),
}
pub enum Event {
RepositoriesUpdated,
pub enum GitEvent {
ActiveRepositoryChanged,
FileSystemUpdated,
GitStateUpdated,
}
impl EventEmitter<Event> for GitState {}
impl EventEmitter<GitEvent> for GitState {}
impl GitState {
pub fn new(
@ -103,7 +105,7 @@ impl GitState {
fn on_worktree_store_event(
&mut self,
worktree_store: Entity<WorktreeStore>,
_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<RepositoryHandle> {
@ -314,7 +323,7 @@ impl RepositoryHandle {
return;
};
git_state.active_index = Some(index);
cx.emit(Event::RepositoriesUpdated);
cx.emit(GitEvent::ActiveRepositoryChanged);
});
}