terminal_panel.rs

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