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(direction) => {
 389                let fut = self.new_pane_with_cloned_active_terminal(window, cx);
 390                let pane = pane.clone();
 391                cx.spawn_in(window, async move |panel, cx| {
 392                    let Some(new_pane) = fut.await else {
 393                        return;
 394                    };
 395                    _ = panel.update_in(cx, |panel, window, cx| {
 396                        panel.center.split(&pane, &new_pane, direction).log_err();
 397                        window.focus(&new_pane.focus_handle(cx));
 398                    });
 399                })
 400                .detach();
 401            }
 402            pane::Event::Focus => {
 403                self.active_pane = pane.clone();
 404            }
 405            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
 406                self.serialize(cx);
 407            }
 408
 409            _ => {}
 410        }
 411    }
 412
 413    fn new_pane_with_cloned_active_terminal(
 414        &mut self,
 415        window: &mut Window,
 416        cx: &mut Context<Self>,
 417    ) -> Task<Option<Entity<Pane>>> {
 418        let Some(workspace) = self.workspace.upgrade() else {
 419            return Task::ready(None);
 420        };
 421        let workspace = workspace.read(cx);
 422        let database_id = workspace.database_id();
 423        let weak_workspace = self.workspace.clone();
 424        let project = workspace.project().clone();
 425        let active_pane = &self.active_pane;
 426        let terminal_view = active_pane
 427            .read(cx)
 428            .active_item()
 429            .and_then(|item| item.downcast::<TerminalView>());
 430        let working_directory = terminal_view.as_ref().and_then(|terminal_view| {
 431            let terminal = terminal_view.read(cx).terminal().read(cx);
 432            terminal
 433                .working_directory()
 434                .or_else(|| default_working_directory(workspace, cx))
 435        });
 436        let is_zoomed = active_pane.read(cx).is_zoomed();
 437        cx.spawn_in(window, async move |panel, cx| {
 438            let terminal = project
 439                .update(cx, |project, cx| match terminal_view {
 440                    Some(view) => Task::ready(project.clone_terminal(
 441                        &view.read(cx).terminal.clone(),
 442                        cx,
 443                        || working_directory,
 444                    )),
 445                    None => project.create_terminal_shell(working_directory, cx),
 446                })
 447                .ok()?
 448                .await
 449                .ok()?;
 450
 451            panel
 452                .update_in(cx, move |terminal_panel, window, cx| {
 453                    let terminal_view = Box::new(cx.new(|cx| {
 454                        TerminalView::new(
 455                            terminal.clone(),
 456                            weak_workspace.clone(),
 457                            database_id,
 458                            project.downgrade(),
 459                            window,
 460                            cx,
 461                        )
 462                    }));
 463                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
 464                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 465                    pane.update(cx, |pane, cx| {
 466                        pane.add_item(terminal_view, true, true, None, window, cx);
 467                    });
 468                    Some(pane)
 469                })
 470                .ok()
 471                .flatten()
 472        })
 473    }
 474
 475    pub fn open_terminal(
 476        workspace: &mut Workspace,
 477        action: &workspace::OpenTerminal,
 478        window: &mut Window,
 479        cx: &mut Context<Workspace>,
 480    ) {
 481        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 482            return;
 483        };
 484
 485        terminal_panel
 486            .update(cx, |panel, cx| {
 487                panel.add_terminal_shell(
 488                    Some(action.working_directory.clone()),
 489                    RevealStrategy::Always,
 490                    window,
 491                    cx,
 492                )
 493            })
 494            .detach_and_log_err(cx);
 495    }
 496
 497    pub fn spawn_task(
 498        &mut self,
 499        task: SpawnInTerminal,
 500        window: &mut Window,
 501        cx: &mut Context<Self>,
 502    ) -> Task<Result<WeakEntity<Terminal>>> {
 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        match reveal_target {
 562            RevealTarget::Center => self
 563                .workspace
 564                .update(cx, |workspace, cx| {
 565                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
 566                        project.create_terminal_task(spawn_task, cx)
 567                    })
 568                })
 569                .unwrap_or_else(|e| Task::ready(Err(e))),
 570            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
 571        }
 572    }
 573
 574    /// Create a new Terminal in the current working directory or the user's home directory
 575    fn new_terminal(
 576        workspace: &mut Workspace,
 577        _: &workspace::NewTerminal,
 578        window: &mut Window,
 579        cx: &mut Context<Workspace>,
 580    ) {
 581        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 582            return;
 583        };
 584
 585        terminal_panel
 586            .update(cx, |this, cx| {
 587                this.add_terminal_shell(
 588                    default_working_directory(workspace, cx),
 589                    RevealStrategy::Always,
 590                    window,
 591                    cx,
 592                )
 593            })
 594            .detach_and_log_err(cx);
 595    }
 596
 597    fn terminals_for_task(
 598        &self,
 599        label: &str,
 600        cx: &mut App,
 601    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 602        let Some(workspace) = self.workspace.upgrade() else {
 603            return Vec::new();
 604        };
 605
 606        let pane_terminal_views = |pane: Entity<Pane>| {
 607            pane.read(cx)
 608                .items()
 609                .enumerate()
 610                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 611                .filter_map(|(index, terminal_view)| {
 612                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 613                    if &task_state.full_label == label {
 614                        Some((index, terminal_view))
 615                    } else {
 616                        None
 617                    }
 618                })
 619                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 620        };
 621
 622        self.center
 623            .panes()
 624            .into_iter()
 625            .cloned()
 626            .flat_map(pane_terminal_views)
 627            .chain(
 628                workspace
 629                    .read(cx)
 630                    .panes()
 631                    .iter()
 632                    .cloned()
 633                    .flat_map(pane_terminal_views),
 634            )
 635            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 636            .collect()
 637    }
 638
 639    fn activate_terminal_view(
 640        &self,
 641        pane: &Entity<Pane>,
 642        item_index: usize,
 643        focus: bool,
 644        window: &mut Window,
 645        cx: &mut App,
 646    ) {
 647        pane.update(cx, |pane, cx| {
 648            pane.activate_item(item_index, true, focus, window, cx)
 649        })
 650    }
 651
 652    pub fn add_center_terminal(
 653        workspace: &mut Workspace,
 654        window: &mut Window,
 655        cx: &mut Context<Workspace>,
 656        create_terminal: impl FnOnce(
 657            &mut Project,
 658            &mut Context<Project>,
 659        ) -> Task<Result<Entity<Terminal>>>
 660        + 'static,
 661    ) -> Task<Result<WeakEntity<Terminal>>> {
 662        if !is_enabled_in_workspace(workspace, cx) {
 663            return Task::ready(Err(anyhow!(
 664                "terminal not yet supported for remote projects"
 665            )));
 666        }
 667        let project = workspace.project().downgrade();
 668        cx.spawn_in(window, async move |workspace, cx| {
 669            let terminal = project.update(cx, create_terminal)?.await?;
 670
 671            workspace.update_in(cx, |workspace, window, cx| {
 672                let terminal_view = cx.new(|cx| {
 673                    TerminalView::new(
 674                        terminal.clone(),
 675                        workspace.weak_handle(),
 676                        workspace.database_id(),
 677                        workspace.project().downgrade(),
 678                        window,
 679                        cx,
 680                    )
 681                });
 682                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 683            })?;
 684            Ok(terminal.downgrade())
 685        })
 686    }
 687
 688    pub fn add_terminal_task(
 689        &mut self,
 690        task: SpawnInTerminal,
 691        reveal_strategy: RevealStrategy,
 692        window: &mut Window,
 693        cx: &mut Context<Self>,
 694    ) -> Task<Result<WeakEntity<Terminal>>> {
 695        let workspace = self.workspace.clone();
 696        cx.spawn_in(window, async move |terminal_panel, cx| {
 697            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 698                anyhow::bail!("terminal not yet supported for remote projects");
 699            }
 700            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 701                terminal_panel.pending_terminals_to_add += 1;
 702                terminal_panel.active_pane.clone()
 703            })?;
 704            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 705            let terminal = project
 706                .update(cx, |project, cx| project.create_terminal_task(task, cx))?
 707                .await?;
 708            let result = workspace.update_in(cx, |workspace, window, cx| {
 709                let terminal_view = Box::new(cx.new(|cx| {
 710                    TerminalView::new(
 711                        terminal.clone(),
 712                        workspace.weak_handle(),
 713                        workspace.database_id(),
 714                        workspace.project().downgrade(),
 715                        window,
 716                        cx,
 717                    )
 718                }));
 719
 720                match reveal_strategy {
 721                    RevealStrategy::Always => {
 722                        workspace.focus_panel::<Self>(window, cx);
 723                    }
 724                    RevealStrategy::NoFocus => {
 725                        workspace.open_panel::<Self>(window, cx);
 726                    }
 727                    RevealStrategy::Never => {}
 728                }
 729
 730                pane.update(cx, |pane, cx| {
 731                    let focus = pane.has_focus(window, cx)
 732                        || matches!(reveal_strategy, RevealStrategy::Always);
 733                    pane.add_item(terminal_view, true, focus, None, window, cx);
 734                });
 735
 736                Ok(terminal.downgrade())
 737            })?;
 738            terminal_panel.update(cx, |terminal_panel, cx| {
 739                terminal_panel.pending_terminals_to_add =
 740                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 741                terminal_panel.serialize(cx)
 742            })?;
 743            result
 744        })
 745    }
 746
 747    pub fn add_terminal_shell(
 748        &mut self,
 749        cwd: Option<PathBuf>,
 750        reveal_strategy: RevealStrategy,
 751        window: &mut Window,
 752        cx: &mut Context<Self>,
 753    ) -> Task<Result<WeakEntity<Terminal>>> {
 754        let workspace = self.workspace.clone();
 755        cx.spawn_in(window, async move |terminal_panel, cx| {
 756            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 757                anyhow::bail!("terminal not yet supported for remote projects");
 758            }
 759            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 760                terminal_panel.pending_terminals_to_add += 1;
 761                terminal_panel.active_pane.clone()
 762            })?;
 763            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 764            let terminal = project
 765                .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
 766                .await?;
 767            let result = workspace.update_in(cx, |workspace, window, cx| {
 768                let terminal_view = Box::new(cx.new(|cx| {
 769                    TerminalView::new(
 770                        terminal.clone(),
 771                        workspace.weak_handle(),
 772                        workspace.database_id(),
 773                        workspace.project().downgrade(),
 774                        window,
 775                        cx,
 776                    )
 777                }));
 778
 779                match reveal_strategy {
 780                    RevealStrategy::Always => {
 781                        workspace.focus_panel::<Self>(window, cx);
 782                    }
 783                    RevealStrategy::NoFocus => {
 784                        workspace.open_panel::<Self>(window, cx);
 785                    }
 786                    RevealStrategy::Never => {}
 787                }
 788
 789                pane.update(cx, |pane, cx| {
 790                    let focus = pane.has_focus(window, cx)
 791                        || matches!(reveal_strategy, RevealStrategy::Always);
 792                    pane.add_item(terminal_view, true, focus, None, window, cx);
 793                });
 794
 795                Ok(terminal.downgrade())
 796            })?;
 797            terminal_panel.update(cx, |terminal_panel, cx| {
 798                terminal_panel.pending_terminals_to_add =
 799                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 800                terminal_panel.serialize(cx)
 801            })?;
 802            result
 803        })
 804    }
 805
 806    fn serialize(&mut self, cx: &mut Context<Self>) {
 807        let height = self.height;
 808        let width = self.width;
 809        let Some(serialization_key) = self
 810            .workspace
 811            .read_with(cx, |workspace, _| {
 812                TerminalPanel::serialization_key(workspace)
 813            })
 814            .ok()
 815            .flatten()
 816        else {
 817            return;
 818        };
 819        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
 820            cx.background_executor()
 821                .timer(Duration::from_millis(50))
 822                .await;
 823            let terminal_panel = terminal_panel.upgrade()?;
 824            let items = terminal_panel
 825                .update(cx, |terminal_panel, cx| {
 826                    SerializedItems::WithSplits(serialize_pane_group(
 827                        &terminal_panel.center,
 828                        &terminal_panel.active_pane,
 829                        cx,
 830                    ))
 831                })
 832                .ok()?;
 833            cx.background_spawn(
 834                async move {
 835                    KEY_VALUE_STORE
 836                        .write_kvp(
 837                            serialization_key,
 838                            serde_json::to_string(&SerializedTerminalPanel {
 839                                items,
 840                                active_item_id: None,
 841                                height,
 842                                width,
 843                            })?,
 844                        )
 845                        .await?;
 846                    anyhow::Ok(())
 847                }
 848                .log_err(),
 849            )
 850            .await;
 851            Some(())
 852        });
 853    }
 854
 855    fn replace_terminal(
 856        &self,
 857        spawn_task: SpawnInTerminal,
 858        task_pane: Entity<Pane>,
 859        terminal_item_index: usize,
 860        terminal_to_replace: Entity<TerminalView>,
 861        window: &mut Window,
 862        cx: &mut Context<Self>,
 863    ) -> Task<Result<WeakEntity<Terminal>>> {
 864        let reveal = spawn_task.reveal;
 865        let reveal_target = spawn_task.reveal_target;
 866        let task_workspace = self.workspace.clone();
 867        cx.spawn_in(window, async move |terminal_panel, cx| {
 868            let project = terminal_panel.update(cx, |this, cx| {
 869                this.workspace
 870                    .update(cx, |workspace, _| workspace.project().clone())
 871            })??;
 872            let new_terminal = project
 873                .update(cx, |project, cx| {
 874                    project.create_terminal_task(spawn_task, cx)
 875                })?
 876                .await?;
 877            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
 878                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
 879            })?;
 880
 881            match reveal {
 882                RevealStrategy::Always => match reveal_target {
 883                    RevealTarget::Center => {
 884                        task_workspace.update_in(cx, |workspace, window, cx| {
 885                            let did_activate = workspace.activate_item(
 886                                &terminal_to_replace,
 887                                true,
 888                                true,
 889                                window,
 890                                cx,
 891                            );
 892
 893                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
 894
 895                            anyhow::Ok(())
 896                        })??;
 897                    }
 898                    RevealTarget::Dock => {
 899                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 900                            terminal_panel.activate_terminal_view(
 901                                &task_pane,
 902                                terminal_item_index,
 903                                true,
 904                                window,
 905                                cx,
 906                            )
 907                        })?;
 908
 909                        cx.spawn(async move |cx| {
 910                            task_workspace
 911                                .update_in(cx, |workspace, window, cx| {
 912                                    workspace.focus_panel::<Self>(window, cx)
 913                                })
 914                                .ok()
 915                        })
 916                        .detach();
 917                    }
 918                },
 919                RevealStrategy::NoFocus => match reveal_target {
 920                    RevealTarget::Center => {
 921                        task_workspace.update_in(cx, |workspace, window, cx| {
 922                            workspace.active_pane().focus_handle(cx).focus(window);
 923                        })?;
 924                    }
 925                    RevealTarget::Dock => {
 926                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 927                            terminal_panel.activate_terminal_view(
 928                                &task_pane,
 929                                terminal_item_index,
 930                                false,
 931                                window,
 932                                cx,
 933                            )
 934                        })?;
 935
 936                        cx.spawn(async move |cx| {
 937                            task_workspace
 938                                .update_in(cx, |workspace, window, cx| {
 939                                    workspace.open_panel::<Self>(window, cx)
 940                                })
 941                                .ok()
 942                        })
 943                        .detach();
 944                    }
 945                },
 946                RevealStrategy::Never => {}
 947            }
 948
 949            Ok(new_terminal.downgrade())
 950        })
 951    }
 952
 953    fn has_no_terminals(&self, cx: &App) -> bool {
 954        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
 955    }
 956
 957    pub fn assistant_enabled(&self) -> bool {
 958        self.assistant_enabled
 959    }
 960
 961    fn is_enabled(&self, cx: &App) -> bool {
 962        self.workspace
 963            .upgrade()
 964            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
 965    }
 966
 967    fn activate_pane_in_direction(
 968        &mut self,
 969        direction: SplitDirection,
 970        window: &mut Window,
 971        cx: &mut Context<Self>,
 972    ) {
 973        if let Some(pane) = self
 974            .center
 975            .find_pane_in_direction(&self.active_pane, direction, cx)
 976        {
 977            window.focus(&pane.focus_handle(cx));
 978        } else {
 979            self.workspace
 980                .update(cx, |workspace, cx| {
 981                    workspace.activate_pane_in_direction(direction, window, cx)
 982                })
 983                .ok();
 984        }
 985    }
 986
 987    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
 988        if let Some(to) = self
 989            .center
 990            .find_pane_in_direction(&self.active_pane, direction, cx)
 991            .cloned()
 992        {
 993            self.center.swap(&self.active_pane, &to);
 994            cx.notify();
 995        }
 996    }
 997}
 998
 999fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1000    workspace.project().read(cx).supports_terminal(cx)
