diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 6bee662a75..779bc2c4a0 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -94,7 +94,6 @@ impl Project { } } }; - let ssh_details = self.ssh_details(cx); let mut settings_location = None; if let Some(path) = path.as_ref() { @@ -107,10 +106,57 @@ impl Project { } let settings = TerminalSettings::get(settings_location, cx).clone(); + cx.spawn(move |project, mut cx| async move { + let python_venv_directory = if let Some(path) = path.clone() { + project + .update(&mut cx, |this, cx| { + this.python_venv_directory(path, settings.detect_venv.clone(), cx) + })? + .await + } else { + None + }; + project.update(&mut cx, |project, cx| { + project.create_terminal_with_venv(kind, python_venv_directory, window, cx) + })? + }) + } + + pub fn create_terminal_with_venv( + &mut self, + kind: TerminalKind, + python_venv_directory: Option, + window: AnyWindowHandle, + cx: &mut ModelContext, + ) -> Result> { + let this = &mut *self; + let path: Option> = match &kind { + TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), + TerminalKind::Task(spawn_task) => { + if let Some(cwd) = &spawn_task.cwd { + Some(Arc::from(cwd.as_ref())) + } else { + this.active_project_directory(cx) + } + } + }; + let ssh_details = this.ssh_details(cx); + + let mut settings_location = None; + if let Some(path) = path.as_ref() { + if let Some((worktree, _)) = this.find_worktree(path, cx) { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); + } + } + let settings = TerminalSettings::get(settings_location, cx).clone(); + let (completion_tx, completion_rx) = bounded(1); // Start with the environment that we might have inherited from the Zed CLI. - let mut env = self + let mut env = this .environment .read(cx) .get_cli_environment() @@ -125,165 +171,141 @@ impl Project { None }; - cx.spawn(move |this, mut cx| async move { - let python_venv_directory = if let Some(path) = path.clone() { - this.update(&mut cx, |this, cx| { - this.python_venv_directory(path, settings.detect_venv.clone(), cx) - })? - .await - } else { - None - }; - let mut python_venv_activate_command = None; + let mut python_venv_activate_command = None; - let (spawn_task, shell) = match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = python_venv_directory { - python_venv_activate_command = this - .update(&mut cx, |this, _| { - this.python_activate_command( - &python_venv_directory, - &settings.detect_venv, - ) - }) - .ok() - .flatten(); + let (spawn_task, shell) = match kind { + TerminalKind::Shell(_) => { + if let Some(python_venv_directory) = &python_venv_directory { + python_venv_activate_command = + this.python_activate_command(python_venv_directory, &settings.detect_venv); + } + + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = + wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None); + env = HashMap::default(); + ( + Option::::None, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) } + None => (None, settings.shell.clone()), + } + } + TerminalKind::Task(spawn_task) => { + let task_state = Some(TaskState { + id: spawn_task.id, + full_label: spawn_task.full_label, + label: spawn_task.label, + command_label: spawn_task.command_label, + hide: spawn_task.hide, + status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, + completion_rx, + }); - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.extend(spawn_task.env); - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); + if let Some(venv_path) = &python_venv_directory { + env.insert( + "VIRTUAL_ENV".to_string(), + venv_path.to_string_lossy().to_string(), + ); + } - let (program, args) = - wrap_for_ssh(ssh_command, None, path.as_deref(), env, None); - env = HashMap::default(); - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + let (program, args) = wrap_for_ssh( + &ssh_command, + Some((&spawn_task.command, &spawn_task.args)), + path.as_deref(), + env, + python_venv_directory.as_deref(), + ); + env = HashMap::default(); + ( + task_state, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => { + if let Some(venv_path) = &python_venv_directory { + add_environment_path(&mut env, &venv_path.join("bin")).log_err(); } - None => (None, settings.shell.clone()), + + ( + task_state, + Shell::WithArguments { + program: spawn_task.command, + args: spawn_task.args, + title_override: None, + }, + ) } } - TerminalKind::Task(spawn_task) => { - let task_state = Some(TaskState { - id: spawn_task.id, - full_label: spawn_task.full_label, - label: spawn_task.label, - command_label: spawn_task.command_label, - hide: spawn_task.hide, - status: TaskStatus::Running, - show_summary: spawn_task.show_summary, - show_command: spawn_task.show_command, - completion_rx, - }); + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + python_venv_directory, + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + ssh_details.is_some(), + window, + completion_tx, + cx, + ) + .map(|builder| { + let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); - env.extend(spawn_task.env); + this.terminals + .local_handles + .push(terminal_handle.downgrade()); - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - ssh_command, - Some((&spawn_task.command, &spawn_task.args)), - path.as_deref(), - env, - python_venv_directory, - ); - env = HashMap::default(); - ( - task_state, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); - } - - ( - task_state, - Shell::WithArguments { - program: spawn_task.command, - args: spawn_task.args, - title_override: None, - }, - ) - } - } + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); } - }; - let terminal = this.update(&mut cx, |this, cx| { - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - ssh_details.is_some(), - window, - completion_tx, - cx, - ) - .map(|builder| { - let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); + }) + .detach(); - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); - } - }) - .detach(); - - if let Some(activate_command) = python_venv_activate_command { - this.activate_python_virtual_environment( - activate_command, - &terminal_handle, - cx, - ); - } - terminal_handle - }) - })?; - - terminal + if let Some(activate_command) = python_venv_activate_command { + this.activate_python_virtual_environment(activate_command, &terminal_handle, cx); + } + terminal_handle }) } @@ -418,9 +440,9 @@ impl Project { &self, command: String, terminal_handle: &Model, - cx: &mut ModelContext, + cx: &mut AppContext, ) { - terminal_handle.update(cx, |this, _| this.input_bytes(command.into_bytes())); + terminal_handle.update(cx, |terminal, _| terminal.input_bytes(command.into_bytes())); } pub fn local_terminal_handles(&self) -> &Vec> { @@ -433,7 +455,7 @@ pub fn wrap_for_ssh( command: Option<(&String, &Vec)>, path: Option<&Path>, env: HashMap, - venv_directory: Option, + venv_directory: Option<&Path>, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { let command = Cow::Borrowed(command.as_str()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6610ac567d..51a95bc3ee 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -324,6 +324,7 @@ impl TerminalBuilder { #[allow(clippy::too_many_arguments)] pub fn new( working_directory: Option, + python_venv_directory: Option, task: Option, shell: Shell, mut env: HashMap, @@ -471,6 +472,7 @@ impl TerminalBuilder { word_regex: RegexSearch::new(WORD_REGEX).unwrap(), vi_mode_enabled: false, is_ssh_terminal, + python_venv_directory, }; Ok(TerminalBuilder { @@ -619,6 +621,7 @@ pub struct Terminal { pub breadcrumb_text: String, pub pty_info: PtyProcessInfo, title_override: Option, + pub python_venv_directory: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index f4653014a1..4e88cb9515 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -251,7 +251,13 @@ async fn deserialize_pane_group( let terminal = terminal.await.ok()?; pane.update(cx, |pane, cx| { let terminal_view = Box::new(cx.new_view(|cx| { - TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx) + TerminalView::new( + terminal, + workspace.clone(), + Some(workspace_id), + project.downgrade(), + cx, + ) })); pane.add_item(terminal_view, true, false, None, cx); }) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index a4f5e7df4f..da1bb7c406 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -335,24 +335,13 @@ impl TerminalPanel { self.serialize(cx); } pane::Event::Split(direction) => { - let new_pane = self.new_pane_with_cloned_active_terminal(cx); + let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { + return; + }; let pane = pane.clone(); let direction = *direction; - cx.spawn(move |terminal_panel, mut cx| async move { - let Some(new_pane) = new_pane.await else { - return; - }; - terminal_panel - .update(&mut cx, |terminal_panel, cx| { - terminal_panel - .center - .split(&pane, &new_pane, direction) - .log_err(); - cx.focus_view(&new_pane); - }) - .ok(); - }) - .detach(); + self.center.split(&pane, &new_pane, direction).log_err(); + cx.focus_view(&new_pane); } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -365,63 +354,56 @@ impl TerminalPanel { fn new_pane_with_cloned_active_terminal( &mut self, cx: &mut ViewContext, - ) -> Task>> { - let Some(workspace) = self.workspace.clone().upgrade() else { - return Task::ready(None); - }; - let database_id = workspace.read(cx).database_id(); + ) -> Option> { + let workspace = self.workspace.clone().upgrade()?; + let workspace = workspace.read(cx); + let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); - let project = workspace.read(cx).project().clone(); - let working_directory = self + let project = workspace.project().clone(); + let (working_directory, python_venv_directory) = self .active_pane .read(cx) .active_item() .and_then(|item| item.downcast::()) - .and_then(|terminal_view| { - terminal_view - .read(cx) - .terminal() - .read(cx) - .working_directory() + .map(|terminal_view| { + let terminal = terminal_view.read(cx).terminal().read(cx); + ( + terminal + .working_directory() + .or_else(|| default_working_directory(workspace, cx)), + terminal.python_venv_directory.clone(), + ) }) - .or_else(|| default_working_directory(workspace.read(cx), cx)); + .unwrap_or((None, None)); let kind = TerminalKind::Shell(working_directory); let window = cx.window_handle(); - cx.spawn(move |terminal_panel, mut cx| async move { - let terminal = project - .update(&mut cx, |project, cx| { - project.create_terminal(kind, window, cx) - }) - .log_err()? - .await - .log_err()?; - - let terminal_view = Box::new( - cx.new_view(|cx| { - TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx) - }) - .ok()?, - ); - let pane = terminal_panel - .update(&mut cx, |terminal_panel, cx| { - let pane = new_terminal_pane( - weak_workspace, - project, - terminal_panel.active_pane.read(cx).is_zoomed(), - cx, - ); - terminal_panel.apply_tab_bar_buttons(&pane, cx); - pane - }) - .ok()?; - - pane.update(&mut cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, cx); + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_with_venv(kind, python_venv_directory, window, cx) }) .ok()?; - Some(pane) - }) + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new( + terminal.clone(), + weak_workspace.clone(), + database_id, + project.downgrade(), + cx, + ) + })); + let pane = new_terminal_pane( + weak_workspace, + project, + self.active_pane.read(cx).is_zoomed(), + cx, + ); + self.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, cx); + }); + + Some(pane) } pub fn open_terminal( @@ -724,6 +706,7 @@ impl TerminalPanel { terminal.clone(), workspace.weak_handle(), workspace.database_id(), + workspace.project().downgrade(), cx, ) }); @@ -739,17 +722,19 @@ impl TerminalPanel { reveal_strategy: RevealStrategy, cx: &mut ViewContext, ) -> Task>> { - if !self.is_enabled(cx) { - return Task::ready(Err(anyhow!( - "terminal not yet supported for remote projects" - ))); - } - let workspace = self.workspace.clone(); self.pending_terminals_to_add += 1; cx.spawn(|terminal_panel, mut cx| async move { - let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?; + if workspace.update(&mut cx, |workspace, cx| { + !is_enabled_in_workspace(workspace, cx) + })? { + anyhow::bail!("terminal not yet supported for remote projects"); + } + let pane = terminal_panel.update(&mut cx, |terminal_panel, _| { + terminal_panel.pending_terminals_to_add += 1; + terminal_panel.active_pane.clone() + })?; let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?; let window = cx.window_handle(); let terminal = project @@ -763,6 +748,7 @@ impl TerminalPanel { terminal.clone(), workspace.weak_handle(), workspace.database_id(), + workspace.project().downgrade(), cx, ) })); @@ -1218,25 +1204,19 @@ impl Render for TerminalPanel { if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { cx.focus_view(&pane); } else { - let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx); - cx.spawn(|terminal_panel, mut cx| async move { - if let Some(new_pane) = new_pane.await { - terminal_panel - .update(&mut cx, |terminal_panel, cx| { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - cx.focus_view(&new_pane); - }) - .ok(); - } - }) - .detach(); + if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + cx.focus_view(&new_pane); + } } })) .on_action(cx.listener( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9cc7b3ccec..9f101fe057 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -9,7 +9,7 @@ use gpui::{ anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View, - VisualContext, WeakView, + VisualContext, WeakModel, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; @@ -97,6 +97,7 @@ pub struct BlockContext<'a, 'b> { pub struct TerminalView { terminal: Model, workspace: WeakView, + project: WeakModel, focus_handle: FocusHandle, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, @@ -141,6 +142,7 @@ impl TerminalView { terminal: Model, workspace: WeakView, workspace_id: Option, + project: WeakModel, cx: &mut ViewContext, ) -> Self { let workspace_handle = workspace.clone(); @@ -160,6 +162,7 @@ impl TerminalView { Self { terminal, workspace: workspace_handle, + project, has_bell: false, focus_handle, context_menu: None, @@ -1075,21 +1078,37 @@ impl Item for TerminalView { fn clone_on_split( &self, - _workspace_id: Option, - _cx: &mut ViewContext, + workspace_id: Option, + cx: &mut ViewContext, ) -> Option> { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC + let window = cx.window_handle(); + let terminal = self + .project + .update(cx, |project, cx| { + let terminal = self.terminal().read(cx); + let working_directory = terminal + .working_directory() + .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf())); + let python_venv_directory = terminal.python_venv_directory.clone(); + project.create_terminal_with_venv( + TerminalKind::Shell(working_directory), + python_venv_directory, + window, + cx, + ) + }) + .ok()? + .log_err()?; - // Some(TerminalContainer::new( - // Err(anyhow::anyhow!("failed to instantiate terminal")), - // workspace_id, - // cx, - // )) - - // TODO - None + Some(cx.new_view(|cx| { + TerminalView::new( + terminal, + self.workspace.clone(), + workspace_id, + self.project.clone(), + cx, + ) + })) } fn is_dirty(&self, cx: &gpui::AppContext) -> bool { @@ -1218,7 +1237,15 @@ impl SerializableItem for TerminalView { })? .await?; cx.update(|cx| { - cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) + cx.new_view(|cx| { + TerminalView::new( + terminal, + workspace, + Some(workspace_id), + project.downgrade(), + cx, + ) + }) }) }) } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 45f5a4e094..ae2c383baf 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -173,7 +173,7 @@ This could be useful for launching a terminal application that you want to use i "bindings": { "alt-g": [ "task::Spawn", - { "task_name": "start lazygit", "target": "center" } + { "task_name": "start lazygit", "reveal_target": "center" } ] } }