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;
  22use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
  23use terminal::{
  24    Terminal,
  25    terminal_settings::{TerminalDockPosition, TerminalSettings},
  26};
  27use ui::{
  28    ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip,
  29    prelude::*,
  30};
  31use util::{ResultExt, TryFutureExt};
  32use workspace::{
  33    ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
  34    ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
  35    MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft,
  36    SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom,
  37    Workspace,
  38    dock::{DockPosition, Panel, PanelEvent, PanelHandle},
  39    item::SerializableItem,
  40    move_active_item, move_item, pane,
  41    ui::IconName,
  42};
  43
  44use anyhow::{Result, anyhow};
  45use zed_actions::assistant::InlineAssist;
  46
  47const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  48
  49actions!(
  50    terminal_panel,
  51    [
  52        /// Toggles the terminal panel.
  53        Toggle,
  54        /// Toggles focus on the terminal panel.
  55        ToggleFocus
  56    ]
  57);
  58
  59pub fn init(cx: &mut App) {
  60    cx.observe_new(
  61        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
  62            workspace.register_action(TerminalPanel::new_terminal);
  63            workspace.register_action(TerminalPanel::open_terminal);
  64            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  65                if is_enabled_in_workspace(workspace, cx) {
  66                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
  67                }
  68            });
  69            workspace.register_action(|workspace, _: &Toggle, window, cx| {
  70                if is_enabled_in_workspace(workspace, cx) {
  71                    if !workspace.toggle_panel_focus::<TerminalPanel>(window, cx) {
  72                        workspace.close_panel::<TerminalPanel>(window, cx);
  73                    }
  74                }
  75            });
  76        },
  77    )
  78    .detach();
  79}
  80
  81pub struct TerminalPanel {
  82    pub(crate) active_pane: Entity<Pane>,
  83    pub(crate) center: PaneGroup,
  84    fs: Arc<dyn Fs>,
  85    workspace: WeakEntity<Workspace>,
  86    pub(crate) width: Option<Pixels>,
  87    pub(crate) height: Option<Pixels>,
  88    pending_serialization: Task<Option<()>>,
  89    pending_terminals_to_add: usize,
  90    deferred_tasks: HashMap<TaskId, Task<()>>,
  91    assistant_enabled: bool,
  92    assistant_tab_bar_button: Option<AnyView>,
  93    active: bool,
  94}
  95
  96impl TerminalPanel {
  97    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
  98        let project = workspace.project();
  99        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
 100        let center = PaneGroup::new(pane.clone());
 101        let terminal_panel = Self {
 102            center,
 103            active_pane: pane,
 104            fs: workspace.app_state().fs.clone(),
 105            workspace: workspace.weak_handle(),
 106            pending_serialization: Task::ready(None),
 107            width: None,
 108            height: None,
 109            pending_terminals_to_add: 0,
 110            deferred_tasks: HashMap::default(),
 111            assistant_enabled: false,
 112            assistant_tab_bar_button: None,
 113            active: false,
 114        };
 115        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 116        terminal_panel
 117    }
 118
 119    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
 120        self.assistant_enabled = enabled;
 121        if enabled {
 122            let focus_handle = self
 123                .active_pane
 124                .read(cx)
 125                .active_item()
 126                .map(|item| item.item_focus_handle(cx))
 127                .unwrap_or(self.focus_handle(cx));
 128            self.assistant_tab_bar_button = Some(
 129                cx.new(move |_| InlineAssistTabBarButton { focus_handle })
 130                    .into(),
 131            );
 132        } else {
 133            self.assistant_tab_bar_button = None;
 134        }
 135        for pane in self.center.panes() {
 136            self.apply_tab_bar_buttons(pane, cx);
 137        }
 138    }
 139
 140    fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
 141        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 142        terminal_pane.update(cx, |pane, cx| {
 143            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
 144                let split_context = pane
 145                    .active_item()
 146                    .and_then(|item| item.downcast::<TerminalView>())
 147                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
 148                if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
 149                    return (None, None);
 150                }
 151                let focus_handle = pane.focus_handle(cx);
 152                let right_children = h_flex()
 153                    .gap(DynamicSpacing::Base02.rems(cx))
 154                    .child(
 155                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 156                            .trigger_with_tooltip(
 157                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
 158                                Tooltip::text("New…"),
 159                            )
 160                            .anchor(Corner::TopRight)
 161                            .with_handle(pane.new_item_context_menu_handle.clone())
 162                            .menu(move |window, cx| {
 163                                let focus_handle = focus_handle.clone();
 164                                let menu = ContextMenu::build(window, cx, |menu, _, _| {
 165                                    menu.context(focus_handle.clone())
 166                                        .action(
 167                                            "New Terminal",
 168                                            workspace::NewTerminal.boxed_clone(),
 169                                        )
 170                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 171                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 172                                        // context menu will be gone the moment we spawn the modal.
 173                                        .action(
 174                                            "Spawn task",
 175                                            zed_actions::Spawn::modal().boxed_clone(),
 176                                        )
 177                                });
 178
 179                                Some(menu)
 180                            }),
 181                    )
 182                    .children(assistant_tab_bar_button.clone())
 183                    .child(
 184                        PopoverMenu::new("terminal-pane-tab-bar-split")
 185                            .trigger_with_tooltip(
 186                                IconButton::new("terminal-pane-split", IconName::Split)
 187                                    .icon_size(IconSize::Small),
 188                                Tooltip::text("Split Pane"),
 189                            )
 190                            .anchor(Corner::TopRight)
 191                            .with_handle(pane.split_item_context_menu_handle.clone())
 192                            .menu({
 193                                move |window, cx| {
 194                                    ContextMenu::build(window, cx, |menu, _, _| {
 195                                        menu.when_some(
 196                                            split_context.clone(),
 197                                            |menu, split_context| menu.context(split_context),
 198                                        )
 199                                        .action("Split Right", SplitRight.boxed_clone())
 200                                        .action("Split Left", SplitLeft.boxed_clone())
 201                                        .action("Split Up", SplitUp.boxed_clone())
 202                                        .action("Split Down", SplitDown.boxed_clone())
 203                                    })
 204                                    .into()
 205                                }
 206                            }),
 207                    )
 208                    .child({
 209                        let zoomed = pane.is_zoomed();
 210                        IconButton::new("toggle_zoom", IconName::Maximize)
 211                            .icon_size(IconSize::Small)
 212                            .toggle_state(zoomed)
 213                            .selected_icon(IconName::Minimize)
 214                            .on_click(cx.listener(|pane, _, window, cx| {
 215                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
 216                            }))
 217                            .tooltip(move |window, cx| {
 218                                Tooltip::for_action(
 219                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 220                                    &ToggleZoom,
 221                                    window,
 222                                    cx,
 223                                )
 224                            })
 225                    })
 226                    .into_any_element()
 227                    .into();
 228                (None, right_children)
 229            });
 230        });
 231    }
 232
 233    fn serialization_key(workspace: &Workspace) -> Option<String> {
 234        workspace
 235            .database_id()
 236            .map(|id| i64::from(id).to_string())
 237            .or(workspace.session_id())
 238            .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
 239    }
 240
 241    pub async fn load(
 242        workspace: WeakEntity<Workspace>,
 243        mut cx: AsyncWindowContext,
 244    ) -> Result<Entity<Self>> {
 245        let mut terminal_panel = None;
 246
 247        if let Some((database_id, serialization_key)) = workspace
 248            .read_with(&cx, |workspace, _| {
 249                workspace
 250                    .database_id()
 251                    .zip(TerminalPanel::serialization_key(workspace))
 252            })
 253            .ok()
 254            .flatten()
 255            && let Some(serialized_panel) = cx
 256                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
 257                .await
 258                .log_err()
 259                .flatten()
 260                .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 261                .transpose()
 262                .log_err()
 263                .flatten()
 264            && let Ok(serialized) = workspace
 265                .update_in(&mut cx, |workspace, window, cx| {
 266                    deserialize_terminal_panel(
 267                        workspace.weak_handle(),
 268                        workspace.project().clone(),
 269                        database_id,
 270                        serialized_panel,
 271                        window,
 272                        cx,
 273                    )
 274                })?
 275                .await
 276        {
 277            terminal_panel = Some(serialized);
 278        }
 279
 280        let terminal_panel = if let Some(panel) = terminal_panel {
 281            panel
 282        } else {
 283            workspace.update_in(&mut cx, |workspace, window, cx| {
 284                cx.new(|cx| TerminalPanel::new(workspace, window, cx))
 285            })?
 286        };
 287
 288        if let Some(workspace) = workspace.upgrade() {
 289            workspace
 290                .update(&mut cx, |workspace, _| {
 291                    workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
 292                })
 293                .ok();
 294        }
 295
 296        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 297        if let Some(workspace) = workspace.upgrade() {
 298            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
 299                let alive_item_ids = terminal_panel
 300                    .read(cx)
 301                    .center
 302                    .panes()
 303                    .into_iter()
 304                    .flat_map(|pane| pane.read(cx).items())
 305                    .map(|item| item.item_id().as_u64() as ItemId)
 306                    .collect();
 307                workspace.database_id().map(|workspace_id| {
 308                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
 309                })
 310            })?;
 311            if let Some(task) = cleanup_task {
 312                task.await.log_err();
 313            }
 314        }
 315
 316        if let Some(workspace) = workspace.upgrade() {
 317            let should_focus = workspace
 318                .update_in(&mut cx, |workspace, window, cx| {
 319                    workspace.active_item(cx).is_none()
 320                        && workspace
 321                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
 322                })
 323                .unwrap_or(false);
 324
 325            if should_focus {
 326                terminal_panel
 327                    .update_in(&mut cx, |panel, window, cx| {
 328                        panel.active_pane.update(cx, |pane, cx| {
 329                            pane.focus_active_item(window, cx);
 330                        });
 331                    })
 332                    .ok();
 333            }
 334        }
 335        Ok(terminal_panel)
 336    }
 337
 338    fn handle_pane_event(
 339        &mut self,
 340        pane: &Entity<Pane>,
 341        event: &pane::Event,
 342        window: &mut Window,
 343        cx: &mut Context<Self>,
 344    ) {
 345        match event {
 346            pane::Event::ActivateItem { .. } => self.serialize(cx),
 347            pane::Event::RemovedItem { .. } => self.serialize(cx),
 348            pane::Event::Remove { focus_on_pane } => {
 349                let pane_count_before_removal = self.center.panes().len();
 350                let _removal_result = self.center.remove(pane);
 351                if pane_count_before_removal == 1 {
 352                    self.center.first_pane().update(cx, |pane, cx| {
 353                        pane.set_zoomed(false, cx);
 354                    });
 355                    cx.emit(PanelEvent::Close);
 356                } else if let Some(focus_on_pane) =
 357                    focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 358                {
 359                    focus_on_pane.focus_handle(cx).focus(window);
 360                }
 361            }
 362            pane::Event::ZoomIn => {
 363                for pane in self.center.panes() {
 364                    pane.update(cx, |pane, cx| {
 365                        pane.set_zoomed(true, cx);
 366                    })
 367                }
 368                cx.emit(PanelEvent::ZoomIn);
 369                cx.notify();
 370            }
 371            pane::Event::ZoomOut => {
 372                for pane in self.center.panes() {
 373                    pane.update(cx, |pane, cx| {
 374                        pane.set_zoomed(false, cx);
 375                    })
 376                }
 377                cx.emit(PanelEvent::ZoomOut);
 378                cx.notify();
 379            }
 380            pane::Event::AddItem { item } => {
 381                if let Some(workspace) = self.workspace.upgrade() {
 382                    workspace.update(cx, |workspace, cx| {
 383                        item.added_to_pane(workspace, pane.clone(), window, cx)
 384                    })
 385                }
 386                self.serialize(cx);
 387            }
 388            &pane::Event::Split {
 389                direction,
 390                clone_active_item,
 391            } => {
 392                if clone_active_item {
 393                    let fut = self.new_pane_with_cloned_active_terminal(window, cx);
 394                    let pane = pane.clone();
 395                    cx.spawn_in(window, async move |panel, cx| {
 396                        let Some(new_pane) = fut.await else {
 397                            return;
 398                        };
 399                        panel
 400                            .update_in(cx, |panel, window, cx| {
 401                                panel.center.split(&pane, &new_pane, direction).log_err();
 402                                window.focus(&new_pane.focus_handle(cx));
 403                            })
 404                            .ok();
 405                    })
 406                    .detach();
 407                } else {
 408                    let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
 409                    else {
 410                        return;
 411                    };
 412                    let Ok(project) = self
 413                        .workspace
 414                        .update(cx, |workspace, _| workspace.project().clone())
 415                    else {
 416                        return;
 417                    };
 418                    let new_pane =
 419                        new_terminal_pane(self.workspace.clone(), project, false, window, cx);
 420                    new_pane.update(cx, |pane, cx| {
 421                        pane.add_item(item, true, true, None, window, cx);
 422                    });
 423                    self.center.split(&pane, &new_pane, direction).log_err();
 424                    window.focus(&new_pane.focus_handle(cx));
 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_cloned_active_terminal(
 439        &mut self,
 440        window: &mut Window,
 441        cx: &mut Context<Self>,
 442    ) -> Task<Option<Entity<Pane>>> {
 443        let Some(workspace) = self.workspace.upgrade() else {
 444            return Task::ready(None);
 445        };
 446        let workspace = workspace.read(cx);
 447        let database_id = workspace.database_id();
 448        let weak_workspace = self.workspace.clone();
 449        let project = workspace.project().clone();
 450        let active_pane = &self.active_pane;
 451        let terminal_view = active_pane
 452            .read(cx)
 453            .active_item()
 454            .and_then(|item| item.downcast::<TerminalView>());
 455        let working_directory = terminal_view.as_ref().and_then(|terminal_view| {
 456            let terminal = terminal_view.read(cx).terminal().read(cx);
 457            terminal
 458                .working_directory()
 459                .or_else(|| default_working_directory(workspace, cx))
 460        });
 461        let is_zoomed = active_pane.read(cx).is_zoomed();
 462        cx.spawn_in(window, async move |panel, cx| {
 463            let terminal = project
 464                .update(cx, |project, cx| match terminal_view {
 465                    Some(view) => Task::ready(project.clone_terminal(
 466                        &view.read(cx).terminal.clone(),
 467                        cx,
 468                        || working_directory,
 469                    )),
 470                    None => project.create_terminal_shell(working_directory, cx),
 471                })
 472                .ok()?
 473                .await
 474                .log_err()?;
 475
 476            panel
 477                .update_in(cx, move |terminal_panel, window, cx| {
 478                    let terminal_view = Box::new(cx.new(|cx| {
 479                        TerminalView::new(
 480                            terminal.clone(),
 481                            weak_workspace.clone(),
 482                            database_id,
 483                            project.downgrade(),
 484                            window,
 485                            cx,
 486                        )
 487                    }));
 488                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
 489                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 490                    pane.update(cx, |pane, cx| {
 491                        pane.add_item(terminal_view, true, true, None, window, cx);
 492                    });
 493                    Some(pane)
 494                })
 495                .ok()
 496                .flatten()
 497        })
 498    }
 499
 500    pub fn open_terminal(
 501        workspace: &mut Workspace,
 502        action: &workspace::OpenTerminal,
 503        window: &mut Window,
 504        cx: &mut Context<Workspace>,
 505    ) {
 506        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 507            return;
 508        };
 509
 510        terminal_panel
 511            .update(cx, |panel, cx| {
 512                panel.add_terminal_shell(
 513                    Some(action.working_directory.clone()),
 514                    RevealStrategy::Always,
 515                    window,
 516                    cx,
 517                )
 518            })
 519            .detach_and_log_err(cx);
 520    }
 521
 522    pub fn spawn_task(
 523        &mut self,
 524        task: &SpawnInTerminal,
 525        window: &mut Window,
 526        cx: &mut Context<Self>,
 527    ) -> Task<Result<WeakEntity<Terminal>>> {
 528        let remote_client = self
 529            .workspace
 530            .update(cx, |workspace, cx| {
 531                let project = workspace.project().read(cx);
 532                if project.is_via_collab() {
 533                    Err(anyhow!("cannot spawn tasks as a guest"))
 534                } else {
 535                    Ok(project.remote_client())
 536                }
 537            })
 538            .flatten();
 539
 540        let remote_client = match remote_client {
 541            Ok(remote_client) => remote_client,
 542            Err(e) => return Task::ready(Err(e)),
 543        };
 544
 545        let remote_shell = remote_client
 546            .as_ref()
 547            .and_then(|remote_client| remote_client.read(cx).shell());
 548
 549        let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell);
 550        let command_label = builder.command_label(&task.command_label);
 551        let (command, args) = builder.build(task.command.clone(), &task.args);
 552
 553        let task = SpawnInTerminal {
 554            command_label,
 555            command: Some(command),
 556            args,
 557            ..task.clone()
 558        };
 559
 560        if task.allow_concurrent_runs && task.use_new_terminal {
 561            return self.spawn_in_new_terminal(task, window, cx);
 562        }
 563
 564        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
 565        let Some(existing) = terminals_for_task.pop() else {
 566            return self.spawn_in_new_terminal(task, window, cx);
 567        };
 568
 569        let (existing_item_index, task_pane, existing_terminal) = existing;
 570        if task.allow_concurrent_runs {
 571            return self.replace_terminal(
 572                task,
 573                task_pane,
 574                existing_item_index,
 575                existing_terminal,
 576                window,
 577                cx,
 578            );
 579        }
 580
 581        let (tx, rx) = oneshot::channel();
 582
 583        self.deferred_tasks.insert(
 584            task.id.clone(),
 585            cx.spawn_in(window, async move |terminal_panel, cx| {
 586                wait_for_terminals_tasks(terminals_for_task, cx).await;
 587                let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 588                    if task.use_new_terminal {
 589                        terminal_panel.spawn_in_new_terminal(task, window, cx)
 590                    } else {
 591                        terminal_panel.replace_terminal(
 592                            task,
 593                            task_pane,
 594                            existing_item_index,
 595                            existing_terminal,
 596                            window,
 597                            cx,
 598                        )
 599                    }
 600                });
 601                if let Ok(task) = task {
 602                    tx.send(task.await).ok();
 603                }
 604            }),
 605        );
 606
 607        cx.spawn(async move |_, _| rx.await?)
 608    }
 609
 610    fn spawn_in_new_terminal(
 611        &mut self,
 612        spawn_task: SpawnInTerminal,
 613        window: &mut Window,
 614        cx: &mut Context<Self>,
 615    ) -> Task<Result<WeakEntity<Terminal>>> {
 616        let reveal = spawn_task.reveal;
 617        let reveal_target = spawn_task.reveal_target;
 618        match reveal_target {
 619            RevealTarget::Center => self
 620                .workspace
 621                .update(cx, |workspace, cx| {
 622                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
 623                        project.create_terminal_task(spawn_task, cx)
 624                    })
 625                })
 626                .unwrap_or_else(|e| Task::ready(Err(e))),
 627            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
 628        }
 629    }
 630
 631    /// Create a new Terminal in the current working directory or the user's home directory
 632    fn new_terminal(
 633        workspace: &mut Workspace,
 634        _: &workspace::NewTerminal,
 635        window: &mut Window,
 636        cx: &mut Context<Workspace>,
 637    ) {
 638        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 639            return;
 640        };
 641
 642        terminal_panel
 643            .update(cx, |this, cx| {
 644                this.add_terminal_shell(
 645                    default_working_directory(workspace, cx),
 646                    RevealStrategy::Always,
 647                    window,
 648                    cx,
 649                )
 650            })
 651            .detach_and_log_err(cx);
 652    }
 653
 654    fn terminals_for_task(
 655        &self,
 656        label: &str,
 657        cx: &mut App,
 658    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 659        let Some(workspace) = self.workspace.upgrade() else {
 660            return Vec::new();
 661        };
 662
 663        let pane_terminal_views = |pane: Entity<Pane>| {
 664            pane.read(cx)
 665                .items()
 666                .enumerate()
 667                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 668                .filter_map(|(index, terminal_view)| {
 669                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 670                    if &task_state.full_label == label {
 671                        Some((index, terminal_view))
 672                    } else {
 673                        None
 674                    }
 675                })
 676                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 677        };
 678
 679        self.center
 680            .panes()
 681            .into_iter()
 682            .cloned()
 683            .flat_map(pane_terminal_views)
 684            .chain(
 685                workspace
 686                    .read(cx)
 687                    .panes()
 688                    .iter()
 689                    .cloned()
 690                    .flat_map(pane_terminal_views),
 691            )
 692            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 693            .collect()
 694    }
 695
 696    fn activate_terminal_view(
 697        &self,
 698        pane: &Entity<Pane>,
 699        item_index: usize,
 700        focus: bool,
 701        window: &mut Window,
 702        cx: &mut App,
 703    ) {
 704        pane.update(cx, |pane, cx| {
 705            pane.activate_item(item_index, true, focus, window, cx)
 706        })
 707    }
 708
 709    pub fn add_center_terminal(
 710        workspace: &mut Workspace,
 711        window: &mut Window,
 712        cx: &mut Context<Workspace>,
 713        create_terminal: impl FnOnce(
 714            &mut Project,
 715            &mut Context<Project>,
 716        ) -> Task<Result<Entity<Terminal>>>
 717        + 'static,
 718    ) -> Task<Result<WeakEntity<Terminal>>> {
 719        if !is_enabled_in_workspace(workspace, cx) {
 720            return Task::ready(Err(anyhow!(
 721                "terminal not yet supported for remote projects"
 722            )));
 723        }
 724        let project = workspace.project().downgrade();
 725        cx.spawn_in(window, async move |workspace, cx| {
 726            let terminal = project.update(cx, create_terminal)?.await?;
 727
 728            workspace.update_in(cx, |workspace, window, cx| {
 729                let terminal_view = cx.new(|cx| {
 730                    TerminalView::new(
 731                        terminal.clone(),
 732                        workspace.weak_handle(),
 733                        workspace.database_id(),
 734                        workspace.project().downgrade(),
 735                        window,
 736                        cx,
 737                    )
 738                });
 739                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 740            })?;
 741            Ok(terminal.downgrade())
 742        })
 743    }
 744
 745    pub fn add_terminal_task(
 746        &mut self,
 747        task: SpawnInTerminal,
 748        reveal_strategy: RevealStrategy,
 749        window: &mut Window,
 750        cx: &mut Context<Self>,
 751    ) -> Task<Result<WeakEntity<Terminal>>> {
 752        let workspace = self.workspace.clone();
 753        cx.spawn_in(window, async move |terminal_panel, cx| {
 754            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 755                anyhow::bail!("terminal not yet supported for remote projects");
 756            }
 757            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 758                terminal_panel.pending_terminals_to_add += 1;
 759                terminal_panel.active_pane.clone()
 760            })?;
 761            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 762            let terminal = project
 763                .update(cx, |project, cx| project.create_terminal_task(task, cx))?
 764                .await?;
 765            let result = workspace.update_in(cx, |workspace, window, cx| {
 766                let terminal_view = Box::new(cx.new(|cx| {
 767                    TerminalView::new(
 768                        terminal.clone(),
 769                        workspace.weak_handle(),
 770                        workspace.database_id(),
 771                        workspace.project().downgrade(),
 772                        window,
 773                        cx,
 774                    )
 775                }));
 776
 777                match reveal_strategy {
 778                    RevealStrategy::Always => {
 779                        workspace.focus_panel::<Self>(window, cx);
 780                    }
 781                    RevealStrategy::NoFocus => {
 782                        workspace.open_panel::<Self>(window, cx);
 783                    }
 784                    RevealStrategy::Never => {}
 785                }
 786
 787                pane.update(cx, |pane, cx| {
 788                    let focus = pane.has_focus(window, cx)
 789                        || matches!(reveal_strategy, RevealStrategy::Always);
 790                    pane.add_item(terminal_view, true, focus, None, window, cx);
 791                });
 792
 793                Ok(terminal.downgrade())
 794            })?;
 795            terminal_panel.update(cx, |terminal_panel, cx| {
 796                terminal_panel.pending_terminals_to_add =
 797                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 798                terminal_panel.serialize(cx)
 799            })?;
 800            result
 801        })
 802    }
 803
 804    fn add_terminal_shell(
 805        &mut self,
 806        cwd: Option<PathBuf>,
 807        reveal_strategy: RevealStrategy,
 808        window: &mut Window,
 809        cx: &mut Context<Self>,
 810    ) -> Task<Result<WeakEntity<Terminal>>> {
 811        let workspace = self.workspace.clone();
 812        cx.spawn_in(window, async move |terminal_panel, cx| {
 813            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 814                anyhow::bail!("terminal not yet supported for collaborative projects");
 815            }
 816            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 817                terminal_panel.pending_terminals_to_add += 1;
 818                terminal_panel.active_pane.clone()
 819            })?;
 820            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 821            let terminal = project
 822                .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
 823                .await?;
 824            let result = workspace.update_in(cx, |workspace, window, cx| {
 825                let terminal_view = Box::new(cx.new(|cx| {
 826                    TerminalView::new(
 827                        terminal.clone(),
 828                        workspace.weak_handle(),
 829                        workspace.database_id(),
 830                        workspace.project().downgrade(),
 831                        window,
 832                        cx,
 833                    )
 834                }));
 835
 836                match reveal_strategy {
 837                    RevealStrategy::Always => {
 838                        workspace.focus_panel::<Self>(window, cx);
 839                    }
 840                    RevealStrategy::NoFocus => {
 841                        workspace.open_panel::<Self>(window, cx);
 842                    }
 843                    RevealStrategy::Never => {}
 844                }
 845
 846                pane.update(cx, |pane, cx| {
 847                    let focus = pane.has_focus(window, cx)
 848                        || matches!(reveal_strategy, RevealStrategy::Always);
 849                    pane.add_item(terminal_view, true, focus, None, window, cx);
 850                });
 851
 852                Ok(terminal.downgrade())
 853            })?;
 854            terminal_panel.update(cx, |terminal_panel, cx| {
 855                terminal_panel.pending_terminals_to_add =
 856                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 857                terminal_panel.serialize(cx)
 858            })?;
 859            result
 860        })
 861    }
 862
 863    fn serialize(&mut self, cx: &mut Context<Self>) {
 864        let height = self.height;
 865        let width = self.width;
 866        let Some(serialization_key) = self
 867            .workspace
 868            .read_with(cx, |workspace, _| {
 869                TerminalPanel::serialization_key(workspace)
 870            })
 871            .ok()
 872            .flatten()
 873        else {
 874            return;
 875        };
 876        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
 877            cx.background_executor()
 878                .timer(Duration::from_millis(50))
 879                .await;
 880            let terminal_panel = terminal_panel.upgrade()?;
 881            let items = terminal_panel
 882                .update(cx, |terminal_panel, cx| {
 883                    SerializedItems::WithSplits(serialize_pane_group(
 884                        &terminal_panel.center,
 885                        &terminal_panel.active_pane,
 886                        cx,
 887                    ))
 888                })
 889                .ok()?;
 890            cx.background_spawn(
 891                async move {
 892                    KEY_VALUE_STORE
 893                        .write_kvp(
 894                            serialization_key,
 895                            serde_json::to_string(&SerializedTerminalPanel {
 896                                items,
 897                                active_item_id: None,
 898                                height,
 899                                width,
 900                            })?,
 901                        )
 902                        .await?;
 903                    anyhow::Ok(())
 904                }
 905                .log_err(),
 906            )
 907            .await;
 908            Some(())
 909        });
 910    }
 911
 912    fn replace_terminal(
 913        &self,
 914        spawn_task: SpawnInTerminal,
 915        task_pane: Entity<Pane>,
 916        terminal_item_index: usize,
 917        terminal_to_replace: Entity<TerminalView>,
 918        window: &mut Window,
 919        cx: &mut Context<Self>,
 920    ) -> Task<Result<WeakEntity<Terminal>>> {
 921        let reveal = spawn_task.reveal;
 922        let reveal_target = spawn_task.reveal_target;
 923        let task_workspace = self.workspace.clone();
 924        cx.spawn_in(window, async move |terminal_panel, cx| {
 925            let project = terminal_panel.update(cx, |this, cx| {
 926                this.workspace
 927                    .update(cx, |workspace, _| workspace.project().clone())
 928            })??;
 929            let new_terminal = project
 930                .update(cx, |project, cx| {
 931                    project.create_terminal_task(spawn_task, cx)
 932                })?
 933                .await?;
 934            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
 935                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
 936            })?;
 937
 938            match reveal {
 939                RevealStrategy::Always => match reveal_target {
 940                    RevealTarget::Center => {
 941                        task_workspace.update_in(cx, |workspace, window, cx| {
 942                            let did_activate = workspace.activate_item(
 943                                &terminal_to_replace,
 944                                true,
 945                                true,
 946                                window,
 947                                cx,
 948                            );
 949
 950                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
 951
 952                            anyhow::Ok(())
 953                        })??;
 954                    }
 955                    RevealTarget::Dock => {
 956                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 957                            terminal_panel.activate_terminal_view(
 958                                &task_pane,
 959                                terminal_item_index,
 960                                true,
 961                                window,
 962                                cx,
 963                            )
 964                        })?;
 965
 966                        cx.spawn(async move |cx| {
 967                            task_workspace
 968                                .update_in(cx, |workspace, window, cx| {
 969                                    workspace.focus_panel::<Self>(window, cx)
 970                                })
 971                                .ok()
 972                        })
 973                        .detach();
 974                    }
 975                },
 976                RevealStrategy::NoFocus => match reveal_target {
 977                    RevealTarget::Center => {
 978                        task_workspace.update_in(cx, |workspace, window, cx| {
 979                            workspace.active_pane().focus_handle(cx).focus(window);
 980                        })?;
 981                    }
 982                    RevealTarget::Dock => {
 983                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 984                            terminal_panel.activate_terminal_view(
 985                                &task_pane,
 986                                terminal_item_index,
 987                                false,
 988                                window,
 989                                cx,
 990                            )
 991                        })?;
 992
 993                        cx.spawn(async move |cx| {
 994                            task_workspace
 995                                .update_in(cx, |workspace, window, cx| {
 996                                    workspace.open_panel::<Self>(window, cx)
 997                                })
 998                                .ok()
 999                        })
