Continues the work from #35927 to add a git diff view for stash entries. [Screencast From 2025-09-17 19-46-01.webm](https://github.com/user-attachments/assets/ded33782-adef-4696-8e34-3665911c09c7) Stash entries are [represented as commits](https://git-scm.com/docs/git-stash#_discussion) except they have up to 3 parents: ``` .----W (this is the stash entry) / /| -----H----I | \| U ``` Where `H` is the `HEAD` commit, `I` is a commit that records the state of the index, and `U` is another commit that records untracked files (when using `git stash -u`). Given this, I modified the existing commit view struct to allow loading stash and commits entries with git sha identifier so that we can get a similar git diff view for both of them. The stash diff is generated by comparing the stash commit with its parent (`<commit>^` or `H` in the diagram) which generates the same diff as doing `git stash show -p <stash entry>`. This *can* be counter-intuitive since a user may expect the comparison to be made between the stash commit and the current commit (`HEAD`), but given that the default behavior in git cli is to compare with the stash parent, I went for that approach. Hoping to get some feedback from a Zed team member to see if they agree with this approach. Release Notes: - Add git diff view for stash entries - Add toolbar on git diff view for stash entries - Prompt before executing a destructive stash action on diff view - Fix commit view for merge commits (see #38289)
Unified · Split
| @@ -1080,7 +1080,8 @@ · 1079 unmodified lines | |||
| 1080 | { | 1080 | { |
| 1081 | "context": "StashList || (StashList > Picker > Editor)", | 1081 | "context": "StashList || (StashList > Picker > Editor)", |
| 1082 | "bindings": { | 1082 | "bindings": { |
| 1083 | "ctrl-shift-backspace": "stash_picker::DropStashItem" | 1083 | "ctrl-shift-backspace": "stash_picker::DropStashItem", |
| 1084 | "ctrl-shift-v": "stash_picker::ShowStashItem" | ||
| 1084 | } | 1085 | } |
| 1085 | }, | 1086 | }, |
| 1086 | { | 1087 | { |
| @@ -1266,6 +1267,14 @@ · 179 unmodified lines | |||
| 1266 | "ctrl-pagedown": "settings_editor::FocusNextFile" | 1267 | "ctrl-pagedown": "settings_editor::FocusNextFile" |
| 1267 | } | 1268 | } |
| 1268 | }, | 1269 | }, |
| 1270 | { | ||
| 1271 | "context": "StashDiff > Editor", | ||
| 1272 | "bindings": { | ||
| 1273 | "ctrl-space": "git::ApplyCurrentStash", | ||
| 1274 | "ctrl-shift-space": "git::PopCurrentStash", | ||
| 1275 | "ctrl-shift-backspace": "git::DropCurrentStash" | ||
| 1276 | } | ||
| 1277 | }, | ||
| 1269 | { | 1278 | { |
| 1270 | "context": "SettingsWindow > NavigationMenu", | 1279 | "context": "SettingsWindow > NavigationMenu", |
| 1271 | "use_key_equivalents": true, | 1280 | "use_key_equivalents": true, |
| @@ -1153,7 +1153,8 @@ · 1152 unmodified lines | |||
| 1153 | "context": "StashList || (StashList > Picker > Editor)", | 1153 | "context": "StashList || (StashList > Picker > Editor)", |
| 1154 | "use_key_equivalents": true, | 1154 | "use_key_equivalents": true, |
| 1155 | "bindings": { | 1155 | "bindings": { |
| 1156 | "ctrl-shift-backspace": "stash_picker::DropStashItem" | 1156 | "ctrl-shift-backspace": "stash_picker::DropStashItem", |
| 1157 | "ctrl-shift-v": "stash_picker::ShowStashItem" | ||
| 1157 | } | 1158 | } |
| 1158 | }, | 1159 | }, |
| 1159 | { | 1160 | { |
| @@ -1371,6 +1372,15 @@ · 211 unmodified lines | |||
| 1371 | "cmd-}": "settings_editor::FocusNextFile" | 1372 | "cmd-}": "settings_editor::FocusNextFile" |
| 1372 | } | 1373 | } |
| 1373 | }, | 1374 | }, |
| 1375 | { | ||
| 1376 | "context": "StashDiff > Editor", | ||
| 1377 | "use_key_equivalents": true, | ||
| 1378 | "bindings": { | ||
| 1379 | "ctrl-space": "git::ApplyCurrentStash", | ||
| 1380 | "ctrl-shift-space": "git::PopCurrentStash", | ||
| 1381 | "ctrl-shift-backspace": "git::DropCurrentStash" | ||
| 1382 | } | ||
| 1383 | }, | ||
| 1374 | { | 1384 | { |
| 1375 | "context": "SettingsWindow > NavigationMenu", | 1385 | "context": "SettingsWindow > NavigationMenu", |
| 1376 | "use_key_equivalents": true, | 1386 | "use_key_equivalents": true, |
| @@ -1106,7 +1106,8 @@ · 1105 unmodified lines | |||
| 1106 | "context": "StashList || (StashList > Picker > Editor)", | 1106 | "context": "StashList || (StashList > Picker > Editor)", |
| 1107 | "use_key_equivalents": true, | 1107 | "use_key_equivalents": true, |
| 1108 | "bindings": { | 1108 | "bindings": { |
| 1109 | "ctrl-shift-backspace": "stash_picker::DropStashItem" | 1109 | "ctrl-shift-backspace": "stash_picker::DropStashItem", |
| 1110 | "ctrl-shift-v": "stash_picker::ShowStashItem" | ||
| 1110 | } | 1111 | } |
| 1111 | }, | 1112 | }, |
| 1112 | { | 1113 | { |
| @@ -1294,6 +1295,15 @@ · 181 unmodified lines | |||
| 1294 | "ctrl-pagedown": "settings_editor::FocusNextFile" | 1295 | "ctrl-pagedown": "settings_editor::FocusNextFile" |
| 1295 | } | 1296 | } |
| 1296 | }, | 1297 | }, |
| 1298 | { | ||
| 1299 | "context": "StashDiff > Editor", | ||
| 1300 | "use_key_equivalents": true, | ||
| 1301 | "bindings": { | ||
| 1302 | "ctrl-space": "git::ApplyCurrentStash", | ||
| 1303 | "ctrl-shift-space": "git::PopCurrentStash", | ||
| 1304 | "ctrl-shift-backspace": "git::DropCurrentStash" | ||
| 1305 | } | ||
| 1306 | }, | ||
| 1297 | { | 1307 | { |
| 1298 | "context": "SettingsWindow > NavigationMenu", | 1308 | "context": "SettingsWindow > NavigationMenu", |
| 1299 | "use_key_equivalents": true, | 1309 | "use_key_equivalents": true, |
| @@ -693,10 +693,11 @@ impl GitRepository for RealGitRepository { · 692 unmodified lines | |||
| 693 | .args([ | 693 | .args([ |
| 694 | "--no-optional-locks", | 694 | "--no-optional-locks", |
| 695 | "show", | 695 | "show", |
| 696 | "--format=%P", | 696 | "--format=", |
| 697 | "-z", | 697 | "-z", |
| 698 | "--no-renames", | 698 | "--no-renames", |
| 699 | "--name-status", | 699 | "--name-status", |
| 700 | "--first-parent", | ||
| 700 | ]) | 701 | ]) |
| 701 | .arg(&commit) | 702 | .arg(&commit) |
| 702 | .stdin(Stdio::null()) | 703 | .stdin(Stdio::null()) |
| @@ -707,9 +708,8 @@ impl GitRepository for RealGitRepository { · 4 unmodified lines | |||
| 707 | .context("starting git show process")?; | 708 | .context("starting git show process")?; |
| 708 | 709 | ||
| 709 | let show_stdout = String::from_utf8_lossy(&show_output.stdout); | 710 | let show_stdout = String::from_utf8_lossy(&show_output.stdout); |
| 710 | let mut lines = show_stdout.split('\n'); | 711 | let changes = parse_git_diff_name_status(&show_stdout); |
| 711 | let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0'); | 712 | let parent_sha = format!("{}^", commit); |
| 712 | let changes = parse_git_diff_name_status(lines.next().unwrap_or("")); | ||
| 713 | 713 | ||
| 714 | let mut cat_file_process = util::command::new_smol_command(&git_binary_path) | 714 | let mut cat_file_process = util::command::new_smol_command(&git_binary_path) |
| 715 | .current_dir(&working_directory) | 715 | .current_dir(&working_directory) |
| @@ -98,25 +98,10 @@ impl BlameRenderer for GitBlameRenderer { · 97 unmodified lines | |||
| 98 | let workspace = workspace.clone(); | 98 | let workspace = workspace.clone(); |
| 99 | move |_, window, cx| { | 99 | move |_, window, cx| { |
| 100 | CommitView::open( | 100 | CommitView::open( |
| 101 | CommitSummary { | 101 | blame_entry.sha.to_string(), |
| 102 | sha: blame_entry.sha.to_string().into(), | ||
| 103 | subject: blame_entry | ||
| 104 | .summary | ||
| 105 | .clone() | ||
| 106 | .unwrap_or_default() | ||
| 107 | .into(), | ||
| 108 | commit_timestamp: blame_entry | ||
| 109 | .committer_time | ||
| 110 | .unwrap_or_default(), | ||
| 111 | author_name: blame_entry | ||
| 112 | .committer_name | ||
| 113 | .clone() | ||
| 114 | .unwrap_or_default() | ||
| 115 | .into(), | ||
| 116 | has_parent: true, | ||
| 117 | }, | ||
| 118 | repository.downgrade(), | 102 | repository.downgrade(), |
| 119 | workspace.clone(), | 103 | workspace.clone(), |
| 104 | None, | ||
| 120 | window, | 105 | window, |
| 121 | cx, | 106 | cx, |
| 122 | ) | 107 | ) |
| @@ -335,9 +320,10 @@ impl BlameRenderer for GitBlameRenderer { · 212 unmodified lines | |||
| 335 | .icon_size(IconSize::Small) | 320 | .icon_size(IconSize::Small) |
| 336 | .on_click(move |_, window, cx| { | 321 | .on_click(move |_, window, cx| { |
| 337 | CommitView::open( | 322 | CommitView::open( |
| 338 | commit_summary.clone(), | 323 | commit_summary.sha.clone().into(), |
| 339 | repository.downgrade(), | 324 | repository.downgrade(), |
| 340 | workspace.clone(), | 325 | workspace.clone(), |
| 326 | None, | ||
| 341 | window, | 327 | window, |
| 342 | cx, | 328 | cx, |
| 343 | ); | 329 | ); |
| @@ -374,15 +360,10 @@ impl BlameRenderer for GitBlameRenderer { · 30 unmodified lines | |||
| 374 | cx: &mut App, | 360 | cx: &mut App, |
| 375 | ) { | 361 | ) { |
| 376 | CommitView::open( | 362 | CommitView::open( |
| 377 | CommitSummary { | 363 | blame_entry.sha.to_string(), |
| 378 | sha: blame_entry.sha.to_string().into(), | ||
| 379 | subject: blame_entry.summary.clone().unwrap_or_default().into(), | ||
| 380 | commit_timestamp: blame_entry.committer_time.unwrap_or_default(), | ||
| 381 | author_name: blame_entry.committer_name.unwrap_or_default().into(), | ||
| 382 | has_parent: true, | ||
| 383 | }, | ||
| 384 | repository.downgrade(), | 364 | repository.downgrade(), |
| 385 | workspace, | 365 | workspace, |
| 366 | None, | ||
| 386 | window, | 367 | window, |
| 387 | cx, | 368 | cx, |
| 388 | ) | 369 | ) |
| @@ -318,9 +318,10 @@ impl Render for CommitTooltip { · 317 unmodified lines | |||
| 318 | .on_click( | 318 | .on_click( |
| 319 | move |_, window, cx| { | 319 | move |_, window, cx| { |
| 320 | CommitView::open( | 320 | CommitView::open( |
| 321 | commit_summary.clone(), | 321 | commit_summary.sha.to_string(), |
| 322 | repo.downgrade(), | 322 | repo.downgrade(), |
| 323 | workspace.clone(), | 323 | workspace.clone(), |
| 324 | None, | ||
| 324 | window, | 325 | window, |
| 325 | cx, | 326 | cx, |
| 326 | ); | 327 | ); |
| @@ -1,10 +1,11 @@ | |||
| 1 | use anyhow::{Context as _, Result}; | 1 | use anyhow::{Context as _, Result}; |
| 2 | use buffer_diff::{BufferDiff, BufferDiffSnapshot}; | 2 | use buffer_diff::{BufferDiff, BufferDiffSnapshot}; |
| 3 | use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines}; | 3 | use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines}; |
| 4 | use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath}; | 4 | use git::repository::{CommitDetails, CommitDiff, RepoPath}; |
| 5 | use gpui::{ | 5 | use gpui::{ |
| 6 | AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, | 6 | Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, |
| 7 | FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window, | 7 | Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity, |
| 8 | Window, actions, | ||
| 8 | }; | 9 | }; |
| 9 | use language::{ | 10 | use language::{ |
| 10 | Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _, | 11 | Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _, |
| @@ -18,17 +19,42 @@ use std::{ · 7 unmodified lines | |||
| 18 | path::PathBuf, | 19 | path::PathBuf, |
| 19 | sync::Arc, | 20 | sync::Arc, |
| 20 | }; | 21 | }; |
| 21 | use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; | 22 | use ui::{ |
| 23 | Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*, | ||
| 24 | }; | ||
| 22 | use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff}; | 25 | use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff}; |
| 23 | use workspace::{ | 26 | use workspace::{ |
| 24 | Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, | 27 | Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, |
| 28 | Workspace, | ||
| 25 | item::{BreadcrumbText, ItemEvent, TabContentParams}, | 29 | item::{BreadcrumbText, ItemEvent, TabContentParams}, |
| 30 | notifications::NotifyTaskExt, | ||
| 31 | pane::SaveIntent, | ||
| 26 | searchable::SearchableItemHandle, | 32 | searchable::SearchableItemHandle, |
| 27 | }; | 33 | }; |
| 28 | 34 | ||
| 35 | use crate::git_panel::GitPanel; | ||
| 36 | |||
| 37 | actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]); | ||
| 38 | |||
| 39 | pub fn init(cx: &mut App) { | ||
| 40 | cx.observe_new(|workspace: &mut Workspace, _window, _cx| { | ||
| 41 | register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| { | ||
| 42 | toolbar.apply_stash(window, cx); | ||
| 43 | }); | ||
| 44 | register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| { | ||
| 45 | toolbar.remove_stash(window, cx); | ||
| 46 | }); | ||
| 47 | register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| { | ||
| 48 | toolbar.pop_stash(window, cx); | ||
| 49 | }); | ||
| 50 | }) | ||
| 51 | .detach(); | ||
| 52 | } | ||
| 53 | |||
| 29 | pub struct CommitView { | 54 | pub struct CommitView { |
| 30 | commit: CommitDetails, | 55 | commit: CommitDetails, |
| 31 | editor: Entity<Editor>, | 56 | editor: Entity<Editor>, |
| 57 | stash: Option<usize>, | ||
| 32 | multibuffer: Entity<MultiBuffer>, | 58 | multibuffer: Entity<MultiBuffer>, |
| 33 | } | 59 | } |
| 34 | 60 | ||
| @@ -48,17 +74,18 @@ const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; · 13 unmodified lines | |||
| 48 | 74 | ||
| 49 | impl CommitView { | 75 | impl CommitView { |
| 50 | pub fn open( | 76 | pub fn open( |
| 51 | commit: CommitSummary, | 77 | commit_sha: String, |
| 52 | repo: WeakEntity<Repository>, | 78 | repo: WeakEntity<Repository>, |
| 53 | workspace: WeakEntity<Workspace>, | 79 | workspace: WeakEntity<Workspace>, |
| 80 | stash: Option<usize>, | ||
| 54 | window: &mut Window, | 81 | window: &mut Window, |
| 55 | cx: &mut App, | 82 | cx: &mut App, |
| 56 | ) { | 83 | ) { |
| 57 | let commit_diff = repo | 84 | let commit_diff = repo |
| 58 | .update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string())) | 85 | .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone())) |
| 59 | .ok(); | 86 | .ok(); |
| 60 | let commit_details = repo | 87 | let commit_details = repo |
| 61 | .update(cx, |repo, _| repo.show(commit.sha.to_string())) | 88 | .update(cx, |repo, _| repo.show(commit_sha.clone())) |
| 62 | .ok(); | 89 | .ok(); |
| 63 | 90 | ||
| 64 | window | 91 | window |
| @@ -77,6 +104,7 @@ impl CommitView { · 12 unmodified lines | |||
| 77 | commit_diff, | 104 | commit_diff, |
| 78 | repo, | 105 | repo, |
| 79 | project.clone(), | 106 | project.clone(), |
| 107 | stash, | ||
| 80 | window, | 108 | window, |
| 81 | cx, | 109 | cx, |
| 82 | ) | 110 | ) |
| @@ -87,7 +115,7 @@ impl CommitView { · 4 unmodified lines | |||
| 87 | let ix = pane.items().position(|item| { | 115 | let ix = pane.items().position(|item| { |
| 88 | let commit_view = item.downcast::<CommitView>(); | 116 | let commit_view = item.downcast::<CommitView>(); |
| 89 | commit_view | 117 | commit_view |
| 90 | .is_some_and(|view| view.read(cx).commit.sha == commit.sha) | 118 | .is_some_and(|view| view.read(cx).commit.sha == commit_sha) |
| 91 | }); | 119 | }); |
| 92 | if let Some(ix) = ix { | 120 | if let Some(ix) = ix { |
| 93 | pane.activate_item(ix, true, true, window, cx); | 121 | pane.activate_item(ix, true, true, window, cx); |
| @@ -106,6 +134,7 @@ impl CommitView { · 12 unmodified lines | |||
| 106 | commit_diff: CommitDiff, | 134 | commit_diff: CommitDiff, |
| 107 | repository: Entity<Repository>, | 135 | repository: Entity<Repository>, |
| 108 | project: Entity<Project>, | 136 | project: Entity<Project>, |
| 137 | stash: Option<usize>, | ||
| 109 | window: &mut Window, | 138 | window: &mut Window, |
| 110 | cx: &mut Context<Self>, | 139 | cx: &mut Context<Self>, |
| 111 | ) -> Self { | 140 | ) -> Self { |
| @@ -127,10 +156,13 @@ impl CommitView { · 15 unmodified lines | |||
| 127 | 156 | ||
| 128 | let mut metadata_buffer_id = None; | 157 | let mut metadata_buffer_id = None; |
| 129 | if let Some(worktree_id) = first_worktree_id { | 158 | if let Some(worktree_id) = first_worktree_id { |
| 159 | let title = if let Some(stash) = stash { | ||
| 160 | format!("stash@{{{}}}", stash) | ||
| 161 | } else { | ||
| 162 | format!("commit {}", commit.sha) | ||
| 163 | }; | ||
| 130 | let file = Arc::new(CommitMetadataFile { | 164 | let file = Arc::new(CommitMetadataFile { |
| 131 | title: RelPath::unix(&format!("commit {}", commit.sha)) | 165 | title: RelPath::unix(&title).unwrap().into(), |
| 132 | .unwrap() | ||
| 133 | .into(), | ||
| 134 | worktree_id, | 166 | worktree_id, |
| 135 | }); | 167 | }); |
| 136 | let buffer = cx.new(|cx| { | 168 | let buffer = cx.new(|cx| { |
| @@ -138,7 +170,7 @@ impl CommitView { · 1 unmodified line | |||
| 138 | ReplicaId::LOCAL, | 170 | ReplicaId::LOCAL, |
| 139 | cx.entity_id().as_non_zero_u64().into(), | 171 | cx.entity_id().as_non_zero_u64().into(), |
| 140 | LineEnding::default(), | 172 | LineEnding::default(), |
| 141 | format_commit(&commit).into(), | 173 | format_commit(&commit, stash.is_some()).into(), |
| 142 | ); | 174 | ); |
| 143 | metadata_buffer_id = Some(buffer.remote_id()); | 175 | metadata_buffer_id = Some(buffer.remote_id()); |
| 144 | Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite) | 176 | Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite) |
| @@ -211,6 +243,7 @@ impl CommitView { · 66 unmodified lines | |||
| 211 | commit, | 243 | commit, |
| 212 | editor, | 244 | editor, |
| 213 | multibuffer, | 245 | multibuffer, |
| 246 | stash, | ||
| 214 | } | 247 | } |
| 215 | } | 248 | } |
| 216 | } | 249 | } |
| @@ -369,9 +402,13 @@ async fn build_buffer_diff( · 152 unmodified lines | |||
| 369 | }) | 402 | }) |
| 370 | } | 403 | } |
| 371 | 404 | ||
| 372 | fn format_commit(commit: &CommitDetails) -> String { | 405 | fn format_commit(commit: &CommitDetails, is_stash: bool) -> String { |
| 373 | let mut result = String::new(); | 406 | let mut result = String::new(); |
| 374 | writeln!(&mut result, "commit {}", commit.sha).unwrap(); | 407 | if is_stash { |
| 408 | writeln!(&mut result, "stash commit {}", commit.sha).unwrap(); | ||
| 409 | } else { | ||
| 410 | writeln!(&mut result, "commit {}", commit.sha).unwrap(); | ||
| 411 | } | ||
| 375 | writeln!( | 412 | writeln!( |
| 376 | &mut result, | 413 | &mut result, |
| 377 | "Author: {} <{}>", | 414 | "Author: {} <{}>", |
| @@ -538,13 +575,296 @@ impl Item for CommitView { · 160 unmodified lines | |||
| 538 | editor, | 575 | editor, |
| 539 | multibuffer, | 576 | multibuffer, |
| 540 | commit: self.commit.clone(), | 577 | commit: self.commit.clone(), |
| 578 | stash: self.stash, | ||
| 541 | } | 579 | } |
| 542 | })) | 580 | })) |
| 543 | } | 581 | } |
| 544 | } | 582 | } |
| 545 | 583 | ||
| 546 | impl Render for CommitView { | 584 | impl Render for CommitView { |
| 547 | fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { | 585 | fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { |
| 548 | self.editor.clone() | 586 | let is_stash = self.stash.is_some(); |
| 587 | div() | ||
| 588 | .key_context(if is_stash { "StashDiff" } else { "CommitDiff" }) | ||
| 589 | .bg(cx.theme().colors().editor_background) | ||
| 590 | .flex() | ||
| 591 | .items_center() | ||
| 592 | .justify_center() | ||
| 593 | .size_full() | ||
| 594 | .child(self.editor.clone()) | ||
| 595 | } | ||
| 596 | } | ||
| 597 | |||
| 598 | pub struct CommitViewToolbar { | ||
| 599 | commit_view: Option<WeakEntity<CommitView>>, | ||
| 600 | workspace: WeakEntity<Workspace>, | ||
| 601 | } | ||
| 602 | |||
| 603 | impl CommitViewToolbar { | ||
| 604 | pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self { | ||
| 605 | Self { | ||
| 606 | commit_view: None, | ||
| 607 | workspace: workspace.weak_handle(), | ||
| 608 | } | ||
| 609 | } | ||
| 610 | |||
| 611 | fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> { | ||
| 612 | self.commit_view.as_ref()?.upgrade() | ||
| 613 | } | ||
| 614 | |||
| 615 | async fn close_commit_view( | ||
| 616 | commit_view: Entity<CommitView>, | ||
| 617 | workspace: WeakEntity<Workspace>, | ||
| 618 | cx: &mut AsyncWindowContext, | ||
| 619 | ) -> anyhow::Result<()> { | ||
| 620 | workspace | ||
| 621 | .update_in(cx, |workspace, window, cx| { | ||
| 622 | let active_pane = workspace.active_pane(); | ||
| 623 | let commit_view_id = commit_view.entity_id(); | ||
| 624 | active_pane.update(cx, |pane, cx| { | ||
| 625 | pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx) | ||
| 626 | }) | ||
| 627 | })? | ||
| 628 | .await?; | ||
| 629 | anyhow::Ok(()) | ||
| 630 | } | ||
| 631 | |||
| 632 | fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) { | ||
| 633 | self.stash_action( | ||
| 634 | "Apply", | ||
| 635 | window, | ||
| 636 | cx, | ||
| 637 | async move |repository, sha, stash, commit_view, workspace, cx| { | ||
| 638 | let result = repository.update(cx, |repo, cx| { | ||
| 639 | if !stash_matches_index(&sha, stash, repo) { | ||
| 640 | return Err(anyhow::anyhow!("Stash has changed, not applying")); | ||
| 641 | } | ||
| 642 | Ok(repo.stash_apply(Some(stash), cx)) | ||
| 643 | })?; | ||
| 644 | |||
| 645 | match result { | ||
| 646 | Ok(task) => task.await?, | ||
| 647 | Err(err) => { | ||
| 648 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 649 | return Err(err); | ||
| 650 | } | ||
| 651 | }; | ||
| 652 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 653 | anyhow::Ok(()) | ||
| 654 | }, | ||
| 655 | ); | ||
| 656 | } | ||
| 657 | |||
| 658 | fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) { | ||
| 659 | self.stash_action( | ||
| 660 | "Pop", | ||
| 661 | window, | ||
| 662 | cx, | ||
| 663 | async move |repository, sha, stash, commit_view, workspace, cx| { | ||
| 664 | let result = repository.update(cx, |repo, cx| { | ||
| 665 | if !stash_matches_index(&sha, stash, repo) { | ||
| 666 | return Err(anyhow::anyhow!("Stash has changed, pop aborted")); | ||
| 667 | } | ||
| 668 | Ok(repo.stash_pop(Some(stash), cx)) | ||
| 669 | })?; | ||
| 670 | |||
| 671 | match result { | ||
| 672 | Ok(task) => task.await?, | ||
| 673 | Err(err) => { | ||
| 674 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 675 | return Err(err); | ||
| 676 | } | ||
| 677 | }; | ||
| 678 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 679 | anyhow::Ok(()) | ||
| 680 | }, | ||
| 681 | ); | ||
| 682 | } | ||
| 683 | |||
| 684 | fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) { | ||
| 685 | self.stash_action( | ||
| 686 | "Drop", | ||
| 687 | window, | ||
| 688 | cx, | ||
| 689 | async move |repository, sha, stash, commit_view, workspace, cx| { | ||
| 690 | let result = repository.update(cx, |repo, cx| { | ||
| 691 | if !stash_matches_index(&sha, stash, repo) { | ||
| 692 | return Err(anyhow::anyhow!("Stash has changed, drop aborted")); | ||
| 693 | } | ||
| 694 | Ok(repo.stash_drop(Some(stash), cx)) | ||
| 695 | })?; | ||
| 696 | |||
| 697 | match result { | ||
| 698 | Ok(task) => task.await??, | ||
| 699 | Err(err) => { | ||
| 700 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 701 | return Err(err); | ||
| 702 | } | ||
| 703 | }; | ||
| 704 | Self::close_commit_view(commit_view, workspace, cx).await?; | ||
| 705 | anyhow::Ok(()) | ||
| 706 | }, | ||
| 707 | ); | ||
| 708 | } | ||
| 709 | |||
| 710 | fn stash_action<AsyncFn>( | ||
| 711 | &mut self, | ||
| 712 | str_action: &str, | ||
| 713 | window: &mut Window, | ||
| 714 | cx: &mut Context<Self>, | ||
| 715 | callback: AsyncFn, | ||
| 716 | ) where | ||
| 717 | AsyncFn: AsyncFnOnce( | ||
| 718 | Entity<Repository>, | ||
| 719 | &SharedString, | ||
| 720 | usize, | ||
| 721 | Entity<CommitView>, | ||
| 722 | WeakEntity<Workspace>, | ||
| 723 | &mut AsyncWindowContext, | ||
| 724 | ) -> anyhow::Result<()> | ||
| 725 | + 'static, | ||
| 726 | { | ||
| 727 | let Some(commit_view) = self.commit_view(cx) else { | ||
| 728 | return; | ||
| 729 | }; | ||
| 730 | let Some(stash) = commit_view.read(cx).stash else { | ||
| 731 | return; | ||
| 732 | }; | ||
| 733 | let sha = commit_view.read(cx).commit.sha.clone(); | ||
| 734 | let answer = window.prompt( | ||
| 735 | PromptLevel::Info, | ||
| 736 | &format!("{} stash@{{{}}}?", str_action, stash), | ||
| 737 | None, | ||
| 738 | &[str_action, "Cancel"], | ||
| 739 | cx, | ||
| 740 | ); | ||
| 741 | |||
| 742 | let workspace = self.workspace.clone(); | ||
| 743 | cx.spawn_in(window, async move |_, cx| { | ||
| 744 | if answer.await != Ok(0) { | ||
| 745 | return anyhow::Ok(()); | ||
| 746 | } | ||
| 747 | let repo = workspace.update(cx, |workspace, cx| { | ||
| 748 | workspace | ||
| 749 | .panel::<GitPanel>(cx) | ||
| 750 | .and_then(|p| p.read(cx).active_repository.clone()) | ||
| 751 | })?; | ||
| 752 | |||
| 753 | let Some(repo) = repo else { | ||
| 754 | return Ok(()); | ||
| 755 | }; | ||
| 756 | callback(repo, &sha, stash, commit_view, workspace, cx).await?; | ||
| 757 | anyhow::Ok(()) | ||
| 758 | }) | ||
| 759 | .detach_and_notify_err(window, cx); | ||
| 760 | } | ||
| 761 | } | ||
| 762 | |||
| 763 | impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {} | ||
| 764 | |||
| 765 | impl ToolbarItemView for CommitViewToolbar { | ||
| 766 | fn set_active_pane_item( | ||
| 767 | &mut self, | ||
| 768 | active_pane_item: Option<&dyn ItemHandle>, | ||
| 769 | _: &mut Window, | ||
| 770 | cx: &mut Context<Self>, | ||
| 771 | ) -> ToolbarItemLocation { | ||
| 772 | if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx)) | ||
| 773 | && entity.read(cx).stash.is_some() | ||
| 774 | { | ||
| 775 | self.commit_view = Some(entity.downgrade()); | ||
| 776 | return ToolbarItemLocation::PrimaryRight; | ||
| 777 | } | ||
| 778 | ToolbarItemLocation::Hidden | ||
| 779 | } | ||
| 780 | |||
| 781 | fn pane_focus_update( | ||
| 782 | &mut self, | ||
| 783 | _pane_focused: bool, | ||
| 784 | _window: &mut Window, | ||
| 785 | _cx: &mut Context<Self>, | ||
| 786 | ) { | ||
| 787 | } | ||
| 788 | } | ||
| 789 | |||
| 790 | impl Render for CommitViewToolbar { | ||
| 791 | fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 792 | let Some(commit_view) = self.commit_view(cx) else { | ||
| 793 | return div(); | ||
| 794 | }; | ||
| 795 | |||
| 796 | let is_stash = commit_view.read(cx).stash.is_some(); | ||
| 797 | if !is_stash { | ||
| 798 | return div(); | ||
| 799 | } | ||
| 800 | |||
| 801 | let focus_handle = commit_view.focus_handle(cx); | ||
| 802 | |||
| 803 | h_group_xl().my_neg_1().py_1().items_center().child( | ||
| 804 | h_group_sm() | ||
| 805 | .child( | ||
| 806 | Button::new("apply-stash", "Apply") | ||
| 807 | .tooltip(Tooltip::for_action_title_in( | ||
| 808 | "Apply current stash", | ||
| 809 | &ApplyCurrentStash, | ||
| 810 | &focus_handle, | ||
| 811 | )) | ||
| 812 | .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))), | ||
| 813 | ) | ||
| 814 | .child( | ||
| 815 | Button::new("pop-stash", "Pop") | ||
| 816 | .tooltip(Tooltip::for_action_title_in( | ||
| 817 | "Pop current stash", | ||
| 818 | &PopCurrentStash, | ||
| 819 | &focus_handle, | ||
| 820 | )) | ||
| 821 | .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))), | ||
| 822 | ) | ||
| 823 | .child( | ||
| 824 | Button::new("remove-stash", "Remove") | ||
| 825 | .icon(IconName::Trash) | ||
| 826 | .tooltip(Tooltip::for_action_title_in( | ||
| 827 | "Remove current stash", | ||
| 828 | &DropCurrentStash, | ||
| 829 | &focus_handle, | ||
| 830 | )) | ||
| 831 | .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))), | ||
| 832 | ), | ||
| 833 | ) | ||
| 834 | } | ||
| 835 | } | ||
| 836 | |||
| 837 | fn register_workspace_action<A: Action>( | ||
| 838 | workspace: &mut Workspace, | ||
| 839 | callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>), | ||
| 840 | ) { | ||
| 841 | workspace.register_action(move |workspace, action: &A, window, cx| { | ||
| 842 | if workspace.has_active_modal(window, cx) { | ||
| 843 | cx.propagate(); | ||
| 844 | return; | ||
| 845 | } | ||
| 846 | |||
| 847 | workspace.active_pane().update(cx, |pane, cx| { | ||
| 848 | pane.toolbar().update(cx, move |workspace, cx| { | ||
| 849 | if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() { | ||
| 850 | toolbar.update(cx, move |toolbar, cx| { | ||
| 851 | callback(toolbar, action, window, cx); | ||
| 852 | cx.notify(); | ||
| 853 | }); | ||
| 854 | } | ||
| 855 | }); | ||
| 856 | }) | ||
| 857 | }); | ||
| 858 | } | ||
| 859 | |||
| 860 | fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool { | ||
| 861 | match repo | ||
| 862 | .cached_stash() | ||
| 863 | .entries | ||
| 864 | .iter() | ||
| 865 | .find(|entry| entry.index == index) | ||
| 866 | { | ||
| 867 | Some(entry) => entry.oid.to_string() == sha, | ||
| 868 | None => false, | ||
| 549 | } | 869 | } |
| 550 | } | 870 | } |
| @@ -3611,9 +3611,10 @@ impl GitPanel { · 3610 unmodified lines | |||
| 3611 | let repo = active_repository.downgrade(); | 3611 | let repo = active_repository.downgrade(); |
| 3612 | move |_, window, cx| { | 3612 | move |_, window, cx| { |
| 3613 | CommitView::open( | 3613 | CommitView::open( |
| 3614 | commit.clone(), | 3614 | commit.sha.to_string(), |
| 3615 | repo.clone(), | 3615 | repo.clone(), |
| 3616 | workspace.clone(), | 3616 | workspace.clone(), |
| 3617 | None, | ||
| 3617 | window, | 3618 | window, |
| 3618 | cx, | 3619 | cx, |
| 3619 | ); | 3620 | ); |
| @@ -34,7 +34,7 @@ mod askpass_modal; · 33 unmodified lines | |||
| 34 | pub mod branch_picker; | 34 | pub mod branch_picker; |
| 35 | mod commit_modal; | 35 | mod commit_modal; |
| 36 | pub mod commit_tooltip; | 36 | pub mod commit_tooltip; |
| 37 | mod commit_view; | 37 | pub mod commit_view; |
| 38 | mod conflict_view; | 38 | mod conflict_view; |
| 39 | pub mod file_diff_view; | 39 | pub mod file_diff_view; |
| 40 | pub mod git_panel; | 40 | pub mod git_panel; |
| @@ -59,6 +59,7 @@ pub fn init(cx: &mut App) { · 18 unmodified lines | |||
| 59 | GitPanelSettings::register(cx); | 59 | GitPanelSettings::register(cx); |
| 60 | 60 | ||
| 61 | editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx); | 61 | editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx); |
| 62 | commit_view::init(cx); | ||
| 62 | 63 | ||
| 63 | cx.observe_new(|editor: &mut Editor, _, cx| { | 64 | cx.observe_new(|editor: &mut Editor, _, cx| { |
| 64 | conflict_view::register_editor(editor, editor.buffer().clone(), cx); | 65 | conflict_view::register_editor(editor, editor.buffer().clone(), cx); |
| @@ -5,18 +5,21 @@ use git::stash::StashEntry; · 4 unmodified lines | |||
| 5 | use gpui::{ | 5 | use gpui::{ |
| 6 | Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, | 6 | Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, |
| 7 | InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, | 7 | InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, |
| 8 | SharedString, Styled, Subscription, Task, Window, actions, rems, | 8 | SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg, |
| 9 | }; | 9 | }; |
| 10 | use picker::{Picker, PickerDelegate}; | 10 | use picker::{Picker, PickerDelegate}; |
| 11 | use project::git_store::{Repository, RepositoryEvent}; | 11 | use project::git_store::{Repository, RepositoryEvent}; |
| 12 | use std::sync::Arc; | 12 | use std::sync::Arc; |
| 13 | use time::{OffsetDateTime, UtcOffset}; | 13 | use time::{OffsetDateTime, UtcOffset}; |
| 14 | use time_format; | 14 | use time_format; |
| 15 | use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; | 15 | use ui::{ |
| 16 | ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, | ||
| 17 | }; | ||
| 16 | use util::ResultExt; | 18 | use util::ResultExt; |
| 17 | use workspace::notifications::DetachAndPromptErr; | 19 | use workspace::notifications::DetachAndPromptErr; |
| 18 | use workspace::{ModalView, Workspace}; | 20 | use workspace::{ModalView, Workspace}; |
| 19 | 21 | ||
| 22 | use crate::commit_view::CommitView; | ||
| 20 | use crate::stash_picker; | 23 | use crate::stash_picker; |
| 21 | 24 | ||
| 22 | actions!( | 25 | actions!( |
| @@ -24,6 +27,8 @@ actions!( · 1 unmodified line | |||
| 24 | [ | 27 | [ |
| 25 | /// Drop the selected stash entry. | 28 | /// Drop the selected stash entry. |
| 26 | DropStashItem, | 29 | DropStashItem, |
| 30 | /// Show the diff view of the selected stash entry. | ||
| 31 | ShowStashItem, | ||
| 27 | ] | 32 | ] |
| 28 | ); | 33 | ); |
| 29 | 34 | ||
| @@ -38,8 +43,9 @@ pub fn open( · 8 unmodified lines | |||
| 38 | cx: &mut Context<Workspace>, | 43 | cx: &mut Context<Workspace>, |
| 39 | ) { | 44 | ) { |
| 40 | let repository = workspace.project().read(cx).active_repository(cx); | 45 | let repository = workspace.project().read(cx).active_repository(cx); |
| 46 | let weak_workspace = workspace.weak_handle(); | ||
| 41 | workspace.toggle_modal(window, cx, |window, cx| { | 47 | workspace.toggle_modal(window, cx, |window, cx| { |
| 42 | StashList::new(repository, rems(34.), window, cx) | 48 | StashList::new(repository, weak_workspace, rems(34.), window, cx) |
| 43 | }) | 49 | }) |
| 44 | } | 50 | } |
| 45 | 51 | ||
| @@ -53,6 +59,7 @@ pub struct StashList { · 7 unmodified lines | |||
| 53 | impl StashList { | 59 | impl StashList { |
| 54 | fn new( | 60 | fn new( |
| 55 | repository: Option<Entity<Repository>>, | 61 | repository: Option<Entity<Repository>>, |
| 62 | workspace: WeakEntity<Workspace>, | ||
| 56 | width: Rems, | 63 | width: Rems, |
| 57 | window: &mut Window, | 64 | window: &mut Window, |
| 58 | cx: &mut Context<Self>, | 65 | cx: &mut Context<Self>, |
| @@ -98,7 +105,7 @@ impl StashList { · 39 unmodified lines | |||
| 98 | }) | 105 | }) |
| 99 | .detach_and_log_err(cx); | 106 | .detach_and_log_err(cx); |
| 100 | 107 | ||
| 101 | let delegate = StashListDelegate::new(repository, window, cx); | 108 | let delegate = StashListDelegate::new(repository, workspace, window, cx); |
| 102 | let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); | 109 | let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); |
| 103 | let picker_focus_handle = picker.focus_handle(cx); | 110 | let picker_focus_handle = picker.focus_handle(cx); |
| 104 | picker.update(cx, |picker, _| { | 111 | picker.update(cx, |picker, _| { |
| @@ -131,6 +138,20 @@ impl StashList { · 26 unmodified lines | |||
| 131 | cx.notify(); | 138 | cx.notify(); |
| 132 | } | 139 | } |
| 133 | 140 | ||
| 141 | fn handle_show_stash( | ||
| 142 | &mut self, | ||
| 143 | _: &ShowStashItem, | ||
| 144 | window: &mut Window, | ||
| 145 | cx: &mut Context<Self>, | ||
| 146 | ) { | ||
| 147 | self.picker.update(cx, |picker, cx| { | ||
| 148 | picker | ||
| 149 | .delegate | ||
| 150 | .show_stash_at(picker.delegate.selected_index(), window, cx); | ||
| 151 | }); | ||
| 152 | cx.notify(); | ||
| 153 | } | ||
| 154 | |||
| 134 | fn handle_modifiers_changed( | 155 | fn handle_modifiers_changed( |
| 135 | &mut self, | 156 | &mut self, |
| 136 | ev: &ModifiersChangedEvent, | 157 | ev: &ModifiersChangedEvent, |
| @@ -157,6 +178,7 @@ impl Render for StashList { · 20 unmodified lines | |||
| 157 | .w(self.width) | 178 | .w(self.width) |
| 158 | .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) | 179 | .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) |
| 159 | .on_action(cx.listener(Self::handle_drop_stash)) | 180 | .on_action(cx.listener(Self::handle_drop_stash)) |
| 181 | .on_action(cx.listener(Self::handle_show_stash)) | ||
| 160 | .child(self.picker.clone()) | 182 | .child(self.picker.clone()) |
| 161 | } | 183 | } |
| 162 | } | 184 | } |
| @@ -172,6 +194,7 @@ pub struct StashListDelegate { · 9 unmodified lines | |||
| 172 | matches: Vec<StashEntryMatch>, | 194 | matches: Vec<StashEntryMatch>, |
| 173 | all_stash_entries: Option<Vec<StashEntry>>, | 195 | all_stash_entries: Option<Vec<StashEntry>>, |
| 174 | repo: Option<Entity<Repository>>, | 196 | repo: Option<Entity<Repository>>, |
| 197 | workspace: WeakEntity<Workspace>, | ||
| 175 | selected_index: usize, | 198 | selected_index: usize, |
| 176 | last_query: String, | 199 | last_query: String, |
| 177 | modifiers: Modifiers, | 200 | modifiers: Modifiers, |
| @@ -182,6 +205,7 @@ pub struct StashListDelegate { · 4 unmodified lines | |||
| 182 | impl StashListDelegate { | 205 | impl StashListDelegate { |
| 183 | fn new( | 206 | fn new( |
| 184 | repo: Option<Entity<Repository>>, | 207 | repo: Option<Entity<Repository>>, |
| 208 | workspace: WeakEntity<Workspace>, | ||
| 185 | _window: &mut Window, | 209 | _window: &mut Window, |
| 186 | cx: &mut Context<StashList>, | 210 | cx: &mut Context<StashList>, |
| 187 | ) -> Self { | 211 | ) -> Self { |
| @@ -192,6 +216,7 @@ impl StashListDelegate { · 4 unmodified lines | |||
| 192 | Self { | 216 | Self { |
| 193 | matches: vec![], | 217 | matches: vec![], |
| 194 | repo, | 218 | repo, |
| 219 | workspace, | ||
| 195 | all_stash_entries: None, | 220 | all_stash_entries: None, |
| 196 | selected_index: 0, | 221 | selected_index: 0, |
| 197 | last_query: Default::default(), | 222 | last_query: Default::default(), |
| @@ -235,6 +260,25 @@ impl StashListDelegate { · 37 unmodified lines | |||
| 235 | }); | 260 | }); |
| 236 | } | 261 | } |
| 237 | 262 | ||
| 263 | fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) { | ||
| 264 | let Some(entry_match) = self.matches.get(ix) else { | ||
| 265 | return; | ||
| 266 | }; | ||
| 267 | let stash_sha = entry_match.entry.oid.to_string(); | ||
| 268 | let stash_index = entry_match.entry.index; | ||
| 269 | let Some(repo) = self.repo.clone() else { | ||
| 270 | return; | ||
| 271 | }; | ||
| 272 | CommitView::open( | ||
| 273 | stash_sha, | ||
| 274 | repo.downgrade(), | ||
| 275 | self.workspace.clone(), | ||
| 276 | Some(stash_index), | ||
| 277 | window, | ||
| 278 | cx, | ||
| 279 | ); | ||
| 280 | } | ||
| 281 | |||
| 238 | fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) { | 282 | fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) { |
| 239 | let Some(repo) = self.repo.clone() else { | 283 | let Some(repo) = self.repo.clone() else { |
| 240 | return; | 284 | return; |
| @@ -390,7 +434,7 @@ impl PickerDelegate for StashListDelegate { · 149 unmodified lines | |||
| 390 | ix: usize, | 434 | ix: usize, |
| 391 | selected: bool, | 435 | selected: bool, |
| 392 | _window: &mut Window, | 436 | _window: &mut Window, |
| 393 | _cx: &mut Context<Picker<Self>>, | 437 | cx: &mut Context<Picker<Self>>, |
| 394 | ) -> Option<Self::ListItem> { | 438 | ) -> Option<Self::ListItem> { |
| 395 | let entry_match = &self.matches[ix]; | 439 | let entry_match = &self.matches[ix]; |
| 396 | 440 | ||
| @@ -432,11 +476,35 @@ impl PickerDelegate for StashListDelegate { · 35 unmodified lines | |||
| 432 | .size(LabelSize::Small), | 476 | .size(LabelSize::Small), |
| 433 | ); | 477 | ); |
| 434 | 478 | ||
| 479 | let show_button = div() | ||
| 480 | .group("show-button-hover") | ||
| 481 | .child( | ||
| 482 | ButtonLike::new("show-button") | ||
| 483 | .child( | ||
| 484 | svg() | ||
| 485 | .size(IconSize::Medium.rems()) | ||
| 486 | .flex_none() | ||
| 487 | .path(IconName::Eye.path()) | ||
| 488 | .text_color(Color::Default.color(cx)) | ||
| 489 | .group_hover("show-button-hover", |this| { | ||
| 490 | this.text_color(Color::Accent.color(cx)) | ||
| 491 | }) | ||
| 492 | .hover(|this| this.text_color(Color::Accent.color(cx))), | ||
| 493 | ) | ||
| 494 | .tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem)) | ||
| 495 | .on_click(cx.listener(move |picker, _, window, cx| { | ||
| 496 | cx.stop_propagation(); | ||
| 497 | picker.delegate.show_stash_at(ix, window, cx); | ||
| 498 | })), | ||
| 499 | ) | ||
| 500 | .into_any_element(); | ||
| 501 | |||
| 435 | Some( | 502 | Some( |
| 436 | ListItem::new(SharedString::from(format!("stash-{ix}"))) | 503 | ListItem::new(SharedString::from(format!("stash-{ix}"))) |
| 437 | .inset(true) | 504 | .inset(true) |
| 438 | .spacing(ListItemSpacing::Sparse) | 505 | .spacing(ListItemSpacing::Sparse) |
| 439 | .toggle_state(selected) | 506 | .toggle_state(selected) |
| 507 | .end_slot(show_button) | ||
| 440 | .child( | 508 | .child( |
| 441 | v_flex() | 509 | v_flex() |
| 442 | .w_full() | 510 | .w_full() |
| @@ -25,6 +25,7 @@ use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; · 24 unmodified lines | |||
| 25 | use fs::Fs; | 25 | use fs::Fs; |
| 26 | use futures::future::Either; | 26 | use futures::future::Either; |
| 27 | use futures::{StreamExt, channel::mpsc, select_biased}; | 27 | use futures::{StreamExt, channel::mpsc, select_biased}; |
| 28 | use git_ui::commit_view::CommitViewToolbar; | ||
| 28 | use git_ui::git_panel::GitPanel; | 29 | use git_ui::git_panel::GitPanel; |
| 29 | use git_ui::project_diff::ProjectDiffToolbar; | 30 | use git_ui::project_diff::ProjectDiffToolbar; |
| 30 | use gpui::{ | 31 | use gpui::{ |
| @@ -1049,6 +1050,8 @@ fn initialize_pane( · 1018 unmodified lines | |||
| 1049 | toolbar.add_item(migration_banner, window, cx); | 1050 | toolbar.add_item(migration_banner, window, cx); |
| 1050 | let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); | 1051 | let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); |
| 1051 | toolbar.add_item(project_diff_toolbar, window, cx); | 1052 | toolbar.add_item(project_diff_toolbar, window, cx); |
| 1053 | let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx)); | ||
| 1054 | toolbar.add_item(commit_view_toolbar, window, cx); | ||
| 1052 | let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); | 1055 | let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); |
| 1053 | toolbar.add_item(agent_diff_toolbar, window, cx); | 1056 | toolbar.add_item(agent_diff_toolbar, window, cx); |
| 1054 | let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx)); | 1057 | let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx)); |