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 => retrieve_system_shell().map(|shell| (shell, Vec::new())),
399            Shell::Program(shell) => Some((shell, Vec::new())),
400            Shell::WithArguments { program, args } => Some((program, args)),
401        }) else {
402            return;
403        };
404        #[cfg(target_os = "windows")]
405        let windows_shell_type = to_windows_shell_type(&shell);
406
407        #[cfg(not(target_os = "windows"))]
408        {
409            spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
410        }
411        #[cfg(target_os = "windows")]
412        {
413            use crate::terminal_panel::WindowsShellType;
414
415            match windows_shell_type {
416                WindowsShellType::Powershell => {
417                    spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
418                }
419                WindowsShellType::Cmd => {
420                    spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
421                }
422                WindowsShellType::Other => {
423                    spawn_task.command_label =
424                        format!("{shell} -i -c '{}'", spawn_task.command_label)
425                }
426            }
427        }
428
429        let task_command = std::mem::replace(&mut spawn_task.command, shell);
430        let task_args = std::mem::take(&mut spawn_task.args);
431        let combined_command = task_args
432            .into_iter()
433            .fold(task_command, |mut command, arg| {
434                command.push(' ');
435                #[cfg(not(target_os = "windows"))]
436                command.push_str(&arg);
437                #[cfg(target_os = "windows")]
438                command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
439                command
440            });
441
442        #[cfg(not(target_os = "windows"))]
443        user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
444        #[cfg(target_os = "windows")]
445        {
446            use crate::terminal_panel::WindowsShellType;
447
448            match windows_shell_type {
449                WindowsShellType::Powershell => {
450                    user_args.extend(["-C".to_owned(), combined_command])
451                }
452                WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
453                WindowsShellType::Other => {
454                    user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
455                }
456            }
457        }
458        spawn_task.args = user_args;
459        let spawn_task = spawn_task;
460
461        let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
462        let use_new_terminal = spawn_in_terminal.use_new_terminal;
463
464        if allow_concurrent_runs && use_new_terminal {
465            self.spawn_in_new_terminal(spawn_task, cx)
466                .detach_and_log_err(cx);
467            return;
468        }
469
470        let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
471        if terminals_for_task.is_empty() {
472            self.spawn_in_new_terminal(spawn_task, cx)
473                .detach_and_log_err(cx);
474            return;
475        }
476        let (existing_item_index, existing_terminal) = terminals_for_task
477            .last()
478            .expect("covered no terminals case above")
479            .clone();
480        if allow_concurrent_runs {
481            debug_assert!(
482                !use_new_terminal,
483                "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
484            );
485            self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx);
486        } else {
487            self.deferred_tasks.insert(
488                spawn_in_terminal.id.clone(),
489                cx.spawn(|terminal_panel, mut cx| async move {
490                    wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
491                    terminal_panel
492                        .update(&mut cx, |terminal_panel, cx| {
493                            if use_new_terminal {
494                                terminal_panel
495                                    .spawn_in_new_terminal(spawn_task, cx)
496                                    .detach_and_log_err(cx);
497                            } else {
498                                terminal_panel.replace_terminal(
499                                    spawn_task,
500                                    existing_item_index,
501                                    existing_terminal,
502                                    cx,
503                                );
504                            }
505                        })
506                        .ok();
507                }),
508            );
509        }
510    }
511
512    pub fn spawn_in_new_terminal(
513        &mut self,
514        spawn_task: SpawnInTerminal,
515        cx: &mut ViewContext<Self>,
516    ) -> Task<Result<Model<Terminal>>> {
517        let reveal = spawn_task.reveal;
518        self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
519    }
520
521    /// Create a new Terminal in the current working directory or the user's home directory
522    fn new_terminal(
523        workspace: &mut Workspace,
524        _: &workspace::NewTerminal,
525        cx: &mut ViewContext<Workspace>,
526    ) {
527        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
528            return;
529        };
530
531        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
532
533        terminal_panel
534            .update(cx, |this, cx| {
535                this.add_terminal(kind, RevealStrategy::Always, cx)
536            })
537            .detach_and_log_err(cx);
538    }
539
540    fn terminals_for_task(
541        &self,
542        label: &str,
543        cx: &mut AppContext,
544    ) -> Vec<(usize, View<TerminalView>)> {
545        self.pane
546            .read(cx)
547            .items()
548            .enumerate()
549            .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
550            .filter_map(|(index, terminal_view)| {
551                let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
552                if &task_state.full_label == label {
553                    Some((index, terminal_view))
554                } else {
555                    None
556                }
557            })
558            .collect()
559    }
560
561    fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
562        self.pane.update(cx, |pane, cx| {
563            pane.activate_item(item_index, true, true, cx)
564        })
565    }
566
567    fn add_terminal(
568        &mut self,
569        kind: TerminalKind,
570        reveal_strategy: RevealStrategy,
571        cx: &mut ViewContext<Self>,
572    ) -> Task<Result<Model<Terminal>>> {
573        if !self.enabled {
574            return Task::ready(Err(anyhow::anyhow!(
575                "terminal not yet supported for remote projects"
576            )));
577        }
578
579        let workspace = self.workspace.clone();
580        self.pending_terminals_to_add += 1;
581
582        cx.spawn(|terminal_panel, mut cx| async move {
583            let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
584            let result = workspace.update(&mut cx, |workspace, cx| {
585                let window = cx.window_handle();
586                let terminal = workspace
587                    .project()
588                    .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
589                let terminal_view = Box::new(cx.new_view(|cx| {
590                    TerminalView::new(
591                        terminal.clone(),
592                        workspace.weak_handle(),
593                        workspace.database_id(),
594                        cx,
595                    )
596                }));
597                pane.update(cx, |pane, cx| {
598                    let focus = pane.has_focus(cx);
599                    pane.add_item(terminal_view, true, focus, None, cx);
600                });
601
602                if reveal_strategy == RevealStrategy::Always {
603                    workspace.focus_panel::<Self>(cx);
604                }
605                Ok(terminal)
606            })?;
607            terminal_panel.update(&mut cx, |this, cx| {
608                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
609                this.serialize(cx)
610            })?;
611            result
612        })
613    }
614
615    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
616        let mut items_to_serialize = HashSet::default();
617        let items = self
618            .pane
619            .read(cx)
620            .items()
621            .filter_map(|item| {
622                let terminal_view = item.act_as::<TerminalView>(cx)?;
623                if terminal_view.read(cx).terminal().read(cx).task().is_some() {
624                    None
625                } else {
626                    let id = item.item_id().as_u64();
627                    items_to_serialize.insert(id);
628                    Some(id)
629                }
630            })
631            .collect::<Vec<_>>();
632        let active_item_id = self
633            .pane
634            .read(cx)
635            .active_item()
636            .map(|item| item.item_id().as_u64())
637            .filter(|active_id| items_to_serialize.contains(active_id));
638        let height = self.height;
639        let width = self.width;
640        self.pending_serialization = cx.background_executor().spawn(
641            async move {
642                KEY_VALUE_STORE
643                    .write_kvp(
644                        TERMINAL_PANEL_KEY.into(),
645                        serde_json::to_string(&SerializedTerminalPanel {
646                            items,
647                            active_item_id,
648                            height,
649                            width,
650                        })?,
651                    )
652                    .await?;
653                anyhow::Ok(())
654            }
655            .log_err(),
656        );
657    }
658
659    fn replace_terminal(
660        &self,
661        spawn_task: SpawnInTerminal,
662        terminal_item_index: usize,
663        terminal_to_replace: View<TerminalView>,
664        cx: &mut ViewContext<'_, Self>,
665    ) -> Option<()> {
666        let project = self
667            .workspace
668            .update(cx, |workspace, _| workspace.project().clone())
669            .ok()?;
670
671        let reveal = spawn_task.reveal;
672        let window = cx.window_handle();
673        let new_terminal = project.update(cx, |project, cx| {
674            project
675                .create_terminal(TerminalKind::Task(spawn_task), window, cx)
676                .log_err()
677        })?;
678        terminal_to_replace.update(cx, |terminal_to_replace, cx| {
679            terminal_to_replace.set_terminal(new_terminal, cx);
680        });
681
682        match reveal {
683            RevealStrategy::Always => {
684                self.activate_terminal_view(terminal_item_index, cx);
685                let task_workspace = self.workspace.clone();
686                cx.spawn(|_, mut cx| async move {
687                    task_workspace
688                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
689                        .ok()
690                })
691                .detach();
692            }
693            RevealStrategy::Never => {}
694        }
695
696        Some(())
697    }
698
699    fn has_no_terminals(&self, cx: &WindowContext) -> bool {
700        self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
701    }
702
703    pub fn assistant_enabled(&self) -> bool {
704        self.assistant_enabled
705    }
706}
707
708async fn wait_for_terminals_tasks(
709    terminals_for_task: Vec<(usize, View<TerminalView>)>,
710    cx: &mut AsyncWindowContext,
711) {
712    let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| {
713        terminal
714            .update(cx, |terminal_view, cx| {
715                terminal_view
716                    .terminal()
717                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
718            })
719            .ok()
720    });
721    let _: Vec<()> = join_all(pending_tasks).await;
722}
723
724fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
725    if let Some(terminal_view) = pane
726        .active_item()
727        .and_then(|item| item.downcast::<TerminalView>())
728    {
729        cx.focus_view(&terminal_view);
730        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
731        new_text.push(' ');
732        terminal_view.update(cx, |terminal_view, cx| {
733            terminal_view.terminal().update(cx, |terminal, _| {
734                terminal.paste(&new_text);
735            });
736        });
737    }
738}
739
740impl EventEmitter<PanelEvent> for TerminalPanel {}
741
742impl Render for TerminalPanel {
743    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
744        let mut registrar = DivRegistrar::new(
745            |panel, cx| {
746                panel
747                    .pane
748                    .read(cx)
749                    .toolbar()
750                    .read(cx)
751                    .item_of_type::<BufferSearchBar>()
752            },
753            cx,
754        );
755        BufferSearchBar::register(&mut registrar);
756        registrar.into_div().size_full().child(self.pane.clone())
757    }
758}
759
760impl FocusableView for TerminalPanel {
761    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
762        self.pane.focus_handle(cx)
763    }
764}
765
766impl Panel for TerminalPanel {
767    fn position(&self, cx: &WindowContext) -> DockPosition {
768        match TerminalSettings::get_global(cx).dock {
769            TerminalDockPosition::Left => DockPosition::Left,
770            TerminalDockPosition::Bottom => DockPosition::Bottom,
771            TerminalDockPosition::Right => DockPosition::Right,
772        }
773    }
774
775    fn position_is_valid(&self, _: DockPosition) -> bool {
776        true
777    }
778
779    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
780        settings::update_settings_file::<TerminalSettings>(
781            self.fs.clone(),
782            cx,
783            move |settings, _| {
784                let dock = match position {
785                    DockPosition::Left => TerminalDockPosition::Left,
786                    DockPosition::Bottom => TerminalDockPosition::Bottom,
787                    DockPosition::Right => TerminalDockPosition::Right,
788                };
789                settings.dock = Some(dock);
790            },
791        );
792    }
793
794    fn size(&self, cx: &WindowContext) -> Pixels {
795        let settings = TerminalSettings::get_global(cx);
796        match self.position(cx) {
797            DockPosition::Left | DockPosition::Right => {
798                self.width.unwrap_or(settings.default_width)
799            }
800            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
801        }
802    }
803
804    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
805        match self.position(cx) {
806            DockPosition::Left | DockPosition::Right => self.width = size,
807            DockPosition::Bottom => self.height = size,
808        }
809        self.serialize(cx);
810        cx.notify();
811    }
812
813    fn is_zoomed(&self, cx: &WindowContext) -> bool {
814        self.pane.read(cx).is_zoomed()
815    }
816
817    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
818        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
819    }
820
821    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
822        if !active || !self.has_no_terminals(cx) {
823            return;
824        }
825        cx.defer(|this, cx| {
826            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
827                TerminalKind::Shell(default_working_directory(workspace, cx))
828            }) else {
829                return;
830            };
831
832            this.add_terminal(kind, RevealStrategy::Never, cx)
833                .detach_and_log_err(cx)
834        })
835    }
836
837    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
838        let count = self.pane.read(cx).items_len();
839        if count == 0 {
840            None
841        } else {
842            Some(count.to_string())
843        }
844    }
845
846    fn persistent_name() -> &'static str {
847        "TerminalPanel"
848    }
849
850    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
851        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
852            Some(IconName::Terminal)
853        } else {
854            None
855        }
856    }
857
858    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
859        Some("Terminal Panel")
860    }
861
862    fn toggle_action(&self) -> Box<dyn gpui::Action> {
863        Box::new(ToggleFocus)
864    }
865
866    fn pane(&self) -> Option<View<Pane>> {
867        Some(self.pane.clone())
868    }
869}
870
871struct InlineAssistTabBarButton {
872    focus_handle: FocusHandle,
873}
874
875impl Render for InlineAssistTabBarButton {
876    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
877        let focus_handle = self.focus_handle.clone();
878        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
879            .icon_size(IconSize::Small)
880            .on_click(cx.listener(|_, _, cx| {
881                cx.dispatch_action(InlineAssist::default().boxed_clone());
882            }))
883            .tooltip(move |cx| {
884                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
885            })
886    }
887}
888
889#[derive(Serialize, Deserialize)]
890struct SerializedTerminalPanel {
891    items: Vec<u64>,
892    active_item_id: Option<u64>,
893    width: Option<Pixels>,
894    height: Option<Pixels>,
895}
896
897fn retrieve_system_shell() -> Option<String> {
898    #[cfg(not(target_os = "windows"))]
899    {
900        use anyhow::Context;
901        use util::ResultExt;
902
903        std::env::var("SHELL")
904            .context("Error finding SHELL in env.")
905            .log_err()
906    }
907    // `alacritty_terminal` uses this as default on Windows. See:
908    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
909    #[cfg(target_os = "windows")]
910    return Some("powershell".to_owned());
911}
912
913#[cfg(target_os = "windows")]
914fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
915    match shell_type {
916        WindowsShellType::Powershell => to_powershell_variable(input),
917        WindowsShellType::Cmd => to_cmd_variable(input),
918        WindowsShellType::Other => input,
919    }
920}
921
922#[cfg(target_os = "windows")]
923fn to_windows_shell_type(shell: &str) -> WindowsShellType {
924    if shell == "powershell"
925        || shell.ends_with("powershell.exe")
926        || shell == "pwsh"
927        || shell.ends_with("pwsh.exe")
928    {
929        WindowsShellType::Powershell
930    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
931        WindowsShellType::Cmd
932    } else {
933        // Someother shell detected, the user might install and use a
934        // unix-like shell.
935        WindowsShellType::Other
936    }
937}
938
939/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
940#[inline]
941#[cfg(target_os = "windows")]
942fn to_cmd_variable(input: String) -> String {
943    if let Some(var_str) = input.strip_prefix("${") {
944        if var_str.find(':').is_none() {
945            // If the input starts with "${", remove the trailing "}"
946            format!("%{}%", &var_str[..var_str.len() - 1])
947        } else {
948            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
949            // which will result in the task failing to run in such cases.
950            input
951        }
952    } else if let Some(var_str) = input.strip_prefix('$') {
953        // If the input starts with "$", directly append to "$env:"
954        format!("%{}%", var_str)
955    } else {
956        // If no prefix is found, return the input as is
957        input
958    }
959}
960
961/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
962#[inline]
963#[cfg(target_os = "windows")]
964fn to_powershell_variable(input: String) -> String {
965    if let Some(var_str) = input.strip_prefix("${") {
966        if var_str.find(':').is_none() {
967            // If the input starts with "${", remove the trailing "}"
968            format!("$env:{}", &var_str[..var_str.len() - 1])
969        } else {
970            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
971            // which will result in the task failing to run in such cases.
972            input
973        }
974    } else if let Some(var_str) = input.strip_prefix('$') {
975        // If the input starts with "$", directly append to "$env:"
976        format!("$env:{}", var_str)
977    } else {
978        // If no prefix is found, return the input as is
979        input
980    }
981}
982
983#[cfg(target_os = "windows")]
984#[derive(Debug, Clone, Copy, PartialEq, Eq)]
985enum WindowsShellType {
986    Powershell,
987    Cmd,
988    Other,
989}