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