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