terminal_panel.rs

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