Detailed changes
@@ -1759,7 +1759,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
- "itertools 0.10.5",
+ "itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -1782,7 +1782,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
- "itertools 0.10.5",
+ "itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
@@ -7206,7 +7206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -10316,8 +10316,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.0",
- "heck 0.4.1",
- "itertools 0.10.5",
+ "heck 0.5.0",
+ "itertools 0.12.1",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10350,7 +10350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
- "itertools 0.10.5",
+ "itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -15687,7 +15687,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -742,6 +742,14 @@
"escape": "git_panel::ToggleFocus"
}
},
+ {
+ "context": "GitCommit > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::Newline",
+ "cmd-enter": "git::Commit"
+ }
+ },
{
"context": "GitPanel > Editor",
"use_key_equivalents": true,
@@ -12769,7 +12769,7 @@ impl Editor {
self.toggle_diff_hunks_in_ranges(ranges, cx);
}
- fn diff_hunks_in_ranges<'a>(
+ pub fn diff_hunks_in_ranges<'a>(
&'a self,
ranges: &'a [Range<Anchor>],
buffer: &'a MultiBufferSnapshot,
@@ -12814,9 +12814,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let head = self.selections.newest_anchor().head();
- self.stage_or_unstage_diff_hunks(true, &[head..head], cx);
- self.go_to_next_hunk(&Default::default(), window, cx);
+ self.do_stage_or_unstage_and_next(true, window, cx);
}
pub fn unstage_and_next(
@@ -12825,9 +12823,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let head = self.selections.newest_anchor().head();
- self.stage_or_unstage_diff_hunks(false, &[head..head], cx);
- self.go_to_next_hunk(&Default::default(), window, cx);
+ self.do_stage_or_unstage_and_next(false, window, cx);
}
pub fn stage_or_unstage_diff_hunks(
@@ -12849,6 +12845,43 @@ impl Editor {
}
}
+ fn do_stage_or_unstage_and_next(
+ &mut self,
+ stage: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mut ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
+ if ranges.iter().any(|range| range.start != range.end) {
+ self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx);
+ return;
+ }
+
+ if !self.buffer().read(cx).is_singleton() {
+ if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) {
+ ranges = vec![multi_buffer::Anchor::range_in_buffer(
+ excerpt_id,
+ buffer.read(cx).remote_id(),
+ range,
+ )];
+ self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx);
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+ let mut point = ranges.last().unwrap().end.to_point(&snapshot);
+ if point.row < snapshot.max_row().0 {
+ point.row += 1;
+ point.column = 0;
+ point = snapshot.clip_point(point, Bias::Right);
+ self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| {
+ s.select_ranges([point..point]);
+ })
+ }
+ return;
+ }
+ }
+ self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx);
+ self.go_to_next_hunk(&Default::default(), window, cx);
+ }
+
fn do_stage_or_unstage(
project: &Entity<Project>,
stage: bool,
@@ -4343,21 +4343,26 @@ impl EditorElement {
let y = display_row_range.start.as_f32() * line_height
+ text_hitbox.bounds.top()
- scroll_pixel_position.y;
- let x = text_hitbox.bounds.right() - px(100.);
let mut element = diff_hunk_controls(
display_row_range.start.0,
+ status,
multi_buffer_range.clone(),
line_height,
&editor,
cx,
);
- element.prepaint_as_root(
- gpui::Point::new(x, y),
- size(px(100.0), line_height).into(),
- window,
- cx,
- );
+ let size =
+ element.layout_as_root(size(px(100.0), line_height).into(), window, cx);
+
+ let x = text_hitbox.bounds.right()
+ - self.style.scrollbar_width
+ - px(10.)
+ - size.width;
+
+ window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
+ element.prepaint(window, cx)
+ });
controls.push(element);
}
}
@@ -7750,7 +7755,7 @@ impl Element for EditorElement {
editor.last_position_map = Some(position_map.clone())
});
- let hunk_controls = self.layout_diff_hunk_controls(
+ let diff_hunk_controls = self.layout_diff_hunk_controls(
start_row..end_row,
&row_infos,
&text_hitbox,
@@ -7790,7 +7795,7 @@ impl Element for EditorElement {
visible_cursors,
selections,
inline_completion_popover,
- diff_hunk_controls: hunk_controls,
+ diff_hunk_controls,
mouse_context_menu,
test_indicators,
code_actions_indicator,
@@ -9117,6 +9122,7 @@ mod tests {
fn diff_hunk_controls(
row: u32,
+ status: &DiffHunkStatus,
hunk_range: Range<Anchor>,
line_height: Pixels,
editor: &Entity<Editor>,
@@ -9133,62 +9139,66 @@ fn diff_hunk_controls(
.rounded_b_lg()
.bg(cx.theme().colors().editor_background)
.gap_1()
+ .when(status.secondary == DiffHunkSecondaryStatus::None, |el| {
+ el.child(
+ Button::new("unstage", "Unstage")
+ .tooltip({
+ let focus_handle = editor.focus_handle(cx);
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Unstage Hunk",
+ &::git::ToggleStaged,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let editor = editor.clone();
+ move |_event, _, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.stage_or_unstage_diff_hunks(
+ false,
+ &[hunk_range.start..hunk_range.start],
+ cx,
+ );
+ });
+ }
+ }),
+ )
+ })
+ .when(status.secondary != DiffHunkSecondaryStatus::None, |el| {
+ el.child(
+ Button::new("stage", "Stage")
+ .tooltip({
+ let focus_handle = editor.focus_handle(cx);
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Stage Hunk",
+ &::git::ToggleStaged,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let editor = editor.clone();
+ move |_event, _, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.stage_or_unstage_diff_hunks(
+ true,
+ &[hunk_range.start..hunk_range.start],
+ cx,
+ );
+ });
+ }
+ }),
+ )
+ })
.child(
- IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::Small)
- // .disabled(!has_multiple_hunks)
- .tooltip({
- let focus_handle = editor.focus_handle(cx);
- move |window, cx| {
- Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, window, cx)
- }
- })
- .on_click({
- let editor = editor.clone();
- move |_event, window, cx| {
- editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(window, cx);
- let position = hunk_range.end.to_point(&snapshot.buffer_snapshot);
- editor.go_to_hunk_after_position(&snapshot, position, window, cx);
- editor.expand_selected_diff_hunks(cx);
- });
- }
- }),
- )
- .child(
- IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::Small)
- // .disabled(!has_multiple_hunks)
- .tooltip({
- let focus_handle = editor.focus_handle(cx);
- move |window, cx| {
- Tooltip::for_action_in(
- "Previous Hunk",
- &GoToPrevHunk,
- &focus_handle,
- window,
- cx,
- )
- }
- })
- .on_click({
- let editor = editor.clone();
- move |_event, window, cx| {
- editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(window, cx);
- let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
- editor.go_to_hunk_before_position(&snapshot, point, window, cx);
- editor.expand_selected_diff_hunks(cx);
- });
- }
- }),
- )
- .child(
- IconButton::new("discard", IconName::Undo)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::Small)
+ Button::new("discard", "Restore")
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
@@ -9212,5 +9222,71 @@ fn diff_hunk_controls(
}
}),
)
+ .when(
+ !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
+ |el| {
+ el.child(
+ IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ // .disabled(!has_multiple_hunks)
+ .tooltip({
+ let focus_handle = editor.focus_handle(cx);
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Next Hunk",
+ &GoToHunk,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let editor = editor.clone();
+ move |_event, window, cx| {
+ editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(window, cx);
+ let position =
+ hunk_range.end.to_point(&snapshot.buffer_snapshot);
+ editor
+ .go_to_hunk_after_position(&snapshot, position, window, cx);
+ editor.expand_selected_diff_hunks(cx);
+ });
+ }
+ }),
+ )
+ .child(
+ IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ // .disabled(!has_multiple_hunks)
+ .tooltip({
+ let focus_handle = editor.focus_handle(cx);
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Previous Hunk",
+ &GoToPrevHunk,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let editor = editor.clone();
+ move |_event, window, cx| {
+ editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(window, cx);
+ let point =
+ hunk_range.start.to_point(&snapshot.buffer_snapshot);
+ editor.go_to_hunk_before_position(&snapshot, point, window, cx);
+ editor.expand_selected_diff_hunks(cx);
+ });
+ }
+ }),
+ )
+ },
+ )
.into_any_element()
}
@@ -0,0 +1,244 @@
+#![allow(unused, dead_code)]
+
+use crate::git_panel::{commit_message_editor, GitPanel};
+use crate::repository_selector::RepositorySelector;
+use anyhow::Result;
+use git::Commit;
+use language::Buffer;
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::{prelude::*, Tooltip};
+
+use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
+use gpui::*;
+use project::git::Repository;
+use project::{Fs, Project};
+use std::sync::Arc;
+use workspace::dock::{Dock, DockPosition, PanelHandle};
+use workspace::{ModalView, Workspace};
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, window, cx| {
+ let Some(window) = window else {
+ return;
+ };
+ CommitModal::register(workspace, window, cx)
+ })
+ .detach();
+}
+
+pub struct CommitModal {
+ git_panel: Entity<GitPanel>,
+ commit_editor: Entity<Editor>,
+ restore_dock: RestoreDock,
+}
+
+impl Focusable for CommitModal {
+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+ self.commit_editor.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for CommitModal {}
+impl ModalView for CommitModal {
+ fn on_before_dismiss(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> workspace::DismissDecision {
+ self.git_panel.update(cx, |git_panel, cx| {
+ git_panel.set_modal_open(false, cx);
+ });
+ self.restore_dock.dock.update(cx, |dock, cx| {
+ if let Some(active_index) = self.restore_dock.active_index {
+ dock.activate_panel(active_index, window, cx)
+ }
+ dock.set_open(self.restore_dock.is_open, window, cx)
+ });
+ workspace::DismissDecision::Dismiss(true)
+ }
+}
+
+struct RestoreDock {
+ dock: WeakEntity<Dock>,
+ is_open: bool,
+ active_index: Option<usize>,
+}
+
+impl CommitModal {
+ pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
+ workspace.register_action(|workspace, _: &Commit, window, cx| {
+ let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
+ return;
+ };
+
+ let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
+ let can_commit = git_panel.can_commit();
+ let conflict = git_panel.has_unstaged_conflicts();
+ (can_commit, conflict)
+ });
+ if !can_commit {
+ let message = if conflict {
+ "There are still conflicts. You must stage these before committing."
+ } else {
+ "No changes to commit."
+ };
+ let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
+ cx.spawn(|_, _| async move {
+ prompt.await.ok();
+ })
+ .detach();
+ }
+
+ let dock = workspace.dock_at_position(git_panel.position(window, cx));
+ let is_open = dock.read(cx).is_open();
+ let active_index = dock.read(cx).active_panel_index();
+ let dock = dock.downgrade();
+ let restore_dock_position = RestoreDock {
+ dock,
+ is_open,
+ active_index,
+ };
+ workspace.open_panel::<GitPanel>(window, cx);
+ workspace.toggle_modal(window, cx, move |window, cx| {
+ CommitModal::new(git_panel, restore_dock_position, window, cx)
+ })
+ });
+ }
+
+ fn new(
+ git_panel: Entity<GitPanel>,
+ restore_dock: RestoreDock,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let panel = git_panel.read(cx);
+
+ let commit_editor = git_panel.update(cx, |git_panel, cx| {
+ git_panel.set_modal_open(true, cx);
+ let buffer = git_panel.commit_message_buffer(cx).clone();
+ let project = git_panel.project.clone();
+ cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
+ });
+
+ Self {
+ git_panel,
+ commit_editor,
+ restore_dock,
+ }
+ }
+
+ pub fn render_commit_editor(
+ &self,
+ name_and_email: Option<(SharedString, SharedString)>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ let editor = self.commit_editor.clone();
+
+ let panel_editor_style = panel_editor_style(true, window, cx);
+
+ let settings = ThemeSettings::get_global(cx);
+ let line_height = relative(settings.buffer_line_height.value())
+ .to_pixels(settings.buffer_font_size.into(), window.rem_size());
+
+ v_flex()
+ .justify_between()
+ .relative()
+ .w_full()
+ .h_full()
+ .pt_2()
+ .bg(cx.theme().colors().editor_background)
+ .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+ .child(self.render_footer(window, cx))
+ }
+
+ pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let (branch, tooltip, title, co_authors) = self.git_panel.update(cx, |git_panel, cx| {
+ let branch = git_panel
+ .active_repository
+ .as_ref()
+ .and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
+ .unwrap_or_else(|| "<no branch>".into());
+ let tooltip = if git_panel.has_staged_changes() {
+ "Commit staged changes"
+ } else {
+ "Commit changes to tracked files"
+ };
+ let title = if git_panel.has_staged_changes() {
+ "Commit"
+ } else {
+ "Commit All"
+ };
+ let co_authors = git_panel.render_co_authors(cx);
+ (branch, tooltip, title, co_authors)
+ });
+
+ let branch_selector = Button::new("branch-selector", branch)
+ .color(Color::Muted)
+ .style(ButtonStyle::Subtle)
+ .icon(IconName::GitBranch)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::Compact)
+ .icon_position(IconPosition::Start)
+ .tooltip(Tooltip::for_action_title(
+ "Switch Branch",
+ &zed_actions::git::Branch,
+ ))
+ .on_click(cx.listener(|_, _, window, cx| {
+ window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+ }))
+ .style(ButtonStyle::Transparent);
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(branch_selector)
+ .child(
+ h_flex().children(co_authors).child(
+ panel_filled_button(title)
+ .tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.commit(&Default::default(), window, cx);
+ })),
+ ),
+ )
+ }
+
+ fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+ fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
+ self.git_panel
+ .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
+ cx.emit(DismissEvent);
+ }
+}
+
+impl Render for CommitModal {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+ v_flex()
+ .id("commit-modal")
+ .key_context("GitCommit")
+ .elevation_3(cx)
+ .on_action(cx.listener(Self::dismiss))
+ .on_action(cx.listener(Self::commit))
+ .relative()
+ .bg(cx.theme().colors().editor_background)
+ .rounded(px(16.))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .py_2()
+ .px_4()
+ .w(px(480.))
+ .min_h(rems(18.))
+ .flex_1()
+ .overflow_hidden()
+ .child(
+ v_flex()
+ .flex_1()
+ .child(self.render_commit_editor(None, window, cx)),
+ )
+ }
+}
@@ -1,4 +1,5 @@
use crate::git_panel_settings::StatusStyle;
+use crate::project_diff::Diff;
use crate::repository_selector::RepositorySelectorPopoverMenu;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
@@ -78,16 +79,16 @@ pub fn init(cx: &mut App) {
workspace.toggle_panel_focus::<GitPanel>(window, cx);
});
- workspace.register_action(|workspace, _: &Commit, window, cx| {
- workspace.open_panel::<GitPanel>(window, cx);
- if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
- git_panel
- .read(cx)
- .commit_editor
- .focus_handle(cx)
- .focus(window);
- }
- });
+ // workspace.register_action(|workspace, _: &Commit, window, cx| {
+ // workspace.open_panel::<GitPanel>(window, cx);
+ // if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
+ // git_panel
+ // .read(cx)
+ // .commit_editor
+ // .focus_handle(cx)
+ // .focus(window);
+ // }
+ // });
},
)
.detach();
@@ -174,7 +175,7 @@ struct PendingOperation {
}
pub struct GitPanel {
- active_repository: Option<Entity<Repository>>,
+ pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
conflicted_count: usize,
conflicted_staged_count: usize,
@@ -190,7 +191,7 @@ pub struct GitPanel {
pending: Vec<PendingOperation>,
pending_commit: Option<Task<()>>,
pending_serialization: Task<Option<()>>,
- project: Entity<Project>,
+ pub(crate) project: Entity<Project>,
repository_selector: Entity<RepositorySelector>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
@@ -202,17 +203,20 @@ pub struct GitPanel {
width: Option<Pixels>,
workspace: WeakEntity<Workspace>,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
+ modal_open: bool,
}
-fn commit_message_editor(
+pub(crate) fn commit_message_editor(
commit_message_buffer: Entity<Buffer>,
project: Entity<Project>,
+ in_panel: bool,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) -> Editor {
let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
+ let max_lines = if in_panel { 6 } else { 18 };
let mut commit_editor = Editor::new(
- EditorMode::AutoHeight { max_lines: 6 },
+ EditorMode::AutoHeight { max_lines },
buffer,
None,
false,
@@ -251,8 +255,9 @@ impl GitPanel {
// just to let us render a placeholder editor.
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
- let commit_editor =
- cx.new(|cx| commit_message_editor(temporary_buffer, project.clone(), window, cx));
+ let commit_editor = cx.new(|cx| {
+ commit_message_editor(temporary_buffer, project.clone(), true, window, cx)
+ });
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
});
@@ -309,6 +314,7 @@ impl GitPanel {
width: Some(px(360.)),
context_menu: None,
workspace,
+ modal_open: false,
};
git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -351,6 +357,11 @@ impl GitPanel {
);
}
+ pub(crate) fn set_modal_open(&mut self, open: bool, cx: &mut Context<Self>) {
+ self.modal_open = open;
+ cx.notify();
+ }
+
fn dispatch_context(&self, window: &mut Window, cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("GitPanel");
@@ -592,7 +603,6 @@ impl GitPanel {
})
.ok()
});
- self.focus_handle.focus(window);
}
fn open_file(
@@ -998,6 +1008,16 @@ impl GitPanel {
.detach();
}
+ pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
+ self.commit_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap()
+ .clone()
+ }
+
fn toggle_staged_for_selected(
&mut self,
_: &git::ToggleStaged,
@@ -1022,7 +1042,7 @@ impl GitPanel {
self.commit_changes(window, cx)
}
- fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
@@ -1288,7 +1308,7 @@ impl GitPanel {
!= Some(&buffer)
{
git_panel.commit_editor = cx.new(|cx| {
- commit_message_editor(buffer, git_panel.project.clone(), window, cx)
+ commit_message_editor(buffer, git_panel.project.clone(), true, window, cx)
});
}
})
@@ -1476,12 +1496,18 @@ impl GitPanel {
entry.is_staged
}
- fn has_staged_changes(&self) -> bool {
+ pub(crate) fn has_staged_changes(&self) -> bool {
self.tracked_staged_count > 0
|| self.new_staged_count > 0
|| self.conflicted_staged_count > 0
}
+ pub(crate) fn has_unstaged_changes(&self) -> bool {
+ self.tracked_count > self.tracked_staged_count
+ || self.new_count > self.new_staged_count
+ || self.conflicted_count > self.conflicted_staged_count
+ }
+
fn has_conflicts(&self) -> bool {
self.conflicted_count > 0
}
@@ -1490,7 +1516,7 @@ impl GitPanel {
self.tracked_count > 0
}
- fn has_unstaged_conflicts(&self) -> bool {
+ pub fn has_unstaged_conflicts(&self) -> bool {
self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
}
@@ -1564,7 +1590,17 @@ impl GitPanel {
.size(LabelSize::Small)
.color(Color::Muted),
)
- .child(self.render_repository_selector(cx)),
+ .child(self.render_repository_selector(cx))
+ .child(div().flex_grow())
+ .child(
+ Button::new("diff", "+/-")
+ .tooltip(Tooltip::for_action_title("Open diff", &Diff))
+ .on_click(|_, _, cx| {
+ cx.defer(|cx| {
+ cx.dispatch_action(&Diff);
+ })
+ }),
+ ),
)
} else {
None
@@ -1587,21 +1623,64 @@ impl GitPanel {
)
}
+ pub fn can_commit(&self) -> bool {
+ (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts()
+ }
+
+ pub fn can_stage_all(&self) -> bool {
+ self.has_unstaged_changes()
+ }
+
+ pub fn can_unstage_all(&self) -> bool {
+ self.has_staged_changes()
+ }
+
+ pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
+ let potential_co_authors = self.potential_co_authors(cx);
+ if potential_co_authors.is_empty() {
+ None
+ } else {
+ Some(
+ IconButton::new("co-authors", IconName::Person)
+ .icon_color(Color::Disabled)
+ .selected_icon_color(Color::Selected)
+ .toggle_state(self.add_coauthors)
+ .tooltip(move |_, cx| {
+ let title = format!(
+ "Add co-authored-by:{}{}",
+ if potential_co_authors.len() == 1 {
+ ""
+ } else {
+ "\n"
+ },
+ potential_co_authors
+ .iter()
+ .map(|(name, email)| format!(" {} <{}>", name, email))
+ .join("\n")
+ );
+ Tooltip::simple(title, cx)
+ })
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.add_coauthors = !this.add_coauthors;
+ cx.notify();
+ }))
+ .into_any_element(),
+ )
+ }
+ }
+
pub fn render_commit_editor(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let editor = self.commit_editor.clone();
- let can_commit = (self.has_staged_changes() || self.has_tracked_changes())
+ let can_commit = self.can_commit()
&& self.pending_commit.is_none()
&& !editor.read(cx).is_empty(cx)
- && !self.has_unstaged_conflicts()
&& self.has_write_access(cx);
-
- // let can_commit_all =
- // !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
let panel_editor_style = panel_editor_style(true, window, cx);
+ let enable_coauthors = self.render_co_authors(cx);
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
@@ -1627,37 +1706,6 @@ impl GitPanel {
cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx))
});
- let potential_co_authors = self.potential_co_authors(cx);
- let enable_coauthors = if potential_co_authors.is_empty() {
- None
- } else {
- Some(
- IconButton::new("co-authors", IconName::Person)
- .icon_color(Color::Disabled)
- .selected_icon_color(Color::Selected)
- .toggle_state(self.add_coauthors)
- .tooltip(move |_, cx| {
- let title = format!(
- "Add co-authored-by:{}{}",
- if potential_co_authors.len() == 1 {
- ""
- } else {
- "\n"
- },
- potential_co_authors
- .iter()
- .map(|(name, email)| format!(" {} <{}>", name, email))
- .join("\n")
- );
- Tooltip::simple(title, cx)
- })
- .on_click(cx.listener(|this, _, _, cx| {
- this.add_coauthors = !this.add_coauthors;
- cx.notify();
- })),
- )
- };
-
let branch = self
.active_repository
.as_ref()
@@ -1698,26 +1746,28 @@ impl GitPanel {
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
- .child(EditorElement::new(&self.commit_editor, panel_editor_style))
- .child(
- h_flex()
- .absolute()
- .bottom_0()
- .left_2()
- .h(footer_size)
- .flex_none()
- .child(branch_selector),
- )
- .child(
- h_flex()
- .absolute()
- .bottom_0()
- .right_2()
- .h(footer_size)
- .flex_none()
- .children(enable_coauthors)
- .child(commit_button),
- )
+ .when(!self.modal_open, |el| {
+ el.child(EditorElement::new(&self.commit_editor, panel_editor_style))
+ .child(
+ h_flex()
+ .absolute()
+ .bottom_0()
+ .left_2()
+ .h(footer_size)
+ .flex_none()
+ .child(branch_selector),
+ )
+ .child(
+ h_flex()
+ .absolute()
+ .bottom_0()
+ .right_2()
+ .h(footer_size)
+ .flex_none()
+ .children(enable_coauthors)
+ .child(commit_button),
+ )
+ })
}
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
@@ -1892,8 +1942,8 @@ impl GitPanel {
Some(
h_flex()
.id("start-slot")
+ .text_lg()
.child(checkbox)
- .child(git_status_icon(entry.status_entry()?.status, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
cx.stop_propagation();
@@ -6,17 +6,17 @@ use project_diff::ProjectDiff;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
pub mod branch_picker;
+mod commit_modal;
pub mod git_panel;
mod git_panel_settings;
pub mod project_diff;
-// mod quick_commit;
pub mod repository_selector;
pub fn init(cx: &mut App) {
GitPanelSettings::register(cx);
branch_picker::init(cx);
cx.observe_new(ProjectDiff::register).detach();
- // quick_commit::init(cx);
+ commit_modal::init(cx);
}
// TODO: Add updated status colors to theme
@@ -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);
+ })),
+ ),
+ )
+ }
+}
@@ -1,307 +0,0 @@
-#![allow(unused, dead_code)]
-
-use crate::repository_selector::RepositorySelector;
-use anyhow::Result;
-use git::{CommitAllChanges, CommitChanges};
-use language::Buffer;
-use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
-use ui::{prelude::*, Tooltip};
-
-use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
-use gpui::*;
-use project::git::Repository;
-use project::{Fs, Project};
-use std::sync::Arc;
-use workspace::{ModalView, Workspace};
-
-actions!(
- git,
- [QuickCommitWithMessage, QuickCommitStaged, QuickCommitAll]
-);
-
-pub fn init(cx: &mut App) {
- cx.observe_new(|workspace: &mut Workspace, window, cx| {
- let Some(window) = window else {
- return;
- };
- QuickCommitModal::register(workspace, window, cx)
- })
- .detach();
-}
-
-fn commit_message_editor(
- commit_message_buffer: Option<Entity<Buffer>>,
- window: &mut Window,
- cx: &mut Context<'_, Editor>,
-) -> Editor {
- let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
- let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
- Editor::new(
- EditorMode::AutoHeight { max_lines: 10 },
- buffer,
- None,
- false,
- window,
- cx,
- )
- } else {
- Editor::auto_height(10, window, cx)
- };
- commit_editor.set_use_autoclose(false);
- commit_editor.set_show_gutter(false, cx);
- commit_editor.set_show_wrap_guides(false, cx);
- commit_editor.set_show_indent_guides(false, cx);
- commit_editor.set_placeholder_text("Enter commit message", cx);
- commit_editor
-}
-
-pub struct QuickCommitModal {
- focus_handle: FocusHandle,
- fs: Arc<dyn Fs>,
- project: Entity<Project>,
- active_repository: Option<Entity<Repository>>,
- repository_selector: Entity<RepositorySelector>,
- commit_editor: Entity<Editor>,
- width: Option<Pixels>,
- commit_task: Task<Result<()>>,
- commit_pending: bool,
- can_commit: bool,
- can_commit_all: bool,
- enable_auto_coauthors: bool,
-}
-
-impl Focusable for QuickCommitModal {
- fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl EventEmitter<DismissEvent> for QuickCommitModal {}
-impl ModalView for QuickCommitModal {}
-
-impl QuickCommitModal {
- pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
- workspace.register_action(|workspace, _: &QuickCommitWithMessage, window, cx| {
- let project = workspace.project().clone();
- let fs = workspace.app_state().fs.clone();
-
- workspace.toggle_modal(window, cx, move |window, cx| {
- QuickCommitModal::new(project, fs, window, None, cx)
- });
- });
- }
-
- pub fn new(
- project: Entity<Project>,
- fs: Arc<dyn Fs>,
- window: &mut Window,
- commit_message_buffer: Option<Entity<Buffer>>,
- cx: &mut Context<Self>,
- ) -> Self {
- let git_store = project.read(cx).git_store().clone();
- let active_repository = project.read(cx).active_repository(cx);
-
- let focus_handle = cx.focus_handle();
-
- let commit_editor = cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
- commit_editor.update(cx, |editor, cx| {
- editor.clear(window, cx);
- });
-
- let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
-
- Self {
- focus_handle,
- fs,
- project,
- active_repository,
- repository_selector,
- commit_editor,
- width: None,
- commit_task: Task::ready(Ok(())),
- commit_pending: false,
- can_commit: false,
- can_commit_all: false,
- enable_auto_coauthors: true,
- }
- }
-
- pub fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let all_repositories = self
- .project
- .read(cx)
- .git_store()
- .read(cx)
- .all_repositories();
- let entry_count = self
- .active_repository
- .as_ref()
- .map_or(0, |repo| repo.read(cx).entry_count());
-
- let changes_string = match entry_count {
- 0 => "No changes".to_string(),
- 1 => "1 change".to_string(),
- n => format!("{} changes", n),
- };
-
- div().absolute().top_0().right_0().child(
- panel_icon_button("open_change_list", IconName::PanelRight)
- .disabled(true)
- .tooltip(Tooltip::text("Changes list coming soon!")),
- )
- }
-
- pub fn render_commit_editor(
- &self,
- name_and_email: Option<(SharedString, SharedString)>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- let editor = self.commit_editor.clone();
- let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
- let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
-
- let focus_handle_1 = self.focus_handle(cx).clone();
- let focus_handle_2 = self.focus_handle(cx).clone();
-
- let panel_editor_style = panel_editor_style(true, window, cx);
-
- let commit_staged_button = panel_filled_button("Commit")
- .tooltip(move |window, cx| {
- let focus_handle = focus_handle_1.clone();
- Tooltip::for_action_in(
- "Commit all staged changes",
- &CommitChanges,
- &focus_handle,
- window,
- cx,
- )
- })
- .when(!can_commit, |this| {
- this.disabled(true).style(ButtonStyle::Transparent)
- });
- // .on_click({
- // let name_and_email = name_and_email.clone();
- // cx.listener(move |this, _: &ClickEvent, window, cx| {
- // this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx)
- // })
- // });
-
- let commit_all_button = panel_filled_button("Commit All")
- .tooltip(move |window, cx| {
- let focus_handle = focus_handle_2.clone();
- Tooltip::for_action_in(
- "Commit all changes, including unstaged changes",
- &CommitAllChanges,
- &focus_handle,
- window,
- cx,
- )
- })
- .when(!can_commit, |this| {
- this.disabled(true).style(ButtonStyle::Transparent)
- });
- // .on_click({
- // let name_and_email = name_and_email.clone();
- // cx.listener(move |this, _: &ClickEvent, window, cx| {
- // this.commit_tracked_changes(
- // &CommitAllChanges,
- // name_and_email.clone(),
- // window,
- // cx,
- // )
- // })
- // });
-
- let co_author_button = panel_icon_button("add-co-author", IconName::UserGroup)
- .icon_color(if self.enable_auto_coauthors {
- Color::Muted
- } else {
- Color::Accent
- })
- .icon_size(IconSize::Small)
- .toggle_state(self.enable_auto_coauthors)
- // .on_click({
- // cx.listener(move |this, _: &ClickEvent, _, cx| {
- // this.toggle_auto_coauthors(cx);
- // })
- // })
- .tooltip(move |window, cx| {
- Tooltip::with_meta(
- "Toggle automatic co-authors",
- None,
- "Automatically adds current collaborators",
- window,
- cx,
- )
- });
-
- panel_editor_container(window, cx)
- .id("commit-editor-container")
- .relative()
- .w_full()
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .h(px(140.))
- .bg(cx.theme().colors().editor_background)
- .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
- window.focus(&editor_focus_handle);
- }))
- .child(EditorElement::new(&self.commit_editor, panel_editor_style))
- .child(div().flex_1())
- .child(
- h_flex()
- .items_center()
- .h_8()
- .justify_between()
- .gap_1()
- .child(co_author_button)
- .child(commit_all_button)
- .child(commit_staged_button),
- )
- }
-
- pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- h_flex()
- .w_full()
- .justify_between()
- .child(h_flex().child("cmd+esc clear message"))
- .child(
- h_flex()
- .child(panel_filled_button("Commit"))
- .child(panel_filled_button("Commit All")),
- )
- }
-
- fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
- cx.emit(DismissEvent);
- }
-}
-
-impl Render for QuickCommitModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
- v_flex()
- .id("quick-commit-modal")
- .key_context("QuickCommit")
- .on_action(cx.listener(Self::dismiss))
- .relative()
- .bg(cx.theme().colors().elevated_surface_background)
- .rounded(px(16.))
- .border_1()
- .border_color(cx.theme().colors().border)
- .py_2()
- .px_4()
- .w(self.width.unwrap_or(px(640.)))
- .h(px(450.))
- .flex_1()
- .overflow_hidden()
- .child(self.render_header(window, cx))
- .child(
- v_flex()
- .flex_1()
- // TODO: pass name_and_email
- .child(self.render_commit_editor(None, window, cx)),
- )
- .child(self.render_footer(window, cx))
- }
-}
@@ -6,6 +6,7 @@ mod disclosure;
mod divider;
mod dropdown_menu;
mod facepile;
+mod group;
mod icon;
mod image;
mod indent_guides;
@@ -42,6 +43,7 @@ pub use disclosure::*;
pub use divider::*;
pub use dropdown_menu::*;
pub use facepile::*;
+pub use group::*;
pub use icon::*;
pub use image::*;
pub use indent_guides::*;
@@ -11,14 +11,14 @@ pub fn content_group() -> ContentGroup {
/// A [ContentGroup] that vertically stacks its children.
///
/// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex).
-pub fn v_group() -> ContentGroup {
+pub fn v_container() -> ContentGroup {
content_group().v_flex()
}
/// Creates a new horizontal [ContentGroup].
///
/// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex).
-pub fn h_group() -> ContentGroup {
+pub fn h_container() -> ContentGroup {
content_group().h_flex()
}
@@ -3,6 +3,24 @@ use gpui::{Hsla, IntoElement};
use crate::prelude::*;
+pub fn divider() -> Divider {
+ Divider {
+ style: DividerStyle::Solid,
+ direction: DividerDirection::Horizontal,
+ color: DividerColor::default(),
+ inset: false,
+ }
+}
+
+pub fn vertical_divider() -> Divider {
+ Divider {
+ style: DividerStyle::Solid,
+ direction: DividerDirection::Vertical,
+ color: DividerColor::default(),
+ inset: false,
+ }
+}
+
#[derive(Clone, Copy, PartialEq)]
enum DividerStyle {
Solid,
@@ -0,0 +1,57 @@
+use gpui::{div, prelude::*, Div};
+
+/// Creates a horizontal group with tight, consistent spacing.
+///
+/// xs: ~2px @16px/rem
+pub fn h_group_sm() -> Div {
+ div().flex().gap_0p5()
+}
+
+/// Creates a horizontal group with consistent spacing.
+///
+/// s: ~4px @16px/rem
+pub fn h_group() -> Div {
+ div().flex().gap_1()
+}
+
+/// Creates a horizontal group with consistent spacing.
+///
+/// m: ~6px @16px/rem
+pub fn h_group_lg() -> Div {
+ div().flex().gap_1p5()
+}
+
+/// Creates a horizontal group with consistent spacing.
+///
+/// l: ~8px @16px/rem
+pub fn h_group_xl() -> Div {
+ div().flex().gap_2()
+}
+
+/// Creates a vertical group with tight, consistent spacing.
+///
+/// xs: ~2px @16px/rem
+pub fn v_group_sm() -> Div {
+ div().flex().flex_col().gap_0p5()
+}
+
+/// Creates a vertical group with consistent spacing.
+///
+/// s: ~4px @16px/rem
+pub fn v_group() -> Div {
+ div().flex().flex_col().gap_1()
+}
+
+/// Creates a vertical group with consistent spacing.
+///
+/// m: ~6px @16px/rem
+pub fn v_group_lg() -> Div {
+ div().flex().flex_col().gap_1p5()
+}
+
+/// Creates a vertical group with consistent spacing.
+///
+/// l: ~8px @16px/rem
+pub fn v_group_xl() -> Div {
+ div().flex().flex_col().gap_2()
+}
@@ -52,6 +52,24 @@ impl Tooltip {
}
}
+ pub fn for_action_title_in(
+ title: impl Into<SharedString>,
+ action: &dyn Action,
+ focus_handle: &FocusHandle,
+ ) -> impl Fn(&mut Window, &mut App) -> AnyView {
+ let title = title.into();
+ let action = action.boxed_clone();
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ cx.new(|cx| Self {
+ title: title.clone(),
+ meta: None,
+ key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
+ })
+ .into()
+ }
+ }
+
pub fn for_action(
title: impl Into<SharedString>,
action: &dyn Action,
@@ -18,7 +18,10 @@ pub use crate::traits::styled_ext::*;
pub use crate::traits::toggleable::*;
pub use crate::traits::visible_on_hover::*;
pub use crate::DynamicSpacing;
-pub use crate::{h_flex, h_group, v_flex, v_group};
+pub use crate::{h_container, h_flex, v_container, v_flex};
+pub use crate::{
+ h_group, h_group_lg, h_group_sm, h_group_xl, v_group, v_group_lg, v_group_sm, v_group_xl,
+};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
pub use crate::{ButtonCommon, Color};
pub use crate::{Headline, HeadlineSize};
@@ -265,7 +265,7 @@ impl Render for WelcomePage {
),
)
.child(
- v_group()
+ v_container()
.gap_2()
.child(
h_flex()
@@ -349,7 +349,11 @@ impl Dock {
.and_then(|index| self.panel_entries.get(index))
}
- pub(crate) fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
+ pub fn active_panel_index(&self) -> Option<usize> {
+ self.active_panel_index
+ }
+
+ pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
if open != self.is_open {
self.is_open = open;
if let Some(active_panel) = self.active_panel_entry() {
@@ -22,6 +22,7 @@ use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag};
use futures::{channel::mpsc, select_biased, StreamExt};
+use git_ui::project_diff::ProjectDiffToolbar;
use gpui::{
actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element,
Entity, Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel,
@@ -927,6 +928,8 @@ fn initialize_pane(
toolbar.add_item(syntax_tree_item, window, cx);
let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
toolbar.add_item(migration_banner, window, cx);
+ let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
+ toolbar.add_item(project_diff_toolbar, window, cx);
})
});
}