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:
parent
22b7042b9e
commit
0963401a8d
11 changed files with 241 additions and 231 deletions
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
⋯
|
||||
|
|
|
@ -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())],
|
||||
);
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue