Git panel: Right click menu (#24787)

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-02-12 22:26:34 -07:00 committed by GitHub
parent fc7bf7bcb9
commit d57f5937d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 278 additions and 163 deletions

View file

@ -67,7 +67,10 @@ use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
pub use element::{ pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
}; };
use futures::{future, FutureExt}; use futures::{
future::{self, Shared},
FutureExt,
};
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use code_context_menus::{ use code_context_menus::{
@ -761,6 +764,7 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom, next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>, addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>, registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
load_diff_task: Option<Shared<Task<()>>>,
selection_mark_mode: bool, selection_mark_mode: bool,
toggle_fold_multiple_buffers: Task<()>, toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>,
@ -1318,12 +1322,16 @@ impl Editor {
}; };
let mut code_action_providers = Vec::new(); let mut code_action_providers = Vec::new();
let mut load_uncommitted_diff = None;
if let Some(project) = project.clone() { if let Some(project) = project.clone() {
get_uncommitted_diff_for_buffer( load_uncommitted_diff = Some(
&project, get_uncommitted_diff_for_buffer(
buffer.read(cx).all_buffers(), &project,
buffer.clone(), buffer.read(cx).all_buffers(),
cx, buffer.clone(),
cx,
)
.shared(),
); );
code_action_providers.push(Rc::new(project) as Rc<_>); code_action_providers.push(Rc::new(project) as Rc<_>);
} }
@ -1471,6 +1479,7 @@ impl Editor {
selection_mark_mode: false, selection_mark_mode: false,
toggle_fold_multiple_buffers: Task::ready(()), toggle_fold_multiple_buffers: Task::ready(()),
text_style_refinement: None, text_style_refinement: None,
load_diff_task: load_uncommitted_diff,
}; };
this.tasks_update_task = Some(this.refresh_runnables(window, cx)); this.tasks_update_task = Some(this.refresh_runnables(window, cx));
this._subscriptions.extend(project_subscriptions); this._subscriptions.extend(project_subscriptions);
@ -14120,11 +14129,14 @@ impl Editor {
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
if self.buffer.read(cx).diff_for(buffer_id).is_none() { if self.buffer.read(cx).diff_for(buffer_id).is_none() {
if let Some(project) = &self.project { if let Some(project) = &self.project {
get_uncommitted_diff_for_buffer( self.load_diff_task = Some(
project, get_uncommitted_diff_for_buffer(
[buffer.clone()], project,
self.buffer.clone(), [buffer.clone()],
cx, self.buffer.clone(),
cx,
)
.shared(),
); );
} }
} }
@ -14879,6 +14891,10 @@ impl Editor {
gpui::Size::new(em_width, line_height) gpui::Size::new(em_width, line_height)
} }
pub fn wait_for_diff_to_load(&self) -> Option<Shared<Task<()>>> {
self.load_diff_task.clone()
}
} }
fn get_uncommitted_diff_for_buffer( fn get_uncommitted_diff_for_buffer(
@ -14886,7 +14902,7 @@ fn get_uncommitted_diff_for_buffer(
buffers: impl IntoIterator<Item = Entity<Buffer>>, buffers: impl IntoIterator<Item = Entity<Buffer>>,
buffer: Entity<MultiBuffer>, buffer: Entity<MultiBuffer>,
cx: &mut App, cx: &mut App,
) { ) -> Task<()> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
for buffer in buffers { for buffer in buffers {
@ -14903,7 +14919,6 @@ fn get_uncommitted_diff_for_buffer(
}) })
.ok(); .ok();
}) })
.detach();
} }
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {

View file

@ -16,7 +16,7 @@ use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use gpui::*; use gpui::*;
use itertools::Itertools; use itertools::Itertools;
use language::{markdown, Buffer, File, ParsedMarkdown}; use language::{markdown, Buffer, File, ParsedMarkdown};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
use multi_buffer::ExcerptInfo; use multi_buffer::ExcerptInfo;
use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader}; use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader};
use project::{ use project::{
@ -28,10 +28,11 @@ use settings::Settings as _;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize}; use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
use time::OffsetDateTime; use time::OffsetDateTime;
use ui::{ use ui::{
prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors, prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
}; };
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::SaveIntent;
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotificationId}, notifications::{DetachAndPromptErr, NotificationId},
@ -79,7 +80,6 @@ pub fn init(cx: &mut App) {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {
Focus, Focus,
OpenedEntry { path: ProjectPath },
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -112,7 +112,7 @@ impl GitHeaderEntry {
pub fn title(&self) -> &'static str { pub fn title(&self) -> &'static str {
match self.header { match self.header {
Section::Conflict => "Conflicts", Section::Conflict => "Conflicts",
Section::Tracked => "Changed", Section::Tracked => "Changes",
Section::New => "New", Section::New => "New",
} }
} }
@ -177,6 +177,7 @@ pub struct GitPanel {
update_visible_entries_task: Task<()>, update_visible_entries_task: Task<()>,
width: Option<Pixels>, width: Option<Pixels>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
} }
fn commit_message_editor( fn commit_message_editor(
@ -215,7 +216,7 @@ impl GitPanel {
let active_repository = project.read(cx).active_repository(cx); let active_repository = project.read(cx).active_repository(cx);
let workspace = cx.entity().downgrade(); let workspace = cx.entity().downgrade();
let git_panel = cx.new(|cx| { cx.new(|cx| {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
@ -282,30 +283,13 @@ impl GitPanel {
tracked_staged_count: 0, tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()), update_visible_entries_task: Task::ready(()),
width: Some(px(360.)), width: Some(px(360.)),
context_menu: None,
workspace, workspace,
}; };
git_panel.schedule_update(false, window, cx); git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
git_panel git_panel
}); })
cx.subscribe_in(
&git_panel,
window,
move |workspace, _, event: &Event, window, cx| match event.clone() {
Event::OpenedEntry { path } => {
workspace
.open_path_preview(path, None, false, false, window, cx)
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
Some(format!("{e}"))
});
}
Event::Focus => { /* TODO */ }
},
)
.detach();
git_panel
} }
pub fn select_entry_by_path( pub fn select_entry_by_path(
@ -468,7 +452,7 @@ impl GitPanel {
fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) { fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
if self.entries.first().is_some() { if self.entries.first().is_some() {
self.selected_entry = Some(0); self.selected_entry = Some(1);
self.scroll_to_selected_entry(cx); self.scroll_to_selected_entry(cx);
} }
} }
@ -486,7 +470,16 @@ impl GitPanel {
selected_entry selected_entry
}; };
self.selected_entry = Some(new_selected_entry); if matches!(
self.entries.get(new_selected_entry),
Some(GitListEntry::Header(..))
) {
if new_selected_entry > 0 {
self.selected_entry = Some(new_selected_entry - 1)
}
} else {
self.selected_entry = Some(new_selected_entry);
}
self.scroll_to_selected_entry(cx); self.scroll_to_selected_entry(cx);
} }
@ -506,8 +499,14 @@ impl GitPanel {
} else { } else {
selected_entry selected_entry
}; };
if matches!(
self.selected_entry = Some(new_selected_entry); self.entries.get(new_selected_entry),
Some(GitListEntry::Header(..))
) {
self.selected_entry = Some(new_selected_entry + 1);
} else {
self.selected_entry = Some(new_selected_entry);
}
self.scroll_to_selected_entry(cx); self.scroll_to_selected_entry(cx);
} }
@ -537,7 +536,7 @@ impl GitPanel {
active_repository.read(cx).entry_count() > 0 active_repository.read(cx).entry_count() > 0
}); });
if have_entries && self.selected_entry.is_none() { if have_entries && self.selected_entry.is_none() {
self.selected_entry = Some(0); self.selected_entry = Some(1);
self.scroll_to_selected_entry(cx); self.scroll_to_selected_entry(cx);
cx.notify(); cx.notify();
} }
@ -559,7 +558,7 @@ impl GitPanel {
self.selected_entry.and_then(|i| self.entries.get(i)) self.selected_entry.and_then(|i| self.entries.get(i))
} }
fn open_selected(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
maybe!({ maybe!({
let entry = self.entries.get(self.selected_entry?)?.status_entry()?; let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
@ -572,6 +571,121 @@ impl GitPanel {
self.focus_handle.focus(window); self.focus_handle.focus(window);
} }
fn open_file(
&mut self,
_: &menu::SecondaryConfirm,
window: &mut Window,
cx: &mut Context<Self>,
) {
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)?;
if entry.status.is_deleted() {
return None;
}
self.workspace
.update(cx, |workspace, cx| {
workspace
.open_path_preview(path, None, false, false, window, cx)
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
Some(format!("{e}"))
});
})
.ok()
});
}
fn revert(
&mut self,
_: &editor::actions::RevertFile,
window: &mut Window,
cx: &mut Context<Self>,
) {
maybe!({
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_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)?;
let workspace = self.workspace.clone();
if entry.status.is_staged() != Some(false) {
self.update_staging_area_for_entries(false, vec![entry.repo_path.clone()], cx);
}
if entry.status.is_created() {
let prompt = window.prompt(
PromptLevel::Info,
"Do you want to trash this file?",
None,
&["Trash", "Cancel"],
cx,
);
cx.spawn_in(window, |_, mut cx| async move {
match prompt.await {
Ok(0) => {}
_ => return Ok(()),
}
let task = workspace.update(&mut cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.delete_file(path, true, cx))
})?;
if let Some(task) = task {
task.await?;
}
Ok(())
})
.detach_and_prompt_err(
"Failed to trash file",
window,
cx,
|e, _, _| Some(format!("{e}")),
);
return Some(());
}
let open_path = workspace.update(cx, |workspace, cx| {
workspace.open_path_preview(path, None, true, false, window, cx)
});
cx.spawn_in(window, |_, mut cx| async move {
let item = open_path?.await?;
let editor = cx.update(|_, cx| {
item.act_as::<Editor>(cx)
.ok_or_else(|| anyhow::anyhow!("didn't open editor"))
})??;
if let Some(task) =
editor.update(&mut cx, |editor, _| editor.wait_for_diff_to_load())?
{
task.await
};
editor.update_in(&mut cx, |editor, window, cx| {
editor.revert_file(&Default::default(), window, cx);
})?;
workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.save_active_item(SaveIntent::Save, window, cx)
})?
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
Some(format!("{e}"))
});
Some(())
});
}
fn toggle_staged_for_entry( fn toggle_staged_for_entry(
&mut self, &mut self,
entry: &GitListEntry, entry: &GitListEntry,
@ -606,7 +720,18 @@ impl GitPanel {
(goal_staged_state, entries) (goal_staged_state, entries)
} }
}; };
self.update_staging_area_for_entries(stage, repo_paths, cx);
}
fn update_staging_area_for_entries(
&mut self,
stage: bool,
repo_paths: Vec<RepoPath>,
cx: &mut Context<Self>,
) {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1; let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
self.pending.push(PendingOperation { self.pending.push(PendingOperation {
op_id, op_id,
@ -615,7 +740,6 @@ impl GitPanel {
finished: false, finished: false,
}); });
let repo_paths = repo_paths.clone(); let repo_paths = repo_paths.clone();
let active_repository = active_repository.clone();
let repository = active_repository.read(cx); let repository = active_repository.read(cx);
self.update_counts(repository); self.update_counts(repository);
cx.notify(); cx.notify();
@ -1530,7 +1654,7 @@ impl GitPanel {
fn render_entries( fn render_entries(
&self, &self,
has_write_access: bool, has_write_access: bool,
window: &Window, _: &Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let entry_count = self.entries.len(); let entry_count = self.entries.len();
@ -1571,61 +1695,6 @@ impl GitPanel {
items items
} }
}) })
.with_decoration(
ui::indent_guides(
cx.entity().clone(),
self.indent_size(window, cx),
IndentGuideColors::panel(cx),
|this, range, _windows, _cx| {
this.entries
.iter()
.skip(range.start)
.map(|entry| match entry {
GitListEntry::GitStatusEntry(_) => 1,
GitListEntry::Header(_) => 0,
})
.collect()
},
)
.with_render_fn(
cx.entity().clone(),
move |_, params, _, _| {
let indent_size = params.indent_size;
let left_offset = indent_size - px(3.0);
let item_height = params.item_height;
params
.indent_guides
.into_iter()
.enumerate()
.map(|(_, layout)| {
let offset = if layout.continues_offscreen {
px(0.)
} else {
px(4.0)
};
let bounds = Bounds::new(
point(
px(layout.offset.x as f32) * indent_size + left_offset,
px(layout.offset.y as f32) * item_height + offset,
),
size(
px(1.),
px(layout.length as f32) * item_height
- px(offset.0 * 2.),
),
);
ui::RenderedIndentGuide {
bounds,
layout,
is_active: false,
hitbox: None,
}
})
.collect()
},
),
)
.size_full() .size_full()
.with_sizing_behavior(ListSizingBehavior::Infer) .with_sizing_behavior(ListSizingBehavior::Infer)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained) .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
@ -1642,59 +1711,22 @@ impl GitPanel {
&self, &self,
ix: usize, ix: usize,
header: &GitHeaderEntry, header: &GitHeaderEntry,
has_write_access: bool, _: bool,
window: &Window, _: &Window,
cx: &Context<Self>, _: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let selected = self.selected_entry == Some(ix);
let header_state = if self.has_staged_changes() {
self.header_state(header.header)
} else {
match header.header {
Section::Tracked | Section::Conflict => ToggleState::Selected,
Section::New => ToggleState::Unselected,
}
};
let checkbox = Checkbox::new(("checkbox", ix), header_state)
.disabled(!has_write_access)
.fill()
.placeholder(!self.has_staged_changes())
.elevation(ElevationIndex::Surface)
.on_click({
let header = header.clone();
cx.listener(move |this, _, window, cx| {
this.toggle_staged_for_entry(&GitListEntry::Header(header.clone()), window, cx);
cx.stop_propagation();
})
});
let start_slot = h_flex()
.id(("start-slot", ix))
.gap(DynamicSpacing::Base04.rems(cx))
.child(checkbox)
.tooltip(|window, cx| Tooltip::for_action("Stage File", &ToggleStaged, window, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
cx.stop_propagation();
});
div() div()
.w_full() .w_full()
.child( .child(
ListItem::new(ix) ListItem::new(ix)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.start_slot(start_slot) .disabled(true)
.toggle_state(selected) .child(
.focused(selected && self.focus_handle(cx).is_focused(window)) Label::new(header.title())
.disabled(!has_write_access) .color(Color::Muted)
.on_click({ .size(LabelSize::Small)
cx.listener(move |this, _, _, cx| { .single_line(),
this.selected_entry = Some(ix); ),
cx.notify();
})
})
.child(h_flex().child(self.entry_label(header.title(), Color::Muted))),
) )
.into_any_element() .into_any_element()
} }
@ -1710,6 +1742,50 @@ impl GitPanel {
repo.update(cx, |repo, cx| repo.show(sha, cx)) repo.update(cx, |repo, cx| repo.show(sha, cx))
} }
fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
return;
};
let revert_title = if entry.status.is_deleted() {
"Restore file"
} else if entry.status.is_created() {
"Trash file"
} else {
"Discard changes"
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action("Stage File", ToggleStaged.boxed_clone())
.action(revert_title, editor::actions::RevertFile.boxed_clone())
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
});
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.context_menu.take();
cx.notify();
},
);
self.selected_entry = Some(ix);
self.context_menu = Some((context_menu, position, subscription));
cx.notify();
}
fn render_entry( fn render_entry(
&self, &self,
ix: usize, ix: usize,
@ -1789,26 +1865,31 @@ impl GitPanel {
cx.stop_propagation(); cx.stop_propagation();
}); });
let id = ElementId::Name(format!("entry_{}", display_name).into());
div() div()
.w_full() .w_full()
.child( .child(
ListItem::new(id) ListItem::new(ix)
.indent_level(1)
.indent_step_size(Checkbox::container_size(cx).to_pixels(window.rem_size()))
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.start_slot(start_slot) .start_slot(start_slot)
.toggle_state(selected) .toggle_state(selected)
.focused(selected && self.focus_handle(cx).is_focused(window)) .focused(selected && self.focus_handle(cx).is_focused(window))
.disabled(!has_write_access) .disabled(!has_write_access)
.on_click({ .on_click({
cx.listener(move |this, _, window, cx| { cx.listener(move |this, event: &ClickEvent, window, cx| {
this.selected_entry = Some(ix); this.selected_entry = Some(ix);
cx.notify(); cx.notify();
this.open_selected(&Default::default(), window, cx); if event.modifiers().secondary() {
this.open_file(&Default::default(), window, cx)
} else {
this.open_diff(&Default::default(), window, cx);
}
}) })
}) })
.on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, window, cx| {
this.deploy_context_menu(event.position, ix, window, cx)
},
))
.child( .child(
h_flex() h_flex()
.when_some(repo_path.parent(), |this, parent| { .when_some(repo_path.parent(), |this, parent| {
@ -1870,14 +1951,14 @@ impl Render for GitPanel {
})) }))
.on_action(cx.listener(GitPanel::commit)) .on_action(cx.listener(GitPanel::commit))
}) })
.when(self.is_focused(window, cx), |this| { .on_action(cx.listener(Self::select_first))
this.on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::close_panel))
.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_selected)) .on_action(cx.listener(Self::revert))
.on_action(cx.listener(Self::focus_changes_list)) .on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor)) .on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::toggle_staged_for_selected)) .on_action(cx.listener(Self::toggle_staged_for_selected))
@ -1906,6 +1987,15 @@ impl Render for GitPanel {
}) })
.children(self.render_previous_commit(cx)) .children(self.render_previous_commit(cx))
.child(self.render_commit_editor(window, cx)) .child(self.render_commit_editor(window, cx))
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
.position(*position)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
} }
} }

View file

@ -1612,6 +1612,16 @@ impl Project {
}) })
} }
pub fn delete_file(
&mut self,
path: ProjectPath,
trash: bool,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
let entry = self.entry_for_path(&path, cx)?;
self.delete_entry(entry.id, trash, cx)
}
pub fn delete_entry( pub fn delete_entry(
&mut self, &mut self,
entry_id: ProjectEntryId, entry_id: ProjectEntryId,