terminal_panel.rs

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