1000                        .detach();
1001                    }
1002                },
1003                RevealStrategy::Never => {}
1004            }
1005
1006            Ok(new_terminal.downgrade())
1007        })
1008    }
1009
1010    fn has_no_terminals(&self, cx: &App) -> bool {
1011        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1012    }
1013
1014    pub fn assistant_enabled(&self) -> bool {
1015        self.assistant_enabled
1016    }
1017
1018    fn is_enabled(&self, cx: &App) -> bool {
1019        self.workspace
1020            .upgrade()
1021            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1022    }
1023
1024    fn activate_pane_in_direction(
1025        &mut self,
1026        direction: SplitDirection,
1027        window: &mut Window,
1028        cx: &mut Context<Self>,
1029    ) {
1030        if let Some(pane) = self
1031            .center
1032            .find_pane_in_direction(&self.active_pane, direction, cx)
1033        {
1034            window.focus(&pane.focus_handle(cx));
1035        } else {
1036            self.workspace
1037                .update(cx, |workspace, cx| {
1038                    workspace.activate_pane_in_direction(direction, window, cx)
1039                })
1040                .ok();
1041        }
1042    }
1043
1044    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1045        if let Some(to) = self
1046            .center
1047            .find_pane_in_direction(&self.active_pane, direction, cx)
1048            .cloned()
1049        {
1050            self.center.swap(&self.active_pane, &to);
1051            cx.notify();
1052        }
1053    }
1054}
1055
1056fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1057    workspace.project().read(cx).supports_terminal(cx)
1058}
1059
1060pub fn new_terminal_pane(
1061    workspace: WeakEntity<Workspace>,
1062    project: Entity<Project>,
1063    zoomed: bool,
1064    window: &mut Window,
1065    cx: &mut Context<TerminalPanel>,
1066) -> Entity<Pane> {
1067    let is_local = project.read(cx).is_local();
1068    let terminal_panel = cx.entity();
1069    let pane = cx.new(|cx| {
1070        let mut pane = Pane::new(
1071            workspace.clone(),
1072            project.clone(),
1073            Default::default(),
1074            None,
1075            NewTerminal.boxed_clone(),
1076            window,
1077            cx,
1078        );
1079        pane.set_zoomed(zoomed, cx);
1080        pane.set_can_navigate(false, cx);
1081        pane.display_nav_history_buttons(None);
1082        pane.set_should_display_tab_bar(|_, _| true);
1083        pane.set_zoom_out_on_close(false);
1084
1085        let split_closure_terminal_panel = terminal_panel.downgrade();
1086        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1087            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1088                let is_current_pane = tab.pane == cx.entity();
1089                let Some(can_drag_away) = split_closure_terminal_panel
1090                    .read_with(cx, |terminal_panel, _| {
1091                        let current_panes = terminal_panel.center.panes();
1092                        !current_panes.contains(&&tab.pane)
1093                            || current_panes.len() > 1
1094                            || (!is_current_pane || pane.items_len() > 1)
1095                    })
1096                    .ok()
1097                else {
1098                    return false;
1099                };
1100                if can_drag_away {
1101                    let item = if is_current_pane {
1102                        pane.item_for_index(tab.ix)
1103                    } else {
1104                        tab.pane.read(cx).item_for_index(tab.ix)
1105                    };
1106                    if let Some(item) = item {
1107                        return item.downcast::<TerminalView>().is_some();
1108                    }
1109                }
1110            }
1111            false
1112        })));
1113
1114        let buffer_search_bar = cx.new(|cx| {
1115            search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
1116        });
1117        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1118        pane.toolbar().update(cx, |toolbar, cx| {
1119            toolbar.add_item(buffer_search_bar, window, cx);
1120            toolbar.add_item(breadcrumbs, window, cx);
1121        });
1122
1123        let drop_closure_project = project.downgrade();
1124        let drop_closure_terminal_panel = terminal_panel.downgrade();
1125        pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
1126            let Some(project) = drop_closure_project.upgrade() else {
1127                return ControlFlow::Break(());
1128            };
1129            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
1130                let this_pane = cx.entity();
1131                let item = if tab.pane == this_pane {
1132                    pane.item_for_index(tab.ix)
1133                } else {
1134                    tab.pane.read(cx).item_for_index(tab.ix)
1135                };
1136                if let Some(item) = item {
1137                    if item.downcast::<TerminalView>().is_some() {
1138                        let source = tab.pane.clone();
1139                        let item_id_to_move = item.item_id();
1140
1141                        let Ok(new_split_pane) = pane
1142                            .drag_split_direction()
1143                            .map(|split_direction| {
1144                                drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
1145                                    let is_zoomed = if terminal_panel.active_pane == this_pane {
1146                                        pane.is_zoomed()
1147                                    } else {
1148                                        terminal_panel.active_pane.read(cx).is_zoomed()
1149                                    };
1150                                    let new_pane = new_terminal_pane(
1151                                        workspace.clone(),
1152                                        project.clone(),
1153                                        is_zoomed,
1154                                        window,
1155                                        cx,
1156                                    );
1157                                    terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
1158                                    terminal_panel.center.split(
1159                                        &this_pane,
1160                                        &new_pane,
1161                                        split_direction,
1162                                    )?;
1163                                    anyhow::Ok(new_pane)
1164                                })
1165                            })
1166                            .transpose()
1167                        else {
1168                            return ControlFlow::Break(());
1169                        };
1170
1171                        match new_split_pane.transpose() {
1172                            // Source pane may be the one currently updated, so defer the move.
1173                            Ok(Some(new_pane)) => cx
1174                                .spawn_in(window, async move |_, cx| {
1175                                    cx.update(|window, cx| {
1176                                        move_item(
1177                                            &source,
1178                                            &new_pane,
1179                                            item_id_to_move,
1180                                            new_pane.read(cx).active_item_index(),
1181                                            true,
1182                                            window,
1183                                            cx,
1184                                        );
1185                                    })
1186                                    .ok();
1187                                })
1188                                .detach(),
1189                            // If we drop into existing pane or current pane,
1190                            // regular pane drop handler will take care of it,
1191                            // using the right tab index for the operation.
1192                            Ok(None) => return ControlFlow::Continue(()),
1193                            err @ Err(_) => {
1194                                err.log_err();
1195                                return ControlFlow::Break(());
1196                            }
1197                        };
1198                    } else if let Some(project_path) = item.project_path(cx)
1199                        && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1200                    {
1201                        add_paths_to_terminal(pane, &[entry_path], window, cx);
1202                    }
1203                }
1204            } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1205                let project = project.read(cx);
1206                let paths_to_add = selection
1207                    .items()
1208                    .map(|selected_entry| selected_entry.entry_id)
1209                    .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1210                    .filter_map(|project_path| project.absolute_path(&project_path, cx))
1211                    .collect::<Vec<_>>();
1212                if !paths_to_add.is_empty() {
1213                    add_paths_to_terminal(pane, &paths_to_add, window, cx);
1214                }
1215            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1216                if let Some(entry_path) = project
1217                    .read(cx)
1218                    .path_for_entry(entry_id, cx)
1219                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1220                {
1221                    add_paths_to_terminal(pane, &[entry_path], window, cx);
1222                }
1223            } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1224                add_paths_to_terminal(pane, paths.paths(), window, cx);
1225            }
1226
1227            ControlFlow::Break(())
1228        });
1229
1230        pane
1231    });
1232
1233    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1234        .detach();
1235    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1236
1237    pane
1238}
1239
1240async fn wait_for_terminals_tasks(
1241    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1242    cx: &mut AsyncApp,
1243) {
1244    let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1245        terminal
1246            .update(cx, |terminal_view, cx| {
1247                terminal_view
1248                    .terminal()
1249                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1250            })
1251            .ok()
1252    });
1253    join_all(pending_tasks).await;
1254}
1255
1256fn add_paths_to_terminal(
1257    pane: &mut Pane,
1258    paths: &[PathBuf],
1259    window: &mut Window,
1260    cx: &mut Context<Pane>,
1261) {
1262    if let Some(terminal_view) = pane
1263        .active_item()
1264        .and_then(|item| item.downcast::<TerminalView>())
1265    {
1266        window.focus(&terminal_view.focus_handle(cx));
1267        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1268        new_text.push(' ');
1269        terminal_view.update(cx, |terminal_view, cx| {
1270            terminal_view.terminal().update(cx, |terminal, _| {
1271                terminal.paste(&new_text);
1272            });
1273        });
1274    }
1275}
1276
1277impl EventEmitter<PanelEvent> for TerminalPanel {}
1278
1279impl Render for TerminalPanel {
1280    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1281        let mut registrar = DivRegistrar::new(
1282            |panel, _, cx| {
1283                panel
1284                    .active_pane
1285                    .read(cx)
1286                    .toolbar()
1287                    .read(cx)
1288                    .item_of_type::<BufferSearchBar>()
1289            },
1290            cx,
1291        );
1292        BufferSearchBar::register(&mut registrar);
1293        let registrar = registrar.into_div();
1294        self.workspace
1295            .update(cx, |workspace, cx| {
1296                registrar.size_full().child(self.center.render(
1297                    workspace.zoomed_item(),
1298                    &workspace::PaneRenderContext {
1299                        follower_states: &HashMap::default(),
1300                        active_call: workspace.active_call(),
1301                        active_pane: &self.active_pane,
1302                        app_state: workspace.app_state(),
1303                        project: workspace.project(),
1304                        workspace: &workspace.weak_handle(),
1305                    },
1306                    window,
1307                    cx,
1308                ))
1309            })
1310            .ok()
1311            .map(|div| {
1312                div.on_action({
1313                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1314                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1315                    })
1316                })
1317                .on_action({
1318                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1319                        terminal_panel.activate_pane_in_direction(
1320                            SplitDirection::Right,
1321                            window,
1322                            cx,
1323                        );
1324                    })
1325                })
1326                .on_action({
1327                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1328                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1329                    })
1330                })
1331                .on_action({
1332                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1333                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1334                    })
1335                })
1336                .on_action(
1337                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1338                        let panes = terminal_panel.center.panes();
1339                        if let Some(ix) = panes
1340                            .iter()
1341                            .position(|pane| **pane == terminal_panel.active_pane)
1342                        {
1343                            let next_ix = (ix + 1) % panes.len();
1344                            window.focus(&panes[next_ix].focus_handle(cx));
1345                        }
1346                    }),
1347                )
1348                .on_action(cx.listener(
1349                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1350                        let panes = terminal_panel.center.panes();
1351                        if let Some(ix) = panes
1352                            .iter()
1353                            .position(|pane| **pane == terminal_panel.active_pane)
1354                        {
1355                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1356                            window.focus(&panes[prev_ix].focus_handle(cx));
1357                        }
1358                    },
1359                ))
1360                .on_action(
1361                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1362                        let panes = terminal_panel.center.panes();
1363                        if let Some(&pane) = panes.get(action.0) {
1364                            window.focus(&pane.read(cx).focus_handle(cx));
1365                        } else {
1366                            let future =
1367                                terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
1368                            cx.spawn_in(window, async move |terminal_panel, cx| {
1369                                if let Some(new_pane) = future.await {
1370                                    _ = terminal_panel.update_in(
1371                                        cx,
1372                                        |terminal_panel, window, cx| {
1373                                            terminal_panel
1374                                                .center
1375                                                .split(
1376                                                    &terminal_panel.active_pane,
1377                                                    &new_pane,
1378                                                    SplitDirection::Right,
1379                                                )
1380                                                .log_err();
1381                                            let new_pane = new_pane.read(cx);
1382                                            window.focus(&new_pane.focus_handle(cx));
1383                                        },
1384                                    );
1385                                }
1386                            })
1387                            .detach();
1388                        }
1389                    }),
1390                )
1391                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1392                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1393                }))
1394                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1395                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1396                }))
1397                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1398                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1399                }))
1400                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1401                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1402                }))
1403                .on_action(
1404                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1405                        let Some(&target_pane) =
1406                            terminal_panel.center.panes().get(action.destination)
1407                        else {
1408                            return;
1409                        };
1410                        move_active_item(
1411                            &terminal_panel.active_pane,
1412                            target_pane,
1413                            action.focus,
1414                            true,
1415                            window,
1416                            cx,
1417                        );
1418                    }),
1419                )
1420                .on_action(cx.listener(
1421                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1422                        let source_pane = &terminal_panel.active_pane;
1423                        if let Some(destination_pane) = terminal_panel
1424                            .center
1425                            .find_pane_in_direction(source_pane, action.direction, cx)
1426                        {
1427                            move_active_item(
1428                                source_pane,
1429                                destination_pane,
1430                                action.focus,
1431                                true,
1432                                window,
1433                                cx,
1434                            );
1435                        };
1436                    },
1437                ))
1438            })
1439            .unwrap_or_else(|| div())
1440    }
1441}
1442
1443impl Focusable for TerminalPanel {
1444    fn focus_handle(&self, cx: &App) -> FocusHandle {
1445        self.active_pane.focus_handle(cx)
1446    }
1447}
1448
1449impl Panel for TerminalPanel {
1450    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1451        match TerminalSettings::get_global(cx).dock {
1452            TerminalDockPosition::Left => DockPosition::Left,
1453            TerminalDockPosition::Bottom => DockPosition::Bottom,
1454            TerminalDockPosition::Right => DockPosition::Right,
1455        }
1456    }
1457
1458    fn position_is_valid(&self, _: DockPosition) -> bool {
1459        true
1460    }
1461
1462    fn set_position(
1463        &mut self,
1464        position: DockPosition,
1465        _window: &mut Window,
1466        cx: &mut Context<Self>,
1467    ) {
1468        settings::update_settings_file::<TerminalSettings>(
1469            self.fs.clone(),
1470            cx,
1471            move |settings, _| {
1472                let dock = match position {
1473                    DockPosition::Left => TerminalDockPosition::Left,
1474                    DockPosition::Bottom => TerminalDockPosition::Bottom,
1475                    DockPosition::Right => TerminalDockPosition::Right,
1476                };
1477                settings.dock = Some(dock);
1478            },
1479        );
1480    }
1481
1482    fn size(&self, window: &Window, cx: &App) -> Pixels {
1483        let settings = TerminalSettings::get_global(cx);
1484        match self.position(window, cx) {
1485            DockPosition::Left | DockPosition::Right => {
1486                self.width.unwrap_or(settings.default_width)
1487            }
1488            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1489        }
1490    }
1491
1492    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1493        match self.position(window, cx) {
1494            DockPosition::Left | DockPosition::Right => self.width = size,
1495            DockPosition::Bottom => self.height = size,
1496        }
1497        cx.notify();
1498        cx.defer_in(window, |this, _, cx| {
1499            this.serialize(cx);
1500        })
1501    }
1502
1503    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1504        self.active_pane.read(cx).is_zoomed()
1505    }
1506
1507    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1508        for pane in self.center.panes() {
1509            pane.update(cx, |pane, cx| {
1510                pane.set_zoomed(zoomed, cx);
1511            })
1512        }
1513        cx.notify();
1514    }
1515
1516    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1517        let old_active = self.active;
1518        self.active = active;
1519        if !active || old_active == active || !self.has_no_terminals(cx) {
1520            return;
1521        }
1522        cx.defer_in(window, |this, window, cx| {
1523            let Ok(kind) = this
1524                .workspace
1525                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1526            else {
1527                return;
1528            };
1529
1530            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1531                .detach_and_log_err(cx)
1532        })
1533    }
1534
1535    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1536        let count = self
1537            .center
1538            .panes()
1539            .into_iter()
1540            .map(|pane| pane.read(cx).items_len())
1541            .sum::<usize>();
1542        if count == 0 {
1543            None
1544        } else {
1545            Some(count.to_string())
1546        }
1547    }
1548
1549    fn persistent_name() -> &'static str {
1550        "TerminalPanel"
1551    }
1552
1553    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1554        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1555            && TerminalSettings::get_global(cx).button
1556        {
1557            Some(IconName::TerminalAlt)
1558        } else {
1559            None
1560        }
1561    }
1562
1563    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1564        Some("Terminal Panel")
1565    }
1566
1567    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1568        Box::new(ToggleFocus)
1569    }
1570
1571    fn pane(&self) -> Option<Entity<Pane>> {
1572        Some(self.active_pane.clone())
1573    }
1574
1575    fn activation_priority(&self) -> u32 {
1576        1
1577    }
1578}
1579
1580struct TerminalProvider(Entity<TerminalPanel>);
1581
1582impl workspace::TerminalProvider for TerminalProvider {
1583    fn spawn(
1584        &self,
1585        task: SpawnInTerminal,
1586        window: &mut Window,
1587        cx: &mut App,
1588    ) -> Task<Option<Result<ExitStatus>>> {
1589        let terminal_panel = self.0.clone();
1590        window.spawn(cx, async move |cx| {
1591            let terminal = terminal_panel
1592                .update_in(cx, |terminal_panel, window, cx| {
1593                    terminal_panel.spawn_task(&task, window, cx)
1594                })
1595                .ok()?
1596                .await;
1597            match terminal {
1598                Ok(terminal) => {
1599                    let exit_status = terminal
1600                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1601                        .ok()?
1602                        .await?;
1603                    Some(Ok(exit_status))
1604                }
1605                Err(e) => Some(Err(e)),
1606            }
1607        })
1608    }
1609}
1610
1611struct InlineAssistTabBarButton {
1612    focus_handle: FocusHandle,
1613}
1614
1615impl Render for InlineAssistTabBarButton {
1616    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1617        let focus_handle = self.focus_handle.clone();
1618        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1619            .icon_size(IconSize::Small)
1620            .on_click(cx.listener(|_, _, window, cx| {
1621                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1622            }))
1623            .tooltip(move |window, cx| {
1624                Tooltip::for_action_in(
1625                    "Inline Assist",
1626                    &InlineAssist::default(),
1627                    &focus_handle,
1628                    window,
1629                    cx,
1630                )
1631            })
1632    }
1633}