diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs deleted file mode 100644 index 8420aa99806a65..00000000000000 --- a/crates/editor/src/git/project_diff.rs +++ /dev/null @@ -1,1296 +0,0 @@ -use std::{ - any::{Any, TypeId}, - cmp::Ordering, - collections::HashSet, - ops::Range, - time::Duration, -}; - -use anyhow::{anyhow, Context as _}; -use collections::{BTreeMap, HashMap}; -use feature_flags::FeatureFlagAppExt; -use git::diff::{BufferDiff, DiffHunk}; -use gpui::{ - actions, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, Render, Subscription, Task, WeakEntity, -}; -use language::{Buffer, BufferRow}; -use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}; -use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; -use text::{OffsetRangeExt, ToPoint}; -use theme::ActiveTheme; -use ui::prelude::*; -use util::{paths::compare_paths, ResultExt}; -use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, - ItemNavHistory, ToolbarItemLocation, Workspace, -}; - -use crate::{Editor, EditorEvent, DEFAULT_MULTIBUFFER_CONTEXT}; - -actions!(project_diff, [Deploy]); - -pub fn init(cx: &mut App) { - cx.observe_new(ProjectDiffEditor::register).detach(); -} - -const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); - -struct ProjectDiffEditor { - buffer_changes: BTreeMap>, - entry_order: HashMap>, - excerpts: Entity, - editor: Entity, - - project: Entity, - workspace: WeakEntity, - focus_handle: FocusHandle, - worktree_rescans: HashMap>, - _subscriptions: Vec, -} - -#[derive(Debug)] -struct Changes { - buffer: Entity, - hunks: Vec, -} - -impl ProjectDiffEditor { - fn register( - workspace: &mut Workspace, - _window: Option<&mut Window>, - _: &mut Context, - ) { - workspace.register_action(Self::deploy); - } - - fn deploy( - workspace: &mut Workspace, - _: &Deploy, - window: &mut Window, - cx: &mut Context, - ) { - if !cx.is_staff() { - return; - } - - if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let workspace_handle = cx.entity().downgrade(); - let project_diff = - cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx)); - workspace.add_item_to_active_pane(Box::new(project_diff), None, true, window, cx); - } - } - - fn new( - project: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - // TODO diff change subscriptions. For that, needed: - // * `-20/+50` stats retrieval: some background process that reacts on file changes - let focus_handle = cx.focus_handle(); - let changed_entries_subscription = - cx.subscribe_in(&project, window, |project_diff_editor, _, e, window, cx| { - let mut worktree_to_rescan = None; - match e { - project::Event::WorktreeAdded(id) => { - worktree_to_rescan = Some(*id); - // project_diff_editor - // .buffer_changes - // .insert(*id, HashMap::default()); - } - project::Event::WorktreeRemoved(id) => { - project_diff_editor.buffer_changes.remove(id); - } - project::Event::WorktreeUpdatedEntries(id, _updated_entries) => { - // TODO cannot invalidate buffer entries without invalidating the corresponding excerpts and order entries. - worktree_to_rescan = Some(*id); - // let entry_changes = - // project_diff_editor.buffer_changes.entry(*id).or_default(); - // for (_, entry_id, change) in updated_entries.iter() { - // let changes = entry_changes.entry(*entry_id); - // match change { - // project::PathChange::Removed => { - // if let hash_map::Entry::Occupied(entry) = changes { - // entry.remove(); - // } - // } - // // TODO understand the invalidation case better: now, we do that but still rescan the entire worktree - // // What if we already have the buffer loaded inside the diff multi buffer and it was edited there? We should not do anything. - // _ => match changes { - // hash_map::Entry::Occupied(mut o) => o.get_mut().invalidate(), - // hash_map::Entry::Vacant(v) => { - // v.insert(None); - // } - // }, - // } - // } - } - project::Event::WorktreeUpdatedGitRepositories(id) => { - worktree_to_rescan = Some(*id); - // project_diff_editor.buffer_changes.clear(); - } - project::Event::DeletedEntry(id, _entry_id) => { - worktree_to_rescan = Some(*id); - // if let Some(entries) = project_diff_editor.buffer_changes.get_mut(id) { - // entries.remove(entry_id); - // } - } - project::Event::Closed => { - project_diff_editor.buffer_changes.clear(); - } - _ => {} - } - - if let Some(worktree_to_rescan) = worktree_to_rescan { - project_diff_editor.schedule_worktree_rescan(worktree_to_rescan, window, cx); - } - }); - - let excerpts = cx.new(|cx| MultiBuffer::new(project.read(cx).capability())); - - let editor = cx.new(|cx| { - let mut diff_display_editor = - Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, window, cx); - diff_display_editor.set_expand_all_diff_hunks(cx); - diff_display_editor - }); - - let mut new_self = Self { - project, - workspace, - buffer_changes: BTreeMap::default(), - entry_order: HashMap::default(), - worktree_rescans: HashMap::default(), - focus_handle, - editor, - excerpts, - _subscriptions: vec![changed_entries_subscription], - }; - new_self.schedule_rescan_all(window, cx); - new_self - } - - fn schedule_rescan_all(&mut self, window: &mut Window, cx: &mut Context) { - let mut current_worktrees = HashSet::::default(); - for worktree in self.project.read(cx).worktrees(cx).collect::>() { - let worktree_id = worktree.read(cx).id(); - current_worktrees.insert(worktree_id); - self.schedule_worktree_rescan(worktree_id, window, cx); - } - - self.worktree_rescans - .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); - self.buffer_changes - .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); - self.entry_order - .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); - } - - fn schedule_worktree_rescan( - &mut self, - id: WorktreeId, - window: &mut Window, - cx: &mut Context, - ) { - let project = self.project.clone(); - self.worktree_rescans.insert( - id, - cx.spawn_in(window, |project_diff_editor, mut cx| async move { - cx.background_executor().timer(UPDATE_DEBOUNCE).await; - let open_tasks = project - .update(&mut cx, |project, cx| { - let worktree = project.worktree_for_id(id, cx)?; - let snapshot = worktree.read(cx).snapshot(); - let applicable_entries = snapshot - .repositories() - .iter() - .flat_map(|entry| { - entry - .status() - .map(|git_entry| entry.join(git_entry.repo_path)) - }) - .filter_map(|path| { - let id = snapshot.entry_for_path(&path)?.id; - Some(( - id, - ProjectPath { - worktree_id: snapshot.id(), - path: path.into(), - }, - )) - }) - .collect::>(); - Some( - applicable_entries - .into_iter() - .map(|(entry_id, entry_path)| { - let open_task = project.open_path(entry_path.clone(), cx); - (entry_id, entry_path, open_task) - }) - .collect::>(), - ) - }) - .ok() - .flatten() - .unwrap_or_default(); - - let Some((buffers, mut new_entries, change_sets)) = cx - .spawn(|mut cx| async move { - let mut new_entries = Vec::new(); - let mut buffers = HashMap::< - ProjectEntryId, - (text::BufferSnapshot, Entity, BufferDiff), - >::default(); - let mut change_sets = Vec::new(); - for (entry_id, entry_path, open_task) in open_tasks { - let Some(buffer) = open_task - .await - .and_then(|(_, opened_model)| { - opened_model - .downcast::() - .map_err(|_| anyhow!("Unexpected non-buffer")) - }) - .with_context(|| { - format!("loading {:?} for git diff", entry_path.path) - }) - .log_err() - else { - continue; - }; - - let Some(change_set) = project - .update(&mut cx, |project, cx| { - project.open_unstaged_changes(buffer.clone(), cx) - })? - .await - .log_err() - else { - continue; - }; - - cx.update(|_, cx| { - buffers.insert( - entry_id, - ( - buffer.read(cx).text_snapshot(), - buffer, - change_set.read(cx).diff_to_buffer.clone(), - ), - ); - })?; - change_sets.push(change_set); - new_entries.push((entry_path, entry_id)); - } - - anyhow::Ok((buffers, new_entries, change_sets)) - }) - .await - .log_err() - else { - return; - }; - - let (new_changes, new_entry_order) = cx - .background_executor() - .spawn(async move { - let mut new_changes = HashMap::::default(); - for (entry_id, (buffer_snapshot, buffer, buffer_diff)) in buffers { - new_changes.insert( - entry_id, - Changes { - buffer, - hunks: buffer_diff - .hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot) - .collect::>(), - }, - ); - } - - new_entries.sort_by(|(project_path_a, _), (project_path_b, _)| { - compare_paths( - (project_path_a.path.as_ref(), true), - (project_path_b.path.as_ref(), true), - ) - }); - (new_changes, new_entries) - }) - .await; - - project_diff_editor - .update_in(&mut cx, |project_diff_editor, _window, cx| { - project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); - project_diff_editor.editor.update(cx, |editor, cx| { - editor.buffer.update(cx, |buffer, cx| { - for change_set in change_sets { - buffer.add_change_set(change_set, cx) - } - }); - }); - }) - .ok(); - }), - ); - } - - fn update_excerpts( - &mut self, - worktree_id: WorktreeId, - new_changes: HashMap, - new_entry_order: Vec<(ProjectPath, ProjectEntryId)>, - - cx: &mut Context, - ) { - if let Some(current_order) = self.entry_order.get(&worktree_id) { - let current_entries = self.buffer_changes.entry(worktree_id).or_default(); - let mut new_order_entries = new_entry_order.iter().fuse().peekable(); - let mut excerpts_to_remove = Vec::new(); - let mut new_excerpt_hunks = BTreeMap::< - ExcerptId, - Vec<(ProjectPath, Entity, Vec>)>, - >::new(); - let mut excerpt_to_expand = - HashMap::<(u32, ExpandExcerptDirection), Vec>::default(); - let mut latest_excerpt_id = ExcerptId::min(); - - for (current_path, current_entry_id) in current_order { - let current_changes = match current_entries.get(current_entry_id) { - Some(current_changes) => { - if current_changes.hunks.is_empty() { - continue; - } - current_changes - } - None => continue, - }; - let buffer_excerpts = self - .excerpts - .read(cx) - .excerpts_for_buffer(¤t_changes.buffer, cx); - let last_current_excerpt_id = - buffer_excerpts.last().map(|(excerpt_id, _)| *excerpt_id); - let mut current_excerpts = buffer_excerpts.into_iter().fuse().peekable(); - loop { - match new_order_entries.peek() { - Some((new_path, new_entry)) => { - match compare_paths( - (current_path.path.as_ref(), true), - (new_path.path.as_ref(), true), - ) { - Ordering::Less => { - excerpts_to_remove - .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id)); - break; - } - Ordering::Greater => { - if let Some(new_changes) = new_changes.get(new_entry) { - if !new_changes.hunks.is_empty() { - let hunks = new_excerpt_hunks - .entry(latest_excerpt_id) - .or_default(); - match hunks.binary_search_by(|(probe, ..)| { - compare_paths( - (new_path.path.as_ref(), true), - (probe.path.as_ref(), true), - ) - }) { - Ok(i) => hunks[i].2.extend( - new_changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()), - ), - Err(i) => hunks.insert( - i, - ( - new_path.clone(), - new_changes.buffer.clone(), - new_changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()) - .collect(), - ), - ), - } - } - }; - let _ = new_order_entries.next(); - } - Ordering::Equal => { - match new_changes.get(new_entry) { - Some(new_changes) => { - let buffer_snapshot = - new_changes.buffer.read(cx).snapshot(); - let mut current_hunks = - current_changes.hunks.iter().fuse().peekable(); - let mut new_hunks_unchanged = - Vec::with_capacity(new_changes.hunks.len()); - let mut new_hunks_with_updates = - Vec::with_capacity(new_changes.hunks.len()); - 'new_changes: for new_hunk in &new_changes.hunks { - loop { - match current_hunks.peek() { - Some(current_hunk) => { - match ( - current_hunk - .buffer_range - .start - .cmp( - &new_hunk - .buffer_range - .start, - &buffer_snapshot, - ), - current_hunk.buffer_range.end.cmp( - &new_hunk.buffer_range.end, - &buffer_snapshot, - ), - ) { - ( - Ordering::Equal, - Ordering::Equal, - ) => { - new_hunks_unchanged - .push(new_hunk); - let _ = current_hunks.next(); - continue 'new_changes; - } - (Ordering::Equal, _) - | (_, Ordering::Equal) => { - new_hunks_with_updates - .push(new_hunk); - continue 'new_changes; - } - ( - Ordering::Less, - Ordering::Greater, - ) - | ( - Ordering::Greater, - Ordering::Less, - ) => { - new_hunks_with_updates - .push(new_hunk); - continue 'new_changes; - } - ( - Ordering::Less, - Ordering::Less, - ) => { - if current_hunk - .buffer_range - .start - .cmp( - &new_hunk - .buffer_range - .end, - &buffer_snapshot, - ) - .is_le() - { - new_hunks_with_updates - .push(new_hunk); - continue 'new_changes; - } else { - let _ = - current_hunks.next(); - } - } - ( - Ordering::Greater, - Ordering::Greater, - ) => { - if current_hunk - .buffer_range - .end - .cmp( - &new_hunk - .buffer_range - .start, - &buffer_snapshot, - ) - .is_ge() - { - new_hunks_with_updates - .push(new_hunk); - continue 'new_changes; - } else { - let _ = - current_hunks.next(); - } - } - } - } - None => { - new_hunks_with_updates.push(new_hunk); - continue 'new_changes; - } - } - } - } - - let mut excerpts_with_new_changes = - HashSet::::default(); - 'new_hunks: for new_hunk in new_hunks_with_updates { - loop { - match current_excerpts.peek() { - Some(( - current_excerpt_id, - current_excerpt_range, - )) => { - match ( - current_excerpt_range - .context - .start - .cmp( - &new_hunk - .buffer_range - .start, - &buffer_snapshot, - ), - current_excerpt_range - .context - .end - .cmp( - &new_hunk.buffer_range.end, - &buffer_snapshot, - ), - ) { - ( - Ordering::Less - | Ordering::Equal, - Ordering::Greater - | Ordering::Equal, - ) => { - excerpts_with_new_changes - .insert( - *current_excerpt_id, - ); - continue 'new_hunks; - } - ( - Ordering::Greater - | Ordering::Equal, - Ordering::Less - | Ordering::Equal, - ) => { - let expand_up = current_excerpt_range - .context - .start - .to_point(&buffer_snapshot) - .row - .saturating_sub( - new_hunk - .buffer_range - .start - .to_point(&buffer_snapshot) - .row, - ); - let expand_down = new_hunk - .buffer_range - .end - .to_point(&buffer_snapshot) - .row - .saturating_sub( - current_excerpt_range - .context - .end - .to_point( - &buffer_snapshot, - ) - .row, - ); - excerpt_to_expand.entry((expand_up.max(expand_down).max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::UpAndDown)).or_default().push(*current_excerpt_id); - excerpts_with_new_changes - .insert( - *current_excerpt_id, - ); - continue 'new_hunks; - } - ( - Ordering::Less, - Ordering::Less, - ) => { - if current_excerpt_range - .context - .start - .cmp( - &new_hunk - .buffer_range - .end, - &buffer_snapshot, - ) - .is_le() - { - let expand_up = current_excerpt_range - .context - .start - .to_point(&buffer_snapshot) - .row - .saturating_sub( - new_hunk.buffer_range - .start - .to_point( - &buffer_snapshot, - ) - .row, - ); - excerpt_to_expand.entry((expand_up.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Up)).or_default().push(*current_excerpt_id); - excerpts_with_new_changes - .insert( - *current_excerpt_id, - ); - continue 'new_hunks; - } else { - if !new_changes - .hunks - .is_empty() - { - let hunks = new_excerpt_hunks - .entry(latest_excerpt_id) - .or_default(); - match hunks.binary_search_by(|(probe, ..)| { - compare_paths( - (new_path.path.as_ref(), true), - (probe.path.as_ref(), true), - ) - }) { - Ok(i) => hunks[i].2.extend( - new_changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()), - ), - Err(i) => hunks.insert( - i, - ( - new_path.clone(), - new_changes.buffer.clone(), - new_changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()) - .collect(), - ), - ), - } - } - continue 'new_hunks; - } - } - /* TODO remove or leave? - [ ><<<<<<<--]----<-- - cur_s > cur_e < - > < - new_s>>>>>>>>< - */ - ( - Ordering::Greater, - Ordering::Greater, - ) => { - if current_excerpt_range - .context - .end - .cmp( - &new_hunk - .buffer_range - .start, - &buffer_snapshot, - ) - .is_ge() - { - let expand_down = new_hunk - .buffer_range - .end - .to_point(&buffer_snapshot) - .row - .saturating_sub( - current_excerpt_range - .context - .end - .to_point( - &buffer_snapshot, - ) - .row, - ); - excerpt_to_expand.entry((expand_down.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Down)).or_default().push(*current_excerpt_id); - excerpts_with_new_changes - .insert( - *current_excerpt_id, - ); - continue 'new_hunks; - } else { - latest_excerpt_id = - *current_excerpt_id; - let _ = - current_excerpts.next(); - } - } - } - } - None => { - let hunks = new_excerpt_hunks - .entry(latest_excerpt_id) - .or_default(); - match hunks.binary_search_by( - |(probe, ..)| { - compare_paths( - ( - new_path.path.as_ref(), - true, - ), - (probe.path.as_ref(), true), - ) - }, - ) { - Ok(i) => hunks[i].2.extend( - new_changes.hunks.iter().map( - |hunk| { - hunk.buffer_range - .clone() - }, - ), - ), - Err(i) => hunks.insert( - i, - ( - new_path.clone(), - new_changes.buffer.clone(), - new_changes - .hunks - .iter() - .map(|hunk| { - hunk.buffer_range - .clone() - }) - .collect(), - ), - ), - } - continue 'new_hunks; - } - } - } - } - - for (excerpt_id, excerpt_range) in current_excerpts { - if !excerpts_with_new_changes.contains(&excerpt_id) - && !new_hunks_unchanged.iter().any(|hunk| { - excerpt_range - .context - .start - .cmp( - &hunk.buffer_range.end, - &buffer_snapshot, - ) - .is_le() - && excerpt_range - .context - .end - .cmp( - &hunk.buffer_range.start, - &buffer_snapshot, - ) - .is_ge() - }) - { - excerpts_to_remove.push(excerpt_id); - } - latest_excerpt_id = excerpt_id; - } - } - None => excerpts_to_remove.extend( - current_excerpts.map(|(excerpt_id, _)| excerpt_id), - ), - } - let _ = new_order_entries.next(); - break; - } - } - } - None => { - excerpts_to_remove - .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id)); - break; - } - } - } - latest_excerpt_id = last_current_excerpt_id.unwrap_or(latest_excerpt_id); - } - - for (path, project_entry_id) in new_order_entries { - if let Some(changes) = new_changes.get(project_entry_id) { - if !changes.hunks.is_empty() { - let hunks = new_excerpt_hunks.entry(latest_excerpt_id).or_default(); - match hunks.binary_search_by(|(probe, ..)| { - compare_paths((path.path.as_ref(), true), (probe.path.as_ref(), true)) - }) { - Ok(i) => hunks[i] - .2 - .extend(changes.hunks.iter().map(|hunk| hunk.buffer_range.clone())), - Err(i) => hunks.insert( - i, - ( - path.clone(), - changes.buffer.clone(), - changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()) - .collect(), - ), - ), - } - } - } - } - - self.excerpts.update(cx, |multi_buffer, cx| { - for (mut after_excerpt_id, excerpts_to_add) in new_excerpt_hunks { - for (_, buffer, hunk_ranges) in excerpts_to_add { - let buffer_snapshot = buffer.read(cx).snapshot(); - let max_point = buffer_snapshot.max_point(); - let new_excerpts = multi_buffer.insert_excerpts_after( - after_excerpt_id, - buffer, - hunk_ranges.into_iter().map(|range| { - let mut extended_point_range = range.to_point(&buffer_snapshot); - extended_point_range.start.row = extended_point_range - .start - .row - .saturating_sub(DEFAULT_MULTIBUFFER_CONTEXT); - extended_point_range.end.row = (extended_point_range.end.row - + DEFAULT_MULTIBUFFER_CONTEXT) - .min(max_point.row); - ExcerptRange { - context: extended_point_range, - primary: None, - } - }), - cx, - ); - after_excerpt_id = new_excerpts.last().copied().unwrap_or(after_excerpt_id); - } - } - multi_buffer.remove_excerpts(excerpts_to_remove, cx); - for ((line_count, direction), excerpts) in excerpt_to_expand { - multi_buffer.expand_excerpts(excerpts, line_count, direction, cx); - } - }); - } else { - self.excerpts.update(cx, |multi_buffer, cx| { - for new_changes in new_entry_order - .iter() - .filter_map(|(_, entry_id)| new_changes.get(entry_id)) - { - multi_buffer.push_excerpts_with_context_lines( - new_changes.buffer.clone(), - new_changes - .hunks - .iter() - .map(|hunk| hunk.buffer_range.clone()) - .collect(), - DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - } - }); - }; - - let mut new_changes = new_changes; - let mut new_entry_order = new_entry_order; - std::mem::swap( - self.buffer_changes.entry(worktree_id).or_default(), - &mut new_changes, - ); - std::mem::swap( - self.entry_order.entry(worktree_id).or_default(), - &mut new_entry_order, - ); - } -} - -impl EventEmitter for ProjectDiffEditor {} - -impl Focusable for ProjectDiffEditor { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ProjectDiffEditor { - type Event = EditorEvent; - - fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { - Editor::to_item_events(event, f) - } - - fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.editor - .update(cx, |editor, cx| editor.deactivated(window, cx)); - } - - fn navigate( - &mut self, - data: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.editor - .update(cx, |editor, cx| editor.navigate(data, window, cx)) - } - - fn tab_tooltip_text(&self, _: &App) -> Option { - Some("Project Diff".into()) - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { - if self.buffer_changes.is_empty() { - Label::new("No changes") - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() - } else { - h_flex() - .gap_1() - .when(true, |then| { - then.child( - h_flex() - .gap_1() - .child(Icon::new(IconName::XCircle).color(Color::Error)) - .child(Label::new(self.buffer_changes.len().to_string()).color( - if params.selected { - Color::Default - } else { - Color::Muted - }, - )), - ) - }) - .when(true, |then| { - then.child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Indicator).color(Color::Warning)) - .child(Label::new(self.buffer_changes.len().to_string()).color( - if params.selected { - Color::Default - } else { - Color::Muted - }, - )), - ) - }) - .into_any_element() - } - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Project Diagnostics Opened") - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), - ) { - self.editor.for_each_project_item(cx, f) - } - - fn is_singleton(&self, _: &App) -> bool { - false - } - - fn set_nav_history( - &mut self, - nav_history: ItemNavHistory, - _: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option> - where - Self: Sized, - { - Some(cx.new(|cx| { - ProjectDiffEditor::new(self.project.clone(), self.workspace.clone(), window, cx) - })) - } - - fn is_dirty(&self, cx: &App) -> bool { - self.excerpts.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.excerpts.read(cx).has_conflict(cx) - } - - fn can_save(&self, _: &App) -> bool { - true - } - - fn save( - &mut self, - format: bool, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.editor.save(format, project, window, cx) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - unreachable!() - } - - fn reload( - &mut self, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.editor.reload(project, window, cx) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.to_any()) - } else if type_id == TypeId::of::() { - Some(self.editor.to_any()) - } else { - None - } - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx) - }); - } -} - -impl Render for ProjectDiffEditor { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let child = if self.buffer_changes.is_empty() { - div() - .bg(cx.theme().colors().editor_background) - .flex() - .items_center() - .justify_center() - .size_full() - .child(Label::new("No changes in the workspace")) - } else { - div().size_full().child(self.editor.clone()) - }; - - div() - .track_focus(&self.focus_handle) - .size_full() - .child(child) - } -} - -#[cfg(test)] -mod tests { - use git::status::{StatusCode, TrackedStatus}; - use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - use project::buffer_store::BufferChangeSet; - use serde_json::json; - use settings::SettingsStore; - use std::{ - ops::Deref as _, - path::{Path, PathBuf}, - }; - - use crate::test::editor_test_context::assert_state_with_diff; - - use super::*; - - // TODO finish - // #[gpui::test] - // async fn randomized_tests(cx: &mut TestAppContext) { - // // Create a new project (how?? temp fs?), - // let fs = FakeFs::new(cx.executor()); - // let project = Project::test(fs, [], cx).await; - - // // create random files with random content - - // // Commit it into git somehow (technically can do with "real" fs in a temp dir) - // // - // // Apply randomized changes to the project: select a random file, random change and apply to buffers - // } - - #[gpui::test(iterations = 30)] - async fn simple_edit_test(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - init_test(cx); - - let fs = fs::FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/root", - json!({ - ".git": {}, - "file_a": "This is file_a", - "file_b": "This is file_b", - }), - ) - .await; - - let project = Project::test(fs.clone(), [Path::new("/root")], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - - let file_a_editor = workspace - .update(cx, |workspace, window, cx| { - let file_a_editor = - workspace.open_abs_path(PathBuf::from("/root/file_a"), true, window, cx); - ProjectDiffEditor::deploy(workspace, &Deploy, window, cx); - file_a_editor - }) - .unwrap() - .await - .expect("did not open an item at all") - .downcast::() - .expect("did not open an editor for file_a"); - let project_diff_editor = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - }) - .unwrap() - .expect("did not find a ProjectDiffEditor"); - project_diff_editor.update(cx, |project_diff_editor, cx| { - assert!( - project_diff_editor.editor.read(cx).text(cx).is_empty(), - "Should have no changes after opening the diff on no git changes" - ); - }); - - let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); - let change = "an edit after git add"; - file_a_editor - .update_in(cx, |file_a_editor, window, cx| { - file_a_editor.insert(change, window, cx); - file_a_editor.save(false, project.clone(), window, cx) - }) - .await - .expect("failed to save a file"); - file_a_editor.update_in(cx, |file_a_editor, _window, cx| { - let change_set = cx.new(|cx| { - BufferChangeSet::new_with_base_text( - &old_text, - &file_a_editor.buffer().read(cx).as_singleton().unwrap(), - cx, - ) - }); - file_a_editor.buffer.update(cx, |buffer, cx| { - buffer.add_change_set(change_set.clone(), cx) - }); - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.set_unstaged_change_set( - file_a_editor - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .remote_id(), - change_set, - ); - }); - }); - }); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/.git"), - &[( - Path::new("file_a"), - TrackedStatus { - worktree_status: StatusCode::Modified, - index_status: StatusCode::Unmodified, - } - .into(), - )], - ); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - let editor = project_diff_editor.update(cx, |diff_editor, _| diff_editor.editor.clone()); - - assert_state_with_diff( - &editor, - cx, - indoc::indoc! { - " - - This is file_a - + an edit after git addThis is file_aˇ", - }, - ); - } - - fn init_test(cx: &mut gpui::TestAppContext) { - if std::env::var("RUST_LOG").is_ok() { - env_logger::try_init().ok(); - } - - cx.update(|cx| { - assets::Assets.load_test_fonts(cx); - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(SemanticVersion::default(), cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - crate::init(cx); - cx.set_staff(true); - }); - } -}