terminal_panel.rs

   1use std::{cmp, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration};
   2
   3use crate::{
   4    TerminalView, default_working_directory,
   5    persistence::{
   6        SerializedItems, SerializedTerminalPanel, deserialize_terminal_panel, serialize_pane_group,
   7    },
   8};
   9use breadcrumbs::Breadcrumbs;
  10use collections::HashMap;
  11use db::kvp::KeyValueStore;
  12use futures::{channel::oneshot, future::join_all};
  13use gpui::{
  14    Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter,
  15    FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, WeakEntity,
  16    Window, actions,
  17};
  18use itertools::Itertools;
  19use project::{Fs, Project};
  20
  21use settings::{Settings, TerminalDockPosition};
  22use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
  23use terminal::{Terminal, terminal_settings::TerminalSettings};
  24use ui::{
  25    ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
  26    Tooltip, prelude::*,
  27};
  28use util::{ResultExt, TryFutureExt};
  29use workspace::{
  30    ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
  31    ActivatePaneUp, ActivatePreviousPane, DraggedTab, ItemId, MoveItemToPane,
  32    MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane,
  33    PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown,
  34    SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
  35    dock::{DockPosition, Panel, PanelEvent, PanelHandle},
  36    item::SerializableItem,
  37    move_active_item, pane,
  38};
  39
  40use anyhow::{Result, anyhow};
  41use zed_actions::assistant::InlineAssist;
  42
  43const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  44
  45actions!(
  46    terminal_panel,
  47    [
  48        /// Toggles the terminal panel.
  49        Toggle,
  50        /// Toggles focus on the terminal panel.
  51        ToggleFocus
  52    ]
  53);
  54
  55pub fn init(cx: &mut App) {
  56    cx.observe_new(
  57        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
  58            workspace.register_action(TerminalPanel::new_terminal);
  59            workspace.register_action(TerminalPanel::open_terminal);
  60            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  61                if is_enabled_in_workspace(workspace, cx) {
  62                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
  63                }
  64            });
  65            workspace.register_action(|workspace, _: &Toggle, window, cx| {
  66                if is_enabled_in_workspace(workspace, cx) {
  67                    if !workspace.toggle_panel_focus::<TerminalPanel>(window, cx) {
  68                        workspace.close_panel::<TerminalPanel>(window, cx);
  69                    }
  70                }
  71            });
  72        },
  73    )
  74    .detach();
  75}
  76
  77pub struct TerminalPanel {
  78    pub(crate) active_pane: Entity<Pane>,
  79    pub(crate) center: PaneGroup,
  80    fs: Arc<dyn Fs>,
  81    workspace: WeakEntity<Workspace>,
  82    pending_serialization: Task<Option<()>>,
  83    pending_terminals_to_add: usize,
  84    deferred_tasks: HashMap<TaskId, Task<()>>,
  85    assistant_enabled: bool,
  86    assistant_tab_bar_button: Option<AnyView>,
  87    active: bool,
  88}
  89
  90impl TerminalPanel {
  91    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
  92        let project = workspace.project();
  93        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
  94        let center = PaneGroup::new(pane.clone());
  95        let terminal_panel = Self {
  96            center,
  97            active_pane: pane,
  98            fs: workspace.app_state().fs.clone(),
  99            workspace: workspace.weak_handle(),
 100            pending_serialization: Task::ready(None),
 101            pending_terminals_to_add: 0,
 102            deferred_tasks: HashMap::default(),
 103            assistant_enabled: false,
 104            assistant_tab_bar_button: None,
 105            active: false,
 106        };
 107        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 108        terminal_panel
 109    }
 110
 111    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
 112        self.assistant_enabled = enabled;
 113        if enabled {
 114            let focus_handle = self
 115                .active_pane
 116                .read(cx)
 117                .active_item()
 118                .map(|item| item.item_focus_handle(cx))
 119                .unwrap_or(self.focus_handle(cx));
 120            self.assistant_tab_bar_button = Some(
 121                cx.new(move |_| InlineAssistTabBarButton { focus_handle })
 122                    .into(),
 123            );
 124        } else {
 125            self.assistant_tab_bar_button = None;
 126        }
 127        for pane in self.center.panes() {
 128            self.apply_tab_bar_buttons(pane, cx);
 129        }
 130    }
 131
 132    pub(crate) fn apply_tab_bar_buttons(
 133        &self,
 134        terminal_pane: &Entity<Pane>,
 135        cx: &mut Context<Self>,
 136    ) {
 137        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 138        terminal_pane.update(cx, |pane, cx| {
 139            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
 140                let split_context = pane
 141                    .active_item()
 142                    .and_then(|item| item.downcast::<TerminalView>())
 143                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
 144                let has_focused_rename_editor = pane
 145                    .active_item()
 146                    .and_then(|item| item.downcast::<TerminalView>())
 147                    .is_some_and(|view| view.read(cx).rename_editor_is_focused(window, cx));
 148                if !pane.has_focus(window, cx)
 149                    && !pane.context_menu_focused(window, cx)
 150                    && !has_focused_rename_editor
 151                {
 152                    return (None, None);
 153                }
 154                let focus_handle = pane.focus_handle(cx);
 155                let right_children = h_flex()
 156                    .gap(DynamicSpacing::Base02.rems(cx))
 157                    .child(
 158                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 159                            .trigger_with_tooltip(
 160                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
 161                                Tooltip::text("New…"),
 162                            )
 163                            .anchor(Corner::TopRight)
 164                            .with_handle(pane.new_item_context_menu_handle.clone())
 165                            .menu(move |window, cx| {
 166                                let focus_handle = focus_handle.clone();
 167                                let menu = ContextMenu::build(window, cx, |menu, _, _| {
 168                                    menu.context(focus_handle.clone())
 169                                        .action(
 170                                            "New Terminal",
 171                                            workspace::NewTerminal::default().boxed_clone(),
 172                                        )
 173                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 174                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 175                                        // context menu will be gone the moment we spawn the modal.
 176                                        .action(
 177                                            "Spawn Task",
 178                                            zed_actions::Spawn::modal().boxed_clone(),
 179                                        )
 180                                });
 181
 182                                Some(menu)
 183                            }),
 184                    )
 185                    .children(assistant_tab_bar_button.clone())
 186                    .child(
 187                        PopoverMenu::new("terminal-pane-tab-bar-split")
 188                            .trigger_with_tooltip(
 189                                IconButton::new("terminal-pane-split", IconName::Split)
 190                                    .icon_size(IconSize::Small),
 191                                Tooltip::text("Split Pane"),
 192                            )
 193                            .anchor(Corner::TopRight)
 194                            .with_handle(pane.split_item_context_menu_handle.clone())
 195                            .menu({
 196                                move |window, cx| {
 197                                    ContextMenu::build(window, cx, |menu, _, _| {
 198                                        menu.when_some(
 199                                            split_context.clone(),
 200                                            |menu, split_context| menu.context(split_context),
 201                                        )
 202                                        .action("Split Right", SplitRight::default().boxed_clone())
 203                                        .action("Split Left", SplitLeft::default().boxed_clone())
 204                                        .action("Split Up", SplitUp::default().boxed_clone())
 205                                        .action("Split Down", SplitDown::default().boxed_clone())
 206                                    })
 207                                    .into()
 208                                }
 209                            }),
 210                    )
 211                    .child({
 212                        let zoomed = pane.is_zoomed();
 213                        IconButton::new("toggle_zoom", IconName::Maximize)
 214                            .icon_size(IconSize::Small)
 215                            .toggle_state(zoomed)
 216                            .selected_icon(IconName::Minimize)
 217                            .on_click(cx.listener(|pane, _, window, cx| {
 218                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
 219                            }))
 220                            .tooltip(move |_window, cx| {
 221                                Tooltip::for_action(
 222                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 223                                    &ToggleZoom,
 224                                    cx,
 225                                )
 226                            })
 227                    })
 228                    .into_any_element()
 229                    .into();
 230                (None, right_children)
 231            });
 232        });
 233    }
 234
 235    fn serialization_key(workspace: &Workspace) -> Option<String> {
 236        workspace
 237            .database_id()
 238            .map(|id| i64::from(id).to_string())
 239            .or(workspace.session_id())
 240            .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
 241    }
 242
 243    pub async fn load(
 244        workspace: WeakEntity<Workspace>,
 245        mut cx: AsyncWindowContext,
 246    ) -> Result<Entity<Self>> {
 247        let mut terminal_panel = None;
 248
 249        if let Some((database_id, serialization_key, kvp)) = workspace
 250            .read_with(&cx, |workspace, cx| {
 251                workspace
 252                    .database_id()
 253                    .zip(TerminalPanel::serialization_key(workspace))
 254                    .map(|(id, key)| (id, key, KeyValueStore::global(cx)))
 255            })
 256            .ok()
 257            .flatten()
 258            && let Some(serialized_panel) = cx
 259                .background_spawn(async move { kvp.read_kvp(&serialization_key) })
 260                .await
 261                .log_err()
 262                .flatten()
 263                .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 264                .transpose()
 265                .log_err()
 266                .flatten()
 267            && let Ok(serialized) = workspace
 268                .update_in(&mut cx, |workspace, window, cx| {
 269                    deserialize_terminal_panel(
 270                        workspace.weak_handle(),
 271                        workspace.project().clone(),
 272                        database_id,
 273                        serialized_panel,
 274                        window,
 275                        cx,
 276                    )
 277                })?
 278                .await
 279        {
 280            terminal_panel = Some(serialized);
 281        }
 282
 283        let terminal_panel = if let Some(panel) = terminal_panel {
 284            panel
 285        } else {
 286            workspace.update_in(&mut cx, |workspace, window, cx| {
 287                cx.new(|cx| TerminalPanel::new(workspace, window, cx))
 288            })?
 289        };
 290
 291        if let Some(workspace) = workspace.upgrade() {
 292            workspace.update(&mut cx, |workspace, _| {
 293                workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
 294            });
 295        }
 296
 297        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 298        if let Some(workspace) = workspace.upgrade() {
 299            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
 300                let alive_item_ids = terminal_panel
 301                    .read(cx)
 302                    .center
 303                    .panes()
 304                    .into_iter()
 305                    .flat_map(|pane| pane.read(cx).items())
 306                    .map(|item| item.item_id().as_u64() as ItemId)
 307                    .collect();
 308                workspace.database_id().map(|workspace_id| {
 309                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
 310                })
 311            })?;
 312            if let Some(task) = cleanup_task {
 313                task.await.log_err();
 314            }
 315        }
 316
 317        if let Some(workspace) = workspace.upgrade() {
 318            let should_focus = workspace
 319                .update_in(&mut cx, |workspace, window, cx| {
 320                    workspace.active_item(cx).is_none()
 321                        && workspace
 322                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
 323                })
 324                .unwrap_or(false);
 325
 326            if should_focus {
 327                terminal_panel
 328                    .update_in(&mut cx, |panel, window, cx| {
 329                        panel.active_pane.update(cx, |pane, cx| {
 330                            pane.focus_active_item(window, cx);
 331                        });
 332                    })
 333                    .ok();
 334            }
 335        }
 336        Ok(terminal_panel)
 337    }
 338
 339    fn handle_pane_event(
 340        &mut self,
 341        pane: &Entity<Pane>,
 342        event: &pane::Event,
 343        window: &mut Window,
 344        cx: &mut Context<Self>,
 345    ) {
 346        match event {
 347            pane::Event::ActivateItem { .. } => self.serialize(cx),
 348            pane::Event::RemovedItem { .. } => self.serialize(cx),
 349            pane::Event::Remove { focus_on_pane } => {
 350                let pane_count_before_removal = self.center.panes().len();
 351                let _removal_result = self.center.remove(pane, cx);
 352                if pane_count_before_removal == 1 {
 353                    self.center.first_pane().update(cx, |pane, cx| {
 354                        pane.set_zoomed(false, cx);
 355                    });
 356                    cx.emit(PanelEvent::Close);
 357                } else if let Some(focus_on_pane) =
 358                    focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 359                {
 360                    focus_on_pane.focus_handle(cx).focus(window, cx);
 361                }
 362            }
 363            pane::Event::ZoomIn => {
 364                for pane in self.center.panes() {
 365                    pane.update(cx, |pane, cx| {
 366                        pane.set_zoomed(true, cx);
 367                    })
 368                }
 369                cx.emit(PanelEvent::ZoomIn);
 370                cx.notify();
 371            }
 372            pane::Event::ZoomOut => {
 373                for pane in self.center.panes() {
 374                    pane.update(cx, |pane, cx| {
 375                        pane.set_zoomed(false, cx);
 376                    })
 377                }
 378                cx.emit(PanelEvent::ZoomOut);
 379                cx.notify();
 380            }
 381            pane::Event::AddItem { item } => {
 382                if let Some(workspace) = self.workspace.upgrade() {
 383                    workspace.update(cx, |workspace, cx| {
 384                        item.added_to_pane(workspace, pane.clone(), window, cx)
 385                    })
 386                }
 387                self.serialize(cx);
 388            }
 389            &pane::Event::Split { direction, mode } => {
 390                match mode {
 391                    SplitMode::ClonePane | SplitMode::EmptyPane => {
 392                        let clone = matches!(mode, SplitMode::ClonePane);
 393                        let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
 394                        let pane = pane.clone();
 395                        cx.spawn_in(window, async move |panel, cx| {
 396                            let Some(new_pane) = new_pane.await else {
 397                                return;
 398                            };
 399                            panel
 400                                .update_in(cx, |panel, window, cx| {
 401                                    panel.center.split(&pane, &new_pane, direction, cx);
 402                                    window.focus(&new_pane.focus_handle(cx), cx);
 403                                })
 404                                .ok();
 405                        })
 406                        .detach();
 407                    }
 408                    SplitMode::MovePane => {
 409                        let Some(item) =
 410                            pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
 411                        else {
 412                            return;
 413                        };
 414                        let Ok(project) = self
 415                            .workspace
 416                            .update(cx, |workspace, _| workspace.project().clone())
 417                        else {
 418                            return;
 419                        };
 420                        let new_pane =
 421                            new_terminal_pane(self.workspace.clone(), project, false, window, cx);
 422                        new_pane.update(cx, |pane, cx| {
 423                            pane.add_item(item, true, true, None, window, cx);
 424                        });
 425                        self.center.split(&pane, &new_pane, direction, cx);
 426                        window.focus(&new_pane.focus_handle(cx), cx);
 427                    }
 428                };
 429            }
 430            pane::Event::Focus => {
 431                self.active_pane = pane.clone();
 432            }
 433            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
 434                self.serialize(cx);
 435            }
 436
 437            _ => {}
 438        }
 439    }
 440
 441    fn new_pane_with_active_terminal(
 442        &mut self,
 443        clone: bool,
 444        window: &mut Window,
 445        cx: &mut Context<Self>,
 446    ) -> Task<Option<Entity<Pane>>> {
 447        let Some(workspace) = self.workspace.upgrade() else {
 448            return Task::ready(None);
 449        };
 450        let workspace = workspace.read(cx);
 451        let database_id = workspace.database_id();
 452        let weak_workspace = self.workspace.clone();
 453        let project = workspace.project().clone();
 454        let active_pane = &self.active_pane;
 455        let terminal_view = if clone {
 456            active_pane
 457                .read(cx)
 458                .active_item()
 459                .and_then(|item| item.downcast::<TerminalView>())
 460        } else {
 461            None
 462        };
 463        let working_directory = if clone {
 464            terminal_view
 465                .as_ref()
 466                .and_then(|terminal_view| {
 467                    terminal_view
 468                        .read(cx)
 469                        .terminal()
 470                        .read(cx)
 471                        .working_directory()
 472                })
 473                .or_else(|| default_working_directory(workspace, cx))
 474        } else {
 475            default_working_directory(workspace, cx)
 476        };
 477
 478        let is_zoomed = if clone {
 479            active_pane.read(cx).is_zoomed()
 480        } else {
 481            false
 482        };
 483        cx.spawn_in(window, async move |panel, cx| {
 484            let terminal = project
 485                .update(cx, |project, cx| match terminal_view {
 486                    Some(view) => project.clone_terminal(
 487                        &view.read(cx).terminal.clone(),
 488                        cx,
 489                        working_directory,
 490                    ),
 491                    None => project.create_terminal_shell(working_directory, cx),
 492                })
 493                .await
 494                .log_err()?;
 495
 496            panel
 497                .update_in(cx, move |terminal_panel, window, cx| {
 498                    let terminal_view = Box::new(cx.new(|cx| {
 499                        TerminalView::new(
 500                            terminal.clone(),
 501                            weak_workspace.clone(),
 502                            database_id,
 503                            project.downgrade(),
 504                            window,
 505                            cx,
 506                        )
 507                    }));
 508                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
 509                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 510                    pane.update(cx, |pane, cx| {
 511                        pane.add_item(terminal_view, true, true, None, window, cx);
 512                    });
 513                    Some(pane)
 514                })
 515                .ok()
 516                .flatten()
 517        })
 518    }
 519
 520    pub fn open_terminal(
 521        workspace: &mut Workspace,
 522        action: &workspace::OpenTerminal,
 523        window: &mut Window,
 524        cx: &mut Context<Workspace>,
 525    ) {
 526        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 527            return;
 528        };
 529
 530        terminal_panel
 531            .update(cx, |panel, cx| {
 532                if action.local {
 533                    panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 534                } else {
 535                    panel.add_terminal_shell(
 536                        Some(action.working_directory.clone()),
 537                        RevealStrategy::Always,
 538                        window,
 539                        cx,
 540                    )
 541                }
 542            })
 543            .detach_and_log_err(cx);
 544    }
 545
 546    pub fn spawn_task(
 547        &mut self,
 548        task: &SpawnInTerminal,
 549        window: &mut Window,
 550        cx: &mut Context<Self>,
 551    ) -> Task<Result<WeakEntity<Terminal>>> {
 552        let Some(workspace) = self.workspace.upgrade() else {
 553            return Task::ready(Err(anyhow!("failed to read workspace")));
 554        };
 555
 556        let project = workspace.read(cx).project().read(cx);
 557
 558        if project.is_via_collab() {
 559            return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
 560        }
 561
 562        let remote_client = project.remote_client();
 563        let is_windows = project.path_style(cx).is_windows();
 564        let remote_shell = remote_client
 565            .as_ref()
 566            .and_then(|remote_client| remote_client.read(cx).shell());
 567
 568        let shell = if let Some(remote_shell) = remote_shell
 569            && task.shell == Shell::System
 570        {
 571            Shell::Program(remote_shell)
 572        } else {
 573            task.shell.clone()
 574        };
 575
 576        let task = prepare_task_for_spawn(task, &shell, is_windows);
 577
 578        if task.allow_concurrent_runs && task.use_new_terminal {
 579            return self.spawn_in_new_terminal(task, window, cx);
 580        }
 581
 582        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
 583        let Some(existing) = terminals_for_task.pop() else {
 584            return self.spawn_in_new_terminal(task, window, cx);
 585        };
 586
 587        let (existing_item_index, task_pane, existing_terminal) = existing;
 588        if task.allow_concurrent_runs {
 589            return self.replace_terminal(
 590                task,
 591                task_pane,
 592                existing_item_index,
 593                existing_terminal,
 594                window,
 595                cx,
 596            );
 597        }
 598
 599        let (tx, rx) = oneshot::channel();
 600
 601        self.deferred_tasks.insert(
 602            task.id.clone(),
 603            cx.spawn_in(window, async move |terminal_panel, cx| {
 604                wait_for_terminals_tasks(terminals_for_task, cx).await;
 605                let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 606                    if task.use_new_terminal {
 607                        terminal_panel.spawn_in_new_terminal(task, window, cx)
 608                    } else {
 609                        terminal_panel.replace_terminal(
 610                            task,
 611                            task_pane,
 612                            existing_item_index,
 613                            existing_terminal,
 614                            window,
 615                            cx,
 616                        )
 617                    }
 618                });
 619                if let Ok(task) = task {
 620                    tx.send(task.await).ok();
 621                }
 622            }),
 623        );
 624
 625        cx.spawn(async move |_, _| rx.await?)
 626    }
 627
 628    fn spawn_in_new_terminal(
 629        &mut self,
 630        spawn_task: SpawnInTerminal,
 631        window: &mut Window,
 632        cx: &mut Context<Self>,
 633    ) -> Task<Result<WeakEntity<Terminal>>> {
 634        let reveal = spawn_task.reveal;
 635        let reveal_target = spawn_task.reveal_target;
 636        match reveal_target {
 637            RevealTarget::Center => self
 638                .workspace
 639                .update(cx, |workspace, cx| {
 640                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
 641                        project.create_terminal_task(spawn_task, cx)
 642                    })
 643                })
 644                .unwrap_or_else(|e| Task::ready(Err(e))),
 645            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
 646        }
 647    }
 648
 649    /// Create a new Terminal in the current working directory or the user's home directory
 650    fn new_terminal(
 651        workspace: &mut Workspace,
 652        action: &workspace::NewTerminal,
 653        window: &mut Window,
 654        cx: &mut Context<Workspace>,
 655    ) {
 656        let center_pane = workspace.active_pane();
 657        let center_pane_has_focus = center_pane.focus_handle(cx).contains_focused(window, cx);
 658        let active_center_item_is_terminal = center_pane
 659            .read(cx)
 660            .active_item()
 661            .is_some_and(|item| item.downcast::<TerminalView>().is_some());
 662
 663        if center_pane_has_focus && active_center_item_is_terminal {
 664            let working_directory = default_working_directory(workspace, cx);
 665            let local = action.local;
 666            Self::add_center_terminal(workspace, window, cx, move |project, cx| {
 667                if local {
 668                    project.create_local_terminal(cx)
 669                } else {
 670                    project.create_terminal_shell(working_directory, cx)
 671                }
 672            })
 673            .detach_and_log_err(cx);
 674            return;
 675        }
 676
 677        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 678            return;
 679        };
 680
 681        terminal_panel
 682            .update(cx, |this, cx| {
 683                if action.local {
 684                    this.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 685                } else {
 686                    this.add_terminal_shell(
 687                        default_working_directory(workspace, cx),
 688                        RevealStrategy::Always,
 689                        window,
 690                        cx,
 691                    )
 692                }
 693            })
 694            .detach_and_log_err(cx);
 695    }
 696
 697    fn terminals_for_task(
 698        &self,
 699        label: &str,
 700        cx: &mut App,
 701    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 702        let Some(workspace) = self.workspace.upgrade() else {
 703            return Vec::new();
 704        };
 705
 706        let pane_terminal_views = |pane: Entity<Pane>| {
 707            pane.read(cx)
 708                .items()
 709                .enumerate()
 710                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 711                .filter_map(|(index, terminal_view)| {
 712                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 713                    if &task_state.spawned_task.full_label == label {
 714                        Some((index, terminal_view))
 715                    } else {
 716                        None
 717                    }
 718                })
 719                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 720        };
 721
 722        self.center
 723            .panes()
 724            .into_iter()
 725            .cloned()
 726            .flat_map(pane_terminal_views)
 727            .chain(
 728                workspace
 729                    .read(cx)
 730                    .panes()
 731                    .iter()
 732                    .cloned()
 733                    .flat_map(pane_terminal_views),
 734            )
 735            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 736            .collect()
 737    }
 738
 739    fn activate_terminal_view(
 740        &self,
 741        pane: &Entity<Pane>,
 742        item_index: usize,
 743        focus: bool,
 744        window: &mut Window,
 745        cx: &mut App,
 746    ) {
 747        pane.update(cx, |pane, cx| {
 748            pane.activate_item(item_index, true, focus, window, cx)
 749        })
 750    }
 751
 752    pub fn add_center_terminal(
 753        workspace: &mut Workspace,
 754        window: &mut Window,
 755        cx: &mut Context<Workspace>,
 756        create_terminal: impl FnOnce(
 757            &mut Project,
 758            &mut Context<Project>,
 759        ) -> Task<Result<Entity<Terminal>>>
 760        + 'static,
 761    ) -> Task<Result<WeakEntity<Terminal>>> {
 762        if !is_enabled_in_workspace(workspace, cx) {
 763            return Task::ready(Err(anyhow!(
 764                "terminal not yet supported for remote projects"
 765            )));
 766        }
 767        let project = workspace.project().downgrade();
 768        cx.spawn_in(window, async move |workspace, cx| {
 769            let terminal = project.update(cx, create_terminal)?.await?;
 770
 771            workspace.update_in(cx, |workspace, window, cx| {
 772                let terminal_view = cx.new(|cx| {
 773                    TerminalView::new(
 774                        terminal.clone(),
 775                        workspace.weak_handle(),
 776                        workspace.database_id(),
 777                        workspace.project().downgrade(),
 778                        window,
 779                        cx,
 780                    )
 781                });
 782                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 783            })?;
 784            Ok(terminal.downgrade())
 785        })
 786    }
 787
 788    pub fn add_terminal_task(
 789        &mut self,
 790        task: SpawnInTerminal,
 791        reveal_strategy: RevealStrategy,
 792        window: &mut Window,
 793        cx: &mut Context<Self>,
 794    ) -> Task<Result<WeakEntity<Terminal>>> {
 795        let workspace = self.workspace.clone();
 796        cx.spawn_in(window, async move |terminal_panel, cx| {
 797            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 798                anyhow::bail!("terminal not yet supported for remote projects");
 799            }
 800            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 801                terminal_panel.pending_terminals_to_add += 1;
 802                terminal_panel.active_pane.clone()
 803            })?;
 804            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 805            let terminal = project
 806                .update(cx, |project, cx| project.create_terminal_task(task, cx))
 807                .await?;
 808            let result = workspace.update_in(cx, |workspace, window, cx| {
 809                let terminal_view = Box::new(cx.new(|cx| {
 810                    TerminalView::new(
 811                        terminal.clone(),
 812                        workspace.weak_handle(),
 813                        workspace.database_id(),
 814                        workspace.project().downgrade(),
 815                        window,
 816                        cx,
 817                    )
 818                }));
 819
 820                match reveal_strategy {
 821                    RevealStrategy::Always => {
 822                        workspace.focus_panel::<Self>(window, cx);
 823                    }
 824                    RevealStrategy::NoFocus => {
 825                        workspace.open_panel::<Self>(window, cx);
 826                    }
 827                    RevealStrategy::Never => {}
 828                }
 829
 830                pane.update(cx, |pane, cx| {
 831                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
 832                    pane.add_item(terminal_view, true, focus, None, window, cx);
 833                });
 834
 835                Ok(terminal.downgrade())
 836            })?;
 837            terminal_panel.update(cx, |terminal_panel, cx| {
 838                terminal_panel.pending_terminals_to_add =
 839                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 840                terminal_panel.serialize(cx)
 841            })?;
 842            result
 843        })
 844    }
 845
 846    fn add_terminal_shell(
 847        &mut self,
 848        cwd: Option<PathBuf>,
 849        reveal_strategy: RevealStrategy,
 850        window: &mut Window,
 851        cx: &mut Context<Self>,
 852    ) -> Task<Result<WeakEntity<Terminal>>> {
 853        self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx)
 854    }
 855
 856    fn add_local_terminal_shell(
 857        &mut self,
 858        reveal_strategy: RevealStrategy,
 859        window: &mut Window,
 860        cx: &mut Context<Self>,
 861    ) -> Task<Result<WeakEntity<Terminal>>> {
 862        self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx)
 863    }
 864
 865    fn add_terminal_shell_internal(
 866        &mut self,
 867        force_local: bool,
 868        cwd: Option<PathBuf>,
 869        reveal_strategy: RevealStrategy,
 870        window: &mut Window,
 871        cx: &mut Context<Self>,
 872    ) -> Task<Result<WeakEntity<Terminal>>> {
 873        let workspace = self.workspace.clone();
 874
 875        cx.spawn_in(window, async move |terminal_panel, cx| {
 876            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 877                anyhow::bail!("terminal not yet supported for collaborative projects");
 878            }
 879            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 880                terminal_panel.pending_terminals_to_add += 1;
 881                terminal_panel.active_pane.clone()
 882            })?;
 883            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 884            let terminal = if force_local {
 885                project
 886                    .update(cx, |project, cx| project.create_local_terminal(cx))
 887                    .await
 888            } else {
 889                project
 890                    .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))
 891                    .await
 892            };
 893
 894            match terminal {
 895                Ok(terminal) => {
 896                    let result = workspace.update_in(cx, |workspace, window, cx| {
 897                        let terminal_view = Box::new(cx.new(|cx| {
 898                            TerminalView::new(
 899                                terminal.clone(),
 900                                workspace.weak_handle(),
 901                                workspace.database_id(),
 902                                workspace.project().downgrade(),
 903                                window,
 904                                cx,
 905                            )
 906                        }));
 907
 908                        match reveal_strategy {
 909                            RevealStrategy::Always => {
 910                                workspace.focus_panel::<Self>(window, cx);
 911                            }
 912                            RevealStrategy::NoFocus => {
 913                                workspace.open_panel::<Self>(window, cx);
 914                            }
 915                            RevealStrategy::Never => {}
 916                        }
 917
 918                        pane.update(cx, |pane, cx| {
 919                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
 920                            pane.add_item(terminal_view, true, focus, None, window, cx);
 921                        });
 922
 923                        Ok(terminal.downgrade())
 924                    })?;
 925                    terminal_panel.update(cx, |terminal_panel, cx| {
 926                        terminal_panel.pending_terminals_to_add =
 927                            terminal_panel.pending_terminals_to_add.saturating_sub(1);
 928                        terminal_panel.serialize(cx)
 929                    })?;
 930                    result
 931                }
 932                Err(error) => {
 933                    pane.update_in(cx, |pane, window, cx| {
 934                        let focus = pane.has_focus(window, cx);
 935                        let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
 936                            error: error.to_string(),
 937                            focus_handle: cx.focus_handle(),
 938                        });
 939                        pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
 940                    })?;
 941                    Err(error)
 942                }
 943            }
 944        })
 945    }
 946
 947    fn serialize(&mut self, cx: &mut Context<Self>) {
 948        let Some(serialization_key) = self
 949            .workspace
 950            .read_with(cx, |workspace, _| {
 951                TerminalPanel::serialization_key(workspace)
 952            })
 953            .ok()
 954            .flatten()
 955        else {
 956            return;
 957        };
 958        let kvp = KeyValueStore::global(cx);
 959        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
 960            cx.background_executor()
 961                .timer(Duration::from_millis(50))
 962                .await;
 963            let terminal_panel = terminal_panel.upgrade()?;
 964            let items = terminal_panel.update(cx, |terminal_panel, cx| {
 965                SerializedItems::WithSplits(serialize_pane_group(
 966                    &terminal_panel.center,
 967                    &terminal_panel.active_pane,
 968                    cx,
 969                ))
 970            });
 971            cx.background_spawn(
 972                async move {
 973                    kvp.write_kvp(
 974                        serialization_key,
 975                        serde_json::to_string(&SerializedTerminalPanel {
 976                            items,
 977                            active_item_id: None,
 978                        })?,
 979                    )
 980                    .await?;
 981                    anyhow::Ok(())
 982                }
 983                .log_err(),
 984            )
 985            .await;
 986            Some(())
 987        });
 988    }
 989
 990    fn replace_terminal(
 991        &self,
 992        spawn_task: SpawnInTerminal,
 993        task_pane: Entity<Pane>,
 994        terminal_item_index: usize,
 995        terminal_to_replace: Entity<TerminalView>,
 996        window: &mut Window,
 997        cx: &mut Context<Self>,
 998    ) -> Task<Result<WeakEntity<Terminal>>> {
 999        let reveal = spawn_task.reveal;
1000        let task_workspace = self.workspace.clone();
1001        cx.spawn_in(window, async move |terminal_panel, cx| {
1002            let project = terminal_panel.update(cx, |this, cx| {
1003                this.workspace
1004                    .update(cx, |workspace, _| workspace.project().clone())
1005            })??;
1006            let new_terminal = project
1007                .update(cx, |project, cx| {
1008                    project.create_terminal_task(spawn_task, cx)
1009                })
1010                .await?;
1011            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
1012                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
1013            })?;
1014
1015            let reveal_target = terminal_panel.update(cx, |panel, _| {
1016                if panel.center.panes().iter().any(|p| **p == task_pane) {
1017                    RevealTarget::Dock
1018                } else {
1019                    RevealTarget::Center
1020                }
1021            })?;
1022
1023            match reveal {
1024                RevealStrategy::Always => match reveal_target {
1025                    RevealTarget::Center => {
1026                        task_workspace.update_in(cx, |workspace, window, cx| {
1027                            let did_activate = workspace.activate_item(
1028                                &terminal_to_replace,
1029                                true,
1030                                true,
1031                                window,
1032                                cx,
1033                            );
1034
1035                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
1036
1037                            anyhow::Ok(())
1038                        })??;
1039                    }
1040                    RevealTarget::Dock => {
1041                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1042                            terminal_panel.activate_terminal_view(
1043                                &task_pane,
1044                                terminal_item_index,
1045                                true,
1046                                window,
1047                                cx,
1048                            )
1049                        })?;
1050
1051                        cx.spawn(async move |cx| {
1052                            task_workspace
1053                                .update_in(cx, |workspace, window, cx| {
1054                                    workspace.focus_panel::<Self>(window, cx)
1055                                })
1056                                .ok()
1057                        })
1058                        .detach();
1059                    }
1060                },
1061                RevealStrategy::NoFocus => match reveal_target {
1062                    RevealTarget::Center => {
1063                        task_workspace.update_in(cx, |workspace, window, cx| {
1064                            workspace.active_pane().focus_handle(cx).focus(window, cx);
1065                        })?;
1066                    }
1067                    RevealTarget::Dock => {
1068                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1069                            terminal_panel.activate_terminal_view(
1070                                &task_pane,
1071                                terminal_item_index,
1072                                false,
1073                                window,
1074                                cx,
1075                            )
1076                        })?;
1077
1078                        cx.spawn(async move |cx| {
1079                            task_workspace
1080                                .update_in(cx, |workspace, window, cx| {
1081                                    workspace.open_panel::<Self>(window, cx)
1082                                })
1083                                .ok()
1084                        })
1085                        .detach();
1086                    }
1087                },
1088                RevealStrategy::Never => {}
1089            }
1090
1091            Ok(new_terminal.downgrade())
1092        })
1093    }
1094
1095    fn has_no_terminals(&self, cx: &App) -> bool {
1096        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1097    }
1098
1099    pub fn assistant_enabled(&self) -> bool {
1100        self.assistant_enabled
1101    }
1102
1103    /// Returns all panes in the terminal panel.
1104    pub fn panes(&self) -> Vec<&Entity<Pane>> {
1105        self.center.panes()
1106    }
1107
1108    /// Returns all non-empty terminal selections from all terminal views in all panes.
1109    pub fn terminal_selections(&self, cx: &App) -> Vec<String> {
1110        self.center
1111            .panes()
1112            .iter()
1113            .flat_map(|pane| {
1114                pane.read(cx).items().filter_map(|item| {
1115                    let terminal_view = item.downcast::<crate::TerminalView>()?;
1116                    terminal_view
1117                        .read(cx)
1118                        .terminal()
1119                        .read(cx)
1120                        .last_content
1121                        .selection_text
1122                        .clone()
1123                        .filter(|text| !text.is_empty())
1124                })
1125            })
1126            .collect()
1127    }
1128
1129    fn is_enabled(&self, cx: &App) -> bool {
1130        self.workspace
1131            .upgrade()
1132            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1133    }
1134
1135    fn activate_pane_in_direction(
1136        &mut self,
1137        direction: SplitDirection,
1138        window: &mut Window,
1139        cx: &mut Context<Self>,
1140    ) {
1141        if let Some(pane) = self
1142            .center
1143            .find_pane_in_direction(&self.active_pane, direction, cx)
1144        {
1145            window.focus(&pane.focus_handle(cx), cx);
1146        } else {
1147            self.workspace
1148                .update(cx, |workspace, cx| {
1149                    workspace.activate_pane_in_direction(direction, window, cx)
1150                })
1151                .ok();
1152        }
1153    }
1154
1155    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1156        if let Some(to) = self
1157            .center
1158            .find_pane_in_direction(&self.active_pane, direction, cx)
1159            .cloned()
1160        {
1161            self.center.swap(&self.active_pane, &to, cx);
1162            cx.notify();
1163        }
1164    }
1165
1166    fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1167        if self
1168            .center
1169            .move_to_border(&self.active_pane, direction, cx)
1170            .unwrap()
1171        {
1172            cx.notify();
1173        }
1174    }
1175}
1176
1177/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
1178/// based on the shell configuration. This is a pure function that can be tested
1179/// without spawning actual terminals.
1180pub fn prepare_task_for_spawn(
1181    task: &SpawnInTerminal,
1182    shell: &Shell,
1183    is_windows: bool,
1184) -> SpawnInTerminal {
1185    let builder = ShellBuilder::new(shell, is_windows);
1186    let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
1187    let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
1188
1189    SpawnInTerminal {
1190        command_label,
1191        command: Some(command),
1192        args,
1193        ..task.clone()
1194    }
1195}
1196
1197fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1198    workspace.project().read(cx).supports_terminal(cx)
1199}
1200
1201pub fn new_terminal_pane(
1202    workspace: WeakEntity<Workspace>,
1203    project: Entity<Project>,
1204    zoomed: bool,
1205    window: &mut Window,
1206    cx: &mut Context<TerminalPanel>,
1207) -> Entity<Pane> {
1208    let terminal_panel = cx.entity();
1209    let pane = cx.new(|cx| {
1210        let mut pane = Pane::new(
1211            workspace.clone(),
1212            project.clone(),
1213            Default::default(),
1214            None,
1215            workspace::NewTerminal::default().boxed_clone(),
1216            false,
1217            window,
1218            cx,
1219        );
1220        pane.set_zoomed(zoomed, cx);
1221        pane.set_can_navigate(false, cx);
1222        pane.display_nav_history_buttons(None);
1223        pane.set_should_display_tab_bar(|_, _| true);
1224        pane.set_zoom_out_on_close(false);
1225
1226        let split_closure_terminal_panel = terminal_panel.downgrade();
1227        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1228            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1229                let is_current_pane = tab.pane == cx.entity();
1230                let Some(can_drag_away) = split_closure_terminal_panel
1231                    .read_with(cx, |terminal_panel, _| {
1232                        let current_panes = terminal_panel.center.panes();
1233                        !current_panes.contains(&&tab.pane)
1234                            || current_panes.len() > 1
1235                            || (!is_current_pane || pane.items_len() > 1)
1236                    })
1237                    .ok()
1238                else {
1239                    return false;
1240                };
1241                if can_drag_away {
1242                    let item = if is_current_pane {
1243                        pane.item_for_index(tab.ix)
1244                    } else {
1245                        tab.pane.read(cx).item_for_index(tab.ix)
1246                    };
1247                    if let Some(item) = item {
1248                        return item.downcast::<TerminalView>().is_some();
1249                    }
1250                }
1251            }
1252            false
1253        })));
1254
1255        let toolbar = pane.toolbar().clone();
1256        if let Some(callbacks) = cx.try_global::<workspace::PaneSearchBarCallbacks>() {
1257            let languages = Some(project.read(cx).languages().clone());
1258            (callbacks.setup_search_bar)(languages, &toolbar, window, cx);
1259        }
1260        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1261        toolbar.update(cx, |toolbar, cx| {
1262            toolbar.add_item(breadcrumbs, window, cx);
1263        });
1264
1265        pane
1266    });
1267
1268    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1269        .detach();
1270    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1271
1272    pane
1273}
1274
1275async fn wait_for_terminals_tasks(
1276    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1277    cx: &mut AsyncApp,
1278) {
1279    let pending_tasks = terminals_for_task.iter().map(|(_, _, terminal)| {
1280        terminal.update(cx, |terminal_view, cx| {
1281            terminal_view
1282                .terminal()
1283                .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1284        })
1285    });
1286    join_all(pending_tasks).await;
1287}
1288
1289struct FailedToSpawnTerminal {
1290    error: String,
1291    focus_handle: FocusHandle,
1292}
1293
1294impl Focusable for FailedToSpawnTerminal {
1295    fn focus_handle(&self, _: &App) -> FocusHandle {
1296        self.focus_handle.clone()
1297    }
1298}
1299
1300impl Render for FailedToSpawnTerminal {
1301    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1302        let popover_menu = PopoverMenu::new("settings-popover")
1303            .trigger(
1304                IconButton::new("icon-button-popover", IconName::ChevronDown)
1305                    .icon_size(IconSize::XSmall),
1306            )
1307            .menu(move |window, cx| {
1308                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1309                    context_menu
1310                        .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1311                        .action(
1312                            "Edit settings.json",
1313                            zed_actions::OpenSettingsFile.boxed_clone(),
1314                        )
1315                }))
1316            })
1317            .anchor(Corner::TopRight)
1318            .offset(gpui::Point {
1319                x: px(0.0),
1320                y: px(2.0),
1321            });
1322
1323        v_flex()
1324            .track_focus(&self.focus_handle)
1325            .size_full()
1326            .p_4()
1327            .items_center()
1328            .justify_center()
1329            .bg(cx.theme().colors().editor_background)
1330            .child(
1331                v_flex()
1332                    .max_w_112()
1333                    .items_center()
1334                    .justify_center()
1335                    .text_center()
1336                    .child(Label::new("Failed to spawn terminal"))
1337                    .child(
1338                        Label::new(self.error.to_string())
1339                            .size(LabelSize::Small)
1340                            .color(Color::Muted)
1341                            .mb_4(),
1342                    )
1343                    .child(SplitButton::new(
1344                        ButtonLike::new("open-settings-ui")
1345                            .child(Label::new("Edit Settings").size(LabelSize::Small))
1346                            .on_click(|_, window, cx| {
1347                                window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1348                            }),
1349                        popover_menu.into_any_element(),
1350                    )),
1351            )
1352    }
1353}
1354
1355impl EventEmitter<()> for FailedToSpawnTerminal {}
1356
1357impl workspace::Item for FailedToSpawnTerminal {
1358    type Event = ();
1359
1360    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1361        SharedString::new_static("Failed to spawn terminal")
1362    }
1363}
1364
1365impl EventEmitter<PanelEvent> for TerminalPanel {}
1366
1367impl Render for TerminalPanel {
1368    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1369        let registrar = cx
1370            .try_global::<workspace::PaneSearchBarCallbacks>()
1371            .map(|callbacks| {
1372                (callbacks.wrap_div_with_search_actions)(div(), self.active_pane.clone())
1373            })
1374            .unwrap_or_else(div);
1375        self.workspace
1376            .update(cx, |workspace, cx| {
1377                registrar.size_full().child(self.center.render(
1378                    workspace.zoomed_item(),
1379                    &workspace::PaneRenderContext {
1380                        follower_states: &HashMap::default(),
1381                        active_call: workspace.active_call(),
1382                        active_pane: &self.active_pane,
1383                        app_state: workspace.app_state(),
1384                        project: workspace.project(),
1385                        workspace: &workspace.weak_handle(),
1386                    },
1387                    window,
1388                    cx,
1389                ))
1390            })
1391            .ok()
1392            .map(|div| {
1393                div.on_action({
1394                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1395                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1396                    })
1397                })
1398                .on_action({
1399                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1400                        terminal_panel.activate_pane_in_direction(
1401                            SplitDirection::Right,
1402                            window,
1403                            cx,
1404                        );
1405                    })
1406                })
1407                .on_action({
1408                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1409                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1410                    })
1411                })
1412                .on_action({
1413                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1414                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1415                    })
1416                })
1417                .on_action(
1418                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1419                        let panes = terminal_panel.center.panes();
1420                        if let Some(ix) = panes
1421                            .iter()
1422                            .position(|pane| **pane == terminal_panel.active_pane)
1423                        {
1424                            let next_ix = (ix + 1) % panes.len();
1425                            window.focus(&panes[next_ix].focus_handle(cx), cx);
1426                        }
1427                    }),
1428                )
1429                .on_action(cx.listener(
1430                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1431                        let panes = terminal_panel.center.panes();
1432                        if let Some(ix) = panes
1433                            .iter()
1434                            .position(|pane| **pane == terminal_panel.active_pane)
1435                        {
1436                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1437                            window.focus(&panes[prev_ix].focus_handle(cx), cx);
1438                        }
1439                    },
1440                ))
1441                .on_action(
1442                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1443                        let panes = terminal_panel.center.panes();
1444                        if let Some(&pane) = panes.get(action.0) {
1445                            window.focus(&pane.read(cx).focus_handle(cx), cx);
1446                        } else {
1447                            let future =
1448                                terminal_panel.new_pane_with_active_terminal(true, window, cx);
1449                            cx.spawn_in(window, async move |terminal_panel, cx| {
1450                                if let Some(new_pane) = future.await {
1451                                    _ = terminal_panel.update_in(
1452                                        cx,
1453                                        |terminal_panel, window, cx| {
1454                                            terminal_panel.center.split(
1455                                                &terminal_panel.active_pane,
1456                                                &new_pane,
1457                                                SplitDirection::Right,
1458                                                cx,
1459                                            );
1460                                            let new_pane = new_pane.read(cx);
1461                                            window.focus(&new_pane.focus_handle(cx), cx);
1462                                        },
1463                                    );
1464                                }
1465                            })
1466                            .detach();
1467                        }
1468                    }),
1469                )
1470                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1471                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1472                }))
1473                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1474                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1475                }))
1476                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1477                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1478                }))
1479                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1480                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1481                }))
1482                .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1483                    terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1484                }))
1485                .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1486                    terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1487                }))
1488                .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1489                    terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1490                }))
1491                .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1492                    terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1493                }))
1494                .on_action(
1495                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1496                        let Some(&target_pane) =
1497                            terminal_panel.center.panes().get(action.destination)
1498                        else {
1499                            return;
1500                        };
1501                        move_active_item(
1502                            &terminal_panel.active_pane,
1503                            target_pane,
1504                            action.focus,
1505                            true,
1506                            window,
1507                            cx,
1508                        );
1509                    }),
1510                )
1511                .on_action(cx.listener(
1512                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1513                        let source_pane = &terminal_panel.active_pane;
1514                        if let Some(destination_pane) = terminal_panel
1515                            .center
1516                            .find_pane_in_direction(source_pane, action.direction, cx)
1517                        {
1518                            move_active_item(
1519                                source_pane,
1520                                destination_pane,
1521                                action.focus,
1522                                true,
1523                                window,
1524                                cx,
1525                            );
1526                        };
1527                    },
1528                ))
1529            })
1530            .unwrap_or_else(|| div())
1531    }
1532}
1533
1534impl Focusable for TerminalPanel {
1535    fn focus_handle(&self, cx: &App) -> FocusHandle {
1536        self.active_pane.focus_handle(cx)
1537    }
1538}
1539
1540impl Panel for TerminalPanel {
1541    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1542        match TerminalSettings::get_global(cx).dock {
1543            TerminalDockPosition::Left => DockPosition::Left,
1544            TerminalDockPosition::Bottom => DockPosition::Bottom,
1545            TerminalDockPosition::Right => DockPosition::Right,
1546        }
1547    }
1548
1549    fn position_is_valid(&self, _: DockPosition) -> bool {
1550        true
1551    }
1552
1553    fn set_position(
1554        &mut self,
1555        position: DockPosition,
1556        _window: &mut Window,
1557        cx: &mut Context<Self>,
1558    ) {
1559        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1560            let dock = match position {
1561                DockPosition::Left => TerminalDockPosition::Left,
1562                DockPosition::Bottom => TerminalDockPosition::Bottom,
1563                DockPosition::Right => TerminalDockPosition::Right,
1564            };
1565            settings.terminal.get_or_insert_default().dock = Some(dock);
1566        });
1567    }
1568
1569    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
1570        let settings = TerminalSettings::get_global(cx);
1571        match self.position(window, cx) {
1572            DockPosition::Left | DockPosition::Right => settings.default_width,
1573            DockPosition::Bottom => settings.default_height,
1574        }
1575    }
1576
1577    fn supports_flexible_size(&self) -> bool {
1578        true
1579    }
1580
1581    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
1582        TerminalSettings::get_global(cx).flexible
1583    }
1584
1585    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
1586        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1587            settings.terminal.get_or_insert_default().flexible = Some(flexible);
1588        });
1589    }
1590
1591    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1592        self.active_pane.read(cx).is_zoomed()
1593    }
1594
1595    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1596        for pane in self.center.panes() {
1597            pane.update(cx, |pane, cx| {
1598                pane.set_zoomed(zoomed, cx);
1599            })
1600        }
1601        cx.notify();
1602    }
1603
1604    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1605        let old_active = self.active;
1606        self.active = active;
1607        if !active || old_active == active || !self.has_no_terminals(cx) {
1608            return;
1609        }
1610        cx.defer_in(window, |this, window, cx| {
1611            let Ok(kind) = this
1612                .workspace
1613                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1614            else {
1615                return;
1616            };
1617
1618            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1619                .detach_and_log_err(cx)
1620        })
1621    }
1622
1623    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1624        if !TerminalSettings::get_global(cx).show_count_badge {
1625            return None;
1626        }
1627        let count = self
1628            .center
1629            .panes()
1630            .into_iter()
1631            .map(|pane| pane.read(cx).items_len())
1632            .sum::<usize>();
1633        if count == 0 {
1634            None
1635        } else {
1636            Some(count.to_string())
1637        }
1638    }
1639
1640    fn persistent_name() -> &'static str {
1641        "TerminalPanel"
1642    }
1643
1644    fn panel_key() -> &'static str {
1645        TERMINAL_PANEL_KEY
1646    }
1647
1648    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1649        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1650            && TerminalSettings::get_global(cx).button
1651        {
1652            Some(IconName::TerminalAlt)
1653        } else {
1654            None
1655        }
1656    }
1657
1658    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1659        Some("Terminal Panel")
1660    }
1661
1662    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1663        Box::new(Toggle)
1664    }
1665
1666    fn pane(&self) -> Option<Entity<Pane>> {
1667        Some(self.active_pane.clone())
1668    }
1669
1670    fn activation_priority(&self) -> u32 {
1671        2
1672    }
1673}
1674
1675struct TerminalProvider(Entity<TerminalPanel>);
1676
1677impl workspace::TerminalProvider for TerminalProvider {
1678    fn spawn(
1679        &self,
1680        task: SpawnInTerminal,
1681        window: &mut Window,
1682        cx: &mut App,
1683    ) -> Task<Option<Result<ExitStatus>>> {
1684        let terminal_panel = self.0.clone();
1685        window.spawn(cx, async move |cx| {
1686            let terminal = terminal_panel
1687                .update_in(cx, |terminal_panel, window, cx| {
1688                    terminal_panel.spawn_task(&task, window, cx)
1689                })
1690                .ok()?
1691                .await;
1692            match terminal {
1693                Ok(terminal) => {
1694                    let exit_status = terminal
1695                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1696                        .ok()?
1697                        .await?;
1698                    Some(Ok(exit_status))
1699                }
1700                Err(e) => Some(Err(e)),
1701            }
1702        })
1703    }
1704}
1705
1706struct InlineAssistTabBarButton {
1707    focus_handle: FocusHandle,
1708}
1709
1710impl Render for InlineAssistTabBarButton {
1711    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1712        let focus_handle = self.focus_handle.clone();
1713        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1714            .icon_size(IconSize::Small)
1715            .on_click(cx.listener(|_, _, window, cx| {
1716                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1717            }))
1718            .tooltip(move |_window, cx| {
1719                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1720            })
1721    }
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726    use std::num::NonZero;
1727
1728    use super::*;
1729    use gpui::{TestAppContext, UpdateGlobal as _};
1730    use pretty_assertions::assert_eq;
1731    use project::FakeFs;
1732    use settings::SettingsStore;
1733    use workspace::MultiWorkspace;
1734
1735    #[test]
1736    fn test_prepare_empty_task() {
1737        let input = SpawnInTerminal::default();
1738        let shell = Shell::System;
1739
1740        let result = prepare_task_for_spawn(&input, &shell, false);
1741
1742        let expected_shell = util::get_system_shell();
1743        assert_eq!(result.env, HashMap::default());
1744        assert_eq!(result.cwd, None);
1745        assert_eq!(result.shell, Shell::System);
1746        assert_eq!(
1747            result.command,
1748            Some(expected_shell.clone()),
1749            "Empty tasks should spawn a -i shell"
1750        );
1751        assert_eq!(result.args, Vec::<String>::new());
1752        assert_eq!(
1753            result.command_label, expected_shell,
1754            "We show the shell launch for empty commands"
1755        );
1756    }
1757
1758    #[gpui::test]
1759    async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1760        cx.executor().allow_parking();
1761        init_test(cx);
1762
1763        let fs = FakeFs::new(cx.executor());
1764        let project = Project::test(fs, [], cx).await;
1765        let window_handle =
1766            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1767
1768        let terminal_panel = window_handle
1769            .update(cx, |multi_workspace, window, cx| {
1770                multi_workspace.workspace().update(cx, |workspace, cx| {
1771                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1772                })
1773            })
1774            .unwrap();
1775
1776        set_max_tabs(cx, Some(3));
1777
1778        for _ in 0..5 {
1779            let task = window_handle
1780                .update(cx, |_, window, cx| {
1781                    terminal_panel.update(cx, |panel, cx| {
1782                        panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1783                    })
1784                })
1785                .unwrap();
1786            task.await.unwrap();
1787        }
1788
1789        cx.run_until_parked();
1790
1791        let item_count =
1792            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1793
1794        assert_eq!(
1795            item_count, 5,
1796            "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1797        );
1798    }
1799
1800    #[cfg(unix)]
1801    #[test]
1802    fn test_prepare_script_like_task() {
1803        let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string();
1804        let expected_cwd = PathBuf::from("/some/work");
1805
1806        let input = SpawnInTerminal {
1807            command: Some(user_command.clone()),
1808            cwd: Some(expected_cwd.clone()),
1809            ..SpawnInTerminal::default()
1810        };
1811        let shell = Shell::System;
1812
1813        let result = prepare_task_for_spawn(&input, &shell, false);
1814
1815        let system_shell = util::get_system_shell();
1816        assert_eq!(result.env, HashMap::default());
1817        assert_eq!(result.cwd, Some(expected_cwd));
1818        assert_eq!(result.shell, Shell::System);
1819        assert_eq!(result.command, Some(system_shell.clone()));
1820        assert_eq!(
1821            result.args,
1822            vec!["-i".to_string(), "-c".to_string(), user_command.clone()],
1823            "User command should have been moved into the arguments, as we're spawning a new -i shell",
1824        );
1825        assert_eq!(
1826            result.command_label,
1827            format!(
1828                "{system_shell} {interactive}-c '{user_command}'",
1829                interactive = if cfg!(windows) { "" } else { "-i " }
1830            ),
1831            "We want to show to the user the entire command spawned"
1832        );
1833    }
1834
1835    #[gpui::test]
1836    async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1837        cx.executor().allow_parking();
1838        init_test(cx);
1839
1840        cx.update(|cx| {
1841            SettingsStore::update_global(cx, |store, cx| {
1842                store.update_user_settings(cx, |settings| {
1843                    settings.terminal.get_or_insert_default().project.shell =
1844                        Some(settings::Shell::Program("__nonexistent_shell__".to_owned()));
1845                });
1846            });
1847        });
1848
1849        let fs = FakeFs::new(cx.executor());
1850        let project = Project::test(fs, [], cx).await;
1851        let window_handle =
1852            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1853
1854        let terminal_panel = window_handle
1855            .update(cx, |multi_workspace, window, cx| {
1856                multi_workspace.workspace().update(cx, |workspace, cx| {
1857                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1858                })
1859            })
1860            .unwrap();
1861
1862        window_handle
1863            .update(cx, |_, window, cx| {
1864                terminal_panel.update(cx, |terminal_panel, cx| {
1865                    terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1866                })
1867            })
1868            .unwrap()
1869            .await
1870            .unwrap_err();
1871
1872        window_handle
1873            .update(cx, |_, _, cx| {
1874                terminal_panel.update(cx, |terminal_panel, cx| {
1875                    assert!(
1876                        terminal_panel
1877                            .active_pane
1878                            .read(cx)
1879                            .items()
1880                            .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1881                        "should spawn `FailedToSpawnTerminal` pane"
1882                    );
1883                })
1884            })
1885            .unwrap();
1886    }
1887
1888    #[gpui::test]
1889    async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) {
1890        cx.executor().allow_parking();
1891        init_test(cx);
1892
1893        let fs = FakeFs::new(cx.executor());
1894        let project = Project::test(fs, [], cx).await;
1895        let window_handle =
1896            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1897
1898        let terminal_panel = window_handle
1899            .update(cx, |multi_workspace, window, cx| {
1900                multi_workspace.workspace().update(cx, |workspace, cx| {
1901                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1902                })
1903            })
1904            .unwrap();
1905
1906        let result = window_handle
1907            .update(cx, |_, window, cx| {
1908                terminal_panel.update(cx, |terminal_panel, cx| {
1909                    terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
1910                })
1911            })
1912            .unwrap()
1913            .await;
1914
1915        assert!(
1916            result.is_ok(),
1917            "local terminal should successfully create in local project"
1918        );
1919    }
1920
1921    async fn init_workspace_with_panel(
1922        cx: &mut TestAppContext,
1923    ) -> (gpui::WindowHandle<MultiWorkspace>, Entity<TerminalPanel>) {
1924        let fs = FakeFs::new(cx.executor());
1925        let project = Project::test(fs, [], cx).await;
1926        let window_handle =
1927            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1928
1929        let terminal_panel = window_handle
1930            .update(cx, |multi_workspace, window, cx| {
1931                multi_workspace.workspace().update(cx, |workspace, cx| {
1932                    let panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1933                    workspace.add_panel(panel.clone(), window, cx);
1934                    panel
1935                })
1936            })
1937            .expect("Failed to initialize workspace with terminal panel");
1938
1939        (window_handle, terminal_panel)
1940    }
1941
1942    #[gpui::test]
1943    async fn test_new_terminal_opens_in_panel_by_default(cx: &mut TestAppContext) {
1944        cx.executor().allow_parking();
1945        init_test(cx);
1946
1947        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
1948
1949        let panel_items_before =
1950            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1951        let center_items_before = window_handle
1952            .read_with(cx, |multi_workspace, cx| {
1953                multi_workspace
1954                    .workspace()
1955                    .read(cx)
1956                    .active_pane()
1957                    .read(cx)
1958                    .items_len()
1959            })
1960            .expect("Failed to read center pane items");
1961
1962        window_handle
1963            .update(cx, |multi_workspace, window, cx| {
1964                multi_workspace.workspace().update(cx, |workspace, cx| {
1965                    TerminalPanel::new_terminal(
1966                        workspace,
1967                        &workspace::NewTerminal::default(),
1968                        window,
1969                        cx,
1970                    );
1971                })
1972            })
1973            .expect("Failed to dispatch new_terminal");
1974
1975        cx.run_until_parked();
1976
1977        let panel_items_after =
1978            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1979        let center_items_after = window_handle
1980            .read_with(cx, |multi_workspace, cx| {
1981                multi_workspace
1982                    .workspace()
1983                    .read(cx)
1984                    .active_pane()
1985                    .read(cx)
1986                    .items_len()
1987            })
1988            .expect("Failed to read center pane items");
1989
1990        assert_eq!(
1991            panel_items_after,
1992            panel_items_before + 1,
1993            "Terminal should be added to the panel when no center terminal is focused"
1994        );
1995        assert_eq!(
1996            center_items_after, center_items_before,
1997            "Center pane should not gain a new terminal"
1998        );
1999    }
2000
2001    #[gpui::test]
2002    async fn test_new_terminal_opens_in_center_when_center_terminal_focused(
2003        cx: &mut TestAppContext,
2004    ) {
2005        cx.executor().allow_parking();
2006        init_test(cx);
2007
2008        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2009
2010        window_handle
2011            .update(cx, |multi_workspace, window, cx| {
2012                multi_workspace.workspace().update(cx, |workspace, cx| {
2013                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2014                        project.create_terminal_shell(None, cx)
2015                    })
2016                })
2017            })
2018            .expect("Failed to update workspace")
2019            .await
2020            .expect("Failed to create center terminal");
2021        cx.run_until_parked();
2022
2023        let center_items_before = window_handle
2024            .read_with(cx, |multi_workspace, cx| {
2025                multi_workspace
2026                    .workspace()
2027                    .read(cx)
2028                    .active_pane()
2029                    .read(cx)
2030                    .items_len()
2031            })
2032            .expect("Failed to read center pane items");
2033        assert_eq!(center_items_before, 1, "Center pane should have 1 terminal");
2034
2035        window_handle
2036            .update(cx, |multi_workspace, window, cx| {
2037                multi_workspace.workspace().update(cx, |workspace, cx| {
2038                    let active_item = workspace
2039                        .active_pane()
2040                        .read(cx)
2041                        .active_item()
2042                        .expect("Center pane should have an active item");
2043                    let terminal_view = active_item
2044                        .downcast::<TerminalView>()
2045                        .expect("Active center item should be a TerminalView");
2046                    window.focus(&terminal_view.focus_handle(cx), cx);
2047                })
2048            })
2049            .expect("Failed to focus terminal view");
2050        cx.run_until_parked();
2051
2052        let panel_items_before =
2053            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2054
2055        window_handle
2056            .update(cx, |multi_workspace, window, cx| {
2057                multi_workspace.workspace().update(cx, |workspace, cx| {
2058                    TerminalPanel::new_terminal(
2059                        workspace,
2060                        &workspace::NewTerminal::default(),
2061                        window,
2062                        cx,
2063                    );
2064                })
2065            })
2066            .expect("Failed to dispatch new_terminal");
2067        cx.run_until_parked();
2068
2069        let center_items_after = window_handle
2070            .read_with(cx, |multi_workspace, cx| {
2071                multi_workspace
2072                    .workspace()
2073                    .read(cx)
2074                    .active_pane()
2075                    .read(cx)
2076                    .items_len()
2077            })
2078            .expect("Failed to read center pane items");
2079        let panel_items_after =
2080            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2081
2082        assert_eq!(
2083            center_items_after,
2084            center_items_before + 1,
2085            "New terminal should be added to the center pane"
2086        );
2087        assert_eq!(
2088            panel_items_after, panel_items_before,
2089            "Terminal panel should not gain a new terminal"
2090        );
2091    }
2092
2093    #[gpui::test]
2094    async fn test_new_terminal_opens_in_panel_when_panel_focused(cx: &mut TestAppContext) {
2095        cx.executor().allow_parking();
2096        init_test(cx);
2097
2098        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2099
2100        window_handle
2101            .update(cx, |_, window, cx| {
2102                terminal_panel.update(cx, |panel, cx| {
2103                    panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2104                })
2105            })
2106            .expect("Failed to update workspace")
2107            .await
2108            .expect("Failed to create panel terminal");
2109        cx.run_until_parked();
2110
2111        window_handle
2112            .update(cx, |_, window, cx| {
2113                window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2114            })
2115            .expect("Failed to focus terminal panel");
2116        cx.run_until_parked();
2117
2118        let panel_items_before =
2119            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2120
2121        let center_items_before = window_handle
2122            .read_with(cx, |multi_workspace, cx| {
2123                multi_workspace
2124                    .workspace()
2125                    .read(cx)
2126                    .active_pane()
2127                    .read(cx)
2128                    .items_len()
2129            })
2130            .expect("Failed to read center pane items");
2131
2132        window_handle
2133            .update(cx, |multi_workspace, window, cx| {
2134                multi_workspace.workspace().update(cx, |workspace, cx| {
2135                    TerminalPanel::new_terminal(
2136                        workspace,
2137                        &workspace::NewTerminal::default(),
2138                        window,
2139                        cx,
2140                    );
2141                })
2142            })
2143            .expect("Failed to dispatch new_terminal");
2144        cx.run_until_parked();
2145
2146        let panel_items_after =
2147            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2148        let center_items_after = window_handle
2149            .read_with(cx, |multi_workspace, cx| {
2150                multi_workspace
2151                    .workspace()
2152                    .read(cx)
2153                    .active_pane()
2154                    .read(cx)
2155                    .items_len()
2156            })
2157            .expect("Failed to read center pane items");
2158
2159        assert_eq!(
2160            panel_items_after,
2161            panel_items_before + 1,
2162            "New terminal should be added to the panel when panel is focused"
2163        );
2164        assert_eq!(
2165            center_items_after, center_items_before,
2166            "Center pane should not gain a new terminal"
2167        );
2168    }
2169
2170    #[gpui::test]
2171    async fn test_new_local_terminal_opens_in_center_when_center_terminal_focused(
2172        cx: &mut TestAppContext,
2173    ) {
2174        cx.executor().allow_parking();
2175        init_test(cx);
2176
2177        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2178
2179        window_handle
2180            .update(cx, |multi_workspace, window, cx| {
2181                multi_workspace.workspace().update(cx, |workspace, cx| {
2182                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2183                        project.create_terminal_shell(None, cx)
2184                    })
2185                })
2186            })
2187            .expect("Failed to update workspace")
2188            .await
2189            .expect("Failed to create center terminal");
2190        cx.run_until_parked();
2191
2192        window_handle
2193            .update(cx, |multi_workspace, window, cx| {
2194                multi_workspace.workspace().update(cx, |workspace, cx| {
2195                    let active_item = workspace
2196                        .active_pane()
2197                        .read(cx)
2198                        .active_item()
2199                        .expect("Center pane should have an active item");
2200                    let terminal_view = active_item
2201                        .downcast::<TerminalView>()
2202                        .expect("Active center item should be a TerminalView");
2203                    window.focus(&terminal_view.focus_handle(cx), cx);
2204                })
2205            })
2206            .expect("Failed to focus terminal view");
2207        cx.run_until_parked();
2208
2209        let center_items_before = window_handle
2210            .read_with(cx, |multi_workspace, cx| {
2211                multi_workspace
2212                    .workspace()
2213                    .read(cx)
2214                    .active_pane()
2215                    .read(cx)
2216                    .items_len()
2217            })
2218            .expect("Failed to read center pane items");
2219        let panel_items_before =
2220            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2221
2222        window_handle
2223            .update(cx, |multi_workspace, window, cx| {
2224                multi_workspace.workspace().update(cx, |workspace, cx| {
2225                    TerminalPanel::new_terminal(
2226                        workspace,
2227                        &workspace::NewTerminal { local: true },
2228                        window,
2229                        cx,
2230                    );
2231                })
2232            })
2233            .expect("Failed to dispatch new_terminal with local=true");
2234        cx.run_until_parked();
2235
2236        let center_items_after = window_handle
2237            .read_with(cx, |multi_workspace, cx| {
2238                multi_workspace
2239                    .workspace()
2240                    .read(cx)
2241                    .active_pane()
2242                    .read(cx)
2243                    .items_len()
2244            })
2245            .expect("Failed to read center pane items");
2246        let panel_items_after =
2247            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2248
2249        assert_eq!(
2250            center_items_after,
2251            center_items_before + 1,
2252            "New local terminal should be added to the center pane"
2253        );
2254        assert_eq!(
2255            panel_items_after, panel_items_before,
2256            "Terminal panel should not gain a new terminal"
2257        );
2258    }
2259
2260    #[gpui::test]
2261    async fn test_new_terminal_opens_in_panel_when_panel_focused_and_center_has_terminal(
2262        cx: &mut TestAppContext,
2263    ) {
2264        cx.executor().allow_parking();
2265        init_test(cx);
2266
2267        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2268
2269        window_handle
2270            .update(cx, |multi_workspace, window, cx| {
2271                multi_workspace.workspace().update(cx, |workspace, cx| {
2272                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2273                        project.create_terminal_shell(None, cx)
2274                    })
2275                })
2276            })
2277            .expect("Failed to update workspace")
2278            .await
2279            .expect("Failed to create center terminal");
2280        cx.run_until_parked();
2281
2282        window_handle
2283            .update(cx, |_, window, cx| {
2284                terminal_panel.update(cx, |panel, cx| {
2285                    panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2286                })
2287            })
2288            .expect("Failed to update workspace")
2289            .await
2290            .expect("Failed to create panel terminal");
2291        cx.run_until_parked();
2292
2293        window_handle
2294            .update(cx, |_, window, cx| {
2295                window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2296            })
2297            .expect("Failed to focus terminal panel");
2298        cx.run_until_parked();
2299
2300        let panel_items_before =
2301            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2302        let center_items_before = window_handle
2303            .read_with(cx, |multi_workspace, cx| {
2304                multi_workspace
2305                    .workspace()
2306                    .read(cx)
2307                    .active_pane()
2308                    .read(cx)
2309                    .items_len()
2310            })
2311            .expect("Failed to read center pane items");
2312
2313        window_handle
2314            .update(cx, |multi_workspace, window, cx| {
2315                multi_workspace.workspace().update(cx, |workspace, cx| {
2316                    TerminalPanel::new_terminal(
2317                        workspace,
2318                        &workspace::NewTerminal::default(),
2319                        window,
2320                        cx,
2321                    );
2322                })
2323            })
2324            .expect("Failed to dispatch new_terminal");
2325        cx.run_until_parked();
2326
2327        let panel_items_after =
2328            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2329        let center_items_after = window_handle
2330            .read_with(cx, |multi_workspace, cx| {
2331                multi_workspace
2332                    .workspace()
2333                    .read(cx)
2334                    .active_pane()
2335                    .read(cx)
2336                    .items_len()
2337            })
2338            .expect("Failed to read center pane items");
2339
2340        assert_eq!(
2341            panel_items_after,
2342            panel_items_before + 1,
2343            "New terminal should go to panel when panel is focused, even if center has a terminal"
2344        );
2345        assert_eq!(
2346            center_items_after, center_items_before,
2347            "Center pane should not gain a new terminal when panel is focused"
2348        );
2349    }
2350
2351    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
2352        cx.update_global(|store: &mut SettingsStore, cx| {
2353            store.update_user_settings(cx, |settings| {
2354                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
2355            });
2356        });
2357    }
2358
2359    pub fn init_test(cx: &mut TestAppContext) {
2360        cx.update(|cx| {
2361            let store = SettingsStore::test(cx);
2362            cx.set_global(store);
2363            theme_settings::init(theme::LoadThemes::JustBase, cx);
2364            editor::init(cx);
2365            crate::init(cx);
2366        });
2367    }
2368}