New commit review flow in project diff view (#25229)
Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
parent
6b9397c380
commit
4871d3c9e7
18 changed files with 982 additions and 480 deletions
|
@ -1,25 +1,32 @@
|
|||
use std::any::{Any, TypeId};
|
||||
|
||||
use ::git::UnstageAndNext;
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::HashSet;
|
||||
use editor::{scroll::Autoscroll, Editor, EditorEvent, ToPoint};
|
||||
use editor::{
|
||||
actions::{GoToHunk, GoToPrevHunk},
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ToPoint,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point};
|
||||
use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::{git::GitStore, Project, ProjectPath};
|
||||
use theme::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, vertical_divider, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, SerializableItem, ToolbarItemLocation, Workspace,
|
||||
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
|
||||
|
@ -197,6 +204,69 @@ impl ProjectDiff {
|
|||
}
|
||||
}
|
||||
|
||||
fn button_states(&self, cx: &App) -> ButtonStates {
|
||||
let editor = self.editor.read(cx);
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let prev_next = snapshot.diff_hunks().skip(1).next().is_some();
|
||||
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;
|
||||
if let Some((excerpt_id, buffer, range)) = self.editor.read(cx).active_excerpt(cx) {
|
||||
ranges = vec![multi_buffer::Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
buffer.read(cx).remote_id(),
|
||||
range,
|
||||
)];
|
||||
} else {
|
||||
ranges = Vec::default();
|
||||
}
|
||||
}
|
||||
let mut has_staged_hunks = false;
|
||||
let mut has_unstaged_hunks = false;
|
||||
for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) {
|
||||
match hunk.secondary_status {
|
||||
DiffHunkSecondaryStatus::HasSecondaryHunk => {
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => {
|
||||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::None => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut commit = false;
|
||||
let mut stage_all = false;
|
||||
let mut unstage_all = false;
|
||||
self.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
|
||||
let git_panel = git_panel.read(cx);
|
||||
commit = git_panel.can_commit();
|
||||
stage_all = git_panel.can_stage_all();
|
||||
unstage_all = git_panel.can_unstage_all();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
return ButtonStates {
|
||||
stage: has_unstaged_hunks,
|
||||
unstage: has_staged_hunks,
|
||||
prev_next,
|
||||
selection,
|
||||
commit,
|
||||
stage_all,
|
||||
unstage_all,
|
||||
};
|
||||
}
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
|
@ -598,3 +668,226 @@ impl SerializableItem for ProjectDiff {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectDiffToolbar {
|
||||
project_diff: Option<WeakEntity<ProjectDiff>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl ProjectDiffToolbar {
|
||||
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
project_diff: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_diff(&self, _: &App) -> Option<Entity<ProjectDiff>> {
|
||||
self.project_diff.as_ref()?.upgrade()
|
||||
}
|
||||
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(project_diff) = self.project_diff(cx) {
|
||||
project_diff.focus_handle(cx).focus(window);
|
||||
}
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}
|
||||
fn dispatch_panel_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<GitPanel>(cx) {
|
||||
panel.focus_handle(cx).focus(window)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ProjectDiffToolbar {}
|
||||
|
||||
impl ToolbarItemView for ProjectDiffToolbar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
self.project_diff = active_pane_item
|
||||
.and_then(|item| item.act_as::<ProjectDiff>(cx))
|
||||
.map(|entity| entity.downgrade());
|
||||
if self.project_diff.is_some() {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_update(
|
||||
&mut self,
|
||||
_pane_focused: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonStates {
|
||||
stage: bool,
|
||||
unstage: bool,
|
||||
prev_next: bool,
|
||||
selection: bool,
|
||||
stage_all: bool,
|
||||
unstage_all: bool,
|
||||
commit: bool,
|
||||
}
|
||||
|
||||
impl Render for ProjectDiffToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(project_diff) = self.project_diff(cx) else {
|
||||
return div();
|
||||
};
|
||||
let focus_handle = project_diff.focus_handle(cx);
|
||||
let button_states = project_diff.read(cx).button_states(cx);
|
||||
|
||||
h_group_xl()
|
||||
.my_neg_1()
|
||||
.items_center()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_group_sm()
|
||||
.when(button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Toggle Staged")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Toggle Staged",
|
||||
&ToggleStaged,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.stage && !button_states.unstage)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&ToggleStaged, window, cx)
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(!button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Stage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage",
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
// don't actually disable the button so it's mashable
|
||||
.color(if button_states.stage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage", "Unstage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage",
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.color(if button_states.unstage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
// n.b. the only reason these arrows are here is because we don't
|
||||
// support "undo" for staging so we need a way to go back.
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
IconButton::new("up", IconName::ArrowUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to previous hunk",
|
||||
&GoToPrevHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&GoToPrevHunk, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("down", IconName::ArrowDown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to next hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&GoToHunk, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(vertical_divider())
|
||||
.child(
|
||||
h_group_sm()
|
||||
.when(
|
||||
button_states.unstage_all && !button_states.stage_all,
|
||||
|el| {
|
||||
el.child(Button::new("unstage-all", "Unstage All").on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_panel_action(&UnstageAll, window, cx)
|
||||
}),
|
||||
))
|
||||
},
|
||||
)
|
||||
.when(
|
||||
!button_states.unstage_all || button_states.stage_all,
|
||||
|el| {
|
||||
el.child(
|
||||
// todo make it so that changing to say "Unstaged"
|
||||
// doesn't change the position.
|
||||
div().child(
|
||||
Button::new("stage-all", "Stage All")
|
||||
.disabled(!button_states.stage_all)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_panel_action(&StageAll, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.disabled(!button_states.commit)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
// todo this should open modal, not focus panel.
|
||||
this.dispatch_action(&Commit, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue