use crate::{ conflict_view, git_panel::{GitPanel, GitStatusEntry}, git_panel_settings::GitPanelSettings,};use anyhow::Result;use buffer_diff::BufferDiff;use collections::{HashMap, HashSet};use editor::{ EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::GoToHunk, multibuffer_context_lines, scroll::Autoscroll,};use futures_lite::future::yield_now;use git::{repository::RepoPath, status::FileStatus};use gpui::{ App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Subscription, Task, WeakEntity,};use language::{Anchor, Buffer, BufferId, Capability, OffsetRangeExt};use multi_buffer::{MultiBuffer, PathKey};use project::{ ConflictSet, Project, ProjectPath, git_store::{ Repository, diff_buffer_list::{self, BranchDiffEvent, DiffBase}, },};use settings::{GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore};use std::{collections::BTreeMap, sync::Arc};use theme::ActiveTheme;use ui::{CommonAnimationExt as _, KeyBinding, prelude::*};use util::{ResultExt as _, rel_path::RelPath};use workspace::{ CloseActiveItem, ItemNavHistory, Workspace, item::{Item, SaveOptions},};use ztracing::instrument; struct BufferSubscriptions { _diff: Entity<BufferDiff>, display_buffer: Entity<Buffer>, _diff_subscription: Subscription, _conflict_set: Option<Entity<ConflictSet>>, _conflict_set_subscription: Option<Subscription>,} pub struct DiffMultibuffer { multibuffer: Entity<MultiBuffer>, branch_diff: Entity<diff_buffer_list::DiffBufferList>, editor: Entity<SplittableEditor>, buffer_subscriptions: HashMap<RepoPath, BufferSubscriptions>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, pending_scroll: Option<PathKey>, review_comment_count: usize, empty_label: SharedString, _task: Task<Result<()>>, _subscription: Subscription,} impl DiffMultibuffer { pub(crate) fn new( branch_diff: Entity<diff_buffer_list::DiffBufferList>, multibuffer_capability: Capability, empty_label: impl Into<SharedString>, configure_editor: impl FnOnce(&mut SplittableEditor, &mut Context<SplittableEditor>) + 'static, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(multibuffer_capability); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let editor = cx.new(|cx| { let mut diff_display_editor = SplittableEditor::new( EditorSettings::get_global(cx).diff_view_style, multibuffer.clone(), project.clone(), workspace.clone(), window, cx, ); configure_editor(&mut diff_display_editor, cx); diff_display_editor.rhs_editor().update(cx, |editor, cx| { editor.set_show_diff_review_button(true, cx); }); diff_display_editor }); let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event); let primary_editor = editor.read(cx).rhs_editor().clone(); let review_comment_subscription = cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| { if let EditorEvent::ReviewCommentsChanged { total_count } = event { this.review_comment_count = *total_count; cx.notify(); } }); let branch_diff_subscription = cx.subscribe_in( &branch_diff, window, move |this, _git_store, event, window, cx| match event { BranchDiffEvent::FileListChanged => { this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } BranchDiffEvent::DiffBaseChanged => { this.pending_scroll.take(); this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } }, ); let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by; let mut was_group_by = GitPanelSettings::get_global(cx).group_by; let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view; let mut was_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| { let settings = GitPanelSettings::get_global(cx); let sort_by = settings.sort_by; let group_by = settings.group_by; let tree_view = settings.tree_view; let is_collapse_untracked_diff = settings.collapse_untracked_diff; if sort_by != was_sort_by || group_by != was_group_by || tree_view != was_tree_view || is_collapse_untracked_diff != was_collapse_untracked_diff { this._task = { window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } } was_sort_by = sort_by; was_group_by = group_by; was_tree_view = tree_view; was_collapse_untracked_diff = is_collapse_untracked_diff; }) .detach(); let task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }); Self { workspace: workspace.downgrade(), branch_diff, focus_handle, editor, multibuffer, buffer_subscriptions: Default::default(), pending_scroll: None, review_comment_count: 0, empty_label: empty_label.into(), _task: task, _subscription: Subscription::join( branch_diff_subscription, Subscription::join(editor_subscription, review_comment_subscription), ), } } pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase { self.branch_diff.read(cx).diff_base() } pub(crate) fn branch_diff(&self) -> &Entity<diff_buffer_list::DiffBufferList> { &self.branch_diff } pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> { self.branch_diff.read(cx).repo().cloned() } pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) { self.branch_diff.update(cx, |branch_diff, cx| { branch_diff.set_repo(repo, cx); }); } pub(crate) fn is_dirty(&self, cx: &App) -> bool { self.multibuffer.read(cx).is_dirty(cx) } pub(crate) fn has_conflict(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_conflict(cx) } pub(crate) fn multibuffer(&self) -> &Entity<MultiBuffer> { &self.multibuffer } pub(crate) fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let repo = git_repo.read(cx); let path_key = project_diff_path_key(repo, &entry.repo_path, entry.status, cx); self.move_to_path(path_key, window, cx) } pub(crate) fn move_to_project_path( &mut self, project_path: &ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let Some(repo_path) = git_repo .read(cx) .project_path_to_repo_path(project_path, cx) else { return; }; let status = git_repo .read(cx) .status_for_path(&repo_path) .map(|entry| entry.status) .unwrap_or(FileStatus::Untracked); let path_key = project_diff_path_key(&git_repo.read(cx), &repo_path, status, cx); self.move_to_path(path_key, window, cx) } pub(crate) fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![multi_buffer::Anchor::Min..multi_buffer::Anchor::Min]); }); }); }); } pub(crate) fn move_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.rhs_editor().update(cx, |editor, cx| { editor.change_selections( SelectionEffects::scroll(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }, ) }) }); } else { self.pending_scroll = Some(path_key); } } pub(crate) fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), cx); }) }) } pub(crate) fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) { self.multibuffer.read(cx).snapshot(cx).total_changed_lines() } /// Returns the total count of review comments across all hunks/files. pub(crate) fn total_review_comment_count(&self) -> usize { self.review_comment_count } /// Returns a reference to the splittable editor. pub(crate) fn editor(&self) -> &Entity<SplittableEditor> { &self.editor } pub(crate) fn selected_ranges( &self, cx: &App, ) -> (bool, Vec<std::ops::Range<multi_buffer::Anchor>>) { let editor = self.editor.read(cx).rhs_editor().read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); let mut selection = true; let mut ranges = editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>(); if !ranges.iter().any(|range| range.start != range.end) { selection = false; let anchor = editor.selections.newest_anchor().head(); if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor) && let Some(range) = snapshot .anchor_in_buffer(excerpt_range.context.start) .zip(snapshot.anchor_in_buffer(excerpt_range.context.end)) .map(|(start, end)| start..end) { ranges = vec![range]; } else { ranges = Vec::default(); }; } (selection, ranges) } /// Ranges for a toolbar stage/unstage action: the selection, or the cursor /// (a zero-width range that resolves to the single hunk under it) when /// there is no selection. Unlike [`Self::selected_ranges`], this never /// widens to the whole excerpt, so actions affect one hunk at a time. fn hunk_action_ranges(&self, cx: &App) -> Vec<std::ops::Range<multi_buffer::Anchor>> { self.editor .read(cx) .rhs_editor() .read(cx) .selections .disjoint_anchor_ranges() .collect() } pub(crate) fn stage_or_unstage_selected_hunks( &mut self, stage: bool, move_to_next: bool, window: &mut Window, cx: &mut Context<Self>, ) { let editor = self.editor.read(cx).rhs_editor().clone(); let ranges = self.hunk_action_ranges(cx); // Route through the editor's delegated stage path, the same path taken // by the hunk buttons (on either side of a split) and the keyboard. // For staging, dirty buffers are saved first, exactly as they are when // staging from the uncommitted diff or a normal editor. editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks(stage, ranges, window, cx); }); if move_to_next { editor .focus_handle(cx) .dispatch_action(&GoToHunk, window, cx); } } pub(crate) fn restore_selected_hunks( &mut self, move_to_next: bool, window: &mut Window, cx: &mut Context<Self>, ) { let editor = self.editor.read(cx).rhs_editor().clone(); let ranges = self.hunk_action_ranges(cx); editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let hunks: Vec<_> = editor.diff_hunks_in_ranges(&ranges, &snapshot).collect(); if !hunks.is_empty() { editor.apply_restore(hunks, window, cx); } }); if move_to_next { editor .focus_handle(cx) .dispatch_action(&GoToHunk, window, cx); } } fn handle_editor_event( &mut self, editor: &Entity<SplittableEditor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::SelectionsChanged { local: true } => { // Only follow the git panel selection from the view the user is // actually interacting with. Background (non-active) diff views // refresh on their own and must not hijack the panel selection. if !editor.focus_handle(cx).contains_focused(window, cx) { return; } let Some(project_path) = self.active_project_path(cx) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.select_entry_by_path(project_path, window, cx) }) } }) .ok(); } EditorEvent::Saved => { self._task = cx.spawn_in(window, async move |this, cx| Self::refresh(this, cx).await); } _ => {} } if editor.focus_handle(cx).contains_focused(window, cx) && self.multibuffer.read(cx).is_empty() { self.focus_handle.focus(window, cx) } } #[instrument(skip_all)] fn register_buffer( &mut self, repo_path: RepoPath, path_key: PathKey, file_status: FileStatus, display_buffer: Entity<Buffer>, main_buffer: Entity<Buffer>, diff: Entity<BufferDiff>, conflict_set: Option<Entity<ConflictSet>>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<BufferId> { let diff_subscription = cx.subscribe_in(&diff, window, { let repo_path = repo_path.clone(); let path_key = path_key.clone(); let display_buffer = display_buffer.clone(); let main_buffer = main_buffer.clone(); let diff = diff.clone(); let conflict_set = conflict_set.clone(); move |this, _, event, window, cx| match event { buffer_diff::BufferDiffEvent::DiffChanged(_) => { this.buffer_ranges_changed( repo_path.clone(), path_key.clone(), file_status, display_buffer.clone(), main_buffer.clone(), diff.clone(), conflict_set.clone(), window, cx, ); } buffer_diff::BufferDiffEvent::BaseTextChanged => {} } }); let conflict_set_subscription = conflict_set.as_ref().map(|conflict_set| { cx.subscribe_in(conflict_set, window, { let repo_path = repo_path.clone(); let path_key = path_key.clone(); let display_buffer = display_buffer.clone(); let main_buffer = main_buffer.clone(); let diff = diff.clone(); let conflict_set = Some(conflict_set.clone()); move |this, _, _, window, cx| { this.buffer_ranges_changed( repo_path.clone(), path_key.clone(), file_status, display_buffer.clone(), main_buffer.clone(), diff.clone(), conflict_set.clone(), window, cx, ) } }) }); self.buffer_subscriptions.insert( repo_path, BufferSubscriptions { _diff: diff.clone(), display_buffer: display_buffer.clone(), _diff_subscription: diff_subscription, _conflict_set: conflict_set.clone(), _conflict_set_subscription: conflict_set_subscription, },Lines 1–500 of 1038. Load more
use crate::{ conflict_view, git_panel::{GitPanel, GitStatusEntry}, git_panel_settings::GitPanelSettings, }; use anyhow::Result; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::{ EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::GoToHunk, multibuffer_context_lines, scroll::Autoscroll, }; use futures_lite::future::yield_now; use git::{repository::RepoPath, status::FileStatus}; use gpui::{ App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, BufferId, Capability, OffsetRangeExt}; use multi_buffer::{MultiBuffer, PathKey}; use project::{ ConflictSet, Project, ProjectPath, git_store::{ Repository, diff_buffer_list::{self, BranchDiffEvent, DiffBase}, }, }; use settings::{GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore}; use std::{collections::BTreeMap, sync::Arc}; use theme::ActiveTheme; use ui::{CommonAnimationExt as _, KeyBinding, prelude::*}; use util::{ResultExt as _, rel_path::RelPath}; use workspace::{ CloseActiveItem, ItemNavHistory, Workspace, item::{Item, SaveOptions}, }; use ztracing::instrument; struct BufferSubscriptions { _diff: Entity<BufferDiff>, display_buffer: Entity<Buffer>, _diff_subscription: Subscription, _conflict_set: Option<Entity<ConflictSet>>, _conflict_set_subscription: Option<Subscription>, } pub struct DiffMultibuffer { multibuffer: Entity<MultiBuffer>, branch_diff: Entity<diff_buffer_list::DiffBufferList>, editor: Entity<SplittableEditor>, buffer_subscriptions: HashMap<RepoPath, BufferSubscriptions>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, pending_scroll: Option<PathKey>, review_comment_count: usize, empty_label: SharedString, _task: Task<Result<()>>, _subscription: Subscription, } impl DiffMultibuffer { pub(crate) fn new( branch_diff: Entity<diff_buffer_list::DiffBufferList>, multibuffer_capability: Capability, empty_label: impl Into<SharedString>, configure_editor: impl FnOnce(&mut SplittableEditor, &mut Context<SplittableEditor>) + 'static, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(multibuffer_capability); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let editor = cx.new(|cx| { let mut diff_display_editor = SplittableEditor::new( EditorSettings::get_global(cx).diff_view_style, multibuffer.clone(), project.clone(), workspace.clone(), window, cx, ); configure_editor(&mut diff_display_editor, cx); diff_display_editor.rhs_editor().update(cx, |editor, cx| { editor.set_show_diff_review_button(true, cx); }); diff_display_editor }); let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event); let primary_editor = editor.read(cx).rhs_editor().clone(); let review_comment_subscription = cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| { if let EditorEvent::ReviewCommentsChanged { total_count } = event { this.review_comment_count = *total_count; cx.notify(); } }); let branch_diff_subscription = cx.subscribe_in( &branch_diff, window, move |this, _git_store, event, window, cx| match event { BranchDiffEvent::FileListChanged => { this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } BranchDiffEvent::DiffBaseChanged => { this.pending_scroll.take(); this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } }, ); let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by; let mut was_group_by = GitPanelSettings::get_global(cx).group_by; let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view; let mut was_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| { let settings = GitPanelSettings::get_global(cx); let sort_by = settings.sort_by; let group_by = settings.group_by; let tree_view = settings.tree_view; let is_collapse_untracked_diff = settings.collapse_untracked_diff; if sort_by != was_sort_by || group_by != was_group_by || tree_view != was_tree_view || is_collapse_untracked_diff != was_collapse_untracked_diff { this._task = { window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }) } } was_sort_by = sort_by; was_group_by = group_by; was_tree_view = tree_view; was_collapse_untracked_diff = is_collapse_untracked_diff; }) .detach(); let task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, cx).await }); Self { workspace: workspace.downgrade(), branch_diff, focus_handle, editor, multibuffer, buffer_subscriptions: Default::default(), pending_scroll: None, review_comment_count: 0, empty_label: empty_label.into(), _task: task, _subscription: Subscription::join( branch_diff_subscription, Subscription::join(editor_subscription, review_comment_subscription), ), } } pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase { self.branch_diff.read(cx).diff_base() } pub(crate) fn branch_diff(&self) -> &Entity<diff_buffer_list::DiffBufferList> { &self.branch_diff } pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> { self.branch_diff.read(cx).repo().cloned() } pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) { self.branch_diff.update(cx, |branch_diff, cx| { branch_diff.set_repo(repo, cx); }); } pub(crate) fn is_dirty(&self, cx: &App) -> bool { self.multibuffer.read(cx).is_dirty(cx) } pub(crate) fn has_conflict(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_conflict(cx) } pub(crate) fn multibuffer(&self) -> &Entity<MultiBuffer> { &self.multibuffer } pub(crate) fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let repo = git_repo.read(cx); let path_key = project_diff_path_key(repo, &entry.repo_path, entry.status, cx); self.move_to_path(path_key, window, cx) } pub(crate) fn move_to_project_path( &mut self, project_path: &ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let Some(repo_path) = git_repo .read(cx) .project_path_to_repo_path(project_path, cx) else { return; }; let status = git_repo .read(cx) .status_for_path(&repo_path) .map(|entry| entry.status) .unwrap_or(FileStatus::Untracked); let path_key = project_diff_path_key(&git_repo.read(cx), &repo_path, status, cx); self.move_to_path(path_key, window, cx) } pub(crate) fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![multi_buffer::Anchor::Min..multi_buffer::Anchor::Min]); }); }); }); } pub(crate) fn move_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.rhs_editor().update(cx, |editor, cx| { editor.change_selections( SelectionEffects::scroll(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }, ) }) }); } else { self.pending_scroll = Some(path_key); } } pub(crate) fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), cx); }) }) } pub(crate) fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) { self.multibuffer.read(cx).snapshot(cx).total_changed_lines() } /// Returns the total count of review comments across all hunks/files. pub(crate) fn total_review_comment_count(&self) -> usize { self.review_comment_count } /// Returns a reference to the splittable editor. pub(crate) fn editor(&self) -> &Entity<SplittableEditor> { &self.editor } pub(crate) fn selected_ranges( &self, cx: &App, ) -> (bool, Vec<std::ops::Range<multi_buffer::Anchor>>) { let editor = self.editor.read(cx).rhs_editor().read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); let mut selection = true; let mut ranges = editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>(); if !ranges.iter().any(|range| range.start != range.end) { selection = false; let anchor = editor.selections.newest_anchor().head(); if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor) && let Some(range) = snapshot .anchor_in_buffer(excerpt_range.context.start) .zip(snapshot.anchor_in_buffer(excerpt_range.context.end)) .map(|(start, end)| start..end) { ranges = vec![range]; } else { ranges = Vec::default(); }; } (selection, ranges) } /// Ranges for a toolbar stage/unstage action: the selection, or the cursor /// (a zero-width range that resolves to the single hunk under it) when /// there is no selection. Unlike [`Self::selected_ranges`], this never /// widens to the whole excerpt, so actions affect one hunk at a time. fn hunk_action_ranges(&self, cx: &App) -> Vec<std::ops::Range<multi_buffer::Anchor>> { self.editor .read(cx) .rhs_editor() .read(cx) .selections .disjoint_anchor_ranges() .collect() } pub(crate) fn stage_or_unstage_selected_hunks( &mut self, stage: bool, move_to_next: bool, window: &mut Window, cx: &mut Context<Self>, ) { let editor = self.editor.read(cx).rhs_editor().clone(); let ranges = self.hunk_action_ranges(cx); // Route through the editor's delegated stage path, the same path taken // by the hunk buttons (on either side of a split) and the keyboard. // For staging, dirty buffers are saved first, exactly as they are when // staging from the uncommitted diff or a normal editor. editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks(stage, ranges, window, cx); }); if move_to_next { editor .focus_handle(cx) .dispatch_action(&GoToHunk, window, cx); } } pub(crate) fn restore_selected_hunks( &mut self, move_to_next: bool, window: &mut Window, cx: &mut Context<Self>, ) { let editor = self.editor.read(cx).rhs_editor().clone(); let ranges = self.hunk_action_ranges(cx); editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let hunks: Vec<_> = editor.diff_hunks_in_ranges(&ranges, &snapshot).collect(); if !hunks.is_empty() { editor.apply_restore(hunks, window, cx); } }); if move_to_next { editor .focus_handle(cx) .dispatch_action(&GoToHunk, window, cx); } } fn handle_editor_event( &mut self, editor: &Entity<SplittableEditor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::SelectionsChanged { local: true } => { // Only follow the git panel selection from the view the user is // actually interacting with. Background (non-active) diff views // refresh on their own and must not hijack the panel selection. if !editor.focus_handle(cx).contains_focused(window, cx) { return; } let Some(project_path) = self.active_project_path(cx) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.select_entry_by_path(project_path, window, cx) }) } }) .ok(); } EditorEvent::Saved => { self._task = cx.spawn_in(window, async move |this, cx| Self::refresh(this, cx).await); } _ => {} } if editor.focus_handle(cx).contains_focused(window, cx) && self.multibuffer.read(cx).is_empty() { self.focus_handle.focus(window, cx) } } #[instrument(skip_all)] fn register_buffer( &mut self, repo_path: RepoPath, path_key: PathKey, file_status: FileStatus, display_buffer: Entity<Buffer>, main_buffer: Entity<Buffer>, diff: Entity<BufferDiff>, conflict_set: Option<Entity<ConflictSet>>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<BufferId> { let diff_subscription = cx.subscribe_in(&diff, window, { let repo_path = repo_path.clone(); let path_key = path_key.clone(); let display_buffer = display_buffer.clone(); let main_buffer = main_buffer.clone(); let diff = diff.clone(); let conflict_set = conflict_set.clone(); move |this, _, event, window, cx| match event { buffer_diff::BufferDiffEvent::DiffChanged(_) => { this.buffer_ranges_changed( repo_path.clone(), path_key.clone(), file_status, display_buffer.clone(), main_buffer.clone(), diff.clone(), conflict_set.clone(), window, cx, ); } buffer_diff::BufferDiffEvent::BaseTextChanged => {} } }); let conflict_set_subscription = conflict_set.as_ref().map(|conflict_set| { cx.subscribe_in(conflict_set, window, { let repo_path = repo_path.clone(); let path_key = path_key.clone(); let display_buffer = display_buffer.clone(); let main_buffer = main_buffer.clone(); let diff = diff.clone(); let conflict_set = Some(conflict_set.clone()); move |this, _, _, window, cx| { this.buffer_ranges_changed( repo_path.clone(), path_key.clone(), file_status, display_buffer.clone(), main_buffer.clone(), diff.clone(), conflict_set.clone(), window, cx, ) } }) }); self.buffer_subscriptions.insert( repo_path, BufferSubscriptions { _diff: diff.clone(), display_buffer: display_buffer.clone(), _diff_subscription: diff_subscription, _conflict_set: conflict_set.clone(), _conflict_set_subscription: conflict_set_subscription, },