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