1use crate::{git_status_icon, settings::GitPanelSettings};
2use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
3use anyhow::Result;
4use db::kvp::KEY_VALUE_STORE;
5use git::{
6 diff::DiffHunk,
7 repository::{GitFileStatus, RepoPath},
8};
9use gpui::*;
10use gpui::{
11 actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
12 CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
13 ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
14 MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
15};
16use menu::{SelectNext, SelectPrev};
17use project::{EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
18use serde::{Deserialize, Serialize};
19use settings::Settings as _;
20use std::{
21 cell::OnceCell,
22 collections::HashSet,
23 ffi::OsStr,
24 ops::{Deref, Range},
25 path::PathBuf,
26 rc::Rc,
27 sync::Arc,
28 time::Duration,
29 usize,
30};
31use ui::{
32 prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
33 ScrollbarState, Tooltip,
34};
35use util::{ResultExt, TryFutureExt};
36use workspace::{
37 dock::{DockPosition, Panel, PanelEvent},
38 Workspace,
39};
40use worktree::StatusEntry;
41
42actions!(git_panel, [ToggleFocus]);
43
44const GIT_PANEL_KEY: &str = "GitPanel";
45
46const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
47
48pub fn init(cx: &mut AppContext) {
49 cx.observe_new_views(
50 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
51 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
52 workspace.toggle_panel_focus::<GitPanel>(cx);
53 });
54 },
55 )
56 .detach();
57}
58
59#[derive(Debug)]
60pub enum Event {
61 Focus,
62}
63
64pub struct GitStatusEntry {}
65
66#[derive(Debug, PartialEq, Eq, Clone)]
67struct EntryDetails {
68 filename: String,
69 display_name: String,
70 path: RepoPath,
71 kind: EntryKind,
72 depth: usize,
73 is_expanded: bool,
74 status: Option<GitFileStatus>,
75 hunks: Rc<OnceCell<Vec<DiffHunk>>>,
76 index: usize,
77}
78
79impl EntryDetails {
80 pub fn is_dir(&self) -> bool {
81 self.kind.is_dir()
82 }
83}
84
85#[derive(Serialize, Deserialize)]
86struct SerializedGitPanel {
87 width: Option<Pixels>,
88}
89
90pub struct GitPanel {
91 // workspace: WeakView<Workspace>,
92 current_modifiers: Modifiers,
93 focus_handle: FocusHandle,
94 fs: Arc<dyn Fs>,
95 hide_scrollbar_task: Option<Task<()>>,
96 pending_serialization: Task<Option<()>>,
97 project: Model<Project>,
98 scroll_handle: UniformListScrollHandle,
99 scrollbar_state: ScrollbarState,
100 selected_item: Option<usize>,
101 show_scrollbar: bool,
102 // TODO Reintroduce expanded directories, once we're deriving directories from paths
103 // expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
104
105 // The entries that are currently shown in the panel, aka
106 // not hidden by folding or such
107 visible_entries: Vec<WorktreeEntries>,
108 width: Option<Pixels>,
109 // git_diff_editor: Option<View<Editor>>,
110 // git_diff_editor_updates: Task<()>,
111 reveal_in_editor: Task<()>,
112}
113
114#[derive(Debug, Clone)]
115struct WorktreeEntries {
116 worktree_id: WorktreeId,
117 // TODO support multiple repositories per worktree
118 // work_directory: worktree::WorkDirectory,
119 visible_entries: Vec<GitPanelEntry>,
120 paths: Rc<OnceCell<HashSet<RepoPath>>>,
121}
122
123#[derive(Debug, Clone)]
124struct GitPanelEntry {
125 entry: worktree::StatusEntry,
126 hunks: Rc<OnceCell<Vec<DiffHunk>>>,
127}
128
129impl Deref for GitPanelEntry {
130 type Target = worktree::StatusEntry;
131
132 fn deref(&self) -> &Self::Target {
133 &self.entry
134 }
135}
136
137impl WorktreeEntries {
138 fn paths(&self) -> &HashSet<RepoPath> {
139 self.paths.get_or_init(|| {
140 self.visible_entries
141 .iter()
142 .map(|e| (e.entry.repo_path.clone()))
143 .collect()
144 })
145 }
146}
147
148impl GitPanel {
149 pub fn load(
150 workspace: WeakView<Workspace>,
151 cx: AsyncWindowContext,
152 ) -> Task<Result<View<Self>>> {
153 cx.spawn(|mut cx| async move { workspace.update(&mut cx, Self::new) })
154 }
155
156 pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
157 let fs = workspace.app_state().fs.clone();
158 // let weak_workspace = workspace.weak_handle();
159 let project = workspace.project().clone();
160
161 let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
162 let focus_handle = cx.focus_handle();
163 cx.on_focus(&focus_handle, Self::focus_in).detach();
164 cx.on_focus_out(&focus_handle, |this, _, cx| {
165 this.hide_scrollbar(cx);
166 })
167 .detach();
168 cx.subscribe(&project, |this, _, event, cx| match event {
169 project::Event::WorktreeRemoved(_id) => {
170 // this.expanded_dir_ids.remove(id);
171 this.update_visible_entries(None, None, cx);
172 cx.notify();
173 }
174 project::Event::WorktreeOrderChanged => {
175 this.update_visible_entries(None, None, cx);
176 cx.notify();
177 }
178 project::Event::WorktreeUpdatedEntries(id, _)
179 | project::Event::WorktreeAdded(id)
180 | project::Event::WorktreeUpdatedGitRepositories(id) => {
181 this.update_visible_entries(Some(*id), None, cx);
182 cx.notify();
183 }
184 project::Event::Closed => {
185 // this.git_diff_editor_updates = Task::ready(());
186 this.reveal_in_editor = Task::ready(());
187 // this.expanded_dir_ids.clear();
188 this.visible_entries.clear();
189 // this.git_diff_editor = None;
190 }
191 _ => {}
192 })
193 .detach();
194
195 let scroll_handle = UniformListScrollHandle::new();
196
197 let mut git_panel = Self {
198 // workspace: weak_workspace,
199 focus_handle: cx.focus_handle(),
200 fs,
201 pending_serialization: Task::ready(None),
202 visible_entries: Vec::new(),
203 current_modifiers: cx.modifiers(),
204 // expanded_dir_ids: Default::default(),
205 width: Some(px(360.)),
206 scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
207 scroll_handle,
208 selected_item: None,
209 show_scrollbar: !Self::should_autohide_scrollbar(cx),
210 hide_scrollbar_task: None,
211 // git_diff_editor: Some(diff_display_editor(cx)),
212 // git_diff_editor_updates: Task::ready(()),
213 reveal_in_editor: Task::ready(()),
214 project,
215 };
216 git_panel.update_visible_entries(None, None, cx);
217 git_panel
218 });
219
220 git_panel
221 }
222
223 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
224 let width = self.width;
225 self.pending_serialization = cx.background_executor().spawn(
226 async move {
227 KEY_VALUE_STORE
228 .write_kvp(
229 GIT_PANEL_KEY.into(),
230 serde_json::to_string(&SerializedGitPanel { width })?,
231 )
232 .await?;
233 anyhow::Ok(())
234 }
235 .log_err(),
236 );
237 }
238
239 fn dispatch_context(&self) -> KeyContext {
240 let mut dispatch_context = KeyContext::new_with_defaults();
241 dispatch_context.add("GitPanel");
242 dispatch_context.add("menu");
243
244 dispatch_context
245 }
246
247 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
248 if !self.focus_handle.contains_focused(cx) {
249 cx.emit(Event::Focus);
250 }
251 }
252
253 fn should_show_scrollbar(_cx: &AppContext) -> bool {
254 // TODO: plug into settings
255 true
256 }
257
258 fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
259 // TODO: plug into settings
260 true
261 }
262
263 fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
264 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
265 if !Self::should_autohide_scrollbar(cx) {
266 return;
267 }
268 self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
269 cx.background_executor()
270 .timer(SCROLLBAR_SHOW_INTERVAL)
271 .await;
272 panel
273 .update(&mut cx, |panel, cx| {
274 panel.show_scrollbar = false;
275 cx.notify();
276 })
277 .log_err();
278 }))
279 }
280
281 fn handle_modifiers_changed(
282 &mut self,
283 event: &ModifiersChangedEvent,
284 cx: &mut ViewContext<Self>,
285 ) {
286 self.current_modifiers = event.modifiers;
287 cx.notify();
288 }
289
290 fn calculate_depth_and_difference(
291 entry: &StatusEntry,
292 visible_worktree_entries: &HashSet<RepoPath>,
293 ) -> (usize, usize) {
294 let (depth, difference) = entry
295 .repo_path
296 .ancestors()
297 .skip(1) // Skip the entry itself
298 .find_map(|ancestor| {
299 if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
300 let entry_path_components_count = entry.repo_path.components().count();
301 let parent_path_components_count = parent_entry.components().count();
302 let difference = entry_path_components_count - parent_path_components_count;
303 let depth = parent_entry
304 .ancestors()
305 .skip(1)
306 .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
307 .count();
308 Some((depth + 1, difference))
309 } else {
310 None
311 }
312 })
313 .unwrap_or((0, 0));
314
315 (depth, difference)
316 }
317
318 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
319 let item_count = self
320 .visible_entries
321 .iter()
322 .map(|worktree_entries| worktree_entries.visible_entries.len())
323 .sum::<usize>();
324 if item_count == 0 {
325 return;
326 }
327 let selection = match self.selected_item {
328 Some(i) => {
329 if i < item_count - 1 {
330 self.selected_item = Some(i + 1);
331 i + 1
332 } else {
333 self.selected_item = Some(0);
334 0
335 }
336 }
337 None => {
338 self.selected_item = Some(0);
339 0
340 }
341 };
342 self.scroll_handle
343 .scroll_to_item(selection, ScrollStrategy::Center);
344
345 let mut hunks = None;
346 self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
347 hunks = Some(entry.hunks.clone());
348 });
349 if let Some(hunks) = hunks {
350 self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
351 }
352
353 cx.notify();
354 }
355
356 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
357 let item_count = self
358 .visible_entries
359 .iter()
360 .map(|worktree_entries| worktree_entries.visible_entries.len())
361 .sum::<usize>();
362 if item_count == 0 {
363 return;
364 }
365 let selection = match self.selected_item {
366 Some(i) => {
367 if i > 0 {
368 self.selected_item = Some(i - 1);
369 i - 1
370 } else {
371 self.selected_item = Some(item_count - 1);
372 item_count - 1
373 }
374 }
375 None => {
376 self.selected_item = Some(0);
377 0
378 }
379 };
380 self.scroll_handle
381 .scroll_to_item(selection, ScrollStrategy::Center);
382
383 let mut hunks = None;
384 self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
385 hunks = Some(entry.hunks.clone());
386 });
387 if let Some(hunks) = hunks {
388 self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
389 }
390
391 cx.notify();
392 }
393}
394
395impl GitPanel {
396 fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
397 // TODO: Implement stage all
398 println!("Stage all triggered");
399 }
400
401 fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
402 // TODO: Implement unstage all
403 println!("Unstage all triggered");
404 }
405
406 fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
407 // TODO: Implement discard all
408 println!("Discard all triggered");
409 }
410
411 /// Commit all staged changes
412 fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
413 // TODO: Implement commit all staged
414 println!("Commit staged changes triggered");
415 }
416
417 /// Commit all changes, regardless of whether they are staged or not
418 fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
419 // TODO: Implement commit all changes
420 println!("Commit all changes triggered");
421 }
422
423 fn all_staged(&self) -> bool {
424 // TODO: Implement all_staged
425 true
426 }
427
428 fn no_entries(&self) -> bool {
429 self.visible_entries.is_empty()
430 }
431
432 fn entry_count(&self) -> usize {
433 self.visible_entries
434 .iter()
435 .map(|worktree_entries| worktree_entries.visible_entries.len())
436 .sum()
437 }
438
439 fn for_each_visible_entry(
440 &self,
441 range: Range<usize>,
442 cx: &mut ViewContext<Self>,
443 mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext<Self>),
444 ) {
445 let mut ix = 0;
446 for worktree_entries in &self.visible_entries {
447 if ix >= range.end {
448 return;
449 }
450
451 if ix + worktree_entries.visible_entries.len() <= range.start {
452 ix += worktree_entries.visible_entries.len();
453 continue;
454 }
455
456 let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
457 // let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
458 if let Some(worktree) = self
459 .project
460 .read(cx)
461 .worktree_for_id(worktree_entries.worktree_id, cx)
462 {
463 let snapshot = worktree.read(cx).snapshot();
464 let root_name = OsStr::new(snapshot.root_name());
465 // let expanded_entry_ids = self
466 // .expanded_dir_ids
467 // .get(&snapshot.id())
468 // .map(Vec::as_slice)
469 // .unwrap_or(&[]);
470
471 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
472 let entries = worktree_entries.paths();
473
474 let index_start = entry_range.start;
475 for (i, entry) in worktree_entries.visible_entries[entry_range]
476 .iter()
477 .enumerate()
478 {
479 let index = index_start + i;
480 let status = entry.status;
481 let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
482
483 let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
484
485 let filename = match difference {
486 diff if diff > 1 => entry
487 .repo_path
488 .iter()
489 .skip(entry.repo_path.components().count() - diff)
490 .collect::<PathBuf>()
491 .to_str()
492 .unwrap_or_default()
493 .to_string(),
494 _ => entry
495 .repo_path
496 .file_name()
497 .map(|name| name.to_string_lossy().into_owned())
498 .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
499 };
500
501 let details = EntryDetails {
502 filename,
503 display_name: entry.repo_path.to_string_lossy().into_owned(),
504 // TODO get it from StatusEntry?
505 kind: EntryKind::File,
506 is_expanded,
507 path: entry.repo_path.clone(),
508 status: Some(status),
509 hunks: entry.hunks.clone(),
510 depth,
511 index,
512 };
513 callback(ix, details, cx);
514 }
515 }
516 ix = end_ix;
517 }
518 }
519
520 // TODO: Update expanded directory state
521 // TODO: Updates happen in the main loop, could be long for large workspaces
522 #[track_caller]
523 fn update_visible_entries(
524 &mut self,
525 for_worktree: Option<WorktreeId>,
526 _new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
527 cx: &mut ViewContext<Self>,
528 ) {
529 let project = self.project.read(cx);
530 let mut old_entries_removed = false;
531 let mut after_update = Vec::new();
532 self.visible_entries
533 .retain(|worktree_entries| match for_worktree {
534 Some(for_worktree) => {
535 if worktree_entries.worktree_id == for_worktree {
536 old_entries_removed = true;
537 false
538 } else if old_entries_removed {
539 after_update.push(worktree_entries.clone());
540 false
541 } else {
542 true
543 }
544 }
545 None => false,
546 });
547 for worktree in project.visible_worktrees(cx) {
548 let snapshot = worktree.read(cx).snapshot();
549 let worktree_id = snapshot.id();
550
551 if for_worktree.is_some() && for_worktree != Some(worktree_id) {
552 continue;
553 }
554
555 let mut visible_worktree_entries = Vec::new();
556 // Only use the first repository for now
557 let repositories = snapshot.repositories().take(1);
558 // let mut work_directory = None;
559 for repository in repositories {
560 visible_worktree_entries.extend(repository.status());
561 // work_directory = Some(worktree::WorkDirectory::clone(repository));
562 }
563
564 // TODO use the GitTraversal
565 // let mut visible_worktree_entries = snapshot
566 // .entries(false, 0)
567 // .filter(|entry| !entry.is_external)
568 // .filter(|entry| entry.git_status.is_some())
569 // .cloned()
570 // .collect::<Vec<_>>();
571 // snapshot.propagate_git_statuses(&mut visible_worktree_entries);
572 // project::sort_worktree_entries(&mut visible_worktree_entries);
573
574 if !visible_worktree_entries.is_empty() {
575 self.visible_entries.push(WorktreeEntries {
576 worktree_id,
577 // work_directory: work_directory.unwrap(),
578 visible_entries: visible_worktree_entries
579 .into_iter()
580 .map(|entry| GitPanelEntry {
581 entry,
582 hunks: Rc::default(),
583 })
584 .collect(),
585 paths: Rc::default(),
586 });
587 }
588 }
589 self.visible_entries.extend(after_update);
590
591 // TODO re-implement this
592 // if let Some((worktree_id, entry_id)) = new_selected_entry {
593 // self.selected_item = self.visible_entries.iter().enumerate().find_map(
594 // |(worktree_index, worktree_entries)| {
595 // if worktree_entries.worktree_id == worktree_id {
596 // worktree_entries
597 // .visible_entries
598 // .iter()
599 // .position(|entry| entry.id == entry_id)
600 // .map(|entry_index| {
601 // worktree_index * worktree_entries.visible_entries.len()
602 // + entry_index
603 // })
604 // } else {
605 // None
606 // }
607 // },
608 // );
609 // }
610
611 // let project = self.project.downgrade();
612 // self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
613 // cx.background_executor()
614 // .timer(UPDATE_DEBOUNCE)
615 // .await;
616 // let Some(project_buffers) = git_panel
617 // .update(&mut cx, |git_panel, cx| {
618 // futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
619 // |worktree_entries| {
620 // worktree_entries
621 // .visible_entries
622 // .iter()
623 // .filter_map(|entry| {
624 // let git_status = entry.status;
625 // let entry_hunks = entry.hunks.clone();
626 // let (entry_path, unstaged_changes_task) =
627 // project.update(cx, |project, cx| {
628 // let entry_path = ProjectPath {
629 // worktree_id: worktree_entries.worktree_id,
630 // path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
631 // };
632 // let open_task =
633 // project.open_path(entry_path.clone(), cx);
634 // let unstaged_changes_task =
635 // cx.spawn(|project, mut cx| async move {
636 // let (_, opened_model) = open_task
637 // .await
638 // .context("opening buffer")?;
639 // let buffer = opened_model
640 // .downcast::<Buffer>()
641 // .map_err(|_| {
642 // anyhow::anyhow!(
643 // "accessing buffer for entry"
644 // )
645 // })?;
646 // // TODO added files have noop changes and those are not expanded properly in the multi buffer
647 // let unstaged_changes = project
648 // .update(&mut cx, |project, cx| {
649 // project.open_unstaged_changes(
650 // buffer.clone(),
651 // cx,
652 // )
653 // })?
654 // .await
655 // .context("opening unstaged changes")?;
656
657 // let hunks = cx.update(|cx| {
658 // entry_hunks
659 // .get_or_init(|| {
660 // match git_status {
661 // GitFileStatus::Added => {
662 // let buffer_snapshot = buffer.read(cx).snapshot();
663 // let entire_buffer_range =
664 // buffer_snapshot.anchor_after(0)
665 // ..buffer_snapshot
666 // .anchor_before(
667 // buffer_snapshot.len(),
668 // );
669 // let entire_buffer_point_range =
670 // entire_buffer_range
671 // .clone()
672 // .to_point(&buffer_snapshot);
673
674 // vec![DiffHunk {
675 // row_range: entire_buffer_point_range
676 // .start
677 // .row
678 // ..entire_buffer_point_range
679 // .end
680 // .row,
681 // buffer_range: entire_buffer_range,
682 // diff_base_byte_range: 0..0,
683 // }]
684 // }
685 // GitFileStatus::Modified => {
686 // let buffer_snapshot =
687 // buffer.read(cx).snapshot();
688 // unstaged_changes.read(cx)
689 // .diff_to_buffer
690 // .hunks_in_row_range(
691 // 0..BufferRow::MAX,
692 // &buffer_snapshot,
693 // )
694 // .collect()
695 // }
696 // // TODO support these
697 // GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
698 // }
699 // }).clone()
700 // })?;
701
702 // anyhow::Ok((buffer, unstaged_changes, hunks))
703 // });
704 // Some((entry_path, unstaged_changes_task))
705 // }).ok()??;
706 // Some((entry_path, unstaged_changes_task))
707 // })
708 // .map(|(entry_path, open_task)| async move {
709 // (entry_path, open_task.await)
710 // })
711 // .collect::<Vec<_>>()
712 // },
713 // ))
714 // })
715 // .ok()
716 // else {
717 // return;
718 // };
719
720 // let project_buffers = project_buffers.await;
721 // if project_buffers.is_empty() {
722 // return;
723 // }
724 // let mut change_sets = Vec::with_capacity(project_buffers.len());
725 // if let Some(buffer_update_task) = git_panel
726 // .update(&mut cx, |git_panel, cx| {
727 // let editor = git_panel.git_diff_editor.clone()?;
728 // let multi_buffer = editor.read(cx).buffer().clone();
729 // let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
730 // for (buffer_path, open_result) in project_buffers {
731 // if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
732 // .with_context(|| format!("opening buffer {buffer_path:?}"))
733 // .log_err()
734 // {
735 // change_sets.push(unstaged_changes);
736 // buffers_with_ranges.push((
737 // buffer,
738 // diff_hunks
739 // .into_iter()
740 // .map(|hunk| hunk.buffer_range)
741 // .collect(),
742 // ));
743 // }
744 // }
745
746 // Some(multi_buffer.update(cx, |multi_buffer, cx| {
747 // multi_buffer.clear(cx);
748 // multi_buffer.push_multiple_excerpts_with_context_lines(
749 // buffers_with_ranges,
750 // DEFAULT_MULTIBUFFER_CONTEXT,
751 // cx,
752 // )
753 // }))
754 // })
755 // .ok().flatten()
756 // {
757 // buffer_update_task.await;
758 // git_panel
759 // .update(&mut cx, |git_panel, cx| {
760 // if let Some(diff_editor) = git_panel.git_diff_editor.as_ref() {
761 // diff_editor.update(cx, |editor, cx| {
762 // for change_set in change_sets {
763 // editor.add_change_set(change_set, cx);
764 // }
765 // });
766 // }
767 // })
768 // .ok();
769 // }
770 // });
771
772 cx.notify();
773 }
774}
775
776impl GitPanel {
777 pub fn panel_button(
778 &self,
779 id: impl Into<SharedString>,
780 label: impl Into<SharedString>,
781 ) -> Button {
782 let id = id.into().clone();
783 let label = label.into().clone();
784
785 Button::new(id, label)
786 .label_size(LabelSize::Small)
787 .layer(ElevationIndex::ElevatedSurface)
788 .size(ButtonSize::Compact)
789 .style(ButtonStyle::Filled)
790 }
791
792 pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
793 h_flex()
794 .items_center()
795 .h(px(8.))
796 .child(Divider::horizontal_dashed().color(DividerColor::Border))
797 }
798
799 pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
800 let focus_handle = self.focus_handle(cx).clone();
801
802 let changes_string = format!("{} changes", self.entry_count());
803
804 h_flex()
805 .h(px(32.))
806 .items_center()
807 .px_3()
808 .bg(ElevationIndex::Surface.bg(cx))
809 .child(
810 h_flex()
811 .gap_2()
812 .child(Checkbox::new("all-changes", true.into()).disabled(true))
813 .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
814 )
815 .child(div().flex_grow())
816 .child(
817 h_flex()
818 .gap_2()
819 .child(
820 IconButton::new("discard-changes", IconName::Undo)
821 .tooltip(move |cx| {
822 let focus_handle = focus_handle.clone();
823
824 Tooltip::for_action_in(
825 "Discard all changes",
826 &DiscardAll,
827 &focus_handle,
828 cx,
829 )
830 })
831 .icon_size(IconSize::Small)
832 .disabled(true),
833 )
834 .child(if self.all_staged() {
835 self.panel_button("unstage-all", "Unstage All").on_click(
836 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
837 )
838 } else {
839 self.panel_button("stage-all", "Stage All").on_click(
840 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
841 )
842 }),
843 )
844 }
845
846 pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
847 let focus_handle_1 = self.focus_handle(cx).clone();
848 let focus_handle_2 = self.focus_handle(cx).clone();
849
850 let commit_staged_button = self
851 .panel_button("commit-staged-changes", "Commit")
852 .tooltip(move |cx| {
853 let focus_handle = focus_handle_1.clone();
854 Tooltip::for_action_in(
855 "Commit all staged changes",
856 &CommitStagedChanges,
857 &focus_handle,
858 cx,
859 )
860 })
861 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
862 this.commit_staged_changes(&CommitStagedChanges, cx)
863 }));
864
865 let commit_all_button = self
866 .panel_button("commit-all-changes", "Commit All")
867 .tooltip(move |cx| {
868 let focus_handle = focus_handle_2.clone();
869 Tooltip::for_action_in(
870 "Commit all changes, including unstaged changes",
871 &CommitAllChanges,
872 &focus_handle,
873 cx,
874 )
875 })
876 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
877 this.commit_all_changes(&CommitAllChanges, cx)
878 }));
879
880 div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
881 v_flex()
882 .h_full()
883 .py_2p5()
884 .px_3()
885 .bg(cx.theme().colors().editor_background)
886 .font_buffer(cx)
887 .text_ui_sm(cx)
888 .text_color(cx.theme().colors().text_muted)
889 .child("Add a message")
890 .gap_1()
891 .child(div().flex_grow())
892 .child(h_flex().child(div().gap_1().flex_grow()).child(
893 if self.current_modifiers.alt {
894 commit_all_button
895 } else {
896 commit_staged_button
897 },
898 ))
899 .cursor(CursorStyle::OperationNotAllowed)
900 .opacity(0.5),
901 )
902 }
903
904 fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
905 h_flex()
906 .h_full()
907 .flex_1()
908 .justify_center()
909 .items_center()
910 .child(
911 v_flex()
912 .gap_3()
913 .child("No changes to commit")
914 .text_ui_sm(cx)
915 .mx_auto()
916 .text_color(Color::Placeholder.color(cx)),
917 )
918 }
919
920 fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
921 if !Self::should_show_scrollbar(cx)
922 || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
923 {
924 return None;
925 }
926 Some(
927 div()
928 .occlude()
929 .id("project-panel-vertical-scroll")
930 .on_mouse_move(cx.listener(|_, _, cx| {
931 cx.notify();
932 cx.stop_propagation()
933 }))
934 .on_hover(|_, cx| {
935 cx.stop_propagation();
936 })
937 .on_any_mouse_down(|_, cx| {
938 cx.stop_propagation();
939 })
940 .on_mouse_up(
941 MouseButton::Left,
942 cx.listener(|this, _, cx| {
943 if !this.scrollbar_state.is_dragging()
944 && !this.focus_handle.contains_focused(cx)
945 {
946 this.hide_scrollbar(cx);
947 cx.notify();
948 }
949
950 cx.stop_propagation();
951 }),
952 )
953 .on_scroll_wheel(cx.listener(|_, _, cx| {
954 cx.notify();
955 }))
956 .h_full()
957 .absolute()
958 .right_1()
959 .top_1()
960 .bottom_1()
961 .w(px(12.))
962 .cursor_default()
963 .children(Scrollbar::vertical(
964 // percentage as f32..end_offset as f32,
965 self.scrollbar_state.clone(),
966 )),
967 )
968 }
969
970 fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
971 let item_count = self
972 .visible_entries
973 .iter()
974 .map(|worktree_entries| worktree_entries.visible_entries.len())
975 .sum();
976 let selected_entry = self.selected_item;
977 h_flex()
978 .size_full()
979 .overflow_hidden()
980 .child(
981 uniform_list(cx.view().clone(), "entries", item_count, {
982 move |git_panel, range, cx| {
983 let mut items = Vec::with_capacity(range.end - range.start);
984 git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
985 items.push(git_panel.render_entry(
986 id,
987 Some(details.index) == selected_entry,
988 details,
989 cx,
990 ));
991 });
992 items
993 }
994 })
995 .size_full()
996 .with_sizing_behavior(ListSizingBehavior::Infer)
997 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
998 // .with_width_from_item(self.max_width_item_index)
999 .track_scroll(self.scroll_handle.clone()),
1000 )
1001 .children(self.render_scrollbar(cx))
1002 }
1003
1004 fn render_entry(
1005 &self,
1006 ix: usize,
1007 selected: bool,
1008 details: EntryDetails,
1009 cx: &ViewContext<Self>,
1010 ) -> impl IntoElement {
1011 let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
1012 let is_staged = ToggleState::Selected;
1013 let handle = cx.view().downgrade();
1014
1015 h_flex()
1016 .id(("git-panel-entry", ix))
1017 .h(px(28.))
1018 .w_full()
1019 .pl(px(12. + 12. * details.depth as f32))
1020 .pr(px(4.))
1021 .items_center()
1022 .gap_2()
1023 .font_buffer(cx)
1024 .text_ui_sm(cx)
1025 .when(!details.is_dir(), |this| {
1026 this.child(Checkbox::new(checkbox_id, is_staged))
1027 })
1028 .when_some(details.status, |this, status| {
1029 this.child(git_status_icon(status))
1030 })
1031 .child(
1032 ListItem::new(details.path.0.clone())
1033 .toggle_state(selected)
1034 .child(h_flex().gap_1p5().child(details.display_name.clone()))
1035 .on_click(move |e, cx| {
1036 handle
1037 .update(cx, |git_panel, cx| {
1038 git_panel.selected_item = Some(details.index);
1039 let change_focus = e.down.click_count > 1;
1040 git_panel.reveal_entry_in_git_editor(
1041 details.hunks.clone(),
1042 change_focus,
1043 None,
1044 cx,
1045 );
1046 })
1047 .ok();
1048 }),
1049 )
1050 }
1051
1052 fn reveal_entry_in_git_editor(
1053 &mut self,
1054 _hunks: Rc<OnceCell<Vec<DiffHunk>>>,
1055 _change_focus: bool,
1056 _debounce: Option<Duration>,
1057 _cx: &mut ViewContext<Self>,
1058 ) {
1059 // let workspace = self.workspace.clone();
1060 // let Some(diff_editor) = self.git_diff_editor.clone() else {
1061 // return;
1062 // };
1063 // self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
1064 // if let Some(debounce) = debounce {
1065 // cx.background_executor().timer(debounce).await;
1066 // }
1067
1068 // let Some(editor) = workspace
1069 // .update(&mut cx, |workspace, cx| {
1070 // let git_diff_editor = workspace
1071 // .items_of_type::<Editor>(cx)
1072 // .find(|editor| &diff_editor == editor);
1073 // match git_diff_editor {
1074 // Some(existing_editor) => {
1075 // workspace.activate_item(&existing_editor, true, change_focus, cx);
1076 // existing_editor
1077 // }
1078 // None => {
1079 // workspace.active_pane().update(cx, |pane, cx| {
1080 // pane.add_item(
1081 // ` diff_editor.boxed_clone(),
1082 // true,
1083 // change_focus,
1084 // None,
1085 // cx,
1086 // )
1087 // });
1088 // diff_editor.clone()
1089 // }
1090 // }
1091 // })
1092 // .ok()
1093 // else {
1094 // return;
1095 // };
1096
1097 // if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
1098 // let hunk_buffer_range = &first_hunk.buffer_range;
1099 // if let Some(buffer_id) = hunk_buffer_range
1100 // .start
1101 // .buffer_id
1102 // .or_else(|| first_hunk.buffer_range.end.buffer_id)
1103 // {
1104 // editor
1105 // .update(&mut cx, |editor, cx| {
1106 // let multi_buffer = editor.buffer().read(cx);
1107 // let buffer = multi_buffer.buffer(buffer_id)?;
1108 // let buffer_snapshot = buffer.read(cx).snapshot();
1109 // let (excerpt_id, _) = multi_buffer
1110 // .excerpts_for_buffer(&buffer, cx)
1111 // .into_iter()
1112 // .find(|(_, excerpt)| {
1113 // hunk_buffer_range
1114 // .start
1115 // .cmp(&excerpt.context.start, &buffer_snapshot)
1116 // .is_ge()
1117 // && hunk_buffer_range
1118 // .end
1119 // .cmp(&excerpt.context.end, &buffer_snapshot)
1120 // .is_le()
1121 // })?;
1122 // let multi_buffer_hunk_start = multi_buffer
1123 // .snapshot(cx)
1124 // .anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
1125 // editor.change_selections(
1126 // Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
1127 // cx,
1128 // |s| {
1129 // s.select_ranges(Some(
1130 // multi_buffer_hunk_start..multi_buffer_hunk_start,
1131 // ))
1132 // },
1133 // );
1134 // cx.notify();
1135 // Some(())
1136 // })
1137 // .ok()
1138 // .flatten();
1139 // }
1140 // }
1141 // });
1142 }
1143}
1144
1145impl Render for GitPanel {
1146 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1147 let project = self.project.read(cx);
1148
1149 v_flex()
1150 .id("git_panel")
1151 .key_context(self.dispatch_context())
1152 .track_focus(&self.focus_handle)
1153 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
1154 .when(!project.is_read_only(cx), |this| {
1155 this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
1156 .on_action(
1157 cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
1158 )
1159 .on_action(
1160 cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
1161 )
1162 .on_action(cx.listener(|this, &CommitStagedChanges, cx| {
1163 this.commit_staged_changes(&CommitStagedChanges, cx)
1164 }))
1165 .on_action(cx.listener(|this, &CommitAllChanges, cx| {
1166 this.commit_all_changes(&CommitAllChanges, cx)
1167 }))
1168 })
1169 .on_action(cx.listener(Self::select_next))
1170 .on_action(cx.listener(Self::select_prev))
1171 .on_hover(cx.listener(|this, hovered, cx| {
1172 if *hovered {
1173 this.show_scrollbar = true;
1174 this.hide_scrollbar_task.take();
1175 cx.notify();
1176 } else if !this.focus_handle.contains_focused(cx) {
1177 this.hide_scrollbar(cx);
1178 }
1179 }))
1180 .size_full()
1181 .overflow_hidden()
1182 .font_buffer(cx)
1183 .py_1()
1184 .bg(ElevationIndex::Surface.bg(cx))
1185 .child(self.render_panel_header(cx))
1186 .child(self.render_divider(cx))
1187 .child(if !self.no_entries() {
1188 self.render_entries(cx).into_any_element()
1189 } else {
1190 self.render_empty_state(cx).into_any_element()
1191 })
1192 .child(self.render_divider(cx))
1193 .child(self.render_commit_editor(cx))
1194 }
1195}
1196
1197impl FocusableView for GitPanel {
1198 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1199 self.focus_handle.clone()
1200 }
1201}
1202
1203impl EventEmitter<Event> for GitPanel {}
1204
1205impl EventEmitter<PanelEvent> for GitPanel {}
1206
1207impl Panel for GitPanel {
1208 fn persistent_name() -> &'static str {
1209 "GitPanel"
1210 }
1211
1212 fn position(&self, cx: &WindowContext) -> DockPosition {
1213 GitPanelSettings::get_global(cx).dock
1214 }
1215
1216 fn position_is_valid(&self, position: DockPosition) -> bool {
1217 matches!(position, DockPosition::Left | DockPosition::Right)
1218 }
1219
1220 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1221 settings::update_settings_file::<GitPanelSettings>(
1222 self.fs.clone(),
1223 cx,
1224 move |settings, _| settings.dock = Some(position),
1225 );
1226 }
1227
1228 fn size(&self, cx: &WindowContext) -> Pixels {
1229 self.width
1230 .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
1231 }
1232
1233 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1234 self.width = size;
1235 self.serialize(cx);
1236 cx.notify();
1237 }
1238
1239 fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
1240 Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
1241 }
1242
1243 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1244 Some("Git Panel")
1245 }
1246
1247 fn toggle_action(&self) -> Box<dyn Action> {
1248 Box::new(ToggleFocus)
1249 }
1250
1251 fn activation_priority(&self) -> u32 {
1252 2
1253 }
1254}
1255
1256// fn diff_display_editor(cx: &mut WindowContext) -> View<Editor> {
1257// cx.new_view(|cx| {
1258// let multi_buffer = cx.new_model(|_| {
1259// MultiBuffer::new(language::Capability::ReadWrite).with_title("Project diff".to_string())
1260// });
1261// let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
1262// editor.set_expand_all_diff_hunks();
1263// editor
1264// })
1265// }