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