diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3b78bff39142bcf5e7f5011599d50925e632ff4a..d3c2b91c7f65a38c02dfcb9eee322aa2a8ff9a65 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11283,7 +11283,14 @@ impl Editor { .to_path_buf(); Some(parent) }) { - window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); + window.dispatch_action( + OpenTerminal { + working_directory, + local: false, + } + .boxed_clone(), + cx, + ); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 764ba887bc25898456ca2f1b3656096c58d2ac4b..a1a4898a957c4b381f3da93d8a0cc9fff84931de 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4386,6 +4386,7 @@ impl EditorElement { window.dispatch_action( OpenTerminal { working_directory: parent_abs_path.clone(), + local: false, } .boxed_clone(), cx, diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index d4ef31200542cfeb07a05c6c3705d5d4a1d10cbd..946be32c9717b9563853e222f6e66b340ad3ab81 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2036,7 +2036,11 @@ impl OutlinePanel { if let Some(working_directory) = working_directory { window.dispatch_action( - workspace::OpenTerminal { working_directory }.boxed_clone(), + workspace::OpenTerminal { + working_directory, + local: false, + } + .boxed_clone(), cx, ) } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index f928871f1eb56bdbc38109d8509acb9776edb994..f9f3672cf2f53516725108b761fe3c09b0064fee 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -281,9 +281,38 @@ impl Project { &mut self, cwd: Option, cx: &mut Context, + ) -> Task>> { + self.create_terminal_shell_internal(cwd, false, cx) + } + + /// Creates a local terminal even if the project is remote. + /// In remote projects: opens in Zed's launch directory (bypasses SSH). + /// In local projects: opens in the project directory (same as regular terminals). + pub fn create_local_terminal( + &mut self, + cx: &mut Context, + ) -> Task>> { + let working_directory = if self.remote_client.is_some() { + // Remote project: don't use remote paths, let shell use Zed's cwd + None + } else { + // Local project: use project directory like normal terminals + self.active_project_directory(cx).map(|p| p.to_path_buf()) + }; + self.create_terminal_shell_internal(working_directory, true, cx) + } + + /// Internal method for creating terminal shells. + /// If force_local is true, creates a local terminal even if the project has a remote client. + /// This allows "breaking out" to a local shell in remote projects. + fn create_terminal_shell_internal( + &mut self, + cwd: Option, + force_local: bool, + cx: &mut Context, ) -> Task>> { let path = cwd.map(|p| Arc::from(&*p)); - let is_via_remote = self.remote_client.is_some(); + let is_via_remote = !force_local && self.remote_client.is_some(); let mut settings_location = None; if let Some(path) = path.as_ref() @@ -314,7 +343,11 @@ impl Project { .filter(|_| detect_venv) .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx)) .collect::>(); - let remote_client = self.remote_client.clone(); + let remote_client = if force_local { + None + } else { + self.remote_client.clone() + }; let shell = match &remote_client { Some(remote_client) => remote_client .read(cx) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a8623a62ed2dcb8749eafeadf117f9851327b6f6..64b1ef92b1f7d2e42b0a549a76c61c0c196f9df9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3152,7 +3152,11 @@ impl ProjectPanel { }; if let Some(working_directory) = working_directory { window.dispatch_action( - workspace::OpenTerminal { working_directory }.boxed_clone(), + workspace::OpenTerminal { + working_directory, + local: false, + } + .boxed_clone(), cx, ) } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 0b58400a86ff40849d2e67b0533802ab25f2a88f..f3122983aad67f2f746ea85fb53bf0290f9f5e95 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -29,9 +29,9 @@ use util::{ResultExt, TryFutureExt}; use workspace::{ ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, - MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal, - Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, - SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace, + MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane, + PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown, + SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace, dock::{DockPosition, Panel, PanelEvent, PanelHandle}, item::SerializableItem, move_active_item, move_item, pane, @@ -161,7 +161,7 @@ impl TerminalPanel { menu.context(focus_handle.clone()) .action( "New Terminal", - workspace::NewTerminal.boxed_clone(), + workspace::NewTerminal::default().boxed_clone(), ) // We want the focus to go back to terminal panel once task modal is dismissed, // hence we focus that first. Otherwise, we'd end up without a focused element, as @@ -524,12 +524,16 @@ impl TerminalPanel { terminal_panel .update(cx, |panel, cx| { - panel.add_terminal_shell( - Some(action.working_directory.clone()), - RevealStrategy::Always, - window, - cx, - ) + if action.local { + panel.add_local_terminal_shell(RevealStrategy::Always, window, cx) + } else { + panel.add_terminal_shell( + Some(action.working_directory.clone()), + RevealStrategy::Always, + window, + cx, + ) + } }) .detach_and_log_err(cx); } @@ -649,7 +653,7 @@ impl TerminalPanel { /// Create a new Terminal in the current working directory or the user's home directory fn new_terminal( workspace: &mut Workspace, - _: &workspace::NewTerminal, + action: &workspace::NewTerminal, window: &mut Window, cx: &mut Context, ) { @@ -659,12 +663,16 @@ impl TerminalPanel { terminal_panel .update(cx, |this, cx| { - this.add_terminal_shell( - default_working_directory(workspace, cx), - RevealStrategy::Always, - window, - cx, - ) + if action.local { + this.add_local_terminal_shell(RevealStrategy::Always, window, cx) + } else { + this.add_terminal_shell( + default_working_directory(workspace, cx), + RevealStrategy::Always, + window, + cx, + ) + } }) .detach_and_log_err(cx); } @@ -824,6 +832,26 @@ impl TerminalPanel { reveal_strategy: RevealStrategy, window: &mut Window, cx: &mut Context, + ) -> Task>> { + self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx) + } + + fn add_local_terminal_shell( + &mut self, + reveal_strategy: RevealStrategy, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx) + } + + fn add_terminal_shell_internal( + &mut self, + force_local: bool, + cwd: Option, + reveal_strategy: RevealStrategy, + window: &mut Window, + cx: &mut Context, ) -> Task>> { let workspace = self.workspace.clone(); @@ -836,9 +864,15 @@ impl TerminalPanel { terminal_panel.active_pane.clone() })?; let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let terminal = project - .update(cx, |project, cx| project.create_terminal_shell(cwd, cx)) - .await; + let terminal = if force_local { + project + .update(cx, |project, cx| project.create_local_terminal(cx)) + .await + } else { + project + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx)) + .await + }; match terminal { Ok(terminal) => { @@ -1120,7 +1154,7 @@ pub fn new_terminal_pane( project.clone(), Default::default(), None, - NewTerminal.boxed_clone(), + workspace::NewTerminal::default().boxed_clone(), false, window, cx, @@ -1980,6 +2014,37 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + + let (window_handle, terminal_panel) = workspace + .update(cx, |workspace, window, cx| { + let window_handle = window.window_handle(); + let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); + (window_handle, terminal_panel) + }) + .unwrap(); + + let result = window_handle + .update(cx, |_, window, cx| { + terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx) + }) + }) + .unwrap() + .await; + + assert!( + result.is_ok(), + "local terminal should successfully create in local project" + ); + } + fn set_max_tabs(cx: &mut TestAppContext, value: Option) { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings(cx, |settings| { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 670b3db583d01dcf202090407b309803ec9232d2..82203a5b8ee0994885013dba0c58741f32c780b3 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -85,7 +85,7 @@ actions!( terminal, [ /// Reruns the last executed task in the terminal. - RerunTask + RerunTask, ] ); @@ -194,13 +194,18 @@ impl TerminalView { ///Create a new Terminal in the current working directory or the user's home directory pub fn deploy( workspace: &mut Workspace, - _: &NewCenterTerminal, + action: &NewCenterTerminal, window: &mut Window, cx: &mut Context, ) { + let local = action.local; let working_directory = default_working_directory(workspace, cx); - TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) + TerminalPanel::add_center_terminal(workspace, window, cx, move |project, cx| { + if local { + project.create_local_terminal(cx) + } else { + project.create_terminal_shell(working_directory, cx) + } }) .detach_and_log_err(cx); } @@ -389,7 +394,7 @@ impl TerminalView { .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); let context_menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(self.focus_handle.clone()) - .action("New Terminal", Box::new(NewTerminal)) + .action("New Terminal", Box::new(NewTerminal::default())) .separator() .action("Copy", Box::new(Copy)) .action("Paste", Box::new(Paste)) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d02140b90450b1936c9e4d03956f0acb535e13bc..318a790fdfaaaa2dc2d68ccde6d047f08e186a4e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3134,6 +3134,7 @@ impl Pane { window.dispatch_action( OpenTerminal { working_directory: parent_abs_path.clone(), + local: false, } .boxed_clone(), cx, @@ -4032,7 +4033,7 @@ fn default_render_tab_bar_buttons( ) .action("Search Symbols", ToggleProjectSymbols.boxed_clone()) .separator() - .action("New Terminal", NewTerminal.boxed_clone()) + .action("New Terminal", NewTerminal::default().boxed_clone()) })) }), ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d5811f3528c1d9bbc65fc80b38e394d8b0a975ba..492f0bf70bbf848da5d690d8a548b3dbf6eb62de 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -231,8 +231,6 @@ actions!( FollowNextCollaborator, /// Moves the focused panel to the next position. MoveFocusedPanelToNextPosition, - /// Opens a new terminal in the center. - NewCenterTerminal, /// Creates a new file. NewFile, /// Creates a new file in a vertical split. @@ -241,8 +239,6 @@ actions!( NewFileSplitHorizontal, /// Opens a new search. NewSearch, - /// Opens a new terminal. - NewTerminal, /// Opens a new window. NewWindow, /// Opens a file or directory. @@ -406,6 +402,26 @@ pub struct ToggleFileFinder { pub separate_history: bool, } +/// Opens a new terminal in the center. +#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)] +#[action(namespace = workspace)] +#[serde(deny_unknown_fields)] +pub struct NewCenterTerminal { + /// If true, creates a local terminal even in remote projects. + #[serde(default)] + pub local: bool, +} + +/// Opens a new terminal. +#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)] +#[action(namespace = workspace)] +#[serde(deny_unknown_fields)] +pub struct NewTerminal { + /// If true, creates a local terminal even in remote projects. + #[serde(default)] + pub local: bool, +} + /// Increases size of a currently focused dock by a given amount of pixels. #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = workspace)] @@ -535,6 +551,9 @@ impl PartialEq for Toast { #[serde(deny_unknown_fields)] pub struct OpenTerminal { pub working_directory: PathBuf, + /// If true, creates a local terminal even in remote projects. + #[serde(default)] + pub local: bool, } #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]