Cargo.lock 🔗
@@ -5170,8 +5170,12 @@ dependencies = [
"anyhow",
"collections",
"db",
+ "editor",
+ "futures 0.3.31",
"git",
"gpui",
+ "language",
+ "menu",
"project",
"schemars",
"serde",
Kirill Bulatov created
Makes a first pass over git panel UI, making it more interactive.

* every item can be selected, the selection is shown in the panel
* every item can be clicked, which changes the selection and
creates/focuses the editor with a project changes multi buffer
* the editor is scrolled so that the clicked item is in the center
* it's possible to nagivate up and down the panel, selecting
next/previous items in it, triggering the editor scroll
Known issues:
* entries are updated oddly sometimes (should become better after
DiffMap improvements land?)
* only unstaged diffs are shown currently (entry status storage should
help with this)
* no deleted files are displayed (the underlying work is done by others
now)
* added files have no diff hunks shown (DiffMap will have it?)
* performance story has not improved (again, DiffMap and status storage
should help with this)
Release Notes:
- N/A
Cargo.lock | 4
crates/editor/src/editor.rs | 9
crates/git_ui/Cargo.toml | 4
crates/git_ui/src/git_panel.rs | 575 ++++++++++++++++++++++++++++++++---
4 files changed, 534 insertions(+), 58 deletions(-)
@@ -5170,8 +5170,12 @@ dependencies = [
"anyhow",
"collections",
"db",
+ "editor",
+ "futures 0.3.31",
"git",
"gpui",
+ "language",
+ "menu",
"project",
"schemars",
"serde",
@@ -128,6 +128,7 @@ use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
};
use project::{
+ buffer_store::BufferChangeSet,
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
@@ -12950,6 +12951,14 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
+ pub fn add_change_set(
+ &mut self,
+ change_set: Model<BufferChangeSet>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.diff_map.add_change_set(change_set, cx);
+ }
+
fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
let text_layout_details = self.text_layout_details(cx);
let style = &text_layout_details.editor_style;
@@ -15,7 +15,11 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
db.workspace = true
+editor.workspace = true
+futures.workspace = true
gpui.workspace = true
+language.workspace = true
+menu.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -1,13 +1,19 @@
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
-use git::repository::GitFileStatus;
+use editor::{
+ scroll::{Autoscroll, AutoscrollStrategy},
+ Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
+};
+use git::{diff::DiffHunk, repository::GitFileStatus};
use gpui::{
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
- MouseButton, Stateful, Task, UniformListScrollHandle, View, WeakView,
+ MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
};
+use language::{Buffer, BufferRow, OffsetRangeExt};
+use menu::{SelectNext, SelectPrev};
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
@@ -15,17 +21,22 @@ use std::{
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
- ops::Range,
+ ops::{Deref, Range},
path::{Path, PathBuf},
+ rc::Rc,
sync::Arc,
time::Duration,
+ usize,
};
use ui::{
- prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
+ prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
+ ScrollbarState, Tooltip,
};
use util::{ResultExt, TryFutureExt};
-use workspace::dock::{DockPosition, Panel, PanelEvent};
-use workspace::Workspace;
+use workspace::{
+ dock::{DockPosition, Panel, PanelEvent},
+ ItemHandle, Workspace,
+};
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
@@ -34,6 +45,8 @@ actions!(git_panel, [ToggleFocus]);
const GIT_PANEL_KEY: &str = "GitPanel";
+const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
+
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -61,6 +74,8 @@ struct EntryDetails {
depth: usize,
is_expanded: bool,
status: Option<GitFileStatus>,
+ hunks: Rc<OnceCell<Vec<DiffHunk>>>,
+ index: usize,
}
impl EntryDetails {
@@ -75,7 +90,7 @@ struct SerializedGitPanel {
}
pub struct GitPanel {
- _workspace: WeakView<Workspace>,
+ workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
@@ -90,8 +105,43 @@ pub struct GitPanel {
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
- visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
+ visible_entries: Vec<WorktreeEntries>,
width: Option<Pixels>,
+ git_diff_editor: View<Editor>,
+ git_diff_editor_updates: Task<()>,
+ reveal_in_editor: Task<()>,
+}
+
+#[derive(Debug, Clone)]
+struct WorktreeEntries {
+ worktree_id: WorktreeId,
+ visible_entries: Vec<GitPanelEntry>,
+ paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
+}
+
+#[derive(Debug, Clone)]
+struct GitPanelEntry {
+ entry: Entry,
+ hunks: Rc<OnceCell<Vec<DiffHunk>>>,
+}
+
+impl Deref for GitPanelEntry {
+ type Target = Entry;
+
+ fn deref(&self) -> &Self::Target {
+ &self.entry
+ }
+}
+
+impl WorktreeEntries {
+ fn paths(&self) -> &HashSet<Arc<Path>> {
+ self.paths.get_or_init(|| {
+ self.visible_entries
+ .iter()
+ .map(|e| (e.entry.path.clone()))
+ .collect()
+ })
+ }
}
impl GitPanel {
@@ -118,18 +168,28 @@ impl GitPanel {
this.hide_scrollbar(cx);
})
.detach();
- cx.subscribe(&project, |this, _project, event, cx| match event {
+ cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
- this.update_visible_entries(None, cx);
+ this.update_visible_entries(None, None, cx);
+ cx.notify();
+ }
+ project::Event::WorktreeOrderChanged => {
+ this.update_visible_entries(None, None, cx);
cx.notify();
}
- project::Event::WorktreeUpdatedEntries(_, _)
- | project::Event::WorktreeAdded(_)
- | project::Event::WorktreeOrderChanged => {
- this.update_visible_entries(None, cx);
+ project::Event::WorktreeUpdatedEntries(id, _)
+ | project::Event::WorktreeAdded(id)
+ | project::Event::WorktreeUpdatedGitRepositories(id) => {
+ this.update_visible_entries(Some(*id), None, cx);
cx.notify();
}
+ project::Event::Closed => {
+ this.git_diff_editor_updates = Task::ready(());
+ this.expanded_dir_ids.clear();
+ this.visible_entries.clear();
+ this.git_diff_editor = diff_display_editor(project.clone(), cx);
+ }
_ => {}
})
.detach();
@@ -137,11 +197,10 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
- _workspace: weak_workspace,
+ workspace: weak_workspace,
focus_handle: cx.focus_handle(),
fs,
pending_serialization: Task::ready(None),
- project,
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
@@ -152,8 +211,12 @@ impl GitPanel {
selected_item: None,
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
+ git_diff_editor: diff_display_editor(project.clone(), cx),
+ git_diff_editor_updates: Task::ready(()),
+ reveal_in_editor: Task::ready(()),
+ project,
};
- this.update_visible_entries(None, cx);
+ this.update_visible_entries(None, None, cx);
this
});
@@ -254,6 +317,82 @@ impl GitPanel {
(depth, difference)
}
+
+ fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+ let item_count = self
+ .visible_entries
+ .iter()
+ .map(|worktree_entries| worktree_entries.visible_entries.len())
+ .sum::<usize>();
+ if item_count == 0 {
+ return;
+ }
+ let selection = match self.selected_item {
+ Some(i) => {
+ if i < item_count - 1 {
+ self.selected_item = Some(i + 1);
+ i + 1
+ } else {
+ self.selected_item = Some(0);
+ 0
+ }
+ }
+ None => {
+ self.selected_item = Some(0);
+ 0
+ }
+ };
+ self.scroll_handle
+ .scroll_to_item(selection, ScrollStrategy::Center);
+
+ let mut hunks = None;
+ self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
+ hunks = Some(entry.hunks.clone());
+ });
+ if let Some(hunks) = hunks {
+ self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
+ }
+
+ cx.notify();
+ }
+
+ fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+ let item_count = self
+ .visible_entries
+ .iter()
+ .map(|worktree_entries| worktree_entries.visible_entries.len())
+ .sum::<usize>();
+ if item_count == 0 {
+ return;
+ }
+ let selection = match self.selected_item {
+ Some(i) => {
+ if i > 0 {
+ self.selected_item = Some(i - 1);
+ i - 1
+ } else {
+ self.selected_item = Some(item_count - 1);
+ item_count - 1
+ }
+ }
+ None => {
+ self.selected_item = Some(0);
+ 0
+ }
+ };
+ self.scroll_handle
+ .scroll_to_item(selection, ScrollStrategy::Center);
+
+ let mut hunks = None;
+ self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
+ hunks = Some(entry.hunks.clone());
+ });
+ if let Some(hunks) = hunks {
+ self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
+ }
+
+ cx.notify();
+ }
}
impl GitPanel {
@@ -296,8 +435,9 @@ impl GitPanel {
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
- .map(|(_, entries, _)| {
- entries
+ .map(|worktree_entries| {
+ worktree_entries
+ .visible_entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
@@ -312,19 +452,23 @@ impl GitPanel {
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
- for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
+ for worktree_entries in &self.visible_entries {
if ix >= range.end {
return;
}
- if ix + visible_worktree_entries.len() <= range.start {
- ix += visible_worktree_entries.len();
+ if ix + worktree_entries.visible_entries.len() <= range.start {
+ ix += worktree_entries.visible_entries.len();
continue;
}
- let end_ix = range.end.min(ix + visible_worktree_entries.len());
+ let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
- if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
+ if let Some(worktree) = self
+ .project
+ .read(cx)
+ .worktree_for_id(worktree_entries.worktree_id, cx)
+ {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
@@ -334,14 +478,14 @@ impl GitPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
- let entries = entries_paths.get_or_init(|| {
- visible_worktree_entries
- .iter()
- .map(|e| (e.path.clone()))
- .collect()
- });
+ let entries = worktree_entries.paths();
- for entry in visible_worktree_entries[entry_range].iter() {
+ let index_start = entry_range.start;
+ for (i, entry) in worktree_entries.visible_entries[entry_range]
+ .iter()
+ .enumerate()
+ {
+ let index = index_start + i;
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
@@ -363,16 +507,16 @@ impl GitPanel {
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
};
- let display_name = entry.path.to_string_lossy().into_owned();
-
let details = EntryDetails {
filename,
- display_name,
+ display_name: entry.path.to_string_lossy().into_owned(),
kind: entry.kind,
is_expanded,
path: entry.path.clone(),
status,
+ hunks: entry.hunks.clone(),
depth,
+ index,
};
callback(entry.id, details, cx);
}
@@ -382,44 +526,75 @@ impl GitPanel {
}
// TODO: Update expanded directory state
+ // TODO: Updates happen in the main loop, could be long for large workspaces
fn update_visible_entries(
&mut self,
+ for_worktree: Option<WorktreeId>,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
- self.visible_entries.clear();
- for worktree in project.visible_worktrees(cx) {
- let snapshot = worktree.read(cx).snapshot();
- let worktree_id = snapshot.id();
-
- let mut visible_worktree_entries = Vec::new();
- let mut entry_iter = snapshot.entries(true, 0);
- while let Some(entry) = entry_iter.entry() {
- // Only include entries with a git status
- if entry.git_status.is_some() {
- visible_worktree_entries.push(entry.clone());
+ let mut old_entries_removed = false;
+ let mut after_update = Vec::new();
+ self.visible_entries
+ .retain(|worktree_entries| match for_worktree {
+ Some(for_worktree) => {
+ if worktree_entries.worktree_id == for_worktree {
+ old_entries_removed = true;
+ false
+ } else if old_entries_removed {
+ after_update.push(worktree_entries.clone());
+ false
+ } else {
+ true
+ }
}
- entry_iter.advance();
+ None => false,
+ });
+ for worktree in project.visible_worktrees(cx) {
+ let worktree_id = worktree.read(cx).id();
+ if for_worktree.is_some() && for_worktree != Some(worktree_id) {
+ continue;
}
+ let snapshot = worktree.read(cx).snapshot();
+ let mut visible_worktree_entries = snapshot
+ .entries(false, 0)
+ .filter(|entry| !entry.is_external)
+ .filter(|entry| entry.git_status.is_some())
+ .cloned()
+ .collect::<Vec<_>>();
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
if !visible_worktree_entries.is_empty() {
- self.visible_entries
- .push((worktree_id, visible_worktree_entries, OnceCell::new()));
+ self.visible_entries.push(WorktreeEntries {
+ worktree_id,
+ visible_entries: visible_worktree_entries
+ .into_iter()
+ .map(|entry| GitPanelEntry {
+ entry,
+ hunks: Rc::default(),
+ })
+ .collect(),
+ paths: Rc::default(),
+ });
}
}
+ self.visible_entries.extend(after_update);
if let Some((worktree_id, entry_id)) = new_selected_entry {
self.selected_item = self.visible_entries.iter().enumerate().find_map(
- |(worktree_index, (id, entries, _))| {
- if *id == worktree_id {
- entries
+ |(worktree_index, worktree_entries)| {
+ if worktree_entries.worktree_id == worktree_id {
+ worktree_entries
+ .visible_entries
.iter()
.position(|entry| entry.id == entry_id)
- .map(|entry_index| worktree_index * entries.len() + entry_index)
+ .map(|entry_index| {
+ worktree_index * worktree_entries.visible_entries.len()
+ + entry_index
+ })
} else {
None
}
@@ -427,6 +602,163 @@ impl GitPanel {
);
}
+ let project = self.project.clone();
+ self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
+ cx.background_executor()
+ .timer(UPDATE_DEBOUNCE)
+ .await;
+ let Some(project_buffers) = git_panel
+ .update(&mut cx, |git_panel, cx| {
+ futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
+ move |worktree_entries| {
+ worktree_entries
+ .visible_entries
+ .iter()
+ .filter_map(|entry| {
+ let git_status = entry.git_status()?;
+ let entry_hunks = entry.hunks.clone();
+ let (entry_path, unstaged_changes_task) =
+ project.update(cx, |project, cx| {
+ let entry_path =
+ project.path_for_entry(entry.id, cx)?;
+ let open_task =
+ project.open_path(entry_path.clone(), cx);
+ let unstaged_changes_task =
+ cx.spawn(|project, mut cx| async move {
+ let (_, opened_model) = open_task
+ .await
+ .context("opening buffer")?;
+ let buffer = opened_model
+ .downcast::<Buffer>()
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "accessing buffer for entry"
+ )
+ })?;
+ // TODO added files have noop changes and those are not expanded properly in the multi buffer
+ let unstaged_changes = project
+ .update(&mut cx, |project, cx| {
+ project.open_unstaged_changes(
+ buffer.clone(),
+ cx,
+ )
+ })?
+ .await
+ .context("opening unstaged changes")?;
+
+ let hunks = cx.update(|cx| {
+ entry_hunks
+ .get_or_init(|| {
+ match git_status {
+ GitFileStatus::Added => {
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let entire_buffer_range =
+ buffer_snapshot.anchor_after(0)
+ ..buffer_snapshot
+ .anchor_before(
+ buffer_snapshot.len(),
+ );
+ let entire_buffer_point_range =
+ entire_buffer_range
+ .clone()
+ .to_point(&buffer_snapshot);
+
+ vec![DiffHunk {
+ row_range: entire_buffer_point_range
+ .start
+ .row
+ ..entire_buffer_point_range
+ .end
+ .row,
+ buffer_range: entire_buffer_range,
+ diff_base_byte_range: 0..0,
+ }]
+ }
+ GitFileStatus::Modified => {
+ let buffer_snapshot =
+ buffer.read(cx).snapshot();
+ unstaged_changes.read(cx)
+ .diff_to_buffer
+ .hunks_in_row_range(
+ 0..BufferRow::MAX,
+ &buffer_snapshot,
+ )
+ .collect()
+ }
+ // TODO support conflicts display
+ GitFileStatus::Conflict => Vec::new(),
+ }
+ }).clone()
+ })?;
+
+ anyhow::Ok((buffer, unstaged_changes, hunks))
+ });
+ Some((entry_path, unstaged_changes_task))
+ })?;
+ Some((entry_path, unstaged_changes_task))
+ })
+ .map(|(entry_path, open_task)| async move {
+ (entry_path, open_task.await)
+ })
+ .collect::<Vec<_>>()
+ },
+ ))
+ })
+ .ok()
+ else {
+ return;
+ };
+
+ let project_buffers = project_buffers.await;
+ if project_buffers.is_empty() {
+ return;
+ }
+ let mut change_sets = Vec::with_capacity(project_buffers.len());
+ if let Some(buffer_update_task) = git_panel
+ .update(&mut cx, |git_panel, cx| {
+ let editor = git_panel.git_diff_editor.clone();
+ let multi_buffer = editor.read(cx).buffer().clone();
+ let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
+ for (buffer_path, open_result) in project_buffers {
+ if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
+ .with_context(|| format!("opening buffer {buffer_path:?}"))
+ .log_err()
+ {
+ change_sets.push(unstaged_changes);
+ buffers_with_ranges.push((
+ buffer,
+ diff_hunks
+ .into_iter()
+ .map(|hunk| hunk.buffer_range)
+ .collect(),
+ ));
+ }
+ }
+
+ multi_buffer.update(cx, |multi_buffer, cx| {
+ multi_buffer.clear(cx);
+ multi_buffer.push_multiple_excerpts_with_context_lines(
+ buffers_with_ranges,
+ DEFAULT_MULTIBUFFER_CONTEXT,
+ cx,
+ )
+ })
+ })
+ .ok()
+ {
+ buffer_update_task.await;
+ git_panel
+ .update(&mut cx, |git_panel, cx| {
+ git_panel.git_diff_editor.update(cx, |editor, cx| {
+ for change_set in change_sets {
+ editor.add_change_set(change_set, cx);
+ }
+ })
+ })
+ .ok();
+ }
+ });
+
cx.notify();
}
}
@@ -629,17 +961,23 @@ impl GitPanel {
let item_count = self
.visible_entries
.iter()
- .map(|(_, worktree_entries, _)| worktree_entries.len())
+ .map(|worktree_entries| worktree_entries.visible_entries.len())
.sum();
+ let selected_entry = self.selected_item;
h_flex()
.size_full()
.overflow_hidden()
.child(
uniform_list(cx.view().clone(), "entries", item_count, {
- |this, range, cx| {
+ move |git_panel, range, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
- this.for_each_visible_entry(range, cx, |id, details, cx| {
- items.push(this.render_entry(id, details, cx));
+ git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
+ items.push(git_panel.render_entry(
+ id,
+ Some(details.index) == selected_entry,
+ details,
+ cx,
+ ));
});
items
}
@@ -656,12 +994,14 @@ impl GitPanel {
fn render_entry(
&self,
id: ProjectEntryId,
+ selected: bool,
details: EntryDetails,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let is_staged = ToggleState::Selected;
+ let handle = cx.view().clone();
h_flex()
.id(id)
@@ -679,7 +1019,113 @@ impl GitPanel {
.when_some(details.status, |this, status| {
this.child(git_status_icon(status))
})
- .child(h_flex().gap_1p5().child(details.display_name.clone()))
+ .child(
+ ListItem::new(("label", id))
+ .toggle_state(selected)
+ .child(h_flex().gap_1p5().child(details.display_name.clone()))
+ .on_click(move |e, cx| {
+ handle.update(cx, |git_panel, cx| {
+ git_panel.selected_item = Some(details.index);
+ let change_focus = e.down.click_count > 1;
+ git_panel.reveal_entry_in_git_editor(
+ details.hunks.clone(),
+ change_focus,
+ None,
+ cx,
+ );
+ });
+ }),
+ )
+ }
+
+ fn reveal_entry_in_git_editor(
+ &mut self,
+ hunks: Rc<OnceCell<Vec<DiffHunk>>>,
+ change_focus: bool,
+ debounce: Option<Duration>,
+ cx: &mut ViewContext<'_, Self>,
+ ) {
+ let workspace = self.workspace.clone();
+ let diff_editor = self.git_diff_editor.clone();
+ self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
+ if let Some(debounce) = debounce {
+ cx.background_executor().timer(debounce).await;
+ }
+
+ let Some(editor) = workspace
+ .update(&mut cx, |workspace, cx| {
+ let git_diff_editor = workspace
+ .items_of_type::<Editor>(cx)
+ .find(|editor| &diff_editor == editor);
+ match git_diff_editor {
+ Some(existing_editor) => {
+ workspace.activate_item(&existing_editor, true, change_focus, cx);
+ existing_editor
+ }
+ None => {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ diff_editor.boxed_clone(),
+ true,
+ change_focus,
+ None,
+ cx,
+ )
+ });
+ diff_editor.clone()
+ }
+ }
+ })
+ .ok()
+ else {
+ return;
+ };
+
+ if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
+ let hunk_buffer_range = &first_hunk.buffer_range;
+ if let Some(buffer_id) = hunk_buffer_range
+ .start
+ .buffer_id
+ .or_else(|| first_hunk.buffer_range.end.buffer_id)
+ {
+ editor
+ .update(&mut cx, |editor, cx| {
+ let multi_buffer = editor.buffer().read(cx);
+ let buffer = multi_buffer.buffer(buffer_id)?;
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let (excerpt_id, _) = multi_buffer
+ .excerpts_for_buffer(&buffer, cx)
+ .into_iter()
+ .find(|(_, excerpt)| {
+ hunk_buffer_range
+ .start
+ .cmp(&excerpt.context.start, &buffer_snapshot)
+ .is_ge()
+ && hunk_buffer_range
+ .end
+ .cmp(&excerpt.context.end, &buffer_snapshot)
+ .is_le()
+ })?;
+ let multi_buffer_hunk_start = multi_buffer
+ .snapshot(cx)
+ .anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
+ editor.change_selections(
+ Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
+ cx,
+ |s| {
+ s.select_ranges(Some(
+ multi_buffer_hunk_start..multi_buffer_hunk_start,
+ ))
+ },
+ );
+ cx.notify();
+ Some(())
+ })
+ .ok()
+ .flatten();
+ }
+ }
+ });
}
}
@@ -707,6 +1153,8 @@ impl Render for GitPanel {
this.commit_all_changes(&CommitAllChanges, cx)
}))
})
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
.on_hover(cx.listener(|this, hovered, cx| {
if *hovered {
this.show_scrollbar = true;
@@ -787,3 +1235,14 @@ impl Panel for GitPanel {
Box::new(ToggleFocus)
}
}
+
+fn diff_display_editor(project: Model<Project>, cx: &mut WindowContext) -> View<Editor> {
+ cx.new_view(|cx| {
+ let multi_buffer = cx.new_model(|cx| {
+ MultiBuffer::new(project.read(cx).capability()).with_title("Project diff".to_string())
+ });
+ let mut editor = Editor::for_multibuffer(multi_buffer, Some(project), true, cx);
+ editor.set_expand_all_diff_hunks();
+ editor
+ })
+}