terminal_panel.rs

   1use std::{cmp, ops::ControlFlow, 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::KEY_VALUE_STORE;
  12use futures::{channel::oneshot, future::join_all};
  13use gpui::{
  14    Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter,
  15    ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled,
  16    Task, WeakEntity, Window, actions,
  17};
  18use itertools::Itertools;
  19use project::{Fs, Project, ProjectEntryId};
  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, DraggedSelection, 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, move_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    pub(crate) width: Option<Pixels>,
  83    pub(crate) height: Option<Pixels>,
  84    pending_serialization: Task<Option<()>>,
  85    pending_terminals_to_add: usize,
  86    deferred_tasks: HashMap<TaskId, Task<()>>,
  87    assistant_enabled: bool,
  88    assistant_tab_bar_button: Option<AnyView>,
  89    active: bool,
  90}
  91
  92impl TerminalPanel {
  93    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
  94        let project = workspace.project();
  95        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
  96        let center = PaneGroup::new(pane.clone());
  97        let terminal_panel = Self {
  98            center,
  99            active_pane: pane,
 100            fs: workspace.app_state().fs.clone(),
 101            workspace: workspace.weak_handle(),
 102            pending_serialization: Task::ready(None),
 103            width: None,
 104            height: None,
 105            pending_terminals_to_add: 0,
 106            deferred_tasks: HashMap::default(),
 107            assistant_enabled: false,
 108            assistant_tab_bar_button: None,
 109            active: false,
 110        };
 111        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 112        terminal_panel
 113    }
 114
 115    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
 116        self.assistant_enabled = enabled;
 117        if enabled {
 118            let focus_handle = self
 119                .active_pane
 120                .read(cx)
 121                .active_item()
 122                .map(|item| item.item_focus_handle(cx))
 123                .unwrap_or(self.focus_handle(cx));
 124            self.assistant_tab_bar_button = Some(
 125                cx.new(move |_| InlineAssistTabBarButton { focus_handle })
 126                    .into(),
 127            );
 128        } else {
 129            self.assistant_tab_bar_button = None;
 130        }
 131        for pane in self.center.panes() {
 132            self.apply_tab_bar_buttons(pane, cx);
 133        }
 134    }
 135
 136    fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
 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)) = workspace
 250            .read_with(&cx, |workspace, _| {
 251                workspace
 252                    .database_id()
 253                    .zip(TerminalPanel::serialization_key(workspace))
 254            })
 255            .ok()
 256            .flatten()
 257            && let Some(serialized_panel) = cx
 258                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
 259                .await
 260                .log_err()
 261                .flatten()
 262                .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 263                .transpose()
 264                .log_err()
 265                .flatten()
 266            && let Ok(serialized) = workspace
 267                .update_in(&mut cx, |workspace, window, cx| {
 268                    deserialize_terminal_panel(
 269                        workspace.weak_handle(),
 270                        workspace.project().clone(),
 271                        database_id,
 272                        serialized_panel,
 273                        window,
 274                        cx,
 275                    )
 276                })?
 277                .await
 278        {
 279            terminal_panel = Some(serialized);
 280        }
 281
 282        let terminal_panel = if let Some(panel) = terminal_panel {
 283            panel
 284        } else {
 285            workspace.update_in(&mut cx, |workspace, window, cx| {
 286                cx.new(|cx| TerminalPanel::new(workspace, window, cx))
 287            })?
 288        };
 289
 290        if let Some(workspace) = workspace.upgrade() {
 291            workspace.update(&mut cx, |workspace, _| {
 292                workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
 293            });
 294        }
 295
 296        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 297        if let Some(workspace) = workspace.upgrade() {
 298            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
 299                let alive_item_ids = terminal_panel
 300                    .read(cx)
 301                    .center
 302                    .panes()
 303                    .into_iter()
 304                    .flat_map(|pane| pane.read(cx).items())
 305                    .map(|item| item.item_id().as_u64() as ItemId)
 306                    .collect();
 307                workspace.database_id().map(|workspace_id| {
 308                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
 309                })
 310            })?;
 311            if let Some(task) = cleanup_task {
 312                task.await.log_err();
 313            }
 314        }
 315
 316        if let Some(workspace) = workspace.upgrade() {
 317            let should_focus = workspace
 318                .update_in(&mut cx, |workspace, window, cx| {
 319                    workspace.active_item(cx).is_none()
 320                        && workspace
 321                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
 322                })
 323                .unwrap_or(false);
 324
 325            if should_focus {
 326                terminal_panel
 327                    .update_in(&mut cx, |panel, window, cx| {
 328                        panel.active_pane.update(cx, |pane, cx| {
 329                            pane.focus_active_item(window, cx);
 330                        });
 331                    })
 332                    .ok();
 333            }
 334        }
 335        Ok(terminal_panel)
 336    }
 337
 338    fn handle_pane_event(
 339        &mut self,
 340        pane: &Entity<Pane>,
 341        event: &pane::Event,
 342        window: &mut Window,
 343        cx: &mut Context<Self>,
 344    ) {
 345        match event {
 346            pane::Event::ActivateItem { .. } => self.serialize(cx),
 347            pane::Event::RemovedItem { .. } => self.serialize(cx),
 348            pane::Event::Remove { focus_on_pane } => {
 349                let pane_count_before_removal = self.center.panes().len();
 350                let _removal_result = self.center.remove(pane, cx);
 351                if pane_count_before_removal == 1 {
 352                    self.center.first_pane().update(cx, |pane, cx| {
 353                        pane.set_zoomed(false, cx);
 354                    });
 355                    cx.emit(PanelEvent::Close);
 356                } else if let Some(focus_on_pane) =
 357                    focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 358                {
 359                    focus_on_pane.focus_handle(cx).focus(window, cx);
 360                }
 361            }
 362            pane::Event::ZoomIn => {
 363                for pane in self.center.panes() {
 364                    pane.update(cx, |pane, cx| {
 365                        pane.set_zoomed(true, cx);
 366                    })
 367                }
 368                cx.emit(PanelEvent::ZoomIn);
 369                cx.notify();
 370            }
 371            pane::Event::ZoomOut => {
 372                for pane in self.center.panes() {
 373                    pane.update(cx, |pane, cx| {
 374                        pane.set_zoomed(false, cx);
 375                    })
 376                }
 377                cx.emit(PanelEvent::ZoomOut);
 378                cx.notify();
 379            }
 380            pane::Event::AddItem { item } => {
 381                if let Some(workspace) = self.workspace.upgrade() {
 382                    workspace.update(cx, |workspace, cx| {
 383                        item.added_to_pane(workspace, pane.clone(), window, cx)
 384                    })
 385                }
 386                self.serialize(cx);
 387            }
 388            &pane::Event::Split { direction, mode } => {
 389                match mode {
 390                    SplitMode::ClonePane | SplitMode::EmptyPane => {
 391                        let clone = matches!(mode, SplitMode::ClonePane);
 392                        let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
 393                        let pane = pane.clone();
 394                        cx.spawn_in(window, async move |panel, cx| {
 395                            let Some(new_pane) = new_pane.await else {
 396                                return;
 397                            };
 398                            panel
 399                                .update_in(cx, |panel, window, cx| {
 400                                    panel.center.split(&pane, &new_pane, direction, cx);
 401                                    window.focus(&new_pane.focus_handle(cx), cx);
 402                                })
 403                                .ok();
 404                        })
 405                        .detach();
 406                    }
 407                    SplitMode::MovePane => {
 408                        let Some(item) =
 409                            pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
 410                        else {
 411                            return;
 412                        };
 413                        let Ok(project) = self
 414                            .workspace
 415                            .update(cx, |workspace, _| workspace.project().clone())
 416                        else {
 417                            return;
 418                        };
 419                        let new_pane =
 420                            new_terminal_pane(self.workspace.clone(), project, false, window, cx);
 421                        new_pane.update(cx, |pane, cx| {
 422                            pane.add_item(item, true, true, None, window, cx);
 423                        });
 424                        self.center.split(&pane, &new_pane, direction, cx);
 425                        window.focus(&new_pane.focus_handle(cx), cx);
 426                    }
 427                };
 428            }
 429            pane::Event::Focus => {
 430                self.active_pane = pane.clone();
 431            }
 432            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
 433                self.serialize(cx);
 434            }
 435
 436            _ => {}
 437        }
 438    }
 439
 440    fn new_pane_with_active_terminal(
 441        &mut self,
 442        clone: bool,
 443        window: &mut Window,
 444        cx: &mut Context<Self>,
 445    ) -> Task<Option<Entity<Pane>>> {
 446        let Some(workspace) = self.workspace.upgrade() else {
 447            return Task::ready(None);
 448        };
 449        let workspace = workspace.read(cx);
 450        let database_id = workspace.database_id();
 451        let weak_workspace = self.workspace.clone();
 452        let project = workspace.project().clone();
 453        let active_pane = &self.active_pane;
 454        let terminal_view = if clone {
 455            active_pane
 456                .read(cx)
 457                .active_item()
 458                .and_then(|item| item.downcast::<TerminalView>())
 459        } else {
 460            None
 461        };
 462        let working_directory = if clone {
 463            terminal_view
 464                .as_ref()
 465                .and_then(|terminal_view| {
 466                    terminal_view
 467                        .read(cx)
 468                        .terminal()
 469                        .read(cx)
 470                        .working_directory()
 471                })
 472                .or_else(|| default_working_directory(workspace, cx))
 473        } else {
 474            default_working_directory(workspace, cx)
 475        };
 476
 477        let is_zoomed = if clone {
 478            active_pane.read(cx).is_zoomed()
 479        } else {
 480            false
 481        };
 482        cx.spawn_in(window, async move |panel, cx| {
 483            let terminal = project
 484                .update(cx, |project, cx| match terminal_view {
 485                    Some(view) => project.clone_terminal(
 486                        &view.read(cx).terminal.clone(),
 487                        cx,
 488                        working_directory,
 489                    ),
 490                    None => project.create_terminal_shell(working_directory, cx),
 491                })
 492                .await
 493                .log_err()?;
 494
 495            panel
 496                .update_in(cx, move |terminal_panel, window, cx| {
 497                    let terminal_view = Box::new(cx.new(|cx| {
 498                        TerminalView::new(
 499                            terminal.clone(),
 500                            weak_workspace.clone(),
 501                            database_id,
 502                            project.downgrade(),
 503                            window,
 504                            cx,
 505                        )
 506                    }));
 507                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
 508                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 509                    pane.update(cx, |pane, cx| {
 510                        pane.add_item(terminal_view, true, true, None, window, cx);
 511                    });
 512                    Some(pane)
 513                })
 514                .ok()
 515                .flatten()
 516        })
 517    }
 518
 519    pub fn open_terminal(
 520        workspace: &mut Workspace,
 521        action: &workspace::OpenTerminal,
 522        window: &mut Window,
 523        cx: &mut Context<Workspace>,
 524    ) {
 525        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 526            return;
 527        };
 528
 529        terminal_panel
 530            .update(cx, |panel, cx| {
 531                if action.local {
 532                    panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 533                } else {
 534                    panel.add_terminal_shell(
 535                        Some(action.working_directory.clone()),
 536                        RevealStrategy::Always,
 537                        window,
 538                        cx,
 539                    )
 540                }
 541            })
 542            .detach_and_log_err(cx);
 543    }
 544
 545    pub fn spawn_task(
 546        &mut self,
 547        task: &SpawnInTerminal,
 548        window: &mut Window,
 549        cx: &mut Context<Self>,
 550    ) -> Task<Result<WeakEntity<Terminal>>> {
 551        let Some(workspace) = self.workspace.upgrade() else {
 552            return Task::ready(Err(anyhow!("failed to read workspace")));
 553        };
 554
 555        let project = workspace.read(cx).project().read(cx);
 556
 557        if project.is_via_collab() {
 558            return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
 559        }
 560
 561        let remote_client = project.remote_client();
 562        let is_windows = project.path_style(cx).is_windows();
 563        let remote_shell = remote_client
 564            .as_ref()
 565            .and_then(|remote_client| remote_client.read(cx).shell());
 566
 567        let shell = if let Some(remote_shell) = remote_shell
 568            && task.shell == Shell::System
 569        {
 570            Shell::Program(remote_shell)
 571        } else {
 572            task.shell.clone()
 573        };
 574
 575        let task = prepare_task_for_spawn(task, &shell, is_windows);
 576
 577        if task.allow_concurrent_runs && task.use_new_terminal {
 578            return self.spawn_in_new_terminal(task, window, cx);
 579        }
 580
 581        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
 582        let Some(existing) = terminals_for_task.pop() else {
 583            return self.spawn_in_new_terminal(task, window, cx);
 584        };
 585
 586        let (existing_item_index, task_pane, existing_terminal) = existing;
 587        if task.allow_concurrent_runs {
 588            return self.replace_terminal(
 589                task,
 590                task_pane,
 591                existing_item_index,
 592                existing_terminal,
 593                window,
 594                cx,
 595            );
 596        }
 597
 598        let (tx, rx) = oneshot::channel();
 599
 600        self.deferred_tasks.insert(
 601            task.id.clone(),
 602            cx.spawn_in(window, async move |terminal_panel, cx| {
 603                wait_for_terminals_tasks(terminals_for_task, cx).await;
 604                let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 605                    if task.use_new_terminal {
 606                        terminal_panel.spawn_in_new_terminal(task, window, cx)
 607                    } else {
 608                        terminal_panel.replace_terminal(
 609                            task,
 610                            task_pane,
 611                            existing_item_index,
 612                            existing_terminal,
 613                            window,
 614                            cx,
 615                        )
 616                    }
 617                });
 618                if let Ok(task) = task {
 619                    tx.send(task.await).ok();
 620                }
 621            }),
 622        );
 623
 624        cx.spawn(async move |_, _| rx.await?)
 625    }
 626
 627    fn spawn_in_new_terminal(
 628        &mut self,
 629        spawn_task: SpawnInTerminal,
 630        window: &mut Window,
 631        cx: &mut Context<Self>,
 632    ) -> Task<Result<WeakEntity<Terminal>>> {
 633        let reveal = spawn_task.reveal;
 634        let reveal_target = spawn_task.reveal_target;
 635        match reveal_target {
 636            RevealTarget::Center => self
 637                .workspace
 638                .update(cx, |workspace, cx| {
 639                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
 640                        project.create_terminal_task(spawn_task, cx)
 641                    })
 642                })
 643                .unwrap_or_else(|e| Task::ready(Err(e))),
 644            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
 645        }
 646    }
 647
 648    /// Create a new Terminal in the current working directory or the user's home directory
 649    fn new_terminal(
 650        workspace: &mut Workspace,
 651        action: &workspace::NewTerminal,
 652        window: &mut Window,
 653        cx: &mut Context<Workspace>,
 654    ) {
 655        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 656            return;
 657        };
 658
 659        terminal_panel
 660            .update(cx, |this, cx| {
 661                if action.local {
 662                    this.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 663                } else {
 664                    this.add_terminal_shell(
 665                        default_working_directory(workspace, cx),
 666                        RevealStrategy::Always,
 667                        window,
 668                        cx,
 669                    )
 670                }
 671            })
 672            .detach_and_log_err(cx);
 673    }
 674
 675    fn terminals_for_task(
 676        &self,
 677        label: &str,
 678        cx: &mut App,
 679    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 680        let Some(workspace) = self.workspace.upgrade() else {
 681            return Vec::new();
 682        };
 683
 684        let pane_terminal_views = |pane: Entity<Pane>| {
 685            pane.read(cx)
 686                .items()
 687                .enumerate()
 688                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 689                .filter_map(|(index, terminal_view)| {
 690                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 691                    if &task_state.spawned_task.full_label == label {
 692                        Some((index, terminal_view))
 693                    } else {
 694                        None
 695                    }
 696                })
 697                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 698        };
 699
 700        self.center
 701            .panes()
 702            .into_iter()
 703            .cloned()
 704            .flat_map(pane_terminal_views)
 705            .chain(
 706                workspace
 707                    .read(cx)
 708                    .panes()
 709                    .iter()
 710                    .cloned()
 711                    .flat_map(pane_terminal_views),
 712            )
 713            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 714            .collect()
 715    }
 716
 717    fn activate_terminal_view(
 718        &self,
 719        pane: &Entity<Pane>,
 720        item_index: usize,
 721        focus: bool,
 722        window: &mut Window,
 723        cx: &mut App,
 724    ) {
 725        pane.update(cx, |pane, cx| {
 726            pane.activate_item(item_index, true, focus, window, cx)
 727        })
 728    }
 729
 730    pub fn add_center_terminal(
 731        workspace: &mut Workspace,
 732        window: &mut Window,
 733        cx: &mut Context<Workspace>,
 734        create_terminal: impl FnOnce(
 735            &mut Project,
 736            &mut Context<Project>,
 737        ) -> Task<Result<Entity<Terminal>>>
 738        + 'static,
 739    ) -> Task<Result<WeakEntity<Terminal>>> {
 740        if !is_enabled_in_workspace(workspace, cx) {
 741            return Task::ready(Err(anyhow!(
 742                "terminal not yet supported for remote projects"
 743            )));
 744        }
 745        let project = workspace.project().downgrade();
 746        cx.spawn_in(window, async move |workspace, cx| {
 747            let terminal = project.update(cx, create_terminal)?.await?;
 748
 749            workspace.update_in(cx, |workspace, window, cx| {
 750                let terminal_view = cx.new(|cx| {
 751                    TerminalView::new(
 752                        terminal.clone(),
 753                        workspace.weak_handle(),
 754                        workspace.database_id(),
 755                        workspace.project().downgrade(),
 756                        window,
 757                        cx,
 758                    )
 759                });
 760                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 761            })?;
 762            Ok(terminal.downgrade())
 763        })
 764    }
 765
 766    pub fn add_terminal_task(
 767        &mut self,
 768        task: SpawnInTerminal,
 769        reveal_strategy: RevealStrategy,
 770        window: &mut Window,
 771        cx: &mut Context<Self>,
 772    ) -> Task<Result<WeakEntity<Terminal>>> {
 773        let workspace = self.workspace.clone();
 774        cx.spawn_in(window, async move |terminal_panel, cx| {
 775            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 776                anyhow::bail!("terminal not yet supported for remote projects");
 777            }
 778            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 779                terminal_panel.pending_terminals_to_add += 1;
 780                terminal_panel.active_pane.clone()
 781            })?;
 782            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 783            let terminal = project
 784                .update(cx, |project, cx| project.create_terminal_task(task, cx))
 785                .await?;
 786            let result = workspace.update_in(cx, |workspace, window, cx| {
 787                let terminal_view = Box::new(cx.new(|cx| {
 788                    TerminalView::new(
 789                        terminal.clone(),
 790                        workspace.weak_handle(),
 791                        workspace.database_id(),
 792                        workspace.project().downgrade(),
 793                        window,
 794                        cx,
 795                    )
 796                }));
 797
 798                match reveal_strategy {
 799                    RevealStrategy::Always => {
 800                        workspace.focus_panel::<Self>(window, cx);
 801                    }
 802                    RevealStrategy::NoFocus => {
 803                        workspace.open_panel::<Self>(window, cx);
 804                    }
 805                    RevealStrategy::Never => {}
 806                }
 807
 808                pane.update(cx, |pane, cx| {
 809                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
 810                    pane.add_item(terminal_view, true, focus, None, window, cx);
 811                });
 812
 813                Ok(terminal.downgrade())
 814            })?;
 815            terminal_panel.update(cx, |terminal_panel, cx| {
 816                terminal_panel.pending_terminals_to_add =
 817                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 818                terminal_panel.serialize(cx)
 819            })?;
 820            result
 821        })
 822    }
 823
 824    fn add_terminal_shell(
 825        &mut self,
 826        cwd: Option<PathBuf>,
 827        reveal_strategy: RevealStrategy,
 828        window: &mut Window,
 829        cx: &mut Context<Self>,
 830    ) -> Task<Result<WeakEntity<Terminal>>> {
 831        self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx)
 832    }
 833
 834    fn add_local_terminal_shell(
 835        &mut self,
 836        reveal_strategy: RevealStrategy,
 837        window: &mut Window,
 838        cx: &mut Context<Self>,
 839    ) -> Task<Result<WeakEntity<Terminal>>> {
 840        self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx)
 841    }
 842
 843    fn add_terminal_shell_internal(
 844        &mut self,
 845        force_local: bool,
 846        cwd: Option<PathBuf>,
 847        reveal_strategy: RevealStrategy,
 848        window: &mut Window,
 849        cx: &mut Context<Self>,
 850    ) -> Task<Result<WeakEntity<Terminal>>> {
 851        let workspace = self.workspace.clone();
 852
 853        cx.spawn_in(window, async move |terminal_panel, cx| {
 854            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 855                anyhow::bail!("terminal not yet supported for collaborative projects");
 856            }
 857            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 858                terminal_panel.pending_terminals_to_add += 1;
 859                terminal_panel.active_pane.clone()
 860            })?;
 861            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 862            let terminal = if force_local {
 863                project
 864                    .update(cx, |project, cx| project.create_local_terminal(cx))
 865                    .await
 866            } else {
 867                project
 868                    .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))
 869                    .await
 870            };
 871
 872            match terminal {
 873                Ok(terminal) => {
 874                    let result = workspace.update_in(cx, |workspace, window, cx| {
 875                        let terminal_view = Box::new(cx.new(|cx| {
 876                            TerminalView::new(
 877                                terminal.clone(),
 878                                workspace.weak_handle(),
 879                                workspace.database_id(),
 880                                workspace.project().downgrade(),
 881                                window,
 882                                cx,
 883                            )
 884                        }));
 885
 886                        match reveal_strategy {
 887                            RevealStrategy::Always => {
 888                                workspace.focus_panel::<Self>(window, cx);
 889                            }
 890                            RevealStrategy::NoFocus => {
 891                                workspace.open_panel::<Self>(window, cx);
 892                            }
 893                            RevealStrategy::Never => {}
 894                        }
 895
 896                        pane.update(cx, |pane, cx| {
 897                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
 898                            pane.add_item(terminal_view, true, focus, None, window, cx);
 899                        });
 900
 901                        Ok(terminal.downgrade())
 902                    })?;
 903                    terminal_panel.update(cx, |terminal_panel, cx| {
 904                        terminal_panel.pending_terminals_to_add =
 905                            terminal_panel.pending_terminals_to_add.saturating_sub(1);
 906                        terminal_panel.serialize(cx)
 907                    })?;
 908                    result
 909                }
 910                Err(error) => {
 911                    pane.update_in(cx, |pane, window, cx| {
 912                        let focus = pane.has_focus(window, cx);
 913                        let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
 914                            error: error.to_string(),
 915                            focus_handle: cx.focus_handle(),
 916                        });
 917                        pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
 918                    })?;
 919                    Err(error)
 920                }
 921            }
 922        })
 923    }
 924
 925    fn serialize(&mut self, cx: &mut Context<Self>) {
 926        let height = self.height;
 927        let width = self.width;
 928        let Some(serialization_key) = self
 929            .workspace
 930            .read_with(cx, |workspace, _| {
 931                TerminalPanel::serialization_key(workspace)
 932            })
 933            .ok()
 934            .flatten()
 935        else {
 936            return;
 937        };
 938        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
 939            cx.background_executor()
 940                .timer(Duration::from_millis(50))
 941                .await;
 942            let terminal_panel = terminal_panel.upgrade()?;
 943            let items = terminal_panel.update(cx, |terminal_panel, cx| {
 944                SerializedItems::WithSplits(serialize_pane_group(
 945                    &terminal_panel.center,
 946                    &terminal_panel.active_pane,
 947                    cx,
 948                ))
 949            });
 950            cx.background_spawn(
 951                async move {
 952                    KEY_VALUE_STORE
 953                        .write_kvp(
 954                            serialization_key,
 955                            serde_json::to_string(&SerializedTerminalPanel {
 956                                items,
 957                                active_item_id: None,
 958                                height,
 959                                width,
 960                            })?,
 961                        )
 962                        .await?;
 963                    anyhow::Ok(())
 964                }
 965                .log_err(),
 966            )
 967            .await;
 968            Some(())
 969        });
 970    }
 971
 972    fn replace_terminal(
 973        &self,
 974        spawn_task: SpawnInTerminal,
 975        task_pane: Entity<Pane>,
 976        terminal_item_index: usize,
 977        terminal_to_replace: Entity<TerminalView>,
 978        window: &mut Window,
 979        cx: &mut Context<Self>,
 980    ) -> Task<Result<WeakEntity<Terminal>>> {
 981        let reveal = spawn_task.reveal;
 982        let task_workspace = self.workspace.clone();
 983        cx.spawn_in(window, async move |terminal_panel, cx| {
 984            let project = terminal_panel.update(cx, |this, cx| {
 985                this.workspace
 986                    .update(cx, |workspace, _| workspace.project().clone())
 987            })??;
 988            let new_terminal = project
 989                .update(cx, |project, cx| {
 990                    project.create_terminal_task(spawn_task, cx)
 991                })
 992                .await?;
 993            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
 994                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
 995            })?;
 996
 997            let reveal_target = terminal_panel.update(cx, |panel, _| {
 998                if panel.center.panes().iter().any(|p| **p == task_pane) {
 999                    RevealTarget::Dock
1000                } else {
1001                    RevealTarget::Center
1002                }
1003            })?;
1004
1005            match reveal {
1006                RevealStrategy::Always => match reveal_target {
1007                    RevealTarget::Center => {
1008                        task_workspace.update_in(cx, |workspace, window, cx| {
1009                            let did_activate = workspace.activate_item(
1010                                &terminal_to_replace,
1011                                true,
1012                                true,
1013                                window,
1014                                cx,
1015                            );
1016
1017                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
1018
1019                            anyhow::Ok(())
1020                        })??;
1021                    }
1022                    RevealTarget::Dock => {
1023                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1024                            terminal_panel.activate_terminal_view(
1025                                &task_pane,
1026                                terminal_item_index,
1027                                true,
1028                                window,
1029                                cx,
1030                            )
1031                        })?;
1032
1033                        cx.spawn(async move |cx| {
1034                            task_workspace
1035                                .update_in(cx, |workspace, window, cx| {
1036                                    workspace.focus_panel::<Self>(window, cx)
1037                                })
1038                                .ok()
1039                        })
1040                        .detach();
1041                    }
1042                },
1043                RevealStrategy::NoFocus => match reveal_target {
1044                    RevealTarget::Center => {
1045                        task_workspace.update_in(cx, |workspace, window, cx| {
1046                            workspace.active_pane().focus_handle(cx).focus(window, cx);
1047                        })?;
1048                    }
1049                    RevealTarget::Dock => {
1050                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1051                            terminal_panel.activate_terminal_view(
1052                                &task_pane,
1053                                terminal_item_index,
1054                                false,
1055                                window,
1056                                cx,
1057                            )
1058                        })?;
1059
1060                        cx.spawn(async move |cx| {
1061                            task_workspace
1062                                .update_in(cx, |workspace, window, cx| {
1063                                    workspace.open_panel::<Self>(window, cx)
1064                                })
1065                                .ok()
1066                        })
1067                        .detach();
1068                    }
1069                },
1070                RevealStrategy::Never => {}
1071            }
1072
1073            Ok(new_terminal.downgrade())
1074        })
1075    }
1076
1077    fn has_no_terminals(&self, cx: &App) -> bool {
1078        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1079    }
1080
1081    pub fn assistant_enabled(&self) -> bool {
1082        self.assistant_enabled
1083    }
1084
1085    /// Returns all panes in the terminal panel.
1086    pub fn panes(&self) -> Vec<&Entity<Pane>> {
1087        self.center.panes()
1088    }
1089
1090    /// Returns all non-empty terminal selections from all terminal views in all panes.
1091    pub fn terminal_selections(&self, cx: &App) -> Vec<String> {
1092        self.center
1093            .panes()
1094            .iter()
1095            .flat_map(|pane| {
1096                pane.read(cx).items().filter_map(|item| {
1097                    let terminal_view = item.downcast::<crate::TerminalView>()?;
1098                    terminal_view
1099                        .read(cx)
1100                        .terminal()
1101                        .read(cx)
1102                        .last_content
1103                        .selection_text
1104                        .clone()
1105                        .filter(|text| !text.is_empty())
1106                })
1107            })
1108            .collect()
1109    }
1110
1111    fn is_enabled(&self, cx: &App) -> bool {
1112        self.workspace
1113            .upgrade()
1114            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1115    }
1116
1117    fn activate_pane_in_direction(
1118        &mut self,
1119        direction: SplitDirection,
1120        window: &mut Window,
1121        cx: &mut Context<Self>,
1122    ) {
1123        if let Some(pane) = self
1124            .center
1125            .find_pane_in_direction(&self.active_pane, direction, cx)
1126        {
1127            window.focus(&pane.focus_handle(cx), cx);
1128        } else {
1129            self.workspace
1130                .update(cx, |workspace, cx| {
1131                    workspace.activate_pane_in_direction(direction, window, cx)
1132                })
1133                .ok();
1134        }
1135    }
1136
1137    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1138        if let Some(to) = self
1139            .center
1140            .find_pane_in_direction(&self.active_pane, direction, cx)
1141            .cloned()
1142        {
1143            self.center.swap(&self.active_pane, &to, cx);
1144            cx.notify();
1145        }
1146    }
1147
1148    fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1149        if self
1150            .center
1151            .move_to_border(&self.active_pane, direction, cx)
1152            .unwrap()
1153        {
1154            cx.notify();
1155        }
1156    }
1157}
1158
1159/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
1160/// based on the shell configuration. This is a pure function that can be tested
1161/// without spawning actual terminals.
1162pub fn prepare_task_for_spawn(
1163    task: &SpawnInTerminal,
1164    shell: &Shell,
1165    is_windows: bool,
1166) -> SpawnInTerminal {
1167    let builder = ShellBuilder::new(shell, is_windows);
1168    let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
1169    let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
1170
1171    SpawnInTerminal {
1172        command_label,
1173        command: Some(command),
1174        args,
1175        ..task.clone()
1176    }
1177}
1178
1179fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1180    workspace.project().read(cx).supports_terminal(cx)
1181}
1182
1183pub fn new_terminal_pane(
1184    workspace: WeakEntity<Workspace>,
1185    project: Entity<Project>,
1186    zoomed: bool,
1187    window: &mut Window,
1188    cx: &mut Context<TerminalPanel>,
1189) -> Entity<Pane> {
1190    let is_local = project.read(cx).is_local();
1191    let terminal_panel = cx.entity();
1192    let pane = cx.new(|cx| {
1193        let mut pane = Pane::new(
1194            workspace.clone(),
1195            project.clone(),
1196            Default::default(),
1197            None,
1198            workspace::NewTerminal::default().boxed_clone(),
1199            false,
1200            window,
1201            cx,
1202        );
1203        pane.set_zoomed(zoomed, cx);
1204        pane.set_can_navigate(false, cx);
1205        pane.display_nav_history_buttons(None);
1206        pane.set_should_display_tab_bar(|_, _| true);
1207        pane.set_zoom_out_on_close(false);
1208
1209        let split_closure_terminal_panel = terminal_panel.downgrade();
1210        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1211            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1212                let is_current_pane = tab.pane == cx.entity();
1213                let Some(can_drag_away) = split_closure_terminal_panel
1214                    .read_with(cx, |terminal_panel, _| {
1215                        let current_panes = terminal_panel.center.panes();
1216                        !current_panes.contains(&&tab.pane)
1217                            || current_panes.len() > 1
1218                            || (!is_current_pane || pane.items_len() > 1)
1219                    })
1220                    .ok()
1221                else {
1222                    return false;
1223                };
1224                if can_drag_away {
1225                    let item = if is_current_pane {
1226                        pane.item_for_index(tab.ix)
1227                    } else {
1228                        tab.pane.read(cx).item_for_index(tab.ix)
1229                    };
1230                    if let Some(item) = item {
1231                        return item.downcast::<TerminalView>().is_some();
1232                    }
1233                }
1234            }
1235            false
1236        })));
1237
1238        let toolbar = pane.toolbar().clone();
1239        if let Some(callbacks) = cx.try_global::<workspace::PaneSearchBarCallbacks>() {
1240            let languages = Some(project.read(cx).languages().clone());
1241            (callbacks.setup_search_bar)(languages, &toolbar, window, cx);
1242        }
1243        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1244        toolbar.update(cx, |toolbar, cx| {
1245            toolbar.add_item(breadcrumbs, window, cx);
1246        });
1247
1248        let drop_closure_project = project.downgrade();
1249        let drop_closure_terminal_panel = terminal_panel.downgrade();
1250        pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
1251            let Some(project) = drop_closure_project.upgrade() else {
1252                return ControlFlow::Break(());
1253            };
1254            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
1255                let this_pane = cx.entity();
1256                let item = if tab.pane == this_pane {
1257                    pane.item_for_index(tab.ix)
1258                } else {
1259                    tab.pane.read(cx).item_for_index(tab.ix)
1260                };
1261                if let Some(item) = item {
1262                    if item.downcast::<TerminalView>().is_some() {
1263                        let source = tab.pane.clone();
1264                        let item_id_to_move = item.item_id();
1265
1266                        // If no split direction, let the regular pane drop handler take care of it
1267                        let Some(split_direction) = pane.drag_split_direction() else {
1268                            return ControlFlow::Continue(());
1269                        };
1270
1271                        // Gather data synchronously before deferring
1272                        let is_zoomed = drop_closure_terminal_panel
1273                            .upgrade()
1274                            .map(|terminal_panel| {
1275                                let terminal_panel = terminal_panel.read(cx);
1276                                if terminal_panel.active_pane == this_pane {
1277                                    pane.is_zoomed()
1278                                } else {
1279                                    terminal_panel.active_pane.read(cx).is_zoomed()
1280                                }
1281                            })
1282                            .unwrap_or(false);
1283
1284                        let workspace = workspace.clone();
1285                        let terminal_panel = drop_closure_terminal_panel.clone();
1286
1287                        // Defer the split operation to avoid re-entrancy panic.
1288                        // The pane may be the one currently being updated, so we cannot
1289                        // call mark_positions (via split) synchronously.
1290                        cx.spawn_in(window, async move |_, cx| {
1291                            cx.update(|window, cx| {
1292                                let Ok(new_pane) =
1293                                    terminal_panel.update(cx, |terminal_panel, cx| {
1294                                        let new_pane = new_terminal_pane(
1295                                            workspace, project, is_zoomed, window, cx,
1296                                        );
1297                                        terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
1298                                        terminal_panel.center.split(
1299                                            &this_pane,
1300                                            &new_pane,
1301                                            split_direction,
1302                                            cx,
1303                                        );
1304                                        new_pane
1305                                    })
1306                                else {
1307                                    return;
1308                                };
1309
1310                                move_item(
1311                                    &source,
1312                                    &new_pane,
1313                                    item_id_to_move,
1314                                    new_pane.read(cx).active_item_index(),
1315                                    true,
1316                                    window,
1317                                    cx,
1318                                );
1319                            })
1320                            .ok();
1321                        })
1322                        .detach();
1323                    } else if let Some(project_path) = item.project_path(cx)
1324                        && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1325                    {
1326                        add_paths_to_terminal(pane, &[entry_path], window, cx);
1327                    }
1328                }
1329            } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1330                let project = project.read(cx);
1331                let paths_to_add = selection
1332                    .items()
1333                    .map(|selected_entry| selected_entry.entry_id)
1334                    .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1335                    .filter_map(|project_path| project.absolute_path(&project_path, cx))
1336                    .collect::<Vec<_>>();
1337                if !paths_to_add.is_empty() {
1338                    add_paths_to_terminal(pane, &paths_to_add, window, cx);
1339                }
1340            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1341                if let Some(entry_path) = project
1342                    .read(cx)
1343                    .path_for_entry(entry_id, cx)
1344                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1345                {
1346                    add_paths_to_terminal(pane, &[entry_path], window, cx);
1347                }
1348            } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1349                add_paths_to_terminal(pane, paths.paths(), window, cx);
1350            }
1351
1352            ControlFlow::Break(())
1353        });
1354
1355        pane
1356    });
1357
1358    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1359        .detach();
1360    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1361
1362    pane
1363}
1364
1365async fn wait_for_terminals_tasks(
1366    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1367    cx: &mut AsyncApp,
1368) {
1369    let pending_tasks = terminals_for_task.iter().map(|(_, _, terminal)| {
1370        terminal.update(cx, |terminal_view, cx| {
1371            terminal_view
1372                .terminal()
1373                .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1374        })
1375    });
1376    join_all(pending_tasks).await;
1377}
1378
1379fn add_paths_to_terminal(
1380    pane: &mut Pane,
1381    paths: &[PathBuf],
1382    window: &mut Window,
1383    cx: &mut Context<Pane>,
1384) {
1385    if let Some(terminal_view) = pane
1386        .active_item()
1387        .and_then(|item| item.downcast::<TerminalView>())
1388    {
1389        window.focus(&terminal_view.focus_handle(cx), cx);
1390        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1391        new_text.push(' ');
1392        terminal_view.update(cx, |terminal_view, cx| {
1393            terminal_view.terminal().update(cx, |terminal, _| {
1394                terminal.paste(&new_text);
1395            });
1396        });
1397    }
1398}
1399
1400struct FailedToSpawnTerminal {
1401    error: String,
1402    focus_handle: FocusHandle,
1403}
1404
1405impl Focusable for FailedToSpawnTerminal {
1406    fn focus_handle(&self, _: &App) -> FocusHandle {
1407        self.focus_handle.clone()
1408    }
1409}
1410
1411impl Render for FailedToSpawnTerminal {
1412    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1413        let popover_menu = PopoverMenu::new("settings-popover")
1414            .trigger(
1415                IconButton::new("icon-button-popover", IconName::ChevronDown)
1416                    .icon_size(IconSize::XSmall),
1417            )
1418            .menu(move |window, cx| {
1419                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1420                    context_menu
1421                        .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1422                        .action(
1423                            "Edit settings.json",
1424                            zed_actions::OpenSettingsFile.boxed_clone(),
1425                        )
1426                }))
1427            })
1428            .anchor(Corner::TopRight)
1429            .offset(gpui::Point {
1430                x: px(0.0),
1431                y: px(2.0),
1432            });
1433
1434        v_flex()
1435            .track_focus(&self.focus_handle)
1436            .size_full()
1437            .p_4()
1438            .items_center()
1439            .justify_center()
1440            .bg(cx.theme().colors().editor_background)
1441            .child(
1442                v_flex()
1443                    .max_w_112()
1444                    .items_center()
1445                    .justify_center()
1446                    .text_center()
1447                    .child(Label::new("Failed to spawn terminal"))
1448                    .child(
1449                        Label::new(self.error.to_string())
1450                            .size(LabelSize::Small)
1451                            .color(Color::Muted)
1452                            .mb_4(),
1453                    )
1454                    .child(SplitButton::new(
1455                        ButtonLike::new("open-settings-ui")
1456                            .child(Label::new("Edit Settings").size(LabelSize::Small))
1457                            .on_click(|_, window, cx| {
1458                                window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1459                            }),
1460                        popover_menu.into_any_element(),
1461                    )),
1462            )
1463    }
1464}
1465
1466impl EventEmitter<()> for FailedToSpawnTerminal {}
1467
1468impl workspace::Item for FailedToSpawnTerminal {
1469    type Event = ();
1470
1471    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1472        SharedString::new_static("Failed to spawn terminal")
1473    }
1474}
1475
1476impl EventEmitter<PanelEvent> for TerminalPanel {}
1477
1478impl Render for TerminalPanel {
1479    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1480        let registrar = cx
1481            .try_global::<workspace::PaneSearchBarCallbacks>()
1482            .map(|callbacks| {
1483                (callbacks.wrap_div_with_search_actions)(div(), self.active_pane.clone())
1484            })
1485            .unwrap_or_else(div);
1486        self.workspace
1487            .update(cx, |workspace, cx| {
1488                registrar.size_full().child(self.center.render(
1489                    workspace.zoomed_item(),
1490                    &workspace::PaneRenderContext {
1491                        follower_states: &HashMap::default(),
1492                        active_call: workspace.active_call(),
1493                        active_pane: &self.active_pane,
1494                        app_state: workspace.app_state(),
1495                        project: workspace.project(),
1496                        workspace: &workspace.weak_handle(),
1497                    },
1498                    window,
1499                    cx,
1500                ))
1501            })
1502            .ok()
1503            .map(|div| {
1504                div.on_action({
1505                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1506                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1507                    })
1508                })
1509                .on_action({
1510                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1511                        terminal_panel.activate_pane_in_direction(
1512                            SplitDirection::Right,
1513                            window,
1514                            cx,
1515                        );
1516                    })
1517                })
1518                .on_action({
1519                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1520                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1521                    })
1522                })
1523                .on_action({
1524                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1525                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1526                    })
1527                })
1528                .on_action(
1529                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1530                        let panes = terminal_panel.center.panes();
1531                        if let Some(ix) = panes
1532                            .iter()
1533                            .position(|pane| **pane == terminal_panel.active_pane)
1534                        {
1535                            let next_ix = (ix + 1) % panes.len();
1536                            window.focus(&panes[next_ix].focus_handle(cx), cx);
1537                        }
1538                    }),
1539                )
1540                .on_action(cx.listener(
1541                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1542                        let panes = terminal_panel.center.panes();
1543                        if let Some(ix) = panes
1544                            .iter()
1545                            .position(|pane| **pane == terminal_panel.active_pane)
1546                        {
1547                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1548                            window.focus(&panes[prev_ix].focus_handle(cx), cx);
1549                        }
1550                    },
1551                ))
1552                .on_action(
1553                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1554                        let panes = terminal_panel.center.panes();
1555                        if let Some(&pane) = panes.get(action.0) {
1556                            window.focus(&pane.read(cx).focus_handle(cx), cx);
1557                        } else {
1558                            let future =
1559                                terminal_panel.new_pane_with_active_terminal(true, window, cx);
1560                            cx.spawn_in(window, async move |terminal_panel, cx| {
1561                                if let Some(new_pane) = future.await {
1562                                    _ = terminal_panel.update_in(
1563                                        cx,
1564                                        |terminal_panel, window, cx| {
1565                                            terminal_panel.center.split(
1566                                                &terminal_panel.active_pane,
1567                                                &new_pane,
1568                                                SplitDirection::Right,
1569                                                cx,
1570                                            );
1571                                            let new_pane = new_pane.read(cx);
1572                                            window.focus(&new_pane.focus_handle(cx), cx);
1573                                        },
1574                                    );
1575                                }
1576                            })
1577                            .detach();
1578                        }
1579                    }),
1580                )
1581                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1582                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1583                }))
1584                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1585                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1586                }))
1587                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1588                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1589                }))
1590                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1591                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1592                }))
1593                .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1594                    terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1595                }))
1596                .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1597                    terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1598                }))
1599                .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1600                    terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1601                }))
1602                .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1603                    terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1604                }))
1605                .on_action(
1606                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1607                        let Some(&target_pane) =
1608                            terminal_panel.center.panes().get(action.destination)
1609                        else {
1610                            return;
1611                        };
1612                        move_active_item(
1613                            &terminal_panel.active_pane,
1614                            target_pane,
1615                            action.focus,
1616                            true,
1617                            window,
1618                            cx,
1619                        );
1620                    }),
1621                )
1622                .on_action(cx.listener(
1623                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1624                        let source_pane = &terminal_panel.active_pane;
1625                        if let Some(destination_pane) = terminal_panel
1626                            .center
1627                            .find_pane_in_direction(source_pane, action.direction, cx)
1628                        {
1629                            move_active_item(
1630                                source_pane,
1631                                destination_pane,
1632                                action.focus,
1633                                true,
1634                                window,
1635                                cx,
1636                            );
1637                        };
1638                    },
1639                ))
1640            })
1641            .unwrap_or_else(|| div())
1642    }
1643}
1644
1645impl Focusable for TerminalPanel {
1646    fn focus_handle(&self, cx: &App) -> FocusHandle {
1647        self.active_pane.focus_handle(cx)
1648    }
1649}
1650
1651impl Panel for TerminalPanel {
1652    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1653        match TerminalSettings::get_global(cx).dock {
1654            TerminalDockPosition::Left => DockPosition::Left,
1655            TerminalDockPosition::Bottom => DockPosition::Bottom,
1656            TerminalDockPosition::Right => DockPosition::Right,
1657        }
1658    }
1659
1660    fn position_is_valid(&self, _: DockPosition) -> bool {
1661        true
1662    }
1663
1664    fn set_position(
1665        &mut self,
1666        position: DockPosition,
1667        _window: &mut Window,
1668        cx: &mut Context<Self>,
1669    ) {
1670        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1671            let dock = match position {
1672                DockPosition::Left => TerminalDockPosition::Left,
1673                DockPosition::Bottom => TerminalDockPosition::Bottom,
1674                DockPosition::Right => TerminalDockPosition::Right,
1675            };
1676            settings.terminal.get_or_insert_default().dock = Some(dock);
1677        });
1678    }
1679
1680    fn size(&self, window: &Window, cx: &App) -> Pixels {
1681        let settings = TerminalSettings::get_global(cx);
1682        match self.position(window, cx) {
1683            DockPosition::Left | DockPosition::Right => {
1684                self.width.unwrap_or(settings.default_width)
1685            }
1686            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1687        }
1688    }
1689
1690    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1691        match self.position(window, cx) {
1692            DockPosition::Left | DockPosition::Right => self.width = size,
1693            DockPosition::Bottom => self.height = size,
1694        }
1695        cx.notify();
1696        cx.defer_in(window, |this, _, cx| {
1697            this.serialize(cx);
1698        })
1699    }
1700
1701    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1702        self.active_pane.read(cx).is_zoomed()
1703    }
1704
1705    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1706        for pane in self.center.panes() {
1707            pane.update(cx, |pane, cx| {
1708                pane.set_zoomed(zoomed, cx);
1709            })
1710        }
1711        cx.notify();
1712    }
1713
1714    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1715        let old_active = self.active;
1716        self.active = active;
1717        if !active || old_active == active || !self.has_no_terminals(cx) {
1718            return;
1719        }
1720        cx.defer_in(window, |this, window, cx| {
1721            let Ok(kind) = this
1722                .workspace
1723                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1724            else {
1725                return;
1726            };
1727
1728            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1729                .detach_and_log_err(cx)
1730        })
1731    }
1732
1733    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1734        let count = self
1735            .center
1736            .panes()
1737            .into_iter()
1738            .map(|pane| pane.read(cx).items_len())
1739            .sum::<usize>();
1740        if count == 0 {
1741            None
1742        } else {
1743            Some(count.to_string())
1744        }
1745    }
1746
1747    fn persistent_name() -> &'static str {
1748        "TerminalPanel"
1749    }
1750
1751    fn panel_key() -> &'static str {
1752        TERMINAL_PANEL_KEY
1753    }
1754
1755    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1756        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1757            && TerminalSettings::get_global(cx).button
1758        {
1759            Some(IconName::TerminalAlt)
1760        } else {
1761            None
1762        }
1763    }
1764
1765    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1766        Some("Terminal Panel")
1767    }
1768
1769    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1770        Box::new(ToggleFocus)
1771    }
1772
1773    fn pane(&self) -> Option<Entity<Pane>> {
1774        Some(self.active_pane.clone())
1775    }
1776
1777    fn activation_priority(&self) -> u32 {
1778        1
1779    }
1780}
1781
1782struct TerminalProvider(Entity<TerminalPanel>);
1783
1784impl workspace::TerminalProvider for TerminalProvider {
1785    fn spawn(
1786        &self,
1787        task: SpawnInTerminal,
1788        window: &mut Window,
1789        cx: &mut App,
1790    ) -> Task<Option<Result<ExitStatus>>> {
1791        let terminal_panel = self.0.clone();
1792        window.spawn(cx, async move |cx| {
1793            let terminal = terminal_panel
1794                .update_in(cx, |terminal_panel, window, cx| {
1795                    terminal_panel.spawn_task(&task, window, cx)
1796                })
1797                .ok()?
1798                .await;
1799            match terminal {
1800                Ok(terminal) => {
1801                    let exit_status = terminal
1802                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1803                        .ok()?
1804                        .await?;
1805                    Some(Ok(exit_status))
1806                }
1807                Err(e) => Some(Err(e)),
1808            }
1809        })
1810    }
1811}
1812
1813struct InlineAssistTabBarButton {
1814    focus_handle: FocusHandle,
1815}
1816
1817impl Render for InlineAssistTabBarButton {
1818    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1819        let focus_handle = self.focus_handle.clone();
1820        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1821            .icon_size(IconSize::Small)
1822            .on_click(cx.listener(|_, _, window, cx| {
1823                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1824            }))
1825            .tooltip(move |_window, cx| {
1826                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1827            })
1828    }
1829}
1830
1831#[cfg(test)]
1832mod tests {
1833    use std::num::NonZero;
1834
1835    use super::*;
1836    use gpui::{TestAppContext, UpdateGlobal as _};
1837    use pretty_assertions::assert_eq;
1838    use project::FakeFs;
1839    use settings::SettingsStore;
1840    use workspace::MultiWorkspace;
1841
1842    #[test]
1843    fn test_prepare_empty_task() {
1844        let input = SpawnInTerminal::default();
1845        let shell = Shell::System;
1846
1847        let result = prepare_task_for_spawn(&input, &shell, false);
1848
1849        let expected_shell = util::get_system_shell();
1850        assert_eq!(result.env, HashMap::default());
1851        assert_eq!(result.cwd, None);
1852        assert_eq!(result.shell, Shell::System);
1853        assert_eq!(
1854            result.command,
1855            Some(expected_shell.clone()),
1856            "Empty tasks should spawn a -i shell"
1857        );
1858        assert_eq!(result.args, Vec::<String>::new());
1859        assert_eq!(
1860            result.command_label, expected_shell,
1861            "We show the shell launch for empty commands"
1862        );
1863    }
1864
1865    #[gpui::test]
1866    async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1867        cx.executor().allow_parking();
1868        init_test(cx);
1869
1870        let fs = FakeFs::new(cx.executor());
1871        let project = Project::test(fs, [], cx).await;
1872        let window_handle =
1873            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1874
1875        let terminal_panel = window_handle
1876            .update(cx, |multi_workspace, window, cx| {
1877                multi_workspace.workspace().update(cx, |workspace, cx| {
1878                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1879                })
1880            })
1881            .unwrap();
1882
1883        set_max_tabs(cx, Some(3));
1884
1885        for _ in 0..5 {
1886            let task = window_handle
1887                .update(cx, |_, window, cx| {
1888                    terminal_panel.update(cx, |panel, cx| {
1889                        panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1890                    })
1891                })
1892                .unwrap();
1893            task.await.unwrap();
1894        }
1895
1896        cx.run_until_parked();
1897
1898        let item_count =
1899            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1900
1901        assert_eq!(
1902            item_count, 5,
1903            "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1904        );
1905    }
1906
1907    #[cfg(unix)]
1908    #[test]
1909    fn test_prepare_script_like_task() {
1910        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();
1911        let expected_cwd = PathBuf::from("/some/work");
1912
1913        let input = SpawnInTerminal {
1914            command: Some(user_command.clone()),
1915            cwd: Some(expected_cwd.clone()),
1916            ..SpawnInTerminal::default()
1917        };
1918        let shell = Shell::System;
1919
1920        let result = prepare_task_for_spawn(&input, &shell, false);
1921
1922        let system_shell = util::get_system_shell();
1923        assert_eq!(result.env, HashMap::default());
1924        assert_eq!(result.cwd, Some(expected_cwd));
1925        assert_eq!(result.shell, Shell::System);
1926        assert_eq!(result.command, Some(system_shell.clone()));
1927        assert_eq!(
1928            result.args,
1929            vec!["-i".to_string(), "-c".to_string(), user_command.clone()],
1930            "User command should have been moved into the arguments, as we're spawning a new -i shell",
1931        );
1932        assert_eq!(
1933            result.command_label,
1934            format!(
1935                "{system_shell} {interactive}-c '{user_command}'",
1936                interactive = if cfg!(windows) { "" } else { "-i " }
1937            ),
1938            "We want to show to the user the entire command spawned"
1939        );
1940    }
1941
1942    #[gpui::test]
1943    async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1944        cx.executor().allow_parking();
1945        init_test(cx);
1946
1947        cx.update(|cx| {
1948            SettingsStore::update_global(cx, |store, cx| {
1949                store.update_user_settings(cx, |settings| {
1950                    settings.terminal.get_or_insert_default().project.shell =
1951                        Some(settings::Shell::Program("__nonexistent_shell__".to_owned()));
1952                });
1953            });
1954        });
1955
1956        let fs = FakeFs::new(cx.executor());
1957        let project = Project::test(fs, [], cx).await;
1958        let window_handle =
1959            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1960
1961        let terminal_panel = window_handle
1962            .update(cx, |multi_workspace, window, cx| {
1963                multi_workspace.workspace().update(cx, |workspace, cx| {
1964                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1965                })
1966            })
1967            .unwrap();
1968
1969        window_handle
1970            .update(cx, |_, window, cx| {
1971                terminal_panel.update(cx, |terminal_panel, cx| {
1972                    terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1973                })
1974            })
1975            .unwrap()
1976            .await
1977            .unwrap_err();
1978
1979        window_handle
1980            .update(cx, |_, _, cx| {
1981                terminal_panel.update(cx, |terminal_panel, cx| {
1982                    assert!(
1983                        terminal_panel
1984                            .active_pane
1985                            .read(cx)
1986                            .items()
1987                            .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1988                        "should spawn `FailedToSpawnTerminal` pane"
1989                    );
1990                })
1991            })
1992            .unwrap();
1993    }
1994
1995    #[gpui::test]
1996    async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) {
1997        cx.executor().allow_parking();
1998        init_test(cx);
1999
2000        let fs = FakeFs::new(cx.executor());
2001        let project = Project::test(fs, [], cx).await;
2002        let window_handle =
2003            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
2004
2005        let terminal_panel = window_handle
2006            .update(cx, |multi_workspace, window, cx| {
2007                multi_workspace.workspace().update(cx, |workspace, cx| {
2008                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
2009                })
2010            })
2011            .unwrap();
2012
2013        let result = window_handle
2014            .update(cx, |_, window, cx| {
2015                terminal_panel.update(cx, |terminal_panel, cx| {
2016                    terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
2017                })
2018            })
2019            .unwrap()
2020            .await;
2021
2022        assert!(
2023            result.is_ok(),
2024            "local terminal should successfully create in local project"
2025        );
2026    }
2027
2028    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
2029        cx.update_global(|store: &mut SettingsStore, cx| {
2030            store.update_user_settings(cx, |settings| {
2031                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
2032            });
2033        });
2034    }
2035
2036    pub fn init_test(cx: &mut TestAppContext) {
2037        cx.update(|cx| {
2038            let store = SettingsStore::test(cx);
2039            cx.set_global(store);
2040            theme::init(theme::LoadThemes::JustBase, cx);
2041            editor::init(cx);
2042            crate::init(cx);
2043        });
2044    }
2045}