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