diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b690916fd2f..8e547921842 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -5,6 +5,7 @@ use crate::commit_view::CommitView; use crate::git_panel_settings::GitPanelScrollbarAccessor; use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff}; use crate::remote_output::{self, RemoteAction, SuccessMessage}; +use crate::solo_diff_view::SoloDiffView; use crate::{branch_picker, picker_prompt, render_remote_button}; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, @@ -15,10 +16,7 @@ use anyhow::Context as _; use askpass::AskPassDelegate; use collections::{BTreeMap, HashMap, HashSet}; use db::kvp::KeyValueStore; -use editor::{ - Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior, - actions::ExpandAllDiffHunks, -}; +use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior}; use editor::{EditorStyle, RewrapOptions}; use file_icons::FileIcons; use futures::StreamExt as _; @@ -85,7 +83,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME; use workspace::{ Item, Workspace, dock::{DockPosition, Panel, PanelEvent}, - notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt}, + notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt}, }; use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; @@ -1385,63 +1383,22 @@ impl GitPanel { }); } - fn open_file( + fn open_solo_diff( &mut self, _: &menu::SecondaryConfirm, window: &mut Window, cx: &mut Context, ) { maybe!({ - let entry = self.entries.get(self.selected_entry?)?.status_entry()?; - let active_repo = self.active_repository.as_ref()?; - let path = active_repo - .read(cx) - .repo_path_to_project_path(&entry.repo_path, cx)?; - if entry.status.is_deleted() { - return None; - } + let entry = self + .entries + .get(self.selected_entry?)? + .status_entry()? + .clone(); + let repository = self.active_repository.clone()?; - let open_task = self - .workspace - .update(cx, |workspace, cx| { - workspace.open_path_preview(path, None, false, false, true, window, cx) - }) - .ok()?; - - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |_, mut cx| { - let item = open_task - .await - .notify_workspace_async_err(workspace, &mut cx) - .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?; - if let Some(active_editor) = item.downcast::() { - if let Some(diff_task) = - active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load()) - { - diff_task.await; - } - - cx.update(|window, cx| { - active_editor.update(cx, |editor, cx| { - editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx); - - let snapshot = editor.snapshot(window, cx); - editor.go_to_hunk_before_or_after_position( - &snapshot, - language::Point::new(0, 0), - Direction::Next, - true, - window, - cx, - ); - }) - }) - .log_err(); - } - - anyhow::Ok(()) - }) - .detach(); + SoloDiffView::open_or_focus(entry, repository, self.workspace.clone(), window, cx) + .detach_and_notify_err(self.workspace.clone(), window, cx); Some(()) }); @@ -5970,7 +5927,7 @@ impl GitPanel { ) .separator() .action("Open Diff", menu::Confirm.boxed_clone()) - .action("Open File", menu::SecondaryConfirm.boxed_clone()) + .action("Open Diff (File)", menu::SecondaryConfirm.boxed_clone()) .when(!is_created, |context_menu| { context_menu .separator() @@ -6249,7 +6206,7 @@ impl GitPanel { this.selected_entry = Some(ix); cx.notify(); if event.click_count() > 1 || event.modifiers().secondary() { - this.open_file(&Default::default(), window, cx) + this.open_solo_diff(&Default::default(), window, cx) } else { this.open_diff(&Default::default(), window, cx); this.focus_handle.focus(window, cx); @@ -6699,7 +6656,7 @@ impl Render for GitPanel { .on_action(cx.listener(Self::last_entry)) .on_action(cx.listener(Self::close_panel)) .on_action(cx.listener(Self::open_diff)) - .on_action(cx.listener(Self::open_file)) + .on_action(cx.listener(Self::open_solo_diff)) .on_action(cx.listener(Self::focus_changes_list)) .on_action(cx.listener(Self::focus_editor)) .on_action(cx.listener(Self::expand_commit_editor)) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 58b1af74fb6..d19ac552067 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -45,6 +45,7 @@ pub mod picker_prompt; pub mod project_diff; pub(crate) mod remote_output; pub mod repository_selector; +pub mod solo_diff_view; pub mod stash_picker; pub mod text_diff_view; pub mod worktree_names; diff --git a/crates/git_ui/src/solo_diff_view.rs b/crates/git_ui/src/solo_diff_view.rs new file mode 100644 index 00000000000..052206c3293 --- /dev/null +++ b/crates/git_ui/src/solo_diff_view.rs @@ -0,0 +1,787 @@ +use crate::{git_panel::GitStatusEntry, git_status_icon}; +use anyhow::{Context as _, Result}; +use buffer_diff::DiffHunkSecondaryStatus; +use editor::{ + Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, ToggleSplitDiff, + actions::{GoToHunk, GoToPreviousHunk}, +}; +use fs::Fs; +use git::{ + Commit, Restore, StageAndNext, StageFile, ToggleStaged, UnstageAndNext, UnstageFile, + repository::RepoPath, status::StageStatus, +}; +use gpui::{ + Action, AnyElement, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, + Focusable, IntoElement, Render, Subscription, Task, WeakEntity, Window, +}; +use language::{Buffer, HighlightedText}; +use multi_buffer::MultiBuffer; +use project::{ + Project, + git_store::{Repository, RepositoryId}, +}; +use settings::{DiffViewStyle, Settings, SettingsStore, update_settings_file}; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use ui::{ + Color, DiffStat, Divider, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon as _, + SharedString, Tooltip, prelude::*, vertical_divider, +}; +use util::paths::{PathExt as _, PathStyle}; +use workspace::{ + Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + Workspace, + item::{ItemEvent, SaveOptions, TabContentParams}, + notifications::NotifyTaskExt, + searchable::SearchableItemHandle, +}; + +pub struct SoloDiffView { + repository: Entity, + repository_id: RepositoryId, + repo_path: RepoPath, + buffer: Entity, + editor: Entity, + workspace: WeakEntity, + _settings_subscription: Subscription, +} + +impl SoloDiffView { + pub fn open_or_focus( + entry: GitStatusEntry, + repository: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let Some(workspace_entity) = workspace.upgrade() else { + return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); + }; + + let existing = workspace_entity + .read(cx) + .items_of_type::(cx) + .find(|item| item.read(cx).matches(&repository, &entry.repo_path, cx)); + if let Some(existing) = existing { + workspace_entity.update(cx, |workspace, cx| { + workspace.activate_item(&existing, true, true, window, cx); + }); + existing.focus_handle(cx).focus(window, cx); + return Task::ready(Ok(existing)); + } + + let Some(project_path) = repository + .read(cx) + .repo_path_to_project_path(&entry.repo_path, cx) + else { + return Task::ready(Err(anyhow::anyhow!( + "could not resolve repository path {:?}", + entry.repo_path + ))); + }; + + let project = workspace_entity.read(cx).project().clone(); + let repo_path = entry.repo_path; + window.spawn(cx, async move |cx| { + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + }) + .await?; + let diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await?; + + workspace_entity.update_in(cx, |workspace, window, cx| { + let workspace_handle = cx.entity(); + let view = cx.new(|cx| { + Self::new( + project, + repository, + repo_path, + buffer, + diff, + workspace_handle, + window, + cx, + ) + }); + + workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, window, cx); + view + }) + }) + } + + fn new( + project: Entity, + repository: Entity, + repo_path: RepoPath, + buffer: Entity, + diff: Entity, + workspace: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let repository_id = repository.read(cx).id; + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); + multibuffer.add_diff(diff, cx); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer + }); + let editor = cx.new(|cx| { + let editor = SplittableEditor::new( + EditorSettings::get_global(cx).diff_view_style, + multibuffer, + project.clone(), + workspace.clone(), + window, + cx, + ); + editor.rhs_editor().update(cx, |editor, cx| { + editor.set_should_serialize(false, cx); + let snapshot = editor.snapshot(window, cx); + editor.go_to_hunk_before_or_after_position( + &snapshot, + language::Point::new(0, 0), + Direction::Next, + true, + window, + cx, + ); + }); + editor + }); + + let mut previous_diff_view_style = EditorSettings::get_global(cx).diff_view_style; + let settings_subscription = + cx.observe_global_in::(window, move |this, window, cx| { + let diff_view_style = EditorSettings::get_global(cx).diff_view_style; + if diff_view_style != previous_diff_view_style { + this.editor.update(cx, |editor, cx| { + if editor.diff_view_style() != diff_view_style { + editor.toggle_split(&ToggleSplitDiff, window, cx); + } + }); + previous_diff_view_style = diff_view_style; + cx.notify(); + } + }); + + Self { + repository, + repository_id, + repo_path, + buffer, + editor, + workspace: workspace.downgrade(), + _settings_subscription: settings_subscription, + } + } + + fn matches(&self, repository: &Entity, repo_path: &RepoPath, cx: &App) -> bool { + self.repository_id == repository.read(cx).id && &self.repo_path == repo_path + } + + fn button_states(&self, cx: &App) -> SoloDiffButtonStates { + let editor = self.editor.read(cx).rhs_editor().read(cx); + let multibuffer = editor.buffer().read(cx); + let snapshot = multibuffer.snapshot(cx); + let prev_next = snapshot.diff_hunks().nth(1).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; + let anchor = editor.selections.newest_anchor().head(); + if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor) + && let Some(range) = snapshot + .anchor_in_buffer(excerpt_range.context.start) + .zip(snapshot.anchor_in_buffer(excerpt_range.context.end)) + .map(|(start, end)| start..end) + { + ranges = vec![range]; + } else { + ranges = Vec::new(); + } + } + + let mut stage = false; + let mut unstage = false; + for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) { + match hunk.status.secondary { + DiffHunkSecondaryStatus::HasSecondaryHunk + | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { + stage = true; + } + DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => { + stage = true; + unstage = true; + } + DiffHunkSecondaryStatus::NoSecondaryHunk + | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { + unstage = true; + } + } + } + + let stage_status = self + .repository + .read(cx) + .status_for_path(&self.repo_path) + .map(|entry| entry.status.staging()) + .unwrap_or(StageStatus::Unstaged); + + SoloDiffButtonStates { + stage, + unstage, + restore: stage || unstage, + prev_next, + selection, + stage_file: stage_status.has_unstaged(), + unstage_file: stage_status.has_staged(), + } + } + + fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) { + self.focus_handle(cx).focus(window, cx); + let action = action.boxed_clone(); + cx.defer(move |cx| { + cx.dispatch_action(action.as_ref()); + }); + } + + fn change_file_stage(&self, stage: bool, window: &mut Window, cx: &mut Context) { + let repository = self.repository.clone(); + let repo_path = self.repo_path.clone(); + let workspace = self.workspace.clone(); + let task = cx.spawn(async move |_, cx| { + repository + .update(cx, |repository, cx| { + if stage { + repository.stage_entries(vec![repo_path], cx) + } else { + repository.unstage_entries(vec![repo_path], cx) + } + }) + .await + .with_context(|| { + if stage { + "failed to stage file" + } else { + "failed to unstage file" + } + }) + }); + task.detach_and_notify_err(workspace, window, cx); + } +} + +impl EventEmitter for SoloDiffView {} + +impl Focusable for SoloDiffView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Item for SoloDiffView { + type Event = EditorEvent; + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + Some(Icon::new(IconName::Diff).color(Color::Muted)) + } + + fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { + Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx)) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } + + fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { + self.buffer + .read(cx) + .file() + .and_then(|file| { + Some( + file.full_path(cx) + .file_name()? + .to_string_lossy() + .to_string(), + ) + }) + .unwrap_or_else(|| { + self.repo_path + .as_ref() + .display(PathStyle::local()) + .into_owned() + }) + .into() + } + + fn tab_tooltip_text(&self, cx: &App) -> Option { + Some( + self.buffer + .read(cx) + .file() + .map(|file| file.full_path(cx).compact().to_string_lossy().into_owned()) + .unwrap_or_else(|| { + self.repo_path + .as_ref() + .display(PathStyle::local()) + .into_owned() + }) + .into(), + ) + } + + fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("Solo Diff View Opened") + } + + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.deactivated(window, cx); + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + cx: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.clone().into()) + } else { + self.editor.act_as_type(type_id, cx) + } + } + + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn for_each_project_item( + &self, + cx: &App, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), + ) { + self.editor.for_each_project_item(cx, f) + } + + fn set_nav_history( + &mut self, + nav_history: ItemNavHistory, + _: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.rhs_editor().update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }) + }); + } + + fn navigate( + &mut self, + data: Arc, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.editor.update(cx, |editor, cx| { + editor + .rhs_editor() + .update(cx, |editor, cx| editor.navigate(data, window, cx)) + }) + } + + fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft + } + + fn breadcrumbs(&self, cx: &App) -> Option<(Vec, Option)> { + self.editor.breadcrumbs(cx) + } + + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.rhs_editor().update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx) + }) + }); + } + + fn can_save(&self, cx: &App) -> bool { + self.editor.read(cx).rhs_editor().read(cx).can_save(cx) + } + + fn save( + &mut self, + options: SaveOptions, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor.save(options, project, window, cx) + } +} + +impl Render for SoloDiffView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.editor.clone() + } +} + +pub struct SoloDiffStyleToolbar { + solo_diff: Option>, +} + +pub struct SoloDiffGitToolbar { + solo_diff: Option>, +} + +impl SoloDiffStyleToolbar { + pub fn new(_: &mut Context) -> Self { + Self { solo_diff: None } + } + + fn solo_diff(&self) -> Option> { + self.solo_diff.as_ref()?.upgrade() + } + + fn set_diff_view_style( + &mut self, + diff_view_style: DiffViewStyle, + window: &mut Window, + cx: &mut Context, + ) { + let Some(solo_diff) = self.solo_diff() else { + return; + }; + let workspace = solo_diff.read(cx).workspace.clone(); + + update_settings_file(::global(cx), cx, move |settings, _| { + settings.editor.diff_view_style = Some(diff_view_style); + }); + + if let Some(workspace) = workspace.upgrade() { + let splittable_editors = { + workspace + .read(cx) + .items(cx) + .filter_map(|item| item.act_as_type(TypeId::of::(), cx)) + .filter_map(|item| item.downcast::().ok()) + .collect::>() + }; + + for editor in splittable_editors { + editor.update(cx, |editor, cx| { + if editor.diff_view_style() != diff_view_style { + editor.toggle_split(&ToggleSplitDiff, window, cx); + } + }); + } + } + + cx.notify(); + } +} + +impl EventEmitter for SoloDiffStyleToolbar {} + +impl ToolbarItemView for SoloDiffStyleToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.solo_diff = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|entity| entity.downgrade()); + if self.solo_diff.is_some() { + ToolbarItemLocation::PrimaryLeft + } else { + ToolbarItemLocation::Hidden + } + } +} + +impl Render for SoloDiffStyleToolbar { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(solo_diff) = self.solo_diff() else { + return div(); + }; + let editor_entity = solo_diff.read(cx).editor.clone(); + let editor = editor_entity.read(cx); + let diff_view_style = editor.diff_view_style(); + let is_split_set = diff_view_style == DiffViewStyle::Split; + let split_icon = if is_split_set && !editor.is_split() { + IconName::DiffSplitAuto + } else { + IconName::DiffSplit + }; + + h_flex() + .h_8() + .items_center() + .gap_1() + .child( + IconButton::new("solo-diff-unified", IconName::DiffUnified) + .icon_size(IconSize::Small) + .toggle_state(diff_view_style == DiffViewStyle::Unified) + .tooltip(Tooltip::text("Unified")) + .on_click(cx.listener(|this, _, window, cx| { + this.set_diff_view_style(DiffViewStyle::Unified, window, cx); + })), + ) + .child( + IconButton::new("solo-diff-split", split_icon) + .icon_size(IconSize::Small) + .toggle_state(diff_view_style == DiffViewStyle::Split) + .tooltip(Tooltip::text("Split")) + .on_click(cx.listener(|this, _, window, cx| { + this.set_diff_view_style(DiffViewStyle::Split, window, cx); + })), + ) + .child(vertical_divider()) + .child(div().w_1()) + } +} + +impl SoloDiffGitToolbar { + pub fn new(_: &mut Context) -> Self { + Self { solo_diff: None } + } + + fn solo_diff(&self) -> Option> { + self.solo_diff.as_ref()?.upgrade() + } + + fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context) { + if let Some(solo_diff) = self.solo_diff() { + solo_diff.update(cx, |solo_diff, cx| { + solo_diff.dispatch_action(action, window, cx); + }); + } + } + + fn stage_file(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(solo_diff) = self.solo_diff() { + solo_diff.update(cx, |solo_diff, cx| { + solo_diff.change_file_stage(true, window, cx); + }); + } + } + + fn unstage_file(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(solo_diff) = self.solo_diff() { + solo_diff.update(cx, |solo_diff, cx| { + solo_diff.change_file_stage(false, window, cx); + }); + } + } +} + +impl EventEmitter for SoloDiffGitToolbar {} + +impl ToolbarItemView for SoloDiffGitToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.solo_diff = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|entity| entity.downgrade()); + if self.solo_diff.is_some() { + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } +} + +struct SoloDiffButtonStates { + stage: bool, + unstage: bool, + restore: bool, + prev_next: bool, + selection: bool, + stage_file: bool, + unstage_file: bool, +} + +impl Render for SoloDiffGitToolbar { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(solo_diff) = self.solo_diff() else { + return div(); + }; + let focus_handle = solo_diff.focus_handle(cx); + let solo_diff = solo_diff.read(cx); + let button_states = solo_diff.button_states(cx); + let status_entry = solo_diff + .repository + .read(cx) + .status_for_path(&solo_diff.repo_path); + let status = status_entry.as_ref().map(|entry| entry.status); + let diff_stat = status_entry.and_then(|entry| entry.diff_stat); + + h_group_xl() + .my_neg_1() + .py_1() + .items_center() + .flex_wrap() + .justify_between() + .children(status.map(|status| git_status_icon(status).into_any_element())) + .children(diff_stat.map(|stat| { + DiffStat::new("solo-diff-stat", stat.added as usize, stat.deleted as usize) + .into_any_element() + })) + .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 and go to next hunk", + &StageAndNext, + &focus_handle, + )) + .disabled(!button_states.stage) + .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 and go to next hunk", + &UnstageAndNext, + &focus_handle, + )) + .disabled(!button_states.unstage) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&UnstageAndNext, window, cx) + })), + ) + }) + .child( + Button::new("restore", "Restore") + .tooltip(Tooltip::for_action_title_in( + "Restore selected hunk", + &Restore, + &focus_handle, + )) + .disabled(!button_states.restore) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&Restore, window, cx) + })), + ), + ) + .child( + h_group_sm() + .child( + IconButton::new("up", IconName::ArrowUp) + .shape(IconButtonShape::Square) + .tooltip(Tooltip::for_action_title_in( + "Go to previous hunk", + &GoToPreviousHunk, + &focus_handle, + )) + .disabled(!button_states.prev_next) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToPreviousHunk, window, cx) + })), + ) + .child( + IconButton::new("down", IconName::ArrowDown) + .shape(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() + .child( + Button::new("stage-file", "Stage File") + .tooltip(Tooltip::for_action_title_in( + "Stage file", + &StageFile, + &focus_handle, + )) + .disabled(!button_states.stage_file) + .on_click( + cx.listener(|this, _, window, cx| this.stage_file(window, cx)), + ), + ) + .child( + Button::new("unstage-file", "Unstage File") + .tooltip(Tooltip::for_action_title_in( + "Unstage file", + &UnstageFile, + &focus_handle, + )) + .disabled(!button_states.unstage_file) + .on_click( + cx.listener(|this, _, window, cx| this.unstage_file(window, cx)), + ), + ) + .child(Divider::vertical()) + .child( + Button::new("commit", "Commit") + .tooltip(Tooltip::for_action_title_in( + "Commit", + &Commit, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&Commit, window, cx); + })), + ), + ) + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7ddd5215175..f9c04b7c910 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,6 +34,7 @@ use futures::{StreamExt, channel::mpsc, select_biased}; use git_ui::commit_view::CommitViewToolbar; use git_ui::git_panel::GitPanel; use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar}; +use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar}; use gpui::{ Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement, @@ -1305,6 +1306,8 @@ fn initialize_pane( pane.toolbar().update(cx, |toolbar, cx| { let multibuffer_hint = cx.new(|_| MultibufferHint::new()); toolbar.add_item(multibuffer_hint, window, cx); + let solo_diff_style_toolbar = cx.new(SoloDiffStyleToolbar::new); + toolbar.add_item(solo_diff_style_toolbar, window, cx); let breadcrumbs = cx.new(|_| Breadcrumbs::new()); toolbar.add_item(breadcrumbs, window, cx); let buffer_search_bar = cx.new(|cx| { @@ -1343,6 +1346,8 @@ fn initialize_pane( toolbar.add_item(project_diff_toolbar, window, cx); let branch_diff_toolbar = cx.new(BranchDiffToolbar::new); toolbar.add_item(branch_diff_toolbar, window, cx); + let solo_diff_git_toolbar = cx.new(SoloDiffGitToolbar::new); + toolbar.add_item(solo_diff_git_toolbar, window, cx); let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new()); toolbar.add_item(commit_view_toolbar, window, cx); let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); diff --git a/script/update_top_ranking_issues/uv.lock b/script/update_top_ranking_issues/uv.lock index 872bae37e1f..f1b3ec28515 100644 --- a/script/update_top_ranking_issues/uv.lock +++ b/script/update_top_ranking_issues/uv.lock @@ -134,7 +134,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -142,9 +142,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]]