terminal_panel.rs

   1use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
   2
   3use crate::{default_working_directory, TerminalView};
   4use breadcrumbs::Breadcrumbs;
   5use collections::{HashMap, HashSet};
   6use db::kvp::KEY_VALUE_STORE;
   7use futures::future::join_all;
   8use gpui::{
   9    actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
  10    ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
  11    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
  12};
  13use itertools::Itertools;
  14use project::{terminals::TerminalKind, Fs, ProjectEntryId};
  15use search::{buffer_search::DivRegistrar, BufferSearchBar};
  16use serde::{Deserialize, Serialize};
  17use settings::Settings;
  18use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
  19use terminal::{
  20    terminal_settings::{TerminalDockPosition, TerminalSettings},
  21    Terminal,
  22};
  23use ui::{
  24    h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable,
  25    Tooltip,
  26};
  27use util::{ResultExt, TryFutureExt};
  28use workspace::{
  29    dock::{DockPosition, Panel, PanelEvent},
  30    item::SerializableItem,
  31    pane,
  32    ui::IconName,
  33    DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace,
  34};
  35
  36use anyhow::Result;
  37use zed_actions::InlineAssist;
  38
  39const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  40
  41actions!(terminal_panel, [ToggleFocus]);
  42
  43pub fn init(cx: &mut AppContext) {
  44    cx.observe_new_views(
  45        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
  46            workspace.register_action(TerminalPanel::new_terminal);
  47            workspace.register_action(TerminalPanel::open_terminal);
  48            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
  49                if workspace
  50                    .panel::<TerminalPanel>(cx)
  51                    .as_ref()
  52                    .is_some_and(|panel| panel.read(cx).enabled)
  53                {
  54                    workspace.toggle_panel_focus::<TerminalPanel>(cx);
  55                }
  56            });
  57        },
  58    )
  59    .detach();
  60}
  61
  62pub struct TerminalPanel {
  63    pane: View<Pane>,
  64    fs: Arc<dyn Fs>,
  65    workspace: WeakView<Workspace>,
  66    width: Option<Pixels>,
  67    height: Option<Pixels>,
  68    pending_serialization: Task<Option<()>>,
  69    pending_terminals_to_add: usize,
  70    _subscriptions: Vec<Subscription>,
  71    deferred_tasks: HashMap<TaskId, Task<()>>,
  72    enabled: bool,
  73    assistant_enabled: bool,
  74    assistant_tab_bar_button: Option<AnyView>,
  75}
  76
  77impl TerminalPanel {
  78    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
  79        let pane = cx.new_view(|cx| {
  80            let mut pane = Pane::new(
  81                workspace.weak_handle(),
  82                workspace.project().clone(),
  83                Default::default(),
  84                None,
  85                NewTerminal.boxed_clone(),
  86                cx,
  87            );
  88            pane.set_can_split(false, cx);
  89            pane.set_can_navigate(false, cx);
  90            pane.display_nav_history_buttons(None);
  91            pane.set_should_display_tab_bar(|_| true);
  92
  93            let is_local = workspace.project().read(cx).is_local();
  94            let workspace = workspace.weak_handle();
  95            pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
  96                if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
  97                    let item = if &tab.pane == cx.view() {
  98                        pane.item_for_index(tab.ix)
  99                    } else {
 100                        tab.pane.read(cx).item_for_index(tab.ix)
 101                    };
 102                    if let Some(item) = item {
 103                        if item.downcast::<TerminalView>().is_some() {
 104                            return ControlFlow::Continue(());
 105                        } else if let Some(project_path) = item.project_path(cx) {
 106                            if let Some(entry_path) = workspace
 107                                .update(cx, |workspace, cx| {
 108                                    workspace
 109                                        .project()
 110                                        .read(cx)
 111                                        .absolute_path(&project_path, cx)
 112                                })
 113                                .log_err()
 114                                .flatten()
 115                            {
 116                                add_paths_to_terminal(pane, &[entry_path], cx);
 117                            }
 118                        }
 119                    }
 120                } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
 121                    if let Some(entry_path) = workspace
 122                        .update(cx, |workspace, cx| {
 123                            let project = workspace.project().read(cx);
 124                            project
 125                                .path_for_entry(entry_id, cx)
 126                                .and_then(|project_path| project.absolute_path(&project_path, cx))
 127                        })
 128                        .log_err()
 129                        .flatten()
 130                    {
 131                        add_paths_to_terminal(pane, &[entry_path], cx);
 132                    }
 133                } else if is_local {
 134                    if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
 135                        add_paths_to_terminal(pane, paths.paths(), cx);
 136                    }
 137                }
 138
 139                ControlFlow::Break(())
 140            });
 141            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
 142            let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
 143            pane.toolbar().update(cx, |toolbar, cx| {
 144                toolbar.add_item(buffer_search_bar, cx);
 145                toolbar.add_item(breadcrumbs, cx);
 146            });
 147            pane
 148        });
 149        let subscriptions = vec![
 150            cx.observe(&pane, |_, _, cx| cx.notify()),
 151            cx.subscribe(&pane, Self::handle_pane_event),
 152        ];
 153        let project = workspace.project().read(cx);
 154        let enabled = project.supports_terminal(cx);
 155        let this = Self {
 156            pane,
 157            fs: workspace.app_state().fs.clone(),
 158            workspace: workspace.weak_handle(),
 159            pending_serialization: Task::ready(None),
 160            width: None,
 161            height: None,
 162            pending_terminals_to_add: 0,
 163            deferred_tasks: HashMap::default(),
 164            _subscriptions: subscriptions,
 165            enabled,
 166            assistant_enabled: false,
 167            assistant_tab_bar_button: None,
 168        };
 169        this.apply_tab_bar_buttons(cx);
 170        this
 171    }
 172
 173    pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
 174        self.assistant_enabled = enabled;
 175        if enabled {
 176            let focus_handle = self
 177                .pane
 178                .read(cx)
 179                .active_item()
 180                .map(|item| item.focus_handle(cx))
 181                .unwrap_or(self.focus_handle(cx));
 182            self.assistant_tab_bar_button = Some(
 183                cx.new_view(move |_| InlineAssistTabBarButton { focus_handle })
 184                    .into(),
 185            );
 186        } else {
 187            self.assistant_tab_bar_button = None;
 188        }
 189        self.apply_tab_bar_buttons(cx);
 190    }
 191
 192    fn apply_tab_bar_buttons(&self, cx: &mut ViewContext<Self>) {
 193        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 194        self.pane.update(cx, |pane, cx| {
 195            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 196                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
 197                    return (None, None);
 198                }
 199                let focus_handle = pane.focus_handle(cx);
 200                let right_children = h_flex()
 201                    .gap_2()
 202                    .children(assistant_tab_bar_button.clone())
 203                    .child(
 204                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 205                            .trigger(
 206                                IconButton::new("plus", IconName::Plus)
 207                                    .icon_size(IconSize::Small)
 208                                    .tooltip(|cx| Tooltip::text("New...", cx)),
 209                            )
 210                            .anchor(AnchorCorner::TopRight)
 211                            .with_handle(pane.new_item_context_menu_handle.clone())
 212                            .menu(move |cx| {
 213                                let focus_handle = focus_handle.clone();
 214                                let menu = ContextMenu::build(cx, |menu, _| {
 215                                    menu.context(focus_handle.clone())
 216                                        .action(
 217                                            "New Terminal",
 218                                            workspace::NewTerminal.boxed_clone(),
 219                                        )
 220                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 221                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 222                                        // context menu will be gone the moment we spawn the modal.
 223                                        .action(
 224                                            "Spawn task",
 225                                            zed_actions::Spawn::modal().boxed_clone(),
 226                                        )
 227                                });
 228
 229                                Some(menu)
 230                            }),
 231                    )
 232                    .child({
 233                        let zoomed = pane.is_zoomed();
 234                        IconButton::new("toggle_zoom", IconName::Maximize)
 235                            .icon_size(IconSize::Small)
 236                            .selected(zoomed)
 237                            .selected_icon(IconName::Minimize)
 238                            .on_click(cx.listener(|pane, _, cx| {
 239                                pane.toggle_zoom(&workspace::ToggleZoom, cx);
 240                            }))
 241                            .tooltip(move |cx| {
 242                                Tooltip::for_action(
 243                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 244                                    &ToggleZoom,
 245                                    cx,
 246                                )
 247                            })
 248                    })
 249                    .into_any_element()
 250                    .into();
 251                (None, right_children)
 252            });
 253        });
 254    }
 255
 256    pub async fn load(
 257        workspace: WeakView<Workspace>,
 258        mut cx: AsyncWindowContext,
 259    ) -> Result<View<Self>> {
 260        let serialized_panel = cx
 261            .background_executor()
 262            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
 263            .await
 264            .log_err()
 265            .flatten()
 266            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 267            .transpose()
 268            .log_err()
 269            .flatten();
 270
 271        let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
 272            let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
 273            let items = if let Some((serialized_panel, database_id)) =
 274                serialized_panel.as_ref().zip(workspace.database_id())
 275            {
 276                panel.update(cx, |panel, cx| {
 277                    cx.notify();
 278                    panel.height = serialized_panel.height.map(|h| h.round());
 279                    panel.width = serialized_panel.width.map(|w| w.round());
 280                    panel.pane.update(cx, |_, cx| {
 281                        serialized_panel
 282                            .items
 283                            .iter()
 284                            .map(|item_id| {
 285                                TerminalView::deserialize(
 286                                    workspace.project().clone(),
 287                                    workspace.weak_handle(),
 288                                    database_id,
 289                                    *item_id,
 290                                    cx,
 291                                )
 292                            })
 293                            .collect::<Vec<_>>()
 294                    })
 295                })
 296            } else {
 297                Vec::new()
 298            };
 299            let pane = panel.read(cx).pane.clone();
 300            (panel, pane, items)
 301        })?;
 302
 303        if let Some(workspace) = workspace.upgrade() {
 304            panel
 305                .update(&mut cx, |panel, cx| {
 306                    panel._subscriptions.push(cx.subscribe(
 307                        &workspace,
 308                        |terminal_panel, _, e, cx| {
 309                            if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
 310                                terminal_panel.spawn_task(spawn_in_terminal, cx);
 311                            };
 312                        },
 313                    ))
 314                })
 315                .ok();
 316        }
 317
 318        let pane = pane.downgrade();
 319        let items = futures::future::join_all(items).await;
 320        let mut alive_item_ids = Vec::new();
 321        pane.update(&mut cx, |pane, cx| {
 322            let active_item_id = serialized_panel
 323                .as_ref()
 324                .and_then(|panel| panel.active_item_id);
 325            let mut active_ix = None;
 326            for item in items {
 327                if let Some(item) = item.log_err() {
 328                    let item_id = item.entity_id().as_u64();
 329                    pane.add_item(Box::new(item), false, false, None, cx);
 330                    alive_item_ids.push(item_id as ItemId);
 331                    if Some(item_id) == active_item_id {
 332                        active_ix = Some(pane.items_len() - 1);
 333                    }
 334                }
 335            }
 336
 337            if let Some(active_ix) = active_ix {
 338                pane.activate_item(active_ix, false, false, cx)
 339            }
 340        })?;
 341
 342        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 343        if let Some(workspace) = workspace.upgrade() {
 344            let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
 345                workspace
 346                    .database_id()
 347                    .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
 348            })?;
 349            if let Some(task) = cleanup_task {
 350                task.await.log_err();
 351            }
 352        }
 353
 354        Ok(panel)
 355    }
 356
 357    fn handle_pane_event(
 358        &mut self,
 359        _pane: View<Pane>,
 360        event: &pane::Event,
 361        cx: &mut ViewContext<Self>,
 362    ) {
 363        match event {
 364            pane::Event::ActivateItem { .. } => self.serialize(cx),
 365            pane::Event::RemovedItem { .. } => self.serialize(cx),
 366            pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
 367            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
 368            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
 369
 370            pane::Event::AddItem { item } => {
 371                if let Some(workspace) = self.workspace.upgrade() {
 372                    let pane = self.pane.clone();
 373                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
 374                }
 375            }
 376
 377            _ => {}
 378        }
 379    }
 380
 381    pub fn open_terminal(
 382        workspace: &mut Workspace,
 383        action: &workspace::OpenTerminal,
 384        cx: &mut ViewContext<Workspace>,
 385    ) {
 386        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 387            return;
 388        };
 389
 390        terminal_panel
 391            .update(cx, |panel, cx| {
 392                panel.add_terminal(
 393                    TerminalKind::Shell(Some(action.working_directory.clone())),
 394                    RevealStrategy::Always,
 395                    cx,
 396                )
 397            })
 398            .detach_and_log_err(cx);
 399    }
 400
 401    fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
 402        let mut spawn_task = spawn_in_terminal.clone();
 403        // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
 404        let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
 405            Shell::System => {
 406                match self
 407                    .workspace
 408                    .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
 409                {
 410                    Ok(local) => {
 411                        if local {
 412                            retrieve_system_shell().map(|shell| (shell, Vec::new()))
 413                        } else {
 414                            Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
 415                        }
 416                    }
 417                    Err(_no_window_e) => return,
 418                }
 419            }
 420            Shell::Program(shell) => Some((shell, Vec::new())),
 421            Shell::WithArguments { program, args, .. } => Some((program, args)),
 422        }) else {
 423            return;
 424        };
 425        #[cfg(target_os = "windows")]
 426        let windows_shell_type = to_windows_shell_type(&shell);
 427
 428        #[cfg(not(target_os = "windows"))]
 429        {
 430            spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
 431        }
 432        #[cfg(target_os = "windows")]
 433        {
 434            use crate::terminal_panel::WindowsShellType;
 435
 436            match windows_shell_type {
 437                WindowsShellType::Powershell => {
 438                    spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
 439                }
 440                WindowsShellType::Cmd => {
 441                    spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
 442                }
 443                WindowsShellType::Other => {
 444                    spawn_task.command_label =
 445                        format!("{shell} -i -c '{}'", spawn_task.command_label)
 446                }
 447            }
 448        }
 449
 450        let task_command = std::mem::replace(&mut spawn_task.command, shell);
 451        let task_args = std::mem::take(&mut spawn_task.args);
 452        let combined_command = task_args
 453            .into_iter()
 454            .fold(task_command, |mut command, arg| {
 455                command.push(' ');
 456                #[cfg(not(target_os = "windows"))]
 457                command.push_str(&arg);
 458                #[cfg(target_os = "windows")]
 459                command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
 460                command
 461            });
 462
 463        #[cfg(not(target_os = "windows"))]
 464        user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
 465        #[cfg(target_os = "windows")]
 466        {
 467            use crate::terminal_panel::WindowsShellType;
 468
 469            match windows_shell_type {
 470                WindowsShellType::Powershell => {
 471                    user_args.extend(["-C".to_owned(), combined_command])
 472                }
 473                WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
 474                WindowsShellType::Other => {
 475                    user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
 476                }
 477            }
 478        }
 479        spawn_task.args = user_args;
 480        let spawn_task = spawn_task;
 481
 482        let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
 483        let use_new_terminal = spawn_in_terminal.use_new_terminal;
 484
 485        if allow_concurrent_runs && use_new_terminal {
 486            self.spawn_in_new_terminal(spawn_task, cx)
 487                .detach_and_log_err(cx);
 488            return;
 489        }
 490
 491        let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
 492        if terminals_for_task.is_empty() {
 493            self.spawn_in_new_terminal(spawn_task, cx)
 494                .detach_and_log_err(cx);
 495            return;
 496        }
 497        let (existing_item_index, existing_terminal) = terminals_for_task
 498            .last()
 499            .expect("covered no terminals case above")
 500            .clone();
 501        if allow_concurrent_runs {
 502            debug_assert!(
 503                !use_new_terminal,
 504                "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
 505            );
 506            self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx);
 507        } else {
 508            self.deferred_tasks.insert(
 509                spawn_in_terminal.id.clone(),
 510                cx.spawn(|terminal_panel, mut cx| async move {
 511                    wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
 512                    terminal_panel
 513                        .update(&mut cx, |terminal_panel, cx| {
 514                            if use_new_terminal {
 515                                terminal_panel
 516                                    .spawn_in_new_terminal(spawn_task, cx)
 517                                    .detach_and_log_err(cx);
 518                            } else {
 519                                terminal_panel.replace_terminal(
 520                                    spawn_task,
 521                                    existing_item_index,
 522                                    existing_terminal,
 523                                    cx,
 524                                );
 525                            }
 526                        })
 527                        .ok();
 528                }),
 529            );
 530        }
 531    }
 532
 533    pub fn spawn_in_new_terminal(
 534        &mut self,
 535        spawn_task: SpawnInTerminal,
 536        cx: &mut ViewContext<Self>,
 537    ) -> Task<Result<Model<Terminal>>> {
 538        let reveal = spawn_task.reveal;
 539        self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
 540    }
 541
 542    /// Create a new Terminal in the current working directory or the user's home directory
 543    fn new_terminal(
 544        workspace: &mut Workspace,
 545        _: &workspace::NewTerminal,
 546        cx: &mut ViewContext<Workspace>,
 547    ) {
 548        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 549            return;
 550        };
 551
 552        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
 553
 554        terminal_panel
 555            .update(cx, |this, cx| {
 556                this.add_terminal(kind, RevealStrategy::Always, cx)
 557            })
 558            .detach_and_log_err(cx);
 559    }
 560
 561    fn terminals_for_task(
 562        &self,
 563        label: &str,
 564        cx: &mut AppContext,
 565    ) -> Vec<(usize, View<TerminalView>)> {
 566        self.pane
 567            .read(cx)
 568            .items()
 569            .enumerate()
 570            .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 571            .filter_map(|(index, terminal_view)| {
 572                let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 573                if &task_state.full_label == label {
 574                    Some((index, terminal_view))
 575                } else {
 576                    None
 577                }
 578            })
 579            .collect()
 580    }
 581
 582    fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) {
 583        self.pane.update(cx, |pane, cx| {
 584            pane.activate_item(item_index, true, focus, cx)
 585        })
 586    }
 587
 588    fn add_terminal(
 589        &mut self,
 590        kind: TerminalKind,
 591        reveal_strategy: RevealStrategy,
 592        cx: &mut ViewContext<Self>,
 593    ) -> Task<Result<Model<Terminal>>> {
 594        if !self.enabled {
 595            return Task::ready(Err(anyhow::anyhow!(
 596                "terminal not yet supported for remote projects"
 597            )));
 598        }
 599
 600        let workspace = self.workspace.clone();
 601        self.pending_terminals_to_add += 1;
 602
 603        cx.spawn(|terminal_panel, mut cx| async move {
 604            let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
 605            let result = workspace.update(&mut cx, |workspace, cx| {
 606                let window = cx.window_handle();
 607                let terminal = workspace
 608                    .project()
 609                    .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
 610                let terminal_view = Box::new(cx.new_view(|cx| {
 611                    TerminalView::new(
 612                        terminal.clone(),
 613                        workspace.weak_handle(),
 614                        workspace.database_id(),
 615                        cx,
 616                    )
 617                }));
 618                pane.update(cx, |pane, cx| {
 619                    let focus = pane.has_focus(cx);
 620                    pane.add_item(terminal_view, true, focus, None, cx);
 621                });
 622
 623                match reveal_strategy {
 624                    RevealStrategy::Always => {
 625                        workspace.focus_panel::<Self>(cx);
 626                    }
 627                    RevealStrategy::NoFocus => {
 628                        workspace.open_panel::<Self>(cx);
 629                    }
 630                    RevealStrategy::Never => {}
 631                }
 632                Ok(terminal)
 633            })?;
 634            terminal_panel.update(&mut cx, |this, cx| {
 635                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
 636                this.serialize(cx)
 637            })?;
 638            result
 639        })
 640    }
 641
 642    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 643        let mut items_to_serialize = HashSet::default();
 644        let items = self
 645            .pane
 646            .read(cx)
 647            .items()
 648            .filter_map(|item| {
 649                let terminal_view = item.act_as::<TerminalView>(cx)?;
 650                if terminal_view.read(cx).terminal().read(cx).task().is_some() {
 651                    None
 652                } else {
 653                    let id = item.item_id().as_u64();
 654                    items_to_serialize.insert(id);
 655                    Some(id)
 656                }
 657            })
 658            .collect::<Vec<_>>();
 659        let active_item_id = self
 660            .pane
 661            .read(cx)
 662            .active_item()
 663            .map(|item| item.item_id().as_u64())
 664            .filter(|active_id| items_to_serialize.contains(active_id));
 665        let height = self.height;
 666        let width = self.width;
 667        self.pending_serialization = cx.background_executor().spawn(
 668            async move {
 669                KEY_VALUE_STORE
 670                    .write_kvp(
 671                        TERMINAL_PANEL_KEY.into(),
 672                        serde_json::to_string(&SerializedTerminalPanel {
 673                            items,
 674                            active_item_id,
 675                            height,
 676                            width,
 677                        })?,
 678                    )
 679                    .await?;
 680                anyhow::Ok(())
 681            }
 682            .log_err(),
 683        );
 684    }
 685
 686    fn replace_terminal(
 687        &self,
 688        spawn_task: SpawnInTerminal,
 689        terminal_item_index: usize,
 690        terminal_to_replace: View<TerminalView>,
 691        cx: &mut ViewContext<'_, Self>,
 692    ) -> Option<()> {
 693        let project = self
 694            .workspace
 695            .update(cx, |workspace, _| workspace.project().clone())
 696            .ok()?;
 697
 698        let reveal = spawn_task.reveal;
 699        let window = cx.window_handle();
 700        let new_terminal = project.update(cx, |project, cx| {
 701            project
 702                .create_terminal(TerminalKind::Task(spawn_task), window, cx)
 703                .log_err()
 704        })?;
 705        terminal_to_replace.update(cx, |terminal_to_replace, cx| {
 706            terminal_to_replace.set_terminal(new_terminal, cx);
 707        });
 708
 709        match reveal {
 710            RevealStrategy::Always => {
 711                self.activate_terminal_view(terminal_item_index, true, cx);
 712                let task_workspace = self.workspace.clone();
 713                cx.spawn(|_, mut cx| async move {
 714                    task_workspace
 715                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
 716                        .ok()
 717                })
 718                .detach();
 719            }
 720            RevealStrategy::NoFocus => {
 721                self.activate_terminal_view(terminal_item_index, false, cx);
 722                let task_workspace = self.workspace.clone();
 723                cx.spawn(|_, mut cx| async move {
 724                    task_workspace
 725                        .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
 726                        .ok()
 727                })
 728                .detach();
 729            }
 730            RevealStrategy::Never => {}
 731        }
 732
 733        Some(())
 734    }
 735
 736    fn has_no_terminals(&self, cx: &WindowContext) -> bool {
 737        self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
 738    }
 739
 740    pub fn assistant_enabled(&self) -> bool {
 741        self.assistant_enabled
 742    }
 743}
 744
 745async fn wait_for_terminals_tasks(
 746    terminals_for_task: Vec<(usize, View<TerminalView>)>,
 747    cx: &mut AsyncWindowContext,
 748) {
 749    let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| {
 750        terminal
 751            .update(cx, |terminal_view, cx| {
 752                terminal_view
 753                    .terminal()
 754                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
 755            })
 756            .ok()
 757    });
 758    let _: Vec<()> = join_all(pending_tasks).await;
 759}
 760
 761fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
 762    if let Some(terminal_view) = pane
 763        .active_item()
 764        .and_then(|item| item.downcast::<TerminalView>())
 765    {
 766        cx.focus_view(&terminal_view);
 767        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
 768        new_text.push(' ');
 769        terminal_view.update(cx, |terminal_view, cx| {
 770            terminal_view.terminal().update(cx, |terminal, _| {
 771                terminal.paste(&new_text);
 772            });
 773        });
 774    }
 775}
 776
 777impl EventEmitter<PanelEvent> for TerminalPanel {}
 778
 779impl Render for TerminalPanel {
 780    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 781        let mut registrar = DivRegistrar::new(
 782            |panel, cx| {
 783                panel
 784                    .pane
 785                    .read(cx)
 786                    .toolbar()
 787                    .read(cx)
 788                    .item_of_type::<BufferSearchBar>()
 789            },
 790            cx,
 791        );
 792        BufferSearchBar::register(&mut registrar);
 793        registrar.into_div().size_full().child(self.pane.clone())
 794    }
 795}
 796
 797impl FocusableView for TerminalPanel {
 798    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 799        self.pane.focus_handle(cx)
 800    }
 801}
 802
 803impl Panel for TerminalPanel {
 804    fn position(&self, cx: &WindowContext) -> DockPosition {
 805        match TerminalSettings::get_global(cx).dock {
 806            TerminalDockPosition::Left => DockPosition::Left,
 807            TerminalDockPosition::Bottom => DockPosition::Bottom,
 808            TerminalDockPosition::Right => DockPosition::Right,
 809        }
 810    }
 811
 812    fn position_is_valid(&self, _: DockPosition) -> bool {
 813        true
 814    }
 815
 816    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
 817        settings::update_settings_file::<TerminalSettings>(
 818            self.fs.clone(),
 819            cx,
 820            move |settings, _| {
 821                let dock = match position {
 822                    DockPosition::Left => TerminalDockPosition::Left,
 823                    DockPosition::Bottom => TerminalDockPosition::Bottom,
 824                    DockPosition::Right => TerminalDockPosition::Right,
 825                };
 826                settings.dock = Some(dock);
 827            },
 828        );
 829    }
 830
 831    fn size(&self, cx: &WindowContext) -> Pixels {
 832        let settings = TerminalSettings::get_global(cx);
 833        match self.position(cx) {
 834            DockPosition::Left | DockPosition::Right => {
 835                self.width.unwrap_or(settings.default_width)
 836            }
 837            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
 838        }
 839    }
 840
 841    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
 842        match self.position(cx) {
 843            DockPosition::Left | DockPosition::Right => self.width = size,
 844            DockPosition::Bottom => self.height = size,
 845        }
 846        self.serialize(cx);
 847        cx.notify();
 848    }
 849
 850    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 851        self.pane.read(cx).is_zoomed()
 852    }
 853
 854    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
 855        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
 856    }
 857
 858    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
 859        if !active || !self.has_no_terminals(cx) {
 860            return;
 861        }
 862        cx.defer(|this, cx| {
 863            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
 864                TerminalKind::Shell(default_working_directory(workspace, cx))
 865            }) else {
 866                return;
 867            };
 868
 869            this.add_terminal(kind, RevealStrategy::Never, cx)
 870                .detach_and_log_err(cx)
 871        })
 872    }
 873
 874    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
 875        let count = self.pane.read(cx).items_len();
 876        if count == 0 {
 877            None
 878        } else {
 879            Some(count.to_string())
 880        }
 881    }
 882
 883    fn persistent_name() -> &'static str {
 884        "TerminalPanel"
 885    }
 886
 887    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
 888        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
 889            Some(IconName::Terminal)
 890        } else {
 891            None
 892        }
 893    }
 894
 895    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
 896        Some("Terminal Panel")
 897    }
 898
 899    fn toggle_action(&self) -> Box<dyn gpui::Action> {
 900        Box::new(ToggleFocus)
 901    }
 902
 903    fn pane(&self) -> Option<View<Pane>> {
 904        Some(self.pane.clone())
 905    }
 906}
 907
 908struct InlineAssistTabBarButton {
 909    focus_handle: FocusHandle,
 910}
 911
 912impl Render for InlineAssistTabBarButton {
 913    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 914        let focus_handle = self.focus_handle.clone();
 915        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
 916            .icon_size(IconSize::Small)
 917            .on_click(cx.listener(|_, _, cx| {
 918                cx.dispatch_action(InlineAssist::default().boxed_clone());
 919            }))
 920            .tooltip(move |cx| {
 921                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
 922            })
 923    }
 924}
 925
 926#[derive(Serialize, Deserialize)]
 927struct SerializedTerminalPanel {
 928    items: Vec<u64>,
 929    active_item_id: Option<u64>,
 930    width: Option<Pixels>,
 931    height: Option<Pixels>,
 932}
 933
 934fn retrieve_system_shell() -> Option<String> {
 935    #[cfg(not(target_os = "windows"))]
 936    {
 937        use anyhow::Context;
 938        use util::ResultExt;
 939
 940        std::env::var("SHELL")
 941            .context("Error finding SHELL in env.")
 942            .log_err()
 943    }
 944    // `alacritty_terminal` uses this as default on Windows. See:
 945    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
 946    #[cfg(target_os = "windows")]
 947    return Some("powershell".to_owned());
 948}
 949
 950#[cfg(target_os = "windows")]
 951fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
 952    match shell_type {
 953        WindowsShellType::Powershell => to_powershell_variable(input),
 954        WindowsShellType::Cmd => to_cmd_variable(input),
 955        WindowsShellType::Other => input,
 956    }
 957}
 958
 959#[cfg(target_os = "windows")]
 960fn to_windows_shell_type(shell: &str) -> WindowsShellType {
 961    if shell == "powershell"
 962        || shell.ends_with("powershell.exe")
 963        || shell == "pwsh"
 964        || shell.ends_with("pwsh.exe")
 965    {
 966        WindowsShellType::Powershell
 967    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
 968        WindowsShellType::Cmd
 969    } else {
 970        // Someother shell detected, the user might install and use a
 971        // unix-like shell.
 972        WindowsShellType::Other
 973    }
 974}
 975
 976/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
 977#[inline]
 978#[cfg(target_os = "windows")]
 979fn to_cmd_variable(input: String) -> String {
 980    if let Some(var_str) = input.strip_prefix("${") {
 981        if var_str.find(':').is_none() {
 982            // If the input starts with "${", remove the trailing "}"
 983            format!("%{}%", &var_str[..var_str.len() - 1])
 984        } else {
 985            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
 986            // which will result in the task failing to run in such cases.
 987            input
 988        }
 989    } else if let Some(var_str) = input.strip_prefix('$') {
 990        // If the input starts with "$", directly append to "$env:"
 991        format!("%{}%", var_str)
 992    } else {
 993        // If no prefix is found, return the input as is
 994        input
 995    }
 996}
 997
 998/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
 999#[inline]
1000#[cfg(target_os = "windows")]
1001fn to_powershell_variable(input: String) -> String {
1002    if let Some(var_str) = input.strip_prefix("${") {
1003        if var_str.find(':').is_none() {
1004            // If the input starts with "${", remove the trailing "}"
1005            format!("$env:{}", &var_str[..var_str.len() - 1])
1006        } else {
1007            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1008            // which will result in the task failing to run in such cases.
1009            input
1010        }
1011    } else if let Some(var_str) = input.strip_prefix('$') {
1012        // If the input starts with "$", directly append to "$env:"
1013        format!("$env:{}", var_str)
1014    } else {
1015        // If no prefix is found, return the input as is
1016        input
1017    }
1018}
1019
1020#[cfg(target_os = "windows")]
1021#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1022enum WindowsShellType {
1023    Powershell,
1024    Cmd,
1025    Other,
1026}