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, TerminalDockPosition};
  22use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
  23use terminal::{Terminal, terminal_settings::TerminalSettings};
  24use ui::{
  25    ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
  26    Tooltip, prelude::*,
  27};
  28use util::{ResultExt, TryFutureExt};
  29use workspace::{
  30    ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
  31    ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
  32    MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane,
  33    PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown,
  34    SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
  35    dock::{DockPosition, Panel, PanelEvent, PanelHandle},
  36    item::SerializableItem,
  37    move_active_item, move_item, pane,
  38};
  39
  40use anyhow::{Result, anyhow};
  41use zed_actions::assistant::InlineAssist;
  42
  43const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  44
  45actions!(
  46    terminal_panel,
  47    [
  48        /// Toggles the terminal panel.
  49        Toggle,
  50        /// Toggles focus on the terminal panel.
  51        ToggleFocus
  52    ]
  53);
  54
  55pub fn init(cx: &mut App) {
  56    cx.observe_new(
  57        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
  58            workspace.register_action(TerminalPanel::new_terminal);
  59            workspace.register_action(TerminalPanel::open_terminal);
  60            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  61                if is_enabled_in_workspace(workspace, cx) {
  62                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
  63                }
  64            });
  65            workspace.register_action(|workspace, _: &Toggle, window, cx| {
  66                if is_enabled_in_workspace(workspace, cx) {
  67                    if !workspace.toggle_panel_focus::<TerminalPanel>(window, cx) {
  68                        workspace.close_panel::<TerminalPanel>(window, cx);
  69                    }
  70                }
  71            });
  72        },
  73    )
  74    .detach();
  75}
  76
  77pub struct TerminalPanel {
  78    pub(crate) active_pane: Entity<Pane>,
  79    pub(crate) center: PaneGroup,
  80    fs: Arc<dyn Fs>,
  81    workspace: WeakEntity<Workspace>,
  82    pub(crate) width: Option<Pixels>,
  83    pub(crate) height: Option<Pixels>,
  84    pending_serialization: Task<Option<()>>,
  85    pending_terminals_to_add: usize,
  86    deferred_tasks: HashMap<TaskId, Task<()>>,
  87    assistant_enabled: bool,
  88    assistant_tab_bar_button: Option<AnyView>,
  89    active: bool,
  90}
  91
  92impl TerminalPanel {
  93    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
  94        let project = workspace.project();
  95        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
  96        let center = PaneGroup::new(pane.clone());
  97        let terminal_panel = Self {
  98            center,
  99            active_pane: pane,
 100            fs: workspace.app_state().fs.clone(),
 101            workspace: workspace.weak_handle(),
 102            pending_serialization: Task::ready(None),
 103            width: None,
 104            height: None,
 105            pending_terminals_to_add: 0,
 106            deferred_tasks: HashMap::default(),
 107            assistant_enabled: false,
 108            assistant_tab_bar_button: None,
 109            active: false,
 110        };
 111        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 112        terminal_panel
 113    }
 114
 115    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
 116        self.assistant_enabled = enabled;
 117        if enabled {
 118            let focus_handle = self
 119                .active_pane
 120                .read(cx)
 121                .active_item()
 122                .map(|item| item.item_focus_handle(cx))
 123                .unwrap_or(self.focus_handle(cx));
 124            self.assistant_tab_bar_button = Some(
 125                cx.new(move |_| InlineAssistTabBarButton { focus_handle })
 126                    .into(),
 127            );
 128        } else {
 129            self.assistant_tab_bar_button = None;
 130        }
 131        for pane in self.center.panes() {
 132            self.apply_tab_bar_buttons(pane, cx);
 133        }
 134    }
 135
 136    fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
 137        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 138        terminal_pane.update(cx, |pane, cx| {
 139            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
 140                let split_context = pane
 141                    .active_item()
 142                    .and_then(|item| item.downcast::<TerminalView>())
 143                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
 144                if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
 145                    return (None, None);
 146                }
 147                let focus_handle = pane.focus_handle(cx);
 148                let right_children = h_flex()
 149                    .gap(DynamicSpacing::Base02.rems(cx))
 150                    .child(
 151                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 152                            .trigger_with_tooltip(
 153                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
 154                                Tooltip::text("New…"),
 155                            )
 156                            .anchor(Corner::TopRight)
 157                            .with_handle(pane.new_item_context_menu_handle.clone())
 158                            .menu(move |window, cx| {
 159                                let focus_handle = focus_handle.clone();
 160                                let menu = ContextMenu::build(window, cx, |menu, _, _| {
 161                                    menu.context(focus_handle.clone())
 162                                        .action(
 163                                            "New Terminal",
 164                                            workspace::NewTerminal::default().boxed_clone(),
 165                                        )
 166                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 167                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 168                                        // context menu will be gone the moment we spawn the modal.
 169                                        .action(
 170                                            "Spawn Task",
 171                                            zed_actions::Spawn::modal().boxed_clone(),
 172                                        )
 173                                });
 174
 175                                Some(menu)
 176                            }),
 177                    )
 178                    .children(assistant_tab_bar_button.clone())
 179                    .child(
 180                        PopoverMenu::new("terminal-pane-tab-bar-split")
 181                            .trigger_with_tooltip(
 182                                IconButton::new("terminal-pane-split", IconName::Split)
 183                                    .icon_size(IconSize::Small),
 184                                Tooltip::text("Split Pane"),
 185                            )
 186                            .anchor(Corner::TopRight)
 187                            .with_handle(pane.split_item_context_menu_handle.clone())
 188                            .menu({
 189                                move |window, cx| {
 190                                    ContextMenu::build(window, cx, |menu, _, _| {
 191                                        menu.when_some(
 192                                            split_context.clone(),
 193                                            |menu, split_context| menu.context(split_context),
 194                                        )
 195                                        .action("Split Right", SplitRight::default().boxed_clone())
 196                                        .action("Split Left", SplitLeft::default().boxed_clone())
 197                                        .action("Split Up", SplitUp::default().boxed_clone())
 198                                        .action("Split Down", SplitDown::default().boxed_clone())
 199                                    })
 200                                    .into()
 201                                }
 202                            }),
 203                    )
 204                    .child({
 205                        let zoomed = pane.is_zoomed();
 206                        IconButton::new("toggle_zoom", IconName::Maximize)
 207                            .icon_size(IconSize::Small)
 208                            .toggle_state(zoomed)
 209                            .selected_icon(IconName::Minimize)
 210                            .on_click(cx.listener(|pane, _, window, cx| {
 211                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
 212                            }))
 213                            .tooltip(move |_window, cx| {
 214                                Tooltip::for_action(
 215                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 216                                    &ToggleZoom,
 217                                    cx,
 218                                )
 219                            })
 220                    })
 221                    .into_any_element()
 222                    .into();
 223                (None, right_children)
 224            });
 225        });
 226    }
 227
 228    fn serialization_key(workspace: &Workspace) -> Option<String> {
 229        workspace
 230            .database_id()
 231            .map(|id| i64::from(id).to_string())
 232            .or(workspace.session_id())
 233            .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
 234    }
 235
 236    pub async fn load(
 237        workspace: WeakEntity<Workspace>,
 238        mut cx: AsyncWindowContext,
 239    ) -> Result<Entity<Self>> {
 240        let mut terminal_panel = None;
 241
 242        if let Some((database_id, serialization_key)) = workspace
 243            .read_with(&cx, |workspace, _| {
 244                workspace
 245                    .database_id()
 246                    .zip(TerminalPanel::serialization_key(workspace))
 247            })
 248            .ok()
 249            .flatten()
 250            && let Some(serialized_panel) = cx
 251                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
 252                .await
 253                .log_err()
 254                .flatten()
 255                .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 256                .transpose()
 257                .log_err()
 258                .flatten()
 259            && let Ok(serialized) = workspace
 260                .update_in(&mut cx, |workspace, window, cx| {
 261                    deserialize_terminal_panel(
 262                        workspace.weak_handle(),
 263                        workspace.project().clone(),
 264                        database_id,
 265                        serialized_panel,
 266                        window,
 267                        cx,
 268                    )
 269                })?
 270                .await
 271        {
 272            terminal_panel = Some(serialized);
 273        }
 274
 275        let terminal_panel = if let Some(panel) = terminal_panel {
 276            panel
 277        } else {
 278            workspace.update_in(&mut cx, |workspace, window, cx| {
 279                cx.new(|cx| TerminalPanel::new(workspace, window, cx))
 280            })?
 281        };
 282
 283        if let Some(workspace) = workspace.upgrade() {
 284            workspace.update(&mut cx, |workspace, _| {
 285                workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
 286            });
 287        }
 288
 289        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 290        if let Some(workspace) = workspace.upgrade() {
 291            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
 292                let alive_item_ids = terminal_panel
 293                    .read(cx)
 294                    .center
 295                    .panes()
 296                    .into_iter()
 297                    .flat_map(|pane| pane.read(cx).items())
 298                    .map(|item| item.item_id().as_u64() as ItemId)
 299                    .collect();
 300                workspace.database_id().map(|workspace_id| {
 301                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
 302                })
 303            })?;
 304            if let Some(task) = cleanup_task {
 305                task.await.log_err();
 306            }
 307        }
 308
 309        if let Some(workspace) = workspace.upgrade() {
 310            let should_focus = workspace
 311                .update_in(&mut cx, |workspace, window, cx| {
 312                    workspace.active_item(cx).is_none()
 313                        && workspace
 314                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
 315                })
 316                .unwrap_or(false);
 317
 318            if should_focus {
 319                terminal_panel
 320                    .update_in(&mut cx, |panel, window, cx| {
 321                        panel.active_pane.update(cx, |pane, cx| {
 322                            pane.focus_active_item(window, cx);
 323                        });
 324                    })
 325                    .ok();
 326            }
 327        }
 328        Ok(terminal_panel)
 329    }
 330
 331    fn handle_pane_event(
 332        &mut self,
 333        pane: &Entity<Pane>,
 334        event: &pane::Event,
 335        window: &mut Window,
 336        cx: &mut Context<Self>,
 337    ) {
 338        match event {
 339            pane::Event::ActivateItem { .. } => self.serialize(cx),
 340            pane::Event::RemovedItem { .. } => self.serialize(cx),
 341            pane::Event::Remove { focus_on_pane } => {
 342                let pane_count_before_removal = self.center.panes().len();
 343                let _removal_result = self.center.remove(pane, cx);
 344                if pane_count_before_removal == 1 {
 345                    self.center.first_pane().update(cx, |pane, cx| {
 346                        pane.set_zoomed(false, cx);
 347                    });
 348                    cx.emit(PanelEvent::Close);
 349                } else if let Some(focus_on_pane) =
 350                    focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 351                {
 352                    focus_on_pane.focus_handle(cx).focus(window, cx);
 353                }
 354            }
 355            pane::Event::ZoomIn => {
 356                for pane in self.center.panes() {
 357                    pane.update(cx, |pane, cx| {
 358                        pane.set_zoomed(true, cx);
 359                    })
 360                }
 361                cx.emit(PanelEvent::ZoomIn);
 362                cx.notify();
 363            }
 364            pane::Event::ZoomOut => {
 365                for pane in self.center.panes() {
 366                    pane.update(cx, |pane, cx| {
 367                        pane.set_zoomed(false, cx);
 368                    })
 369                }
 370                cx.emit(PanelEvent::ZoomOut);
 371                cx.notify();
 372            }
 373            pane::Event::AddItem { item } => {
 374                if let Some(workspace) = self.workspace.upgrade() {
 375                    workspace.update(cx, |workspace, cx| {
 376                        item.added_to_pane(workspace, pane.clone(), window, cx)
 377                    })
 378                }
 379                self.serialize(cx);
 380            }
 381            &pane::Event::Split { direction, mode } => {
 382                match mode {
 383                    SplitMode::ClonePane | SplitMode::EmptyPane => {
 384                        let clone = matches!(mode, SplitMode::ClonePane);
 385                        let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
 386                        let pane = pane.clone();
 387                        cx.spawn_in(window, async move |panel, cx| {
 388                            let Some(new_pane) = new_pane.await else {
 389                                return;
 390                            };
 391                            panel
 392                                .update_in(cx, |panel, window, cx| {
 393                                    panel
 394                                        .center
 395                                        .split(&pane, &new_pane, direction, cx)
 396                                        .log_err();
 397                                    window.focus(&new_pane.focus_handle(cx), cx);
 398                                })
 399                                .ok();
 400                        })
 401                        .detach();
 402                    }
 403                    SplitMode::MovePane => {
 404                        let Some(item) =
 405                            pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
 406                        else {
 407                            return;
 408                        };
 409                        let Ok(project) = self
 410                            .workspace
 411                            .update(cx, |workspace, _| workspace.project().clone())
 412                        else {
 413                            return;
 414                        };
 415                        let new_pane =
 416                            new_terminal_pane(self.workspace.clone(), project, false, window, cx);
 417                        new_pane.update(cx, |pane, cx| {
 418                            pane.add_item(item, true, true, None, window, cx);
 419                        });
 420                        self.center.split(&pane, &new_pane, direction, cx).log_err();
 421                        window.focus(&new_pane.focus_handle(cx), cx);
 422                    }
 423                };
 424            }
 425            pane::Event::Focus => {
 426                self.active_pane = pane.clone();
 427            }
 428            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
 429                self.serialize(cx);
 430            }
 431
 432            _ => {}
 433        }
 434    }
 435
 436    fn new_pane_with_active_terminal(
 437        &mut self,
 438        clone: bool,
 439        window: &mut Window,
 440        cx: &mut Context<Self>,
 441    ) -> Task<Option<Entity<Pane>>> {
 442        let Some(workspace) = self.workspace.upgrade() else {
 443            return Task::ready(None);
 444        };
 445        let workspace = workspace.read(cx);
 446        let database_id = workspace.database_id();
 447        let weak_workspace = self.workspace.clone();
 448        let project = workspace.project().clone();
 449        let active_pane = &self.active_pane;
 450        let terminal_view = if clone {
 451            active_pane
 452                .read(cx)
 453                .active_item()
 454                .and_then(|item| item.downcast::<TerminalView>())
 455        } else {
 456            None
 457        };
 458        let working_directory = if clone {
 459            terminal_view
 460                .as_ref()
 461                .and_then(|terminal_view| {
 462                    terminal_view
 463                        .read(cx)
 464                        .terminal()
 465                        .read(cx)
 466                        .working_directory()
 467                })
 468                .or_else(|| default_working_directory(workspace, cx))
 469        } else {
 470            default_working_directory(workspace, cx)
 471        };
 472
 473        let is_zoomed = if clone {
 474            active_pane.read(cx).is_zoomed()
 475        } else {
 476            false
 477        };
 478        cx.spawn_in(window, async move |panel, cx| {
 479            let terminal = project
 480                .update(cx, |project, cx| match terminal_view {
 481                    Some(view) => project.clone_terminal(
 482                        &view.read(cx).terminal.clone(),
 483                        cx,
 484                        working_directory,
 485                    ),
 486                    None => project.create_terminal_shell(working_directory, cx),
 487                })
 488                .await
 489                .log_err()?;
 490
 491            panel
 492                .update_in(cx, move |terminal_panel, window, cx| {
 493                    let terminal_view = Box::new(cx.new(|cx| {
 494                        TerminalView::new(
 495                            terminal.clone(),
 496                            weak_workspace.clone(),
 497                            database_id,
 498                            project.downgrade(),
 499                            window,
 500                            cx,
 501                        )
 502                    }));
 503                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
 504                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 505                    pane.update(cx, |pane, cx| {
 506                        pane.add_item(terminal_view, true, true, None, window, cx);
 507                    });
 508                    Some(pane)
 509                })
 510                .ok()
 511                .flatten()
 512        })
 513    }
 514
 515    pub fn open_terminal(
 516        workspace: &mut Workspace,
 517        action: &workspace::OpenTerminal,
 518        window: &mut Window,
 519        cx: &mut Context<Workspace>,
 520    ) {
 521        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 522            return;
 523        };
 524
 525        terminal_panel
 526            .update(cx, |panel, cx| {
 527                if action.local {
 528                    panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 529                } else {
 530                    panel.add_terminal_shell(
 531                        Some(action.working_directory.clone()),
 532                        RevealStrategy::Always,
 533                        window,
 534                        cx,
 535                    )
 536                }
 537            })
 538            .detach_and_log_err(cx);
 539    }
 540
 541    pub fn spawn_task(
 542        &mut self,
 543        task: &SpawnInTerminal,
 544        window: &mut Window,
 545        cx: &mut Context<Self>,
 546    ) -> Task<Result<WeakEntity<Terminal>>> {
 547        let Some(workspace) = self.workspace.upgrade() else {
 548            return Task::ready(Err(anyhow!("failed to read workspace")));
 549        };
 550
 551        let project = workspace.read(cx).project().read(cx);
 552
 553        if project.is_via_collab() {
 554            return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
 555        }
 556
 557        let remote_client = project.remote_client();
 558        let is_windows = project.path_style(cx).is_windows();
 559        let remote_shell = remote_client
 560            .as_ref()
 561            .and_then(|remote_client| remote_client.read(cx).shell());
 562
 563        let shell = if let Some(remote_shell) = remote_shell
 564            && task.shell == Shell::System
 565        {
 566            Shell::Program(remote_shell)
 567        } else {
 568            task.shell.clone()
 569        };
 570
 571        let task = prepare_task_for_spawn(task, &shell, is_windows);
 572
 573        if task.allow_concurrent_runs && task.use_new_terminal {
 574            return self.spawn_in_new_terminal(task, window, cx);
 575        }
 576
 577        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
 578        let Some(existing) = terminals_for_task.pop() else {
 579            return self.spawn_in_new_terminal(task, window, cx);
 580        };
 581
 582        let (existing_item_index, task_pane, existing_terminal) = existing;
 583        if task.allow_concurrent_runs {
 584            return self.replace_terminal(
 585                task,
 586                task_pane,
 587                existing_item_index,
 588                existing_terminal,
 589                window,
 590                cx,
 591            );
 592        }
 593
 594        let (tx, rx) = oneshot::channel();
 595
 596        self.deferred_tasks.insert(
 597            task.id.clone(),
 598            cx.spawn_in(window, async move |terminal_panel, cx| {
 599                wait_for_terminals_tasks(terminals_for_task, cx).await;
 600                let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
 601                    if task.use_new_terminal {
 602                        terminal_panel.spawn_in_new_terminal(task, window, cx)
 603                    } else {
 604                        terminal_panel.replace_terminal(
 605                            task,
 606                            task_pane,
 607                            existing_item_index,
 608                            existing_terminal,
 609                            window,
 610                            cx,
 611                        )
 612                    }
 613                });
 614                if let Ok(task) = task {
 615                    tx.send(task.await).ok();
 616                }
 617            }),
 618        );
 619
 620        cx.spawn(async move |_, _| rx.await?)
 621    }
 622
 623    fn spawn_in_new_terminal(
 624        &mut self,
 625        spawn_task: SpawnInTerminal,
 626        window: &mut Window,
 627        cx: &mut Context<Self>,
 628    ) -> Task<Result<WeakEntity<Terminal>>> {
 629        let reveal = spawn_task.reveal;
 630        let reveal_target = spawn_task.reveal_target;
 631        match reveal_target {
 632            RevealTarget::Center => self
 633                .workspace
 634                .update(cx, |workspace, cx| {
 635                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
 636                        project.create_terminal_task(spawn_task, cx)
 637                    })
 638                })
 639                .unwrap_or_else(|e| Task::ready(Err(e))),
 640            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
 641        }
 642    }
 643
 644    /// Create a new Terminal in the current working directory or the user's home directory
 645    fn new_terminal(
 646        workspace: &mut Workspace,
 647        action: &workspace::NewTerminal,
 648        window: &mut Window,
 649        cx: &mut Context<Workspace>,
 650    ) {
 651        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 652            return;
 653        };
 654
 655        terminal_panel
 656            .update(cx, |this, cx| {
 657                if action.local {
 658                    this.add_local_terminal_shell(RevealStrategy::Always, window, cx)
 659                } else {
 660                    this.add_terminal_shell(
 661                        default_working_directory(workspace, cx),
 662                        RevealStrategy::Always,
 663                        window,
 664                        cx,
 665                    )
 666                }
 667            })
 668            .detach_and_log_err(cx);
 669    }
 670
 671    fn terminals_for_task(
 672        &self,
 673        label: &str,
 674        cx: &mut App,
 675    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 676        let Some(workspace) = self.workspace.upgrade() else {
 677            return Vec::new();
 678        };
 679
 680        let pane_terminal_views = |pane: Entity<Pane>| {
 681            pane.read(cx)
 682                .items()
 683                .enumerate()
 684                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 685                .filter_map(|(index, terminal_view)| {
 686                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 687                    if &task_state.spawned_task.full_label == label {
 688                        Some((index, terminal_view))
 689                    } else {
 690                        None
 691                    }
 692                })
 693                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 694        };
 695
 696        self.center
 697            .panes()
 698            .into_iter()
 699            .cloned()
 700            .flat_map(pane_terminal_views)
 701            .chain(
 702                workspace
 703                    .read(cx)
 704                    .panes()
 705                    .iter()
 706                    .cloned()
 707                    .flat_map(pane_terminal_views),
 708            )
 709            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 710            .collect()
 711    }
 712
 713    fn activate_terminal_view(
 714        &self,
 715        pane: &Entity<Pane>,
 716        item_index: usize,
 717        focus: bool,
 718        window: &mut Window,
 719        cx: &mut App,
 720    ) {
 721        pane.update(cx, |pane, cx| {
 722            pane.activate_item(item_index, true, focus, window, cx)
 723        })
 724    }
 725
 726    pub fn add_center_terminal(
 727        workspace: &mut Workspace,
 728        window: &mut Window,
 729        cx: &mut Context<Workspace>,
 730        create_terminal: impl FnOnce(
 731            &mut Project,
 732            &mut Context<Project>,
 733        ) -> Task<Result<Entity<Terminal>>>
 734        + 'static,
 735    ) -> Task<Result<WeakEntity<Terminal>>> {
 736        if !is_enabled_in_workspace(workspace, cx) {
 737            return Task::ready(Err(anyhow!(
 738                "terminal not yet supported for remote projects"
 739            )));
 740        }
 741        let project = workspace.project().downgrade();
 742        cx.spawn_in(window, async move |workspace, cx| {
 743            let terminal = project.update(cx, create_terminal)?.await?;
 744
 745            workspace.update_in(cx, |workspace, window, cx| {
 746                let terminal_view = cx.new(|cx| {
 747                    TerminalView::new(
 748                        terminal.clone(),
 749                        workspace.weak_handle(),
 750                        workspace.database_id(),
 751                        workspace.project().downgrade(),
 752                        window,
 753                        cx,
 754                    )
 755                });
 756                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 757            })?;
 758            Ok(terminal.downgrade())
 759        })
 760    }
 761
 762    pub fn add_terminal_task(
 763        &mut self,
 764        task: SpawnInTerminal,
 765        reveal_strategy: RevealStrategy,
 766        window: &mut Window,
 767        cx: &mut Context<Self>,
 768    ) -> Task<Result<WeakEntity<Terminal>>> {
 769        let workspace = self.workspace.clone();
 770        cx.spawn_in(window, async move |terminal_panel, cx| {
 771            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 772                anyhow::bail!("terminal not yet supported for remote projects");
 773            }
 774            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 775                terminal_panel.pending_terminals_to_add += 1;
 776                terminal_panel.active_pane.clone()
 777            })?;
 778            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 779            let terminal = project
 780                .update(cx, |project, cx| project.create_terminal_task(task, cx))
 781                .await?;
 782            let result = workspace.update_in(cx, |workspace, window, cx| {
 783                let terminal_view = Box::new(cx.new(|cx| {
 784                    TerminalView::new(
 785                        terminal.clone(),
 786                        workspace.weak_handle(),
 787                        workspace.database_id(),
 788                        workspace.project().downgrade(),
 789                        window,
 790                        cx,
 791                    )
 792                }));
 793
 794                match reveal_strategy {
 795                    RevealStrategy::Always => {
 796                        workspace.focus_panel::<Self>(window, cx);
 797                    }
 798                    RevealStrategy::NoFocus => {
 799                        workspace.open_panel::<Self>(window, cx);
 800                    }
 801                    RevealStrategy::Never => {}
 802                }
 803
 804                pane.update(cx, |pane, cx| {
 805                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
 806                    pane.add_item(terminal_view, true, focus, None, window, cx);
 807                });
 808
 809                Ok(terminal.downgrade())
 810            })?;
 811            terminal_panel.update(cx, |terminal_panel, cx| {
 812                terminal_panel.pending_terminals_to_add =
 813                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
 814                terminal_panel.serialize(cx)
 815            })?;
 816            result
 817        })
 818    }
 819
 820    fn add_terminal_shell(
 821        &mut self,
 822        cwd: Option<PathBuf>,
 823        reveal_strategy: RevealStrategy,
 824        window: &mut Window,
 825        cx: &mut Context<Self>,
 826    ) -> Task<Result<WeakEntity<Terminal>>> {
 827        self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx)
 828    }
 829
 830    fn add_local_terminal_shell(
 831        &mut self,
 832        reveal_strategy: RevealStrategy,
 833        window: &mut Window,
 834        cx: &mut Context<Self>,
 835    ) -> Task<Result<WeakEntity<Terminal>>> {
 836        self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx)
 837    }
 838
 839    fn add_terminal_shell_internal(
 840        &mut self,
 841        force_local: bool,
 842        cwd: Option<PathBuf>,
 843        reveal_strategy: RevealStrategy,
 844        window: &mut Window,
 845        cx: &mut Context<Self>,
 846    ) -> Task<Result<WeakEntity<Terminal>>> {
 847        let workspace = self.workspace.clone();
 848
 849        cx.spawn_in(window, async move |terminal_panel, cx| {
 850            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
 851                anyhow::bail!("terminal not yet supported for collaborative projects");
 852            }
 853            let pane = terminal_panel.update(cx, |terminal_panel, _| {
 854                terminal_panel.pending_terminals_to_add += 1;
 855                terminal_panel.active_pane.clone()
 856            })?;
 857            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 858            let terminal = if force_local {
 859                project
 860                    .update(cx, |project, cx| project.create_local_terminal(cx))
 861                    .await
 862            } else {
 863                project
 864                    .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))
 865                    .await
 866            };
 867
 868            match terminal {
 869                Ok(terminal) => {
 870                    let result = workspace.update_in(cx, |workspace, window, cx| {
 871                        let terminal_view = Box::new(cx.new(|cx| {
 872                            TerminalView::new(
 873                                terminal.clone(),
 874                                workspace.weak_handle(),
 875                                workspace.database_id(),
 876                                workspace.project().downgrade(),
 877                                window,
 878                                cx,
 879                            )
 880                        }));
 881
 882                        match reveal_strategy {
 883                            RevealStrategy::Always => {
 884                                workspace.focus_panel::<Self>(window, cx);
 885                            }
 886                            RevealStrategy::NoFocus => {
 887                                workspace.open_panel::<Self>(window, cx);
 888                            }
 889                            RevealStrategy::Never => {}
 890                        }
 891
 892                        pane.update(cx, |pane, cx| {
 893                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
 894                            pane.add_item(terminal_view, true, focus, None, window, cx);
 895                        });
 896
 897                        Ok(terminal.downgrade())
 898                    })?;
 899                    terminal_panel.update(cx, |terminal_panel, cx| {
 900                        terminal_panel.pending_terminals_to_add =
 901                            terminal_panel.pending_terminals_to_add.saturating_sub(1);
 902                        terminal_panel.serialize(cx)
 903                    })?;
 904                    result
 905                }
 906                Err(error) => {
 907                    pane.update_in(cx, |pane, window, cx| {
 908                        let focus = pane.has_focus(window, cx);
 909                        let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
 910                            error: error.to_string(),
 911                            focus_handle: cx.focus_handle(),
 912                        });
 913                        pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
 914                    })?;
 915                    Err(error)
 916                }
 917            }
 918        })
 919    }
 920
 921    fn serialize(&mut self, cx: &mut Context<Self>) {
 922        let height = self.height;
 923        let width = self.width;
 924        let Some(serialization_key) = self
 925            .workspace
 926            .read_with(cx, |workspace, _| {
 927                TerminalPanel::serialization_key(workspace)
 928            })
 929            .ok()
 930            .flatten()
 931        else {
 932            return;
 933        };
 934        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
 935            cx.background_executor()
 936                .timer(Duration::from_millis(50))
 937                .await;
 938            let terminal_panel = terminal_panel.upgrade()?;
 939            let items = terminal_panel.update(cx, |terminal_panel, cx| {
 940                SerializedItems::WithSplits(serialize_pane_group(
 941                    &terminal_panel.center,
 942                    &terminal_panel.active_pane,
 943                    cx,
 944                ))
 945            });
 946            cx.background_spawn(
 947                async move {
 948                    KEY_VALUE_STORE
 949                        .write_kvp(
 950                            serialization_key,
 951                            serde_json::to_string(&SerializedTerminalPanel {
 952                                items,
 953                                active_item_id: None,
 954                                height,
 955                                width,
 956                            })?,
 957                        )
 958                        .await?;
 959                    anyhow::Ok(())
 960                }
 961                .log_err(),
 962            )
 963            .await;
 964            Some(())
 965        });
 966    }
 967
 968    fn replace_terminal(
 969        &self,
 970        spawn_task: SpawnInTerminal,
 971        task_pane: Entity<Pane>,
 972        terminal_item_index: usize,
 973        terminal_to_replace: Entity<TerminalView>,
 974        window: &mut Window,
 975        cx: &mut Context<Self>,
 976    ) -> Task<Result<WeakEntity<Terminal>>> {
 977        let reveal = spawn_task.reveal;
 978        let task_workspace = self.workspace.clone();
 979        cx.spawn_in(window, async move |terminal_panel, cx| {
 980            let project = terminal_panel.update(cx, |this, cx| {
 981                this.workspace
 982                    .update(cx, |workspace, _| workspace.project().clone())
 983            })??;
 984            let new_terminal = project
 985                .update(cx, |project, cx| {
 986                    project.create_terminal_task(spawn_task, cx)
 987                })
 988                .await?;
 989            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
 990                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
 991            })?;
 992
 993            let reveal_target = terminal_panel.update(cx, |panel, _| {
 994                if panel.center.panes().iter().any(|p| **p == task_pane) {
 995                    RevealTarget::Dock
 996                } else {
 997                    RevealTarget::Center
 998                }
 999            })?;
1000
1001            match reveal {
1002                RevealStrategy::Always => match reveal_target {
1003                    RevealTarget::Center => {
1004                        task_workspace.update_in(cx, |workspace, window, cx| {
1005                            let did_activate = workspace.activate_item(
1006                                &terminal_to_replace,
1007                                true,
1008                                true,
1009                                window,
1010                                cx,
1011                            );
1012
1013                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
1014
1015                            anyhow::Ok(())
1016                        })??;
1017                    }
1018                    RevealTarget::Dock => {
1019                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1020                            terminal_panel.activate_terminal_view(
1021                                &task_pane,
1022                                terminal_item_index,
1023                                true,
1024                                window,
1025                                cx,
1026                            )
1027                        })?;
1028
1029                        cx.spawn(async move |cx| {
1030                            task_workspace
1031                                .update_in(cx, |workspace, window, cx| {
1032                                    workspace.focus_panel::<Self>(window, cx)
1033                                })
1034                                .ok()
1035                        })
1036                        .detach();
1037                    }
1038                },
1039                RevealStrategy::NoFocus => match reveal_target {
1040                    RevealTarget::Center => {
1041                        task_workspace.update_in(cx, |workspace, window, cx| {
1042                            workspace.active_pane().focus_handle(cx).focus(window, cx);
1043                        })?;
1044                    }
1045                    RevealTarget::Dock => {
1046                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1047                            terminal_panel.activate_terminal_view(
1048                                &task_pane,
1049                                terminal_item_index,
1050                                false,
1051                                window,
1052                                cx,
1053                            )
1054                        })?;
1055
1056                        cx.spawn(async move |cx| {
1057                            task_workspace
1058                                .update_in(cx, |workspace, window, cx| {
1059                                    workspace.open_panel::<Self>(window, cx)
1060                                })
1061                                .ok()
1062                        })
1063                        .detach();
1064                    }
1065                },
1066                RevealStrategy::Never => {}
1067            }
1068
1069            Ok(new_terminal.downgrade())
1070        })
1071    }
1072
1073    fn has_no_terminals(&self, cx: &App) -> bool {
1074        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1075    }
1076
1077    pub fn assistant_enabled(&self) -> bool {
1078        self.assistant_enabled
1079    }
1080
1081    fn is_enabled(&self, cx: &App) -> bool {
1082        self.workspace
1083            .upgrade()
1084            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1085    }
1086
1087    fn activate_pane_in_direction(
1088        &mut self,
1089        direction: SplitDirection,
1090        window: &mut Window,
1091        cx: &mut Context<Self>,
1092    ) {
1093        if let Some(pane) = self
1094            .center
1095            .find_pane_in_direction(&self.active_pane, direction, cx)
1096        {
1097            window.focus(&pane.focus_handle(cx), cx);
1098        } else {
1099            self.workspace
1100                .update(cx, |workspace, cx| {
1101                    workspace.activate_pane_in_direction(direction, window, cx)
1102                })
1103                .ok();
1104        }
1105    }
1106
1107    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1108        if let Some(to) = self
1109            .center
1110            .find_pane_in_direction(&self.active_pane, direction, cx)
1111            .cloned()
1112        {
1113            self.center.swap(&self.active_pane, &to, cx);
1114            cx.notify();
1115        }
1116    }
1117
1118    fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1119        if self
1120            .center
1121            .move_to_border(&self.active_pane, direction, cx)
1122            .unwrap()
1123        {
1124            cx.notify();
1125        }
1126    }
1127}
1128
1129/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
1130/// based on the shell configuration. This is a pure function that can be tested
1131/// without spawning actual terminals.
1132pub fn prepare_task_for_spawn(
1133    task: &SpawnInTerminal,
1134    shell: &Shell,
1135    is_windows: bool,
1136) -> SpawnInTerminal {
1137    let builder = ShellBuilder::new(shell, is_windows);
1138    let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
1139    let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
1140
1141    SpawnInTerminal {
1142        command_label,
1143        command: Some(command),
1144        args,
1145        ..task.clone()
1146    }
1147}
1148
1149fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1150    workspace.project().read(cx).supports_terminal(cx)
1151}
1152
1153pub fn new_terminal_pane(
1154    workspace: WeakEntity<Workspace>,
1155    project: Entity<Project>,
1156    zoomed: bool,
1157    window: &mut Window,
1158    cx: &mut Context<TerminalPanel>,
1159) -> Entity<Pane> {
1160    let is_local = project.read(cx).is_local();
1161    let terminal_panel = cx.entity();
1162    let pane = cx.new(|cx| {
1163        let mut pane = Pane::new(
1164            workspace.clone(),
1165            project.clone(),
1166            Default::default(),
1167            None,
1168            workspace::NewTerminal::default().boxed_clone(),
1169            false,
1170            window,
1171            cx,
1172        );
1173        pane.set_zoomed(zoomed, cx);
1174        pane.set_can_navigate(false, cx);
1175        pane.display_nav_history_buttons(None);
1176        pane.set_should_display_tab_bar(|_, _| true);
1177        pane.set_zoom_out_on_close(false);
1178
1179        let split_closure_terminal_panel = terminal_panel.downgrade();
1180        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1181            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1182                let is_current_pane = tab.pane == cx.entity();
1183                let Some(can_drag_away) = split_closure_terminal_panel
1184                    .read_with(cx, |terminal_panel, _| {
1185                        let current_panes = terminal_panel.center.panes();
1186                        !current_panes.contains(&&tab.pane)
1187                            || current_panes.len() > 1
1188                            || (!is_current_pane || pane.items_len() > 1)
1189                    })
1190                    .ok()
1191                else {
1192                    return false;
1193                };
1194                if can_drag_away {
1195                    let item = if is_current_pane {
1196                        pane.item_for_index(tab.ix)
1197                    } else {
1198                        tab.pane.read(cx).item_for_index(tab.ix)
1199                    };
1200                    if let Some(item) = item {
1201                        return item.downcast::<TerminalView>().is_some();
1202                    }
1203                }
1204            }
1205            false
1206        })));
1207
1208        let buffer_search_bar = cx.new(|cx| {
1209            search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
1210        });
1211        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1212        pane.toolbar().update(cx, |toolbar, cx| {
1213            toolbar.add_item(buffer_search_bar, window, cx);
1214            toolbar.add_item(breadcrumbs, window, cx);
1215        });
1216
1217        let drop_closure_project = project.downgrade();
1218        let drop_closure_terminal_panel = terminal_panel.downgrade();
1219        pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
1220            let Some(project) = drop_closure_project.upgrade() else {
1221                return ControlFlow::Break(());
1222            };
1223            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
1224                let this_pane = cx.entity();
1225                let item = if tab.pane == this_pane {
1226                    pane.item_for_index(tab.ix)
1227                } else {
1228                    tab.pane.read(cx).item_for_index(tab.ix)
1229                };
1230                if let Some(item) = item {
1231                    if item.downcast::<TerminalView>().is_some() {
1232                        let source = tab.pane.clone();
1233                        let item_id_to_move = item.item_id();
1234
1235                        // If no split direction, let the regular pane drop handler take care of it
1236                        let Some(split_direction) = pane.drag_split_direction() else {
1237                            return ControlFlow::Continue(());
1238                        };
1239
1240                        // Gather data synchronously before deferring
1241                        let is_zoomed = drop_closure_terminal_panel
1242                            .upgrade()
1243                            .map(|terminal_panel| {
1244                                let terminal_panel = terminal_panel.read(cx);
1245                                if terminal_panel.active_pane == this_pane {
1246                                    pane.is_zoomed()
1247                                } else {
1248                                    terminal_panel.active_pane.read(cx).is_zoomed()
1249                                }
1250                            })
1251                            .unwrap_or(false);
1252
1253                        let workspace = workspace.clone();
1254                        let terminal_panel = drop_closure_terminal_panel.clone();
1255
1256                        // Defer the split operation to avoid re-entrancy panic.
1257                        // The pane may be the one currently being updated, so we cannot
1258                        // call mark_positions (via split) synchronously.
1259                        cx.spawn_in(window, async move |_, cx| {
1260                            cx.update(|window, cx| {
1261                                let Ok(new_pane) =
1262                                    terminal_panel.update(cx, |terminal_panel, cx| {
1263                                        let new_pane = new_terminal_pane(
1264                                            workspace, project, is_zoomed, window, cx,
1265                                        );
1266                                        terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
1267                                        terminal_panel.center.split(
1268                                            &this_pane,
1269                                            &new_pane,
1270                                            split_direction,
1271                                            cx,
1272                                        )?;
1273                                        anyhow::Ok(new_pane)
1274                                    })
1275                                else {
1276                                    return;
1277                                };
1278
1279                                let Some(new_pane) = new_pane.log_err() else {
1280                                    return;
1281                                };
1282
1283                                move_item(
1284                                    &source,
1285                                    &new_pane,
1286                                    item_id_to_move,
1287                                    new_pane.read(cx).active_item_index(),
1288                                    true,
1289                                    window,
1290                                    cx,
1291                                );
1292                            })
1293                            .ok();
1294                        })
1295                        .detach();
1296                    } else if let Some(project_path) = item.project_path(cx)
1297                        && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1298                    {
1299                        add_paths_to_terminal(pane, &[entry_path], window, cx);
1300                    }
1301                }
1302            } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1303                let project = project.read(cx);
1304                let paths_to_add = selection
1305                    .items()
1306                    .map(|selected_entry| selected_entry.entry_id)
1307                    .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1308                    .filter_map(|project_path| project.absolute_path(&project_path, cx))
1309                    .collect::<Vec<_>>();
1310                if !paths_to_add.is_empty() {
1311                    add_paths_to_terminal(pane, &paths_to_add, window, cx);
1312                }
1313            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1314                if let Some(entry_path) = project
1315                    .read(cx)
1316                    .path_for_entry(entry_id, cx)
1317                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1318                {
1319                    add_paths_to_terminal(pane, &[entry_path], window, cx);
1320                }
1321            } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1322                add_paths_to_terminal(pane, paths.paths(), window, cx);
1323            }
1324
1325            ControlFlow::Break(())
1326        });
1327
1328        pane
1329    });
1330
1331    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1332        .detach();
1333    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1334
1335    pane
1336}
1337
1338async fn wait_for_terminals_tasks(
1339    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1340    cx: &mut AsyncApp,
1341) {
1342    let pending_tasks = terminals_for_task.iter().map(|(_, _, terminal)| {
1343        terminal.update(cx, |terminal_view, cx| {
1344            terminal_view
1345                .terminal()
1346                .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1347        })
1348    });
1349    join_all(pending_tasks).await;
1350}
1351
1352fn add_paths_to_terminal(
1353    pane: &mut Pane,
1354    paths: &[PathBuf],
1355    window: &mut Window,
1356    cx: &mut Context<Pane>,
1357) {
1358    if let Some(terminal_view) = pane
1359        .active_item()
1360        .and_then(|item| item.downcast::<TerminalView>())
1361    {
1362        window.focus(&terminal_view.focus_handle(cx), cx);
1363        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1364        new_text.push(' ');
1365        terminal_view.update(cx, |terminal_view, cx| {
1366            terminal_view.terminal().update(cx, |terminal, _| {
1367                terminal.paste(&new_text);
1368            });
1369        });
1370    }
1371}
1372
1373struct FailedToSpawnTerminal {
1374    error: String,
1375    focus_handle: FocusHandle,
1376}
1377
1378impl Focusable for FailedToSpawnTerminal {
1379    fn focus_handle(&self, _: &App) -> FocusHandle {
1380        self.focus_handle.clone()
1381    }
1382}
1383
1384impl Render for FailedToSpawnTerminal {
1385    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1386        let popover_menu = PopoverMenu::new("settings-popover")
1387            .trigger(
1388                IconButton::new("icon-button-popover", IconName::ChevronDown)
1389                    .icon_size(IconSize::XSmall),
1390            )
1391            .menu(move |window, cx| {
1392                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1393                    context_menu
1394                        .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1395                        .action(
1396                            "Edit settings.json",
1397                            zed_actions::OpenSettingsFile.boxed_clone(),
1398                        )
1399                }))
1400            })
1401            .anchor(Corner::TopRight)
1402            .offset(gpui::Point {
1403                x: px(0.0),
1404                y: px(2.0),
1405            });
1406
1407        v_flex()
1408            .track_focus(&self.focus_handle)
1409            .size_full()
1410            .p_4()
1411            .items_center()
1412            .justify_center()
1413            .bg(cx.theme().colors().editor_background)
1414            .child(
1415                v_flex()
1416                    .max_w_112()
1417                    .items_center()
1418                    .justify_center()
1419                    .text_center()
1420                    .child(Label::new("Failed to spawn terminal"))
1421                    .child(
1422                        Label::new(self.error.to_string())
1423                            .size(LabelSize::Small)
1424                            .color(Color::Muted)
1425                            .mb_4(),
1426                    )
1427                    .child(SplitButton::new(
1428                        ButtonLike::new("open-settings-ui")
1429                            .child(Label::new("Edit Settings").size(LabelSize::Small))
1430                            .on_click(|_, window, cx| {
1431                                window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1432                            }),
1433                        popover_menu.into_any_element(),
1434                    )),
1435            )
1436    }
1437}
1438
1439impl EventEmitter<()> for FailedToSpawnTerminal {}
1440
1441impl workspace::Item for FailedToSpawnTerminal {
1442    type Event = ();
1443
1444    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1445        SharedString::new_static("Failed to spawn terminal")
1446    }
1447}
1448
1449impl EventEmitter<PanelEvent> for TerminalPanel {}
1450
1451impl Render for TerminalPanel {
1452    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1453        let mut registrar = DivRegistrar::new(
1454            |panel, _, cx| {
1455                panel
1456                    .active_pane
1457                    .read(cx)
1458                    .toolbar()
1459                    .read(cx)
1460                    .item_of_type::<BufferSearchBar>()
1461            },
1462            cx,
1463        );
1464        BufferSearchBar::register(&mut registrar);
1465        let registrar = registrar.into_div();
1466        self.workspace
1467            .update(cx, |workspace, cx| {
1468                registrar.size_full().child(self.center.render(
1469                    workspace.zoomed_item(),
1470                    &workspace::PaneRenderContext {
1471                        follower_states: &HashMap::default(),
1472                        active_call: workspace.active_call(),
1473                        active_pane: &self.active_pane,
1474                        app_state: workspace.app_state(),
1475                        project: workspace.project(),
1476                        workspace: &workspace.weak_handle(),
1477                    },
1478                    window,
1479                    cx,
1480                ))
1481            })
1482            .ok()
1483            .map(|div| {
1484                div.on_action({
1485                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1486                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1487                    })
1488                })
1489                .on_action({
1490                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1491                        terminal_panel.activate_pane_in_direction(
1492                            SplitDirection::Right,
1493                            window,
1494                            cx,
1495                        );
1496                    })
1497                })
1498                .on_action({
1499                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1500                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1501                    })
1502                })
1503                .on_action({
1504                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1505                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1506                    })
1507                })
1508                .on_action(
1509                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1510                        let panes = terminal_panel.center.panes();
1511                        if let Some(ix) = panes
1512                            .iter()
1513                            .position(|pane| **pane == terminal_panel.active_pane)
1514                        {
1515                            let next_ix = (ix + 1) % panes.len();
1516                            window.focus(&panes[next_ix].focus_handle(cx), cx);
1517                        }
1518                    }),
1519                )
1520                .on_action(cx.listener(
1521                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1522                        let panes = terminal_panel.center.panes();
1523                        if let Some(ix) = panes
1524                            .iter()
1525                            .position(|pane| **pane == terminal_panel.active_pane)
1526                        {
1527                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1528                            window.focus(&panes[prev_ix].focus_handle(cx), cx);
1529                        }
1530                    },
1531                ))
1532                .on_action(
1533                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1534                        let panes = terminal_panel.center.panes();
1535                        if let Some(&pane) = panes.get(action.0) {
1536                            window.focus(&pane.read(cx).focus_handle(cx), cx);
1537                        } else {
1538                            let future =
1539                                terminal_panel.new_pane_with_active_terminal(true, window, cx);
1540                            cx.spawn_in(window, async move |terminal_panel, cx| {
1541                                if let Some(new_pane) = future.await {
1542                                    _ = terminal_panel.update_in(
1543                                        cx,
1544                                        |terminal_panel, window, cx| {
1545                                            terminal_panel
1546                                                .center
1547                                                .split(
1548                                                    &terminal_panel.active_pane,
1549                                                    &new_pane,
1550                                                    SplitDirection::Right,
1551                                                    cx,
1552                                                )
1553                                                .log_err();
1554                                            let new_pane = new_pane.read(cx);
1555                                            window.focus(&new_pane.focus_handle(cx), cx);
1556                                        },
1557                                    );
1558                                }
1559                            })
1560                            .detach();
1561                        }
1562                    }),
1563                )
1564                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1565                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1566                }))
1567                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1568                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1569                }))
1570                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1571                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1572                }))
1573                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1574                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1575                }))
1576                .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1577                    terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1578                }))
1579                .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1580                    terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1581                }))
1582                .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1583                    terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1584                }))
1585                .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1586                    terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1587                }))
1588                .on_action(
1589                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1590                        let Some(&target_pane) =
1591                            terminal_panel.center.panes().get(action.destination)
1592                        else {
1593                            return;
1594                        };
1595                        move_active_item(
1596                            &terminal_panel.active_pane,
1597                            target_pane,
1598                            action.focus,
1599                            true,
1600                            window,
1601                            cx,
1602                        );
1603                    }),
1604                )
1605                .on_action(cx.listener(
1606                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1607                        let source_pane = &terminal_panel.active_pane;
1608                        if let Some(destination_pane) = terminal_panel
1609                            .center
1610                            .find_pane_in_direction(source_pane, action.direction, cx)
1611                        {
1612                            move_active_item(
1613                                source_pane,
1614                                destination_pane,
1615                                action.focus,
1616                                true,
1617                                window,
1618                                cx,
1619                            );
1620                        };
1621                    },
1622                ))
1623            })
1624            .unwrap_or_else(|| div())
1625    }
1626}
1627
1628impl Focusable for TerminalPanel {
1629    fn focus_handle(&self, cx: &App) -> FocusHandle {
1630        self.active_pane.focus_handle(cx)
1631    }
1632}
1633
1634impl Panel for TerminalPanel {
1635    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1636        match TerminalSettings::get_global(cx).dock {
1637            TerminalDockPosition::Left => DockPosition::Left,
1638            TerminalDockPosition::Bottom => DockPosition::Bottom,
1639            TerminalDockPosition::Right => DockPosition::Right,
1640        }
1641    }
1642
1643    fn position_is_valid(&self, _: DockPosition) -> bool {
1644        true
1645    }
1646
1647    fn set_position(
1648        &mut self,
1649        position: DockPosition,
1650        _window: &mut Window,
1651        cx: &mut Context<Self>,
1652    ) {
1653        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1654            let dock = match position {
1655                DockPosition::Left => TerminalDockPosition::Left,
1656                DockPosition::Bottom => TerminalDockPosition::Bottom,
1657                DockPosition::Right => TerminalDockPosition::Right,
1658            };
1659            settings.terminal.get_or_insert_default().dock = Some(dock);
1660        });
1661    }
1662
1663    fn size(&self, window: &Window, cx: &App) -> Pixels {
1664        let settings = TerminalSettings::get_global(cx);
1665        match self.position(window, cx) {
1666            DockPosition::Left | DockPosition::Right => {
1667                self.width.unwrap_or(settings.default_width)
1668            }
1669            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1670        }
1671    }
1672
1673    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1674        match self.position(window, cx) {
1675            DockPosition::Left | DockPosition::Right => self.width = size,
1676            DockPosition::Bottom => self.height = size,
1677        }
1678        cx.notify();
1679        cx.defer_in(window, |this, _, cx| {
1680            this.serialize(cx);
1681        })
1682    }
1683
1684    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1685        self.active_pane.read(cx).is_zoomed()
1686    }
1687
1688    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1689        for pane in self.center.panes() {
1690            pane.update(cx, |pane, cx| {
1691                pane.set_zoomed(zoomed, cx);
1692            })
1693        }
1694        cx.notify();
1695    }
1696
1697    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1698        let old_active = self.active;
1699        self.active = active;
1700        if !active || old_active == active || !self.has_no_terminals(cx) {
1701            return;
1702        }
1703        cx.defer_in(window, |this, window, cx| {
1704            let Ok(kind) = this
1705                .workspace
1706                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1707            else {
1708                return;
1709            };
1710
1711            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1712                .detach_and_log_err(cx)
1713        })
1714    }
1715
1716    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1717        let count = self
1718            .center
1719            .panes()
1720            .into_iter()
1721            .map(|pane| pane.read(cx).items_len())
1722            .sum::<usize>();
1723        if count == 0 {
1724            None
1725        } else {
1726            Some(count.to_string())
1727        }
1728    }
1729
1730    fn persistent_name() -> &'static str {
1731        "TerminalPanel"
1732    }
1733
1734    fn panel_key() -> &'static str {
1735        TERMINAL_PANEL_KEY
1736    }
1737
1738    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1739        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1740            && TerminalSettings::get_global(cx).button
1741        {
1742            Some(IconName::TerminalAlt)
1743        } else {
1744            None
1745        }
1746    }
1747
1748    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1749        Some("Terminal Panel")
1750    }
1751
1752    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1753        Box::new(ToggleFocus)
1754    }
1755
1756    fn pane(&self) -> Option<Entity<Pane>> {
1757        Some(self.active_pane.clone())
1758    }
1759
1760    fn activation_priority(&self) -> u32 {
1761        1
1762    }
1763}
1764
1765struct TerminalProvider(Entity<TerminalPanel>);
1766
1767impl workspace::TerminalProvider for TerminalProvider {
1768    fn spawn(
1769        &self,
1770        task: SpawnInTerminal,
1771        window: &mut Window,
1772        cx: &mut App,
1773    ) -> Task<Option<Result<ExitStatus>>> {
1774        let terminal_panel = self.0.clone();
1775        window.spawn(cx, async move |cx| {
1776            let terminal = terminal_panel
1777                .update_in(cx, |terminal_panel, window, cx| {
1778                    terminal_panel.spawn_task(&task, window, cx)
1779                })
1780                .ok()?
1781                .await;
1782            match terminal {
1783                Ok(terminal) => {
1784                    let exit_status = terminal
1785                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1786                        .ok()?
1787                        .await?;
1788                    Some(Ok(exit_status))
1789                }
1790                Err(e) => Some(Err(e)),
1791            }
1792        })
1793    }
1794}
1795
1796struct InlineAssistTabBarButton {
1797    focus_handle: FocusHandle,
1798}
1799
1800impl Render for InlineAssistTabBarButton {
1801    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1802        let focus_handle = self.focus_handle.clone();
1803        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1804            .icon_size(IconSize::Small)
1805            .on_click(cx.listener(|_, _, window, cx| {
1806                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1807            }))
1808            .tooltip(move |_window, cx| {
1809                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1810            })
1811    }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816    use std::num::NonZero;
1817
1818    use super::*;
1819    use gpui::{TestAppContext, UpdateGlobal as _};
1820    use pretty_assertions::assert_eq;
1821    use project::FakeFs;
1822    use settings::SettingsStore;
1823
1824    #[test]
1825    fn test_prepare_empty_task() {
1826        let input = SpawnInTerminal::default();
1827        let shell = Shell::System;
1828
1829        let result = prepare_task_for_spawn(&input, &shell, false);
1830
1831        let expected_shell = util::get_system_shell();
1832        assert_eq!(result.env, HashMap::default());
1833        assert_eq!(result.cwd, None);
1834        assert_eq!(result.shell, Shell::System);
1835        assert_eq!(
1836            result.command,
1837            Some(expected_shell.clone()),
1838            "Empty tasks should spawn a -i shell"
1839        );
1840        assert_eq!(result.args, Vec::<String>::new());
1841        assert_eq!(
1842            result.command_label, expected_shell,
1843            "We show the shell launch for empty commands"
1844        );
1845    }
1846
1847    #[gpui::test]
1848    async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1849        cx.executor().allow_parking();
1850        init_test(cx);
1851
1852        let fs = FakeFs::new(cx.executor());
1853        let project = Project::test(fs, [], cx).await;
1854        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1855
1856        let (window_handle, terminal_panel) = workspace
1857            .update(cx, |workspace, window, cx| {
1858                let window_handle = window.window_handle();
1859                let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1860                (window_handle, terminal_panel)
1861            })
1862            .unwrap();
1863
1864        set_max_tabs(cx, Some(3));
1865
1866        for _ in 0..5 {
1867            let task = window_handle
1868                .update(cx, |_, window, cx| {
1869                    terminal_panel.update(cx, |panel, cx| {
1870                        panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1871                    })
1872                })
1873                .unwrap();
1874            task.await.unwrap();
1875        }
1876
1877        cx.run_until_parked();
1878
1879        let item_count =
1880            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1881
1882        assert_eq!(
1883            item_count, 5,
1884            "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1885        );
1886    }
1887
1888    #[cfg(unix)]
1889    #[test]
1890    fn test_prepare_script_like_task() {
1891        let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string();
1892        let expected_cwd = PathBuf::from("/some/work");
1893
1894        let input = SpawnInTerminal {
1895            command: Some(user_command.clone()),
1896            cwd: Some(expected_cwd.clone()),
1897            ..SpawnInTerminal::default()
1898        };
1899        let shell = Shell::System;
1900
1901        let result = prepare_task_for_spawn(&input, &shell, false);
1902
1903        let system_shell = util::get_system_shell();
1904        assert_eq!(result.env, HashMap::default());
1905        assert_eq!(result.cwd, Some(expected_cwd));
1906        assert_eq!(result.shell, Shell::System);
1907        assert_eq!(result.command, Some(system_shell.clone()));
1908        assert_eq!(
1909            result.args,
1910            vec!["-i".to_string(), "-c".to_string(), user_command.clone()],
1911            "User command should have been moved into the arguments, as we're spawning a new -i shell",
1912        );
1913        assert_eq!(
1914            result.command_label,
1915            format!(
1916                "{system_shell} {interactive}-c '{user_command}'",
1917                interactive = if cfg!(windows) { "" } else { "-i " }
1918            ),
1919            "We want to show to the user the entire command spawned"
1920        );
1921    }
1922
1923    #[gpui::test]
1924    async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1925        cx.executor().allow_parking();
1926        init_test(cx);
1927
1928        cx.update(|cx| {
1929            SettingsStore::update_global(cx, |store, cx| {
1930                store.update_user_settings(cx, |settings| {
1931                    settings.terminal.get_or_insert_default().project.shell =
1932                        Some(settings::Shell::Program("asdf".to_owned()));
1933                });
1934            });
1935        });
1936
1937        let fs = FakeFs::new(cx.executor());
1938        let project = Project::test(fs, [], cx).await;
1939        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1940
1941        let (window_handle, terminal_panel) = workspace
1942            .update(cx, |workspace, window, cx| {
1943                let window_handle = window.window_handle();
1944                let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1945                (window_handle, terminal_panel)
1946            })
1947            .unwrap();
1948
1949        window_handle
1950            .update(cx, |_, window, cx| {
1951                terminal_panel.update(cx, |terminal_panel, cx| {
1952                    terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1953                })
1954            })
1955            .unwrap()
1956            .await
1957            .unwrap_err();
1958
1959        window_handle
1960            .update(cx, |_, _, cx| {
1961                terminal_panel.update(cx, |terminal_panel, cx| {
1962                    assert!(
1963                        terminal_panel
1964                            .active_pane
1965                            .read(cx)
1966                            .items()
1967                            .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1968                        "should spawn `FailedToSpawnTerminal` pane"
1969                    );
1970                })
1971            })
1972            .unwrap();
1973    }
1974
1975    #[gpui::test]
1976    async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) {
1977        cx.executor().allow_parking();
1978        init_test(cx);
1979
1980        let fs = FakeFs::new(cx.executor());
1981        let project = Project::test(fs, [], cx).await;
1982        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1983
1984        let (window_handle, terminal_panel) = workspace
1985            .update(cx, |workspace, window, cx| {
1986                let window_handle = window.window_handle();
1987                let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1988                (window_handle, terminal_panel)
1989            })
1990            .unwrap();
1991
1992        let result = window_handle
1993            .update(cx, |_, window, cx| {
1994                terminal_panel.update(cx, |terminal_panel, cx| {
1995                    terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
1996                })
1997            })
1998            .unwrap()
1999            .await;
2000
2001        assert!(
2002            result.is_ok(),
2003            "local terminal should successfully create in local project"
2004        );
2005    }
2006
2007    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
2008        cx.update_global(|store: &mut SettingsStore, cx| {
2009            store.update_user_settings(cx, |settings| {
2010                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
2011            });
2012        });
2013    }
2014
2015    pub fn init_test(cx: &mut TestAppContext) {
2016        cx.update(|cx| {
2017            let store = SettingsStore::test(cx);
2018            cx.set_global(store);
2019            theme::init(theme::LoadThemes::JustBase, cx);
2020            editor::init(cx);
2021            crate::init(cx);
2022        });
2023    }
2024}