Add "Open in Terminal" context menu entries for project panel, editor and tab context menus (#10741)

Closes https://github.com/zed-industries/zed/issues/4566

Pane tabs (does not exist for multibuffer tabs):
<img width="439" alt="Screenshot 2024-04-18 at 23 01 08"
src="https://github.com/zed-industries/zed/assets/2690773/3af79ed8-07ea-4cf2-bcf9-735b1b3be8c4">

Editor context menu:
<img width="404" alt="Screenshot 2024-04-18 at 23 01 14"
src="https://github.com/zed-industries/zed/assets/2690773/38ea7afc-df2b-45ef-8331-eb6a4588af9f">

Project panel context menu (was not shown for file entries before this):
<img width="408" alt="Screenshot 2024-04-18 at 23 01 18"
src="https://github.com/zed-industries/zed/assets/2690773/e336fce1-7da0-4671-b8d2-8d3409c23eb6">

Release Notes:

- (breaking change) Moved `project_panel::OpenInTerminal` into
`workspace::OpenInTerminal` action and add it in editors, tab context
menus and proper panel file entries
([4566](https://github.com/zed-industries/zed/issues/4566))
This commit is contained in:
Kirill Bulatov 2024-04-19 01:43:46 +03:00 committed by GitHub
parent 250b71fb44
commit 870a61dd4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 101 additions and 45 deletions

View file

@ -131,10 +131,10 @@ use ui::{
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle; use workspace::item::ItemHandle;
use workspace::notifications::NotificationId; use workspace::notifications::NotificationId;
use workspace::Toast;
use workspace::{ use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
}; };
use workspace::{OpenInTerminal, OpenTerminal, Toast};
use crate::hover_links::find_url; use crate::hover_links::find_url;
@ -4943,6 +4943,25 @@ impl Editor {
} }
} }
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
let project_path = buffer.read(cx).project_path(cx)?;
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&project_path, cx)?;
let abs_path = project.absolute_path(&project_path, cx)?;
let parent = if entry.is_symlink {
abs_path.canonicalize().ok()?
} else {
abs_path
}
.parent()?
.to_path_buf();
Some(parent)
}) {
cx.dispatch_action(OpenTerminal { working_directory }.boxed_clone());
}
}
fn gather_revert_changes( fn gather_revert_changes(
&mut self, &mut self,
selections: &[Selection<Anchor>], selections: &[Selection<Anchor>],

View file

@ -363,6 +363,7 @@ impl EditorElement {
register_action(view, cx, Editor::unique_lines_case_sensitive); register_action(view, cx, Editor::unique_lines_case_sensitive);
register_action(view, cx, Editor::accept_partial_inline_completion); register_action(view, cx, Editor::accept_partial_inline_completion);
register_action(view, cx, Editor::revert_selected_hunks); register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal)
} }
fn register_key_listeners(&self, cx: &mut ElementContext, layout: &EditorLayout) { fn register_key_listeners(&self, cx: &mut ElementContext, layout: &EditorLayout) {

View file

@ -3,6 +3,7 @@ use crate::{
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions, GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
}; };
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use workspace::OpenInTerminal;
pub struct MouseContextMenu { pub struct MouseContextMenu {
pub(crate) position: Point<Pixels>, pub(crate) position: Point<Pixels>,
@ -83,6 +84,7 @@ pub fn deploy_context_menu(
) )
.separator() .separator()
.action("Reveal in Finder", Box::new(RevealInFinder)) .action("Reveal in Finder", Box::new(RevealInFinder))
.action("Open in Terminal", Box::new(OpenInTerminal))
}) })
}; };
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx); let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);

View file

