use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::{ default_working_directory, persistence::{ deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel, }, TerminalView,};use breadcrumbs::Breadcrumbs;use collections::HashMap;use db::kvp::KEY_VALUE_STORE;use futures::future::join_all;use gpui::{ actions, Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, WeakEntity, Window,};use itertools::Itertools;use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};use search::{buffer_search::DivRegistrar, BufferSearchBar};use settings::Settings;use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};use terminal::{ terminal_settings::{TerminalDockPosition, TerminalSettings}, Terminal,};use ui::{ prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip,};use util::{ResultExt, TryFutureExt};use workspace::{ dock::{DockPosition, Panel, PanelEvent, PanelHandle}, item::SerializableItem, move_active_item, move_item, pane, ui::IconName, ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,}; use anyhow::{anyhow, Context as _, Result};use zed_actions::assistant::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| { workspace.register_action(TerminalPanel::new_terminal); workspace.register_action(TerminalPanel::open_terminal); workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { if is_enabled_in_workspace(workspace, cx) { workspace.toggle_panel_focus::<TerminalPanel>(window, cx); } }); }, ) .detach();} pub struct TerminalPanel { pub(crate) active_pane: Entity<Pane>, pub(crate) center: PaneGroup, fs: Arc<dyn Fs>, workspace: WeakEntity<Workspace>, pub(crate) width: Option<Pixels>, pub(crate) height: Option<Pixels>, pending_serialization: Task<Option<()>>, pending_terminals_to_add: usize, deferred_tasks: HashMap<TaskId, Task<()>>, assistant_enabled: bool, assistant_tab_bar_button: Option<AnyView>, active: bool,} impl TerminalPanel { pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self { let project = workspace.project(); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx); let center = PaneGroup::new(pane.clone()); let terminal_panel = Self { center, active_pane: pane, fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), width: None, height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), assistant_enabled: false, assistant_tab_bar_button: None, active: false, }; terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); terminal_panel } pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) { self.assistant_enabled = enabled; if enabled { let focus_handle = self .active_pane .read(cx) .active_item() .map(|item| item.item_focus_handle(cx)) .unwrap_or(self.focus_handle(cx)); self.assistant_tab_bar_button = Some( cx.new(move |_| InlineAssistTabBarButton { focus_handle }) .into(), ); } else { self.assistant_tab_bar_button = None; } for pane in self.center.panes() { self.apply_tab_bar_buttons(pane, cx); } } fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) { let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| { let split_context = pane .active_item() .and_then(|item| item.downcast::<TerminalView>()) .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { return (None, None); } let focus_handle = pane.focus_handle(cx); let right_children = h_flex() .gap(DynamicSpacing::Base02.rems(cx)) .child( PopoverMenu::new("terminal-tab-bar-popover-menu") .trigger_with_tooltip( IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), Tooltip::text("New…"), ) .anchor(Corner::TopRight) .with_handle(pane.new_item_context_menu_handle.clone()) .menu(move |window, cx| { let focus_handle = focus_handle.clone(); let menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(focus_handle.clone()) .action( "New Terminal", workspace::NewTerminal.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 // context menu will be gone the moment we spawn the modal. .action( "Spawn task", zed_actions::Spawn::modal().boxed_clone(), ) }); Some(menu) }), ) .children(assistant_tab_bar_button.clone()) .child( PopoverMenu::new("terminal-pane-tab-bar-split") .trigger_with_tooltip( IconButton::new("terminal-pane-split", IconName::Split) .icon_size(IconSize::Small), Tooltip::text("Split Pane"), ) .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) .menu({ let split_context = split_context.clone(); move |window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.when_some( split_context.clone(), |menu, split_context| menu.context(split_context), ) .action("Split Right", SplitRight.boxed_clone()) .action("Split Left", SplitLeft.boxed_clone()) .action("Split Up", SplitUp.boxed_clone()) .action("Split Down", SplitDown.boxed_clone()) }) .into() } }), ) .child({ let zoomed = pane.is_zoomed(); IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) .toggle_state(zoomed) .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, window, cx| { pane.toggle_zoom(&workspace::ToggleZoom, window, cx); })) .tooltip(move |window, cx| { Tooltip::for_action( if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, window, cx, ) }) }) .into_any_element() .into(); (None, right_children) }); }); } fn serialization_key(workspace: &Workspace) -> Option<String> { workspace .database_id() .map(|id| i64::from(id).to_string()) .or(workspace.session_id()) .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id)) } pub async fn load( workspace: WeakEntity<Workspace>, mut cx: AsyncWindowContext, ) -> Result<Entity<Self>> { let mut terminal_panel = None; match workspace .read_with(&mut cx, |workspace, _| { workspace .database_id() .zip(TerminalPanel::serialization_key(workspace)) }) .ok() .flatten() { Some((database_id, serialization_key)) => { if let Some(serialized_panel) = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) .await .log_err() .flatten() .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) .transpose() .log_err() .flatten() { if let Ok(serialized) = workspace .update_in(&mut cx, |workspace, window, cx| { deserialize_terminal_panel( workspace.weak_handle(), workspace.project().clone(), database_id, serialized_panel, window, cx, ) })? .await { terminal_panel = Some(serialized); } } } _ => {} } let terminal_panel = if let Some(panel) = terminal_panel { panel } else { workspace.update_in(&mut cx, |workspace, window, cx| { cx.new(|cx| TerminalPanel::new(workspace, window, cx)) })? }; if let Some(workspace) = workspace.upgrade() { terminal_panel .update_in(&mut cx, |_, window, cx| { cx.subscribe_in(&workspace, window, |terminal_panel, _, e, window, cx| { if let workspace::Event::SpawnTask { action: spawn_in_terminal, } = e { terminal_panel.spawn_task(spawn_in_terminal, window, cx); }; }) .detach(); }) .ok(); } // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. if let Some(workspace) = workspace.upgrade() { let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| { let alive_item_ids = terminal_panel .read(cx) .center .panes() .into_iter() .flat_map(|pane| pane.read(cx).items()) .map(|item| item.item_id().as_u64() as ItemId) .collect(); workspace.database_id().map(|workspace_id| { TerminalView::cleanup(workspace_id, alive_item_ids, window, cx) }) })?; if let Some(task) = cleanup_task { task.await.log_err(); } } if let Some(workspace) = workspace.upgrade() { let should_focus = workspace .update_in(&mut cx, |workspace, window, cx| { workspace.active_item(cx).is_none() && workspace .is_dock_at_position_open(terminal_panel.position(window, cx), cx) }) .unwrap_or(false); if should_focus { terminal_panel .update_in(&mut cx, |panel, window, cx| { panel.active_pane.update(cx, |pane, cx| { pane.focus_active_item(window, cx); }); }) .ok(); } } Ok(terminal_panel) } fn handle_pane_event( &mut self, pane: &Entity<Pane>, event: &pane::Event, window: &mut Window, cx: &mut Context<Self>, ) { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemovedItem { .. } => self.serialize(cx), pane::Event::Remove { focus_on_pane } => { let pane_count_before_removal = self.center.panes().len(); let _removal_result = self.center.remove(&pane); if pane_count_before_removal == 1 { self.center.first_pane().update(cx, |pane, cx| { pane.set_zoomed(false, cx); }); cx.emit(PanelEvent::Close); } else { if let Some(focus_on_pane) = focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) { focus_on_pane.focus_handle(cx).focus(window); } } } pane::Event::ZoomIn => { for pane in self.center.panes() { pane.update(cx, |pane, cx| { pane.set_zoomed(true, cx); }) } cx.emit(PanelEvent::ZoomIn); cx.notify(); } pane::Event::ZoomOut => { for pane in self.center.panes() { pane.update(cx, |pane, cx| { pane.set_zoomed(false, cx); }) } cx.emit(PanelEvent::ZoomOut); cx.notify(); } pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { item.added_to_pane(workspace, pane.clone(), window, cx) }) } self.serialize(cx); } pane::Event::Split(direction) => { let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else { return; }; let pane = pane.clone(); let direction = *direction; self.center.split(&pane, &new_pane, direction).log_err(); window.focus(&new_pane.focus_handle(cx)); } pane::Event::Focus => { self.active_pane = pane.clone(); } _ => {} } } fn new_pane_with_cloned_active_terminal( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> Option<Entity<Pane>> { let workspace = self.workspace.upgrade()?; let workspace = workspace.read(cx); let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); let (working_directory, python_venv_directory) = self .active_pane .read(cx) .active_item() .and_then(|item| item.downcast::<TerminalView>()) .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(), ) }) .unwrap_or((None, None)); let kind = TerminalKind::Shell(working_directory); let window_handle = window.window_handle(); let terminal = project .update(cx, |project, cx| { project.create_terminal_with_venv(kind, python_venv_directory, window_handle, cx) }) .ok()?; let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( terminal.clone(), weak_workspace.clone(), database_id, project.downgrade(), window, cx, ) })); let pane = new_terminal_pane( weak_workspace, project, self.active_pane.read(cx).is_zoomed(), window, cx, ); self.apply_tab_bar_buttons(&pane, cx); pane.update(cx, |pane, cx| { pane.add_item(terminal_view, true, true, None, window, cx); }); Some(pane) } pub fn open_terminal( workspace: &mut Workspace, action: &workspace::OpenTerminal, window: &mut Window, cx: &mut Context<Workspace>, ) { let Some(terminal_panel) = workspace.panel::<Self>(cx) else { return; }; terminal_panel .update(cx, |panel, cx| { panel.add_terminal( TerminalKind::Shell(Some(action.working_directory.clone())), RevealStrategy::Always, window, cx, ) }) .detach_and_log_err(cx); } fn spawn_task(&mut self, task: &SpawnInTerminal, window: &mut Window, cx: &mut Context<Self>) { let Ok(is_local) = self .workspace .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) else { return; }; let builder = ShellBuilder::new(is_local, &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args);Lines 1–500 of 1500. Load more
use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::{ default_working_directory, persistence::{ deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel, }, TerminalView, }; use breadcrumbs::Breadcrumbs; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; use gpui::{ actions, Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, WeakEntity, Window, }; use itertools::Itertools; use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId}; use terminal::{ terminal_settings::{TerminalDockPosition, TerminalSettings}, Terminal, }; use ui::{ prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent, PanelHandle}, item::SerializableItem, move_active_item, move_item, pane, ui::IconName, ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace, }; use anyhow::{anyhow, Context as _, Result}; use zed_actions::assistant::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| { workspace.register_action(TerminalPanel::new_terminal); workspace.register_action(TerminalPanel::open_terminal); workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { if is_enabled_in_workspace(workspace, cx) { workspace.toggle_panel_focus::<TerminalPanel>(window, cx); } }); }, ) .detach(); } pub struct TerminalPanel { pub(crate) active_pane: Entity<Pane>, pub(crate) center: PaneGroup, fs: Arc<dyn Fs>, workspace: WeakEntity<Workspace>, pub(crate) width: Option<Pixels>, pub(crate) height: Option<Pixels>, pending_serialization: Task<Option<()>>, pending_terminals_to_add: usize, deferred_tasks: HashMap<TaskId, Task<()>>, assistant_enabled: bool, assistant_tab_bar_button: Option<AnyView>, active: bool, } impl TerminalPanel { pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self { let project = workspace.project(); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx); let center = PaneGroup::new(pane.clone()); let terminal_panel = Self { center, active_pane: pane, fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), width: None, height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), assistant_enabled: false, assistant_tab_bar_button: None, active: false, }; terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); terminal_panel } pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) { self.assistant_enabled = enabled; if enabled { let focus_handle = self .active_pane .read(cx) .active_item() .map(|item| item.item_focus_handle(cx)) .unwrap_or(self.focus_handle(cx)); self.assistant_tab_bar_button = Some( cx.new(move |_| InlineAssistTabBarButton { focus_handle }) .into(), ); } else { self.assistant_tab_bar_button = None; } for pane in self.center.panes() { self.apply_tab_bar_buttons(pane, cx); } } fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) { let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| { let split_context = pane .active_item() .and_then(|item| item.downcast::<TerminalView>()) .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { return (None, None); } let focus_handle = pane.focus_handle(cx); let right_children = h_flex() .gap(DynamicSpacing::Base02.rems(cx)) .child( PopoverMenu::new("terminal-tab-bar-popover-menu") .trigger_with_tooltip( IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), Tooltip::text("New…"), ) .anchor(Corner::TopRight) .with_handle(pane.new_item_context_menu_handle.clone()) .menu(move |window, cx| { let focus_handle = focus_handle.clone(); let menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(focus_handle.clone()) .action( "New Terminal", workspace::NewTerminal.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 // context menu will be gone the moment we spawn the modal. .action( "Spawn task", zed_actions::Spawn::modal().boxed_clone(), ) }); Some(menu) }), ) .children(assistant_tab_bar_button.clone()) .child( PopoverMenu::new("terminal-pane-tab-bar-split") .trigger_with_tooltip( IconButton::new("terminal-pane-split", IconName::Split) .icon_size(IconSize::Small), Tooltip::text("Split Pane"), ) .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) .menu({ let split_context = split_context.clone(); move |window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.when_some( split_context.clone(), |menu, split_context| menu.context(split_context), ) .action("Split Right", SplitRight.boxed_clone()) .action("Split Left", SplitLeft.boxed_clone()) .action("Split Up", SplitUp.boxed_clone()) .action("Split Down", SplitDown.boxed_clone()) }) .into() } }), ) .child({ let zoomed = pane.is_zoomed(); IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) .toggle_state(zoomed) .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, window, cx| { pane.toggle_zoom(&workspace::ToggleZoom, window, cx); })) .tooltip(move |window, cx| { Tooltip::for_action( if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, window, cx, ) }) }) .into_any_element() .into(); (None, right_children) }); }); } fn serialization_key(workspace: &Workspace) -> Option<String> { workspace .database_id() .map(|id| i64::from(id).to_string()) .or(workspace.session_id()) .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id)) } pub async fn load( workspace: WeakEntity<Workspace>, mut cx: AsyncWindowContext, ) -> Result<Entity<Self>> { let mut terminal_panel = None; match workspace .read_with(&mut cx, |workspace, _| { workspace .database_id() .zip(TerminalPanel::serialization_key(workspace)) }) .ok() .flatten() { Some((database_id, serialization_key)) => { if let Some(serialized_panel) = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) .await .log_err() .flatten() .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) .transpose() .log_err() .flatten() { if let Ok(serialized) = workspace .update_in(&mut cx, |workspace, window, cx| { deserialize_terminal_panel( workspace.weak_handle(), workspace.project().clone(), database_id, serialized_panel, window, cx, ) })? .await { terminal_panel = Some(serialized); } } } _ => {} } let terminal_panel = if let Some(panel) = terminal_panel { panel } else { workspace.update_in(&mut cx, |workspace, window, cx| { cx.new(|cx| TerminalPanel::new(workspace, window, cx)) })? }; if let Some(workspace) = workspace.upgrade() { terminal_panel .update_in(&mut cx, |_, window, cx| { cx.subscribe_in(&workspace, window, |terminal_panel, _, e, window, cx| { if let workspace::Event::SpawnTask { action: spawn_in_terminal, } = e { terminal_panel.spawn_task(spawn_in_terminal, window, cx); }; }) .detach(); }) .ok(); } // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. if let Some(workspace) = workspace.upgrade() { let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| { let alive_item_ids = terminal_panel .read(cx) .center .panes() .into_iter() .flat_map(|pane| pane.read(cx).items()) .map(|item| item.item_id().as_u64() as ItemId) .collect(); workspace.database_id().map(|workspace_id| { TerminalView::cleanup(workspace_id, alive_item_ids, window, cx) }) })?; if let Some(task) = cleanup_task { task.await.log_err(); } } if let Some(workspace) = workspace.upgrade() { let should_focus = workspace .update_in(&mut cx, |workspace, window, cx| { workspace.active_item(cx).is_none() && workspace .is_dock_at_position_open(terminal_panel.position(window, cx), cx) }) .unwrap_or(false); if should_focus { terminal_panel .update_in(&mut cx, |panel, window, cx| { panel.active_pane.update(cx, |pane, cx| { pane.focus_active_item(window, cx); }); }) .ok(); } } Ok(terminal_panel) } fn handle_pane_event( &mut self, pane: &Entity<Pane>, event: &pane::Event, window: &mut Window, cx: &mut Context<Self>, ) { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemovedItem { .. } => self.serialize(cx), pane::Event::Remove { focus_on_pane } => { let pane_count_before_removal = self.center.panes().len(); let _removal_result = self.center.remove(&pane); if pane_count_before_removal == 1 { self.center.first_pane().update(cx, |pane, cx| { pane.set_zoomed(false, cx); }); cx.emit(PanelEvent::Close); } else { if let Some(focus_on_pane) = focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) { focus_on_pane.focus_handle(cx).focus(window); } } } pane::Event::ZoomIn => { for pane in self.center.panes() { pane.update(cx, |pane, cx| { pane.set_zoomed(true, cx); }) } cx.emit(PanelEvent::ZoomIn); cx.notify(); } pane::Event::ZoomOut => { for pane in self.center.panes() { pane.update(cx, |pane, cx| { pane.set_zoomed(false, cx); }) } cx.emit(PanelEvent::ZoomOut); cx.notify(); } pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { item.added_to_pane(workspace, pane.clone(), window, cx) }) } self.serialize(cx); } pane::Event::Split(direction) => { let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else { return; }; let pane = pane.clone(); let direction = *direction; self.center.split(&pane, &new_pane, direction).log_err(); window.focus(&new_pane.focus_handle(cx)); } pane::Event::Focus => { self.active_pane = pane.clone(); } _ => {} } } fn new_pane_with_cloned_active_terminal( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> Option<Entity<Pane>> { let workspace = self.workspace.upgrade()?; let workspace = workspace.read(cx); let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); let (working_directory, python_venv_directory) = self .active_pane .read(cx) .active_item() .and_then(|item| item.downcast::<TerminalView>()) .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(), ) }) .unwrap_or((None, None)); let kind = TerminalKind::Shell(working_directory); let window_handle = window.window_handle(); let terminal = project .update(cx, |project, cx| { project.create_terminal_with_venv(kind, python_venv_directory, window_handle, cx) }) .ok()?; let terminal_view = Box::new(cx.new(|cx| { TerminalView::new( terminal.clone(), weak_workspace.clone(), database_id, project.downgrade(), window, cx, ) })); let pane = new_terminal_pane( weak_workspace, project, self.active_pane.read(cx).is_zoomed(), window, cx, ); self.apply_tab_bar_buttons(&pane, cx); pane.update(cx, |pane, cx| { pane.add_item(terminal_view, true, true, None, window, cx); }); Some(pane) } pub fn open_terminal( workspace: &mut Workspace, action: &workspace::OpenTerminal, window: &mut Window, cx: &mut Context<Workspace>, ) { let Some(terminal_panel) = workspace.panel::<Self>(cx) else { return; }; terminal_panel .update(cx, |panel, cx| { panel.add_terminal( TerminalKind::Shell(Some(action.working_directory.clone())), RevealStrategy::Always, window, cx, ) }) .detach_and_log_err(cx); } fn spawn_task(&mut self, task: &SpawnInTerminal, window: &mut Window, cx: &mut Context<Self>) { let Ok(is_local) = self .workspace .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) else { return; }; let builder = ShellBuilder::new(is_local, &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args);