1001}
1002
1003pub fn new_terminal_pane(
1004    workspace: WeakEntity<Workspace>,
1005    project: Entity<Project>,
1006    zoomed: bool,
1007    window: &mut Window,
1008    cx: &mut Context<TerminalPanel>,
1009) -> Entity<Pane> {
1010    let is_local = project.read(cx).is_local();
1011    let terminal_panel = cx.entity();
1012    let pane = cx.new(|cx| {
1013        let mut pane = Pane::new(
1014            workspace.clone(),
1015            project.clone(),
1016            Default::default(),
1017            None,
1018            NewTerminal.boxed_clone(),
1019            window,
1020            cx,
1021        );
1022        pane.set_zoomed(zoomed, cx);
1023        pane.set_can_navigate(false, cx);
1024        pane.display_nav_history_buttons(None);
1025        pane.set_should_display_tab_bar(|_, _| true);
1026        pane.set_zoom_out_on_close(false);
1027
1028        let split_closure_terminal_panel = terminal_panel.downgrade();
1029        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1030            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1031                let is_current_pane = tab.pane == cx.entity();
1032                let Some(can_drag_away) = split_closure_terminal_panel
1033                    .read_with(cx, |terminal_panel, _| {
1034                        let current_panes = terminal_panel.center.panes();
1035                        !current_panes.contains(&&tab.pane)
1036                            || current_panes.len() > 1
1037                            || (!is_current_pane || pane.items_len() > 1)
1038                    })
1039                    .ok()
1040                else {
1041                    return false;
1042                };
1043                if can_drag_away {
1044                    let item = if is_current_pane {
1045                        pane.item_for_index(tab.ix)
1046                    } else {
1047                        tab.pane.read(cx).item_for_index(tab.ix)
1048                    };
1049                    if let Some(item) = item {
1050                        return item.downcast::<TerminalView>().is_some();
1051                    }
1052                }
1053            }
1054            false
1055        })));
1056
1057        let buffer_search_bar = cx.new(|cx| {
1058            search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
1059        });
1060        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1061        pane.toolbar().update(cx, |toolbar, cx| {
1062            toolbar.add_item(buffer_search_bar, window, cx);
1063            toolbar.add_item(breadcrumbs, window, cx);
1064        });
1065
1066        let drop_closure_project = project.downgrade();
1067        let drop_closure_terminal_panel = terminal_panel.downgrade();
1068        pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
1069            let Some(project) = drop_closure_project.upgrade() else {
1070                return ControlFlow::Break(());
1071            };
1072            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
1073                let this_pane = cx.entity();
1074                let item = if tab.pane == this_pane {
1075                    pane.item_for_index(tab.ix)
1076                } else {
1077                    tab.pane.read(cx).item_for_index(tab.ix)
1078                };
1079                if let Some(item) = item {
1080                    if item.downcast::<TerminalView>().is_some() {
1081                        let source = tab.pane.clone();
1082                        let item_id_to_move = item.item_id();
1083
1084                        let Ok(new_split_pane) = pane
1085                            .drag_split_direction()
1086                            .map(|split_direction| {
1087                                drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
1088                                    let is_zoomed = if terminal_panel.active_pane == this_pane {
1089                                        pane.is_zoomed()
1090                                    } else {
1091                                        terminal_panel.active_pane.read(cx).is_zoomed()
1092                                    };
1093                                    let new_pane = new_terminal_pane(
1094                                        workspace.clone(),
1095                                        project.clone(),
1096                                        is_zoomed,
1097                                        window,
1098                                        cx,
1099                                    );
1100                                    terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
1101                                    terminal_panel.center.split(
1102                                        &this_pane,
1103                                        &new_pane,
1104                                        split_direction,
1105                                    )?;
1106                                    anyhow::Ok(new_pane)
1107                                })
1108                            })
1109                            .transpose()
1110                        else {
1111                            return ControlFlow::Break(());
1112                        };
1113
1114                        match new_split_pane.transpose() {
1115                            // Source pane may be the one currently updated, so defer the move.
1116                            Ok(Some(new_pane)) => cx
1117                                .spawn_in(window, async move |_, cx| {
1118                                    cx.update(|window, cx| {
1119                                        move_item(
1120                                            &source,
1121                                            &new_pane,
1122                                            item_id_to_move,
1123                                            new_pane.read(cx).active_item_index(),
1124                                            true,
1125                                            window,
1126                                            cx,
1127                                        );
1128                                    })
1129                                    .ok();
1130                                })
1131                                .detach(),
1132                            // If we drop into existing pane or current pane,
1133                            // regular pane drop handler will take care of it,
1134                            // using the right tab index for the operation.
1135                            Ok(None) => return ControlFlow::Continue(()),
1136                            err @ Err(_) => {
1137                                err.log_err();
1138                                return ControlFlow::Break(());
1139                            }
1140                        };
1141                    } else if let Some(project_path) = item.project_path(cx)
1142                        && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1143                    {
1144                        add_paths_to_terminal(pane, &[entry_path], window, cx);
1145                    }
1146                }
1147            } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1148                let project = project.read(cx);
1149                let paths_to_add = selection
1150                    .items()
1151                    .map(|selected_entry| selected_entry.entry_id)
1152                    .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1153                    .filter_map(|project_path| project.absolute_path(&project_path, cx))
1154                    .collect::<Vec<_>>();
1155                if !paths_to_add.is_empty() {
1156                    add_paths_to_terminal(pane, &paths_to_add, window, cx);
1157                }
1158            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1159                if let Some(entry_path) = project
1160                    .read(cx)
1161                    .path_for_entry(entry_id, cx)
1162                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1163                {
1164                    add_paths_to_terminal(pane, &[entry_path], window, cx);
1165                }
1166            } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1167                add_paths_to_terminal(pane, paths.paths(), window, cx);
1168            }
1169
1170            ControlFlow::Break(())
1171        });
1172
1173        pane
1174    });
1175
1176    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1177        .detach();
1178    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1179
1180    pane
1181}
1182
1183async fn wait_for_terminals_tasks(
1184    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1185    cx: &mut AsyncApp,
1186) {
1187    let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1188        terminal
1189            .update(cx, |terminal_view, cx| {
1190                terminal_view
1191                    .terminal()
1192                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1193            })
1194            .ok()
1195    });
1196    join_all(pending_tasks).await;
1197}
1198
1199fn add_paths_to_terminal(
1200    pane: &mut Pane,
1201    paths: &[PathBuf],
1202    window: &mut Window,
1203    cx: &mut Context<Pane>,
1204) {
1205    if let Some(terminal_view) = pane
1206        .active_item()
1207        .and_then(|item| item.downcast::<TerminalView>())
1208    {
1209        window.focus(&terminal_view.focus_handle(cx));
1210        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1211        new_text.push(' ');
1212        terminal_view.update(cx, |terminal_view, cx| {
1213            terminal_view.terminal().update(cx, |terminal, _| {
1214                terminal.paste(&new_text);
1215            });
1216        });
1217    }
1218}
1219
1220impl EventEmitter<PanelEvent> for TerminalPanel {}
1221
1222impl Render for TerminalPanel {
1223    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1224        let mut registrar = DivRegistrar::new(
1225            |panel, _, cx| {
1226                panel
1227                    .active_pane
1228                    .read(cx)
1229                    .toolbar()
1230                    .read(cx)
1231                    .item_of_type::<BufferSearchBar>()
1232            },
1233            cx,
1234        );
1235        BufferSearchBar::register(&mut registrar);
1236        let registrar = registrar.into_div();
1237        self.workspace
1238            .update(cx, |workspace, cx| {
1239                registrar.size_full().child(self.center.render(
1240                    workspace.zoomed_item(),
1241                    &workspace::PaneRenderContext {
1242                        follower_states: &HashMap::default(),
1243                        active_call: workspace.active_call(),
1244                        active_pane: &self.active_pane,
1245                        app_state: workspace.app_state(),
1246                        project: workspace.project(),
1247                        workspace: &workspace.weak_handle(),
1248                    },
1249                    window,
1250                    cx,
1251                ))
1252            })
1253            .ok()
1254            .map(|div| {
1255                div.on_action({
1256                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1257                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1258                    })
1259                })
1260                .on_action({
1261                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1262                        terminal_panel.activate_pane_in_direction(
1263                            SplitDirection::Right,
1264                            window,
1265                            cx,
1266                        );
1267                    })
1268                })
1269                .on_action({
1270                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1271                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1272                    })
1273                })
1274                .on_action({
1275                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1276                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1277                    })
1278                })
1279                .on_action(
1280                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1281                        let panes = terminal_panel.center.panes();
1282                        if let Some(ix) = panes
1283                            .iter()
1284                            .position(|pane| **pane == terminal_panel.active_pane)
1285                        {
1286                            let next_ix = (ix + 1) % panes.len();
1287                            window.focus(&panes[next_ix].focus_handle(cx));
1288                        }
1289                    }),
1290                )
1291                .on_action(cx.listener(
1292                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1293                        let panes = terminal_panel.center.panes();
1294                        if let Some(ix) = panes
1295                            .iter()
1296                            .position(|pane| **pane == terminal_panel.active_pane)
1297                        {
1298                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1299                            window.focus(&panes[prev_ix].focus_handle(cx));
1300                        }
1301                    },
1302                ))
1303                .on_action(
1304                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1305                        let panes = terminal_panel.center.panes();
1306                        if let Some(&pane) = panes.get(action.0) {
1307                            window.focus(&pane.read(cx).focus_handle(cx));
1308                        } else {
1309                            let future =
1310                                terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
1311                            cx.spawn_in(window, async move |terminal_panel, cx| {
1312                                if let Some(new_pane) = future.await {
1313                                    _ = terminal_panel.update_in(
1314                                        cx,
1315                                        |terminal_panel, window, cx| {
1316                                            terminal_panel
1317                                                .center
1318                                                .split(
1319                                                    &terminal_panel.active_pane,
1320                                                    &new_pane,
1321                                                    SplitDirection::Right,
1322                                                )
1323                                                .log_err();
1324                                            let new_pane = new_pane.read(cx);
1325                                            window.focus(&new_pane.focus_handle(cx));
1326                                        },
1327                                    );
1328                                }
1329                            })
1330                            .detach();
1331                        }
1332                    }),
1333                )
1334                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1335                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1336                }))
1337                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1338                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1339                }))
1340                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1341                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1342                }))
1343                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1344                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1345                }))
1346                .on_action(
1347                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1348                        let Some(&target_pane) =
1349                            terminal_panel.center.panes().get(action.destination)
1350                        else {
1351                            return;
1352                        };
1353                        move_active_item(
1354                            &terminal_panel.active_pane,
1355                            target_pane,
1356                            action.focus,
1357                            true,
1358                            window,
1359                            cx,
1360                        );
1361                    }),
1362                )
1363                .on_action(cx.listener(
1364                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1365                        let source_pane = &terminal_panel.active_pane;
1366                        if let Some(destination_pane) = terminal_panel
1367                            .center
1368                            .find_pane_in_direction(source_pane, action.direction, cx)
1369                        {
1370                            move_active_item(
1371                                source_pane,
1372                                destination_pane,
1373                                action.focus,
1374                                true,
1375                                window,
1376                                cx,
1377                            );
1378                        };
1379                    },
1380                ))
1381            })
1382            .unwrap_or_else(|| div())
1383    }
1384}
1385
1386impl Focusable for TerminalPanel {
1387    fn focus_handle(&self, cx: &App) -> FocusHandle {
1388        self.active_pane.focus_handle(cx)
1389    }
1390}
1391
1392impl Panel for TerminalPanel {
1393    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1394        match TerminalSettings::get_global(cx).dock {
1395            TerminalDockPosition::Left => DockPosition::Left,
1396            TerminalDockPosition::Bottom => DockPosition::Bottom,
1397            TerminalDockPosition::Right => DockPosition::Right,
1398        }
1399    }
1400
1401    fn position_is_valid(&self, _: DockPosition) -> bool {
1402        true
1403    }
1404
1405    fn set_position(
1406        &mut self,
1407        position: DockPosition,
1408        _window: &mut Window,
1409        cx: &mut Context<Self>,
1410    ) {
1411        settings::update_settings_file::<TerminalSettings>(
1412            self.fs.clone(),
1413            cx,
1414            move |settings, _| {
1415                let dock = match position {
1416                    DockPosition::Left => TerminalDockPosition::Left,
1417                    DockPosition::Bottom => TerminalDockPosition::Bottom,
1418                    DockPosition::Right => TerminalDockPosition::Right,
1419                };
1420                settings.dock = Some(dock);
1421            },
1422        );
1423    }
1424
1425    fn size(&self, window: &Window, cx: &App) -> Pixels {
1426        let settings = TerminalSettings::get_global(cx);
1427        match self.position(window, cx) {
1428            DockPosition::Left | DockPosition::Right => {
1429                self.width.unwrap_or(settings.default_width)
1430            }
1431            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1432        }
1433    }
1434
1435    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1436        match self.position(window, cx) {
1437            DockPosition::Left | DockPosition::Right => self.width = size,
1438            DockPosition::Bottom => self.height = size,
1439        }
1440        cx.notify();
1441        cx.defer_in(window, |this, _, cx| {
1442            this.serialize(cx);
1443        })
1444    }
1445
1446    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1447        self.active_pane.read(cx).is_zoomed()
1448    }
1449
1450    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1451        for pane in self.center.panes() {
1452            pane.update(cx, |pane, cx| {
1453                pane.set_zoomed(zoomed, cx);
1454            })
1455        }
1456        cx.notify();
1457    }
1458
1459    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1460        let old_active = self.active;
1461        self.active = active;
1462        if !active || old_active == active || !self.has_no_terminals(cx) {
1463            return;
1464        }
1465        cx.defer_in(window, |this, window, cx| {
1466            let Ok(kind) = this
1467                .workspace
1468                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1469            else {
1470                return;
1471            };
1472
1473            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1474                .detach_and_log_err(cx)
1475        })
1476    }
1477
1478    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1479        let count = self
1480            .center
1481            .panes()
1482            .into_iter()
1483            .map(|pane| pane.read(cx).items_len())
1484            .sum::<usize>();
1485        if count == 0 {
1486            None
1487        } else {
1488            Some(count.to_string())
1489        }
1490    }
1491
1492    fn persistent_name() -> &'static str {
1493        "TerminalPanel"
1494    }
1495
1496    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1497        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1498            && TerminalSettings::get_global(cx).button
1499        {
1500            Some(IconName::TerminalAlt)
1501        } else {
1502            None
1503        }
1504    }
1505
1506    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1507        Some("Terminal Panel")
1508    }
1509
1510    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1511        Box::new(ToggleFocus)
1512    }
1513
1514    fn pane(&self) -> Option<Entity<Pane>> {
1515        Some(self.active_pane.clone())
1516    }
1517
1518    fn activation_priority(&self) -> u32 {
1519        1
1520    }
1521}
1522
1523struct TerminalProvider(Entity<TerminalPanel>);
1524
1525impl workspace::TerminalProvider for TerminalProvider {
1526    fn spawn(
1527        &self,
1528        task: SpawnInTerminal,
1529        window: &mut Window,
1530        cx: &mut App,
1531    ) -> Task<Option<Result<ExitStatus>>> {
1532        let terminal_panel = self.0.clone();
1533        window.spawn(cx, async move |cx| {
1534            let terminal = terminal_panel
1535                .update_in(cx, |terminal_panel, window, cx| {
1536                    terminal_panel.spawn_task(task, window, cx)
1537                })
1538                .ok()?
1539                .await;
1540            match terminal {
1541                Ok(terminal) => {
1542                    let exit_status = terminal
1543                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1544                        .ok()?
1545                        .await?;
1546                    Some(Ok(exit_status))
1547                }
1548                Err(e) => Some(Err(e)),
1549            }
1550        })
1551    }
1552}
1553
1554struct InlineAssistTabBarButton {
1555    focus_handle: FocusHandle,
1556}
1557
1558impl Render for InlineAssistTabBarButton {
1559    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1560        let focus_handle = self.focus_handle.clone();
1561        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1562            .icon_size(IconSize::Small)
1563            .on_click(cx.listener(|_, _, window, cx| {
1564                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1565            }))
1566            .tooltip(move |window, cx| {
1567                Tooltip::for_action_in(
1568                    "Inline Assist",
1569                    &InlineAssist::default(),
1570                    &focus_handle,
1571                    window,
1572                    cx,
1573                )
1574            })
1575    }
1576}