@ -37,7 +37,7 @@ use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr, notifications::DetachAndPromptErr,
Workspace, OpenInTerminal, Workspace,
}; };
const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const PROJECT_PANEL_KEY: &str = "ProjectPanel";
@ -127,7 +127,6 @@ actions!(
CopyPath, CopyPath,
CopyRelativePath, CopyRelativePath,
RevealInFinder, RevealInFinder,
OpenInTerminal,
Cut, Cut,
Paste, Paste,
Rename, Rename,
@ -441,9 +440,7 @@ impl ProjectPanel {
.action("New Folder", Box::new(NewDirectory)) .action("New Folder", Box::new(NewDirectory))
.separator() .separator()
.action("Reveal in Finder", Box::new(RevealInFinder)) .action("Reveal in Finder", Box::new(RevealInFinder))
.when(is_dir, |menu| { .action("Open in Terminal", Box::new(OpenInTerminal))
menu.action("Open in Terminal…", Box::new(OpenInTerminal))
})
.when(is_dir, |menu| { .when(is_dir, |menu| {
menu.separator() menu.separator()
.action("Find in Folder…", Box::new(NewSearchInDirectory)) .action("Find in Folder…", Box::new(NewSearchInDirectory))
@ -1131,13 +1128,20 @@ impl ProjectPanel {
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) { fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) { if let Some((worktree, entry)) = self.selected_entry(cx) {
let path = worktree.abs_path().join(&entry.path); let abs_path = worktree.abs_path().join(&entry.path);
cx.dispatch_action( let working_directory = if entry.is_dir() {
workspace::OpenTerminal { Some(abs_path)
working_directory: path, } else {
if entry.is_symlink {
abs_path.canonicalize().ok()
} else {
Some(abs_path)
} }
.boxed_clone(), .and_then(|path| Some(path.parent()?.to_path_buf()))
) };
if let Some(working_directory) = working_directory {
cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
}
} }
} }

View file

@ -98,7 +98,6 @@ impl TerminalPanel {
.on_click(cx.listener(|pane, _, cx| { .on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx); pane.toggle_zoom(&workspace::ToggleZoom, cx);
})) }))
// TODO kb
.tooltip(move |cx| { .tooltip(move |cx| {
Tooltip::for_action( Tooltip::for_action(
if zoomed { "Zoom Out" } else { "Zoom In" }, if zoomed { "Zoom Out" } else { "Zoom In" },
@ -292,13 +291,13 @@ impl TerminalPanel {
action: &workspace::OpenTerminal, action: &workspace::OpenTerminal,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let Some(this) = workspace.focus_panel::<Self>(cx) else { let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
return; return;
}; };
terminal_panel.update(cx, |panel, cx| {
this.update(cx, |this, cx| { panel.add_terminal(Some(action.working_directory.clone()), None, cx)
this.add_terminal(Some(action.working_directory.clone()), None, cx) });
}) workspace.focus_panel::<Self>(cx);
} }
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) { fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
@ -427,26 +426,17 @@ impl TerminalPanel {
} }
} }
///Create a new Terminal in the current working directory or the user's home directory /// Create a new Terminal in the current working directory or the user's home directory
fn new_terminal( fn new_terminal(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &workspace::NewTerminal, _: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let has_no_terminals = workspace let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
.panel::<Self>(cx)
.map(|terminal_panel| terminal_panel.update(cx, |panel, cx| panel.has_no_terminals(cx)))
.unwrap_or(true);
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return; return;
}; };
if has_no_terminals { terminal_panel.update(cx, |this, cx| this.add_terminal(None, None, cx));
// `set_active` on focus, will already add a new terminal workspace.focus_panel::<Self>(cx);
// into an empty terminal pane, no need to add another one
return;
}
this.update(cx, |this, cx| this.add_terminal(None, None, cx))
} }
fn terminals_for_task( fn terminals_for_task(

View file

@ -5,7 +5,8 @@ use crate::{
}, },
toolbar::Toolbar, toolbar::Toolbar,
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace, NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
SplitDirection, ToggleZoom, Workspace,
}; };
use anyhow::Result; use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque}; use collections::{HashMap, HashSet, VecDeque};
@ -1597,20 +1598,58 @@ impl Pane {
); );
if let Some(entry) = single_entry_to_resolve { if let Some(entry) = single_entry_to_resolve {
let parent_abs_path = pane
.update(cx, |pane, cx| {
pane.workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
project.worktree_for_entry(entry, cx).and_then(|worktree| {
let worktree = worktree.read(cx);
let entry = worktree.entry_for_id(entry)?;
let abs_path = worktree.absolutize(&entry.path).ok()?;
let parent = if entry.is_symlink {
abs_path.canonicalize().ok()?
} else {
abs_path
}
.parent()?
.to_path_buf();
Some(parent)
})
})
})
.ok()
.flatten();
let entry_id = entry.to_proto(); let entry_id = entry.to_proto();
menu = menu.separator().entry( menu = menu
"Reveal In Project Panel", .separator()
Some(Box::new(RevealInProjectPanel { .entry(
entry_id: Some(entry_id), "Reveal In Project Panel",
})), Some(Box::new(RevealInProjectPanel {
cx.handler_for(&pane, move |pane, cx| { entry_id: Some(entry_id),
pane.project.update(cx, |_, cx| { })),
cx.emit(project::Event::RevealInProjectPanel( cx.handler_for(&pane, move |pane, cx| {
ProjectEntryId::from_proto(entry_id), pane.project.update(cx, |_, cx| {
)) cx.emit(project::Event::RevealInProjectPanel(
}); ProjectEntryId::from_proto(entry_id),
}), ))
); });
}),
)
.when_some(parent_abs_path, |menu, abs_path| {
menu.entry(
"Open in Terminal",
Some(Box::new(OpenInTerminal)),
cx.handler_for(&pane, move |_, cx| {
cx.dispatch_action(
OpenTerminal {
working_directory: abs_path.clone(),
}
.boxed_clone(),
);
}),
)
});
} }
} }

View file

@ -112,6 +112,7 @@ actions!(
workspace, workspace,
[ [
Open, Open,
OpenInTerminal,
NewFile, NewFile,
NewWindow, NewWindow,
CloseWindow, CloseWindow,