diff --git a/Cargo.lock b/Cargo.lock index 85907443ea35f8e55ea5d59320eba667affb21f1..f7412a5fb88f38164490c7af935156dfee7eb325 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a263b78406c6c2f1140ef352b62fbd9578432521..889fa2e33e09c442b338597cf1e7d5d1199ed856 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba000921e3348209aa2086b6a6de47316373d8c7..a3a4dfd6bbdb5cbb031e3b5b31fdd63cc74bd3d9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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], buffer: &'a MultiBufferSnapshot, @@ -12814,9 +12814,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - 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, ) { - 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, + ) { + let mut ranges = self.selections.disjoint_anchor_ranges().collect::>(); + 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, stage: bool, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 707c03ed95fda080858889f626ff6414005ae3dd..c8e609b5a89f20d7babec40d31d3eb6690a84ec3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, line_height: Pixels, editor: &Entity, @@ -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() } diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs new file mode 100644 index 0000000000000000000000000000000000000000..e61b8faa4a37bc78c1a498f462bd1ada4fc6fb82 --- /dev/null +++ b/crates/git_ui/src/commit_modal.rs @@ -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, + commit_editor: Entity, + restore_dock: RestoreDock, +} + +impl Focusable for CommitModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.commit_editor.focus_handle(cx) + } +} + +impl EventEmitter for CommitModal {} +impl ModalView for CommitModal { + fn on_before_dismiss( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> 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, + is_open: bool, + active_index: Option, +} + +impl CommitModal { + pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { + workspace.register_action(|workspace, _: &Commit, window, cx| { + let Some(git_panel) = workspace.panel::(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::(window, cx); + workspace.toggle_modal(window, cx, move |window, cx| { + CommitModal::new(git_panel, restore_dock_position, window, cx) + }) + }); + } + + fn new( + git_panel: Entity, + restore_dock: RestoreDock, + window: &mut Window, + cx: &mut Context, + ) -> 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, + ) -> 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) -> 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(|| "".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) { + cx.emit(DismissEvent); + } + fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context) { + 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)), + ) + } +} diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ad2884543be509b5c9d4e722310c7bcc44591f64..dbad09afdacdc7deffd04c7f8f7edd9352b8f0c4 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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::(window, cx); }); - workspace.register_action(|workspace, _: &Commit, window, cx| { - workspace.open_panel::(window, cx); - if let Some(git_panel) = workspace.panel::(cx) { - git_panel - .read(cx) - .commit_editor - .focus_handle(cx) - .focus(window); - } - }); + // workspace.register_action(|workspace, _: &Commit, window, cx| { + // workspace.open_panel::(window, cx); + // if let Some(git_panel) = workspace.panel::(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>, + pub(crate) active_repository: Option>, commit_editor: Entity, conflicted_count: usize, conflicted_staged_count: usize, @@ -190,7 +191,7 @@ pub struct GitPanel { pending: Vec, pending_commit: Option>, pending_serialization: Task>, - project: Entity, + pub(crate) project: Entity, repository_selector: Entity, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, @@ -202,17 +203,20 @@ pub struct GitPanel { width: Option, workspace: WeakEntity, context_menu: Option<(Entity, Point, Subscription)>, + modal_open: bool, } -fn commit_message_editor( +pub(crate) fn commit_message_editor( commit_message_buffer: Entity, project: Entity, + 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.modal_open = open; + cx.notify(); + } + fn dispatch_context(&self, window: &mut Window, cx: &Context) -> 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 { + 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) { + pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context) { 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) -> Option { + 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, ) -> 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) -> Option { @@ -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(); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 300c589ecd910c51e29cef22587930310ae8b68d..7cca2b23a59c9287244a18cb9498fcf5df60ec84 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -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 diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 3d092f0d668a652ada77ca9bb60b818cb6f3dbec..ebec4d7848877b530f3b05a1039fc724918cb57f 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -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::>(); + 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::(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, @@ -598,3 +668,226 @@ impl SerializableItem for ProjectDiff { false } } + +pub struct ProjectDiffToolbar { + project_diff: Option>, + workspace: WeakEntity, +} + +impl ProjectDiffToolbar { + pub fn new(workspace: &Workspace, _: &mut Context) -> Self { + Self { + project_diff: None, + workspace: workspace.weak_handle(), + } + } + + fn project_diff(&self, _: &App) -> Option> { + self.project_diff.as_ref()?.upgrade() + } + fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context) { + 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.workspace + .read_with(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(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 for ProjectDiffToolbar {} + +impl ToolbarItemView for ProjectDiffToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.project_diff = active_pane_item + .and_then(|item| item.act_as::(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, + ) { + } +} + +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) -> 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); + })), + ), + ) + } +} diff --git a/crates/git_ui/src/quick_commit.rs b/crates/git_ui/src/quick_commit.rs index cd8a3154963f66b5db0f8e951e935e3436564a4f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/crates/git_ui/src/quick_commit.rs +++ b/crates/git_ui/src/quick_commit.rs @@ -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>, - 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, - project: Entity, - active_repository: Option>, - repository_selector: Entity, - commit_editor: Entity, - width: Option, - commit_task: Task>, - 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 for QuickCommitModal {} -impl ModalView for QuickCommitModal {} - -impl QuickCommitModal { - pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { - 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, - fs: Arc, - window: &mut Window, - commit_message_buffer: Option>, - cx: &mut Context, - ) -> 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) -> 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, - ) -> 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) -> 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) { - 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)) - } -} diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 184d841bb1caf023b8f1989fd091896d43be5d17..4ad58998379a82881dcc330895bb59149048fa49 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -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::*; diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs index 30c115b2a557acb768730c72981f3699b65a297b..e372580745931dc952e47eed49a7c2fd62534cad 100644 --- a/crates/ui/src/components/content_group.rs +++ b/crates/ui/src/components/content_group.rs @@ -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() } diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 2a0f87a610c73d7618580a456fb320fe3cc56754..c6e31ee7deb569fc7e861fe029e65ee85af36ed1 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -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, diff --git a/crates/ui/src/components/group.rs b/crates/ui/src/components/group.rs new file mode 100644 index 0000000000000000000000000000000000000000..f49ab9fe1377eac4390ae1f1ecc7140f4123699d --- /dev/null +++ b/crates/ui/src/components/group.rs @@ -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() +} diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index cd391bb7f1a0ab0307955e1709176adc8145287d..e88f80edeab6f63f17621444af178836ccdef41b 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -52,6 +52,24 @@ impl Tooltip { } } + pub fn for_action_title_in( + title: impl Into, + 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, action: &dyn Action, diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index cecb91ea43381b3da47d1eca3aee147bd9184e53..0b9ce91f1e9f15d7c316d4fb1816f53a3329e7dc 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -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}; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 5e8e4ff314ae0e3ed02d79e2f782e9659051a3de..c95dccb2f7820f3b55d81e121051394654d3c912 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -265,7 +265,7 @@ impl Render for WelcomePage { ), ) .child( - v_group() + v_container() .gap_2() .child( h_flex() diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index af52d4dc162c93a08f07501e1403f10a77286d64..ea0dc4a12c3da266b216cdc8b437940e3d4c84d1 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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) { + pub fn active_panel_index(&self) -> Option { + self.active_panel_index + } + + pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { if open != self.is_open { self.is_open = open; if let Some(active_panel) = self.active_panel_entry() { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index adb1d2b03f490dd0c445cd490bf9914b0fcd8f61..cfd55155799f3f2c00acc9de4a3e03541e5ad20a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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); }) }); }