1use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
2
3use crate::{
4 default_working_directory,
5 persistence::{
6 deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel,
7 },
8 TerminalView,
9};
10use breadcrumbs::Breadcrumbs;
11use collections::HashMap;
12use db::kvp::KEY_VALUE_STORE;
13use futures::future::join_all;
14use gpui::{
15 actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter,
16 ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
17 Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
18};
19use itertools::Itertools;
20use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
21use search::{buffer_search::DivRegistrar, BufferSearchBar};
22use settings::Settings;
23use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
24use terminal::{
25 terminal_settings::{TerminalDockPosition, TerminalSettings},
26 Terminal,
27};
28use ui::{
29 prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable,
30 Tooltip,
31};
32use util::{ResultExt, TryFutureExt};
33use workspace::{
34 dock::{DockPosition, Panel, PanelEvent},
35 item::SerializableItem,
36 move_item, pane,
37 ui::IconName,
38 ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab,
39 ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight,
40 SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
41};
42
43use anyhow::Result;
44use zed_actions::InlineAssist;
45
46const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
47
48actions!(terminal_panel, [ToggleFocus]);
49
50pub fn init(cx: &mut AppContext) {
51 cx.observe_new_views(
52 |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
53 workspace.register_action(TerminalPanel::new_terminal);
54 workspace.register_action(TerminalPanel::open_terminal);
55 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
56 if workspace
57 .panel::<TerminalPanel>(cx)
58 .as_ref()
59 .is_some_and(|panel| panel.read(cx).enabled)
60 {
61 workspace.toggle_panel_focus::<TerminalPanel>(cx);
62 }
63 });
64 },
65 )
66 .detach();
67}
68
69pub struct TerminalPanel {
70 pub(crate) active_pane: View<Pane>,
71 pub(crate) center: PaneGroup,
72 fs: Arc<dyn Fs>,
73 workspace: WeakView<Workspace>,
74 pub(crate) width: Option<Pixels>,
75 pub(crate) height: Option<Pixels>,
76 pending_serialization: Task<Option<()>>,
77 pending_terminals_to_add: usize,
78 deferred_tasks: HashMap<TaskId, Task<()>>,
79 enabled: bool,
80 assistant_enabled: bool,
81 assistant_tab_bar_button: Option<AnyView>,
82}
83
84impl TerminalPanel {
85 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
86 let project = workspace.project();
87 let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
88 let center = PaneGroup::new(pane.clone());
89 let enabled = project.read(cx).supports_terminal(cx);
90 cx.focus_view(&pane);
91 let terminal_panel = Self {
92 center,
93 active_pane: pane,
94 fs: workspace.app_state().fs.clone(),
95 workspace: workspace.weak_handle(),
96 pending_serialization: Task::ready(None),
97 width: None,
98 height: None,
99 pending_terminals_to_add: 0,
100 deferred_tasks: HashMap::default(),
101 enabled,
102 assistant_enabled: false,
103 assistant_tab_bar_button: None,
104 };
105 terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
106 terminal_panel
107 }
108
109 pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
110 self.assistant_enabled = enabled;
111 if enabled {
112 let focus_handle = self
113 .active_pane
114 .read(cx)
115 .active_item()
116 .map(|item| item.focus_handle(cx))
117 .unwrap_or(self.focus_handle(cx));
118 self.assistant_tab_bar_button = Some(
119 cx.new_view(move |_| InlineAssistTabBarButton { focus_handle })
120 .into(),
121 );
122 } else {
123 self.assistant_tab_bar_button = None;
124 }
125 for pane in self.center.panes() {
126 self.apply_tab_bar_buttons(pane, cx);
127 }
128 }
129
130 fn apply_tab_bar_buttons(&self, terminal_pane: &View<Pane>, cx: &mut ViewContext<Self>) {
131 let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
132 terminal_pane.update(cx, |pane, cx| {
133 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
134 let split_context = pane
135 .active_item()
136 .and_then(|item| item.downcast::<TerminalView>())
137 .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
138 if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
139 return (None, None);
140 }
141 let focus_handle = pane.focus_handle(cx);
142 let right_children = h_flex()
143 .gap(DynamicSpacing::Base02.rems(cx))
144 .child(
145 PopoverMenu::new("terminal-tab-bar-popover-menu")
146 .trigger(
147 IconButton::new("plus", IconName::Plus)
148 .icon_size(IconSize::Small)
149 .tooltip(|cx| Tooltip::text("Newβ¦", cx)),
150 )
151 .anchor(AnchorCorner::TopRight)
152 .with_handle(pane.new_item_context_menu_handle.clone())
153 .menu(move |cx| {
154 let focus_handle = focus_handle.clone();
155 let menu = ContextMenu::build(cx, |menu, _| {
156 menu.context(focus_handle.clone())
157 .action(
158 "New Terminal",
159 workspace::NewTerminal.boxed_clone(),
160 )
161 // We want the focus to go back to terminal panel once task modal is dismissed,
162 // hence we focus that first. Otherwise, we'd end up without a focused element, as
163 // context menu will be gone the moment we spawn the modal.
164 .action(
165 "Spawn task",
166 zed_actions::Spawn::modal().boxed_clone(),
167 )
168 });
169
170 Some(menu)
171 }),
172 )
173 .children(assistant_tab_bar_button.clone())
174 .child(
175 PopoverMenu::new("terminal-pane-tab-bar-split")
176 .trigger(
177 IconButton::new("terminal-pane-split", IconName::Split)
178 .icon_size(IconSize::Small)
179 .tooltip(|cx| Tooltip::text("Split Pane", cx)),
180 )
181 .anchor(AnchorCorner::TopRight)
182 .with_handle(pane.split_item_context_menu_handle.clone())
183 .menu({
184 let split_context = split_context.clone();
185 move |cx| {
186 ContextMenu::build(cx, |menu, _| {
187 menu.when_some(
188 split_context.clone(),
189 |menu, split_context| menu.context(split_context),
190 )
191 .action("Split Right", SplitRight.boxed_clone())
192 .action("Split Left", SplitLeft.boxed_clone())
193 .action("Split Up", SplitUp.boxed_clone())
194 .action("Split Down", SplitDown.boxed_clone())
195 })
196 .into()
197 }
198 }),
199 )
200 .child({
201 let zoomed = pane.is_zoomed();
202 IconButton::new("toggle_zoom", IconName::Maximize)
203 .icon_size(IconSize::Small)
204 .selected(zoomed)
205 .selected_icon(IconName::Minimize)
206 .on_click(cx.listener(|pane, _, cx| {
207 pane.toggle_zoom(&workspace::ToggleZoom, cx);
208 }))
209 .tooltip(move |cx| {
210 Tooltip::for_action(
211 if zoomed { "Zoom Out" } else { "Zoom In" },
212 &ToggleZoom,
213 cx,
214 )
215 })
216 })
217 .into_any_element()
218 .into();
219 (None, right_children)
220 });
221 });
222 }
223
224 pub async fn load(
225 workspace: WeakView<Workspace>,
226 mut cx: AsyncWindowContext,
227 ) -> Result<View<Self>> {
228 let serialized_panel = cx
229 .background_executor()
230 .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
231 .await
232 .log_err()
233 .flatten()
234 .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
235 .transpose()
236 .log_err()
237 .flatten();
238
239 let terminal_panel = workspace
240 .update(&mut cx, |workspace, cx| {
241 match serialized_panel.zip(workspace.database_id()) {
242 Some((serialized_panel, database_id)) => deserialize_terminal_panel(
243 workspace.weak_handle(),
244 workspace.project().clone(),
245 database_id,
246 serialized_panel,
247 cx,
248 ),
249 None => Task::ready(Ok(cx.new_view(|cx| TerminalPanel::new(workspace, cx)))),
250 }
251 })?
252 .await?;
253
254 if let Some(workspace) = workspace.upgrade() {
255 terminal_panel
256 .update(&mut cx, |_, cx| {
257 cx.subscribe(&workspace, |terminal_panel, _, e, cx| {
258 if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
259 terminal_panel.spawn_task(spawn_in_terminal, cx);
260 };
261 })
262 .detach();
263 })
264 .ok();
265 }
266
267 // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
268 if let Some(workspace) = workspace.upgrade() {
269 let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
270 let alive_item_ids = terminal_panel
271 .read(cx)
272 .center
273 .panes()
274 .into_iter()
275 .flat_map(|pane| pane.read(cx).items())
276 .map(|item| item.item_id().as_u64() as ItemId)
277 .collect();
278 workspace
279 .database_id()
280 .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
281 })?;
282 if let Some(task) = cleanup_task {
283 task.await.log_err();
284 }
285 }
286
287 Ok(terminal_panel)
288 }
289
290 fn handle_pane_event(
291 &mut self,
292 pane: View<Pane>,
293 event: &pane::Event,
294 cx: &mut ViewContext<Self>,
295 ) {
296 match event {
297 pane::Event::ActivateItem { .. } => self.serialize(cx),
298 pane::Event::RemovedItem { .. } => self.serialize(cx),
299 pane::Event::Remove { focus_on_pane } => {
300 let pane_count_before_removal = self.center.panes().len();
301 let _removal_result = self.center.remove(&pane);
302 if pane_count_before_removal == 1 {
303 self.center.first_pane().update(cx, |pane, cx| {
304 pane.set_zoomed(false, cx);
305 });
306 cx.emit(PanelEvent::Close);
307 } else {
308 if let Some(focus_on_pane) =
309 focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
310 {
311 focus_on_pane.focus_handle(cx).focus(cx);
312 }
313 }
314 }
315 pane::Event::ZoomIn => {
316 for pane in self.center.panes() {
317 pane.update(cx, |pane, cx| {
318 pane.set_zoomed(true, cx);
319 })
320 }
321 cx.emit(PanelEvent::ZoomIn);
322 cx.notify();
323 }
324 pane::Event::ZoomOut => {
325 for pane in self.center.panes() {
326 pane.update(cx, |pane, cx| {
327 pane.set_zoomed(false, cx);
328 })
329 }
330 cx.emit(PanelEvent::ZoomOut);
331 cx.notify();
332 }
333 pane::Event::AddItem { item } => {
334 if let Some(workspace) = self.workspace.upgrade() {
335 workspace.update(cx, |workspace, cx| {
336 item.added_to_pane(workspace, pane.clone(), cx)
337 })
338 }
339 self.serialize(cx);
340 }
341 pane::Event::Split(direction) => {
342 let new_pane = self.new_pane_with_cloned_active_terminal(cx);
343 let pane = pane.clone();
344 let direction = *direction;
345 cx.spawn(move |terminal_panel, mut cx| async move {
346 let Some(new_pane) = new_pane.await else {
347 return;
348 };
349 terminal_panel
350 .update(&mut cx, |terminal_panel, cx| {
351 terminal_panel
352 .center
353 .split(&pane, &new_pane, direction)
354 .log_err();
355 cx.focus_view(&new_pane);
356 })
357 .ok();
358 })
359 .detach();
360 }
361 pane::Event::Focus => {
362 self.active_pane = pane.clone();
363 }
364
365 _ => {}
366 }
367 }
368
369 fn new_pane_with_cloned_active_terminal(
370 &mut self,
371 cx: &mut ViewContext<Self>,
372 ) -> Task<Option<View<Pane>>> {
373 let Some(workspace) = self.workspace.clone().upgrade() else {
374 return Task::ready(None);
375 };
376 let database_id = workspace.read(cx).database_id();
377 let weak_workspace = self.workspace.clone();
378 let project = workspace.read(cx).project().clone();
379 let working_directory = self
380 .active_pane
381 .read(cx)
382 .active_item()
383 .and_then(|item| item.downcast::<TerminalView>())
384 .and_then(|terminal_view| {
385 terminal_view
386 .read(cx)
387 .terminal()
388 .read(cx)
389 .working_directory()
390 })
391 .or_else(|| default_working_directory(workspace.read(cx), cx));
392 let kind = TerminalKind::Shell(working_directory);
393 let window = cx.window_handle();
394 cx.spawn(move |terminal_panel, mut cx| async move {
395 let terminal = project
396 .update(&mut cx, |project, cx| {
397 project.create_terminal(kind, window, cx)
398 })
399 .log_err()?
400 .await
401 .log_err()?;
402
403 let terminal_view = Box::new(
404 cx.new_view(|cx| {
405 TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx)
406 })
407 .ok()?,
408 );
409 let pane = terminal_panel
410 .update(&mut cx, |terminal_panel, cx| {
411 let pane = new_terminal_pane(
412 weak_workspace,
413 project,
414 terminal_panel.active_pane.read(cx).is_zoomed(),
415 cx,
416 );
417 terminal_panel.apply_tab_bar_buttons(&pane, cx);
418 pane
419 })
420 .ok()?;
421
422 pane.update(&mut cx, |pane, cx| {
423 pane.add_item(terminal_view, true, true, None, cx);
424 })
425 .ok()?;
426
427 Some(pane)
428 })
429 }
430
431 pub fn open_terminal(
432 workspace: &mut Workspace,
433 action: &workspace::OpenTerminal,
434 cx: &mut ViewContext<Workspace>,
435 ) {
436 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
437 return;
438 };
439
440 terminal_panel
441 .update(cx, |panel, cx| {
442 panel.add_terminal(
443 TerminalKind::Shell(Some(action.working_directory.clone())),
444 RevealStrategy::Always,
445 cx,
446 )
447 })
448 .detach_and_log_err(cx);
449 }
450
451 fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
452 let mut spawn_task = spawn_in_terminal.clone();
453 // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
454 let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
455 Shell::System => {
456 match self
457 .workspace
458 .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
459 {
460 Ok(local) => {
461 if local {
462 retrieve_system_shell().map(|shell| (shell, Vec::new()))
463 } else {
464 Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
465 }
466 }
467 Err(_no_window_e) => return,
468 }
469 }
470 Shell::Program(shell) => Some((shell, Vec::new())),
471 Shell::WithArguments { program, args, .. } => Some((program, args)),
472 }) else {
473 return;
474 };
475 #[cfg(target_os = "windows")]
476 let windows_shell_type = to_windows_shell_type(&shell);
477
478 #[cfg(not(target_os = "windows"))]
479 {
480 spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
481 }
482 #[cfg(target_os = "windows")]
483 {
484 use crate::terminal_panel::WindowsShellType;
485
486 match windows_shell_type {
487 WindowsShellType::Powershell => {
488 spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
489 }
490 WindowsShellType::Cmd => {
491 spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
492 }
493 WindowsShellType::Other => {
494 spawn_task.command_label =
495 format!("{shell} -i -c '{}'", spawn_task.command_label)
496 }
497 }
498 }
499
500 let task_command = std::mem::replace(&mut spawn_task.command, shell);
501 let task_args = std::mem::take(&mut spawn_task.args);
502 let combined_command = task_args
503 .into_iter()
504 .fold(task_command, |mut command, arg| {
505 command.push(' ');
506 #[cfg(not(target_os = "windows"))]
507 command.push_str(&arg);
508 #[cfg(target_os = "windows")]
509 command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
510 command
511 });
512
513 #[cfg(not(target_os = "windows"))]
514 user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
515 #[cfg(target_os = "windows")]
516 {
517 use crate::terminal_panel::WindowsShellType;
518
519 match windows_shell_type {
520 WindowsShellType::Powershell => {
521 user_args.extend(["-C".to_owned(), combined_command])
522 }
523 WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
524 WindowsShellType::Other => {
525 user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
526 }
527 }
528 }
529 spawn_task.args = user_args;
530 let spawn_task = spawn_task;
531
532 let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
533 let use_new_terminal = spawn_in_terminal.use_new_terminal;
534
535 if allow_concurrent_runs && use_new_terminal {
536 self.spawn_in_new_terminal(spawn_task, cx)
537 .detach_and_log_err(cx);
538 return;
539 }
540
541 let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
542 if terminals_for_task.is_empty() {
543 self.spawn_in_new_terminal(spawn_task, cx)
544 .detach_and_log_err(cx);
545 return;
546 }
547 let (existing_item_index, task_pane, existing_terminal) = terminals_for_task
548 .last()
549 .expect("covered no terminals case above")
550 .clone();
551 let id = spawn_in_terminal.id.clone();
552 cx.spawn(move |this, mut cx| async move {
553 if allow_concurrent_runs {
554 debug_assert!(
555 !use_new_terminal,
556 "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
557 );
558 this.update(&mut cx, |this, cx| {
559 this.replace_terminal(
560 spawn_task,
561 task_pane,
562 existing_item_index,
563 existing_terminal,
564 cx,
565 )
566 })?
567 .await;
568 } else {
569 this.update(&mut cx, |this, cx| {
570 this.deferred_tasks.insert(
571 id,
572 cx.spawn(|terminal_panel, mut cx| async move {
573 wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
574 let Ok(Some(new_terminal_task)) =
575 terminal_panel.update(&mut cx, |terminal_panel, cx| {
576 if use_new_terminal {
577 terminal_panel
578 .spawn_in_new_terminal(spawn_task, cx)
579 .detach_and_log_err(cx);
580 None
581 } else {
582 Some(terminal_panel.replace_terminal(
583 spawn_task,
584 task_pane,
585 existing_item_index,
586 existing_terminal,
587 cx,
588 ))
589 }
590 })
591 else {
592 return;
593 };
594 new_terminal_task.await;
595 }),
596 );
597 })
598 .ok();
599 }
600 anyhow::Result::<_, anyhow::Error>::Ok(())
601 })
602 .detach()
603 }
604
605 pub fn spawn_in_new_terminal(
606 &mut self,
607 spawn_task: SpawnInTerminal,
608 cx: &mut ViewContext<Self>,
609 ) -> Task<Result<Model<Terminal>>> {
610 let reveal = spawn_task.reveal;
611 self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
612 }
613
614 /// Create a new Terminal in the current working directory or the user's home directory
615 fn new_terminal(
616 workspace: &mut Workspace,
617 _: &workspace::NewTerminal,
618 cx: &mut ViewContext<Workspace>,
619 ) {
620 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
621 return;
622 };
623
624 let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
625
626 terminal_panel
627 .update(cx, |this, cx| {
628 this.add_terminal(kind, RevealStrategy::Always, cx)
629 })
630 .detach_and_log_err(cx);
631 }
632
633 fn terminals_for_task(
634 &self,
635 label: &str,
636 cx: &mut AppContext,
637 ) -> Vec<(usize, View<Pane>, View<TerminalView>)> {
638 self.center
639 .panes()
640 .into_iter()
641 .flat_map(|pane| {
642 pane.read(cx)
643 .items()
644 .enumerate()
645 .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
646 .filter_map(|(index, terminal_view)| {
647 let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
648 if &task_state.full_label == label {
649 Some((index, terminal_view))
650 } else {
651 None
652 }
653 })
654 .map(|(index, terminal_view)| (index, pane.clone(), terminal_view))
655 })
656 .collect()
657 }
658
659 fn activate_terminal_view(
660 &self,
661 pane: &View<Pane>,
662 item_index: usize,
663 focus: bool,
664 cx: &mut WindowContext,
665 ) {
666 pane.update(cx, |pane, cx| {
667 pane.activate_item(item_index, true, focus, cx)
668 })
669 }
670
671 fn add_terminal(
672 &mut self,
673 kind: TerminalKind,
674 reveal_strategy: RevealStrategy,
675 cx: &mut ViewContext<Self>,
676 ) -> Task<Result<Model<Terminal>>> {
677 if !self.enabled {
678 return Task::ready(Err(anyhow::anyhow!(
679 "terminal not yet supported for remote projects"
680 )));
681 }
682
683 let workspace = self.workspace.clone();
684 self.pending_terminals_to_add += 1;
685
686 cx.spawn(|terminal_panel, mut cx| async move {
687 let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
688 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
689 let window = cx.window_handle();
690 let terminal = project
691 .update(&mut cx, |project, cx| {
692 project.create_terminal(kind, window, cx)
693 })?
694 .await?;
695 let result = workspace.update(&mut cx, |workspace, cx| {
696 let terminal_view = Box::new(cx.new_view(|cx| {
697 TerminalView::new(
698 terminal.clone(),
699 workspace.weak_handle(),
700 workspace.database_id(),
701 cx,
702 )
703 }));
704 pane.update(cx, |pane, cx| {
705 let focus = pane.has_focus(cx);
706 pane.add_item(terminal_view, true, focus, None, cx);
707 });
708
709 match reveal_strategy {
710 RevealStrategy::Always => {
711 workspace.focus_panel::<Self>(cx);
712 }
713 RevealStrategy::NoFocus => {
714 workspace.open_panel::<Self>(cx);
715 }
716 RevealStrategy::Never => {}
717 }
718 Ok(terminal)
719 })?;
720 terminal_panel.update(&mut cx, |this, cx| {
721 this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
722 this.serialize(cx)
723 })?;
724 result
725 })
726 }
727
728 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
729 let height = self.height;
730 let width = self.width;
731 self.pending_serialization = cx.spawn(|terminal_panel, mut cx| async move {
732 cx.background_executor()
733 .timer(Duration::from_millis(50))
734 .await;
735 let terminal_panel = terminal_panel.upgrade()?;
736 let items = terminal_panel
737 .update(&mut cx, |terminal_panel, cx| {
738 SerializedItems::WithSplits(serialize_pane_group(
739 &terminal_panel.center,
740 &terminal_panel.active_pane,
741 cx,
742 ))
743 })
744 .ok()?;
745 cx.background_executor()
746 .spawn(
747 async move {
748 KEY_VALUE_STORE
749 .write_kvp(
750 TERMINAL_PANEL_KEY.into(),
751 serde_json::to_string(&SerializedTerminalPanel {
752 items,
753 active_item_id: None,
754 height,
755 width,
756 })?,
757 )
758 .await?;
759 anyhow::Ok(())
760 }
761 .log_err(),
762 )
763 .await;
764 Some(())
765 });
766 }
767
768 fn replace_terminal(
769 &self,
770 spawn_task: SpawnInTerminal,
771 task_pane: View<Pane>,
772 terminal_item_index: usize,
773 terminal_to_replace: View<TerminalView>,
774 cx: &mut ViewContext<'_, Self>,
775 ) -> Task<Option<()>> {
776 let reveal = spawn_task.reveal;
777 let window = cx.window_handle();
778 let task_workspace = self.workspace.clone();
779 cx.spawn(move |this, mut cx| async move {
780 let project = this
781 .update(&mut cx, |this, cx| {
782 this.workspace
783 .update(cx, |workspace, _| workspace.project().clone())
784 .ok()
785 })
786 .ok()
787 .flatten()?;
788 let new_terminal = project
789 .update(&mut cx, |project, cx| {
790 project.create_terminal(TerminalKind::Task(spawn_task), window, cx)
791 })
792 .ok()?
793 .await
794 .log_err()?;
795 terminal_to_replace
796 .update(&mut cx, |terminal_to_replace, cx| {
797 terminal_to_replace.set_terminal(new_terminal, cx);
798 })
799 .ok()?;
800
801 match reveal {
802 RevealStrategy::Always => {
803 this.update(&mut cx, |this, cx| {
804 this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
805 })
806 .ok()?;
807
808 cx.spawn(|mut cx| async move {
809 task_workspace
810 .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
811 .ok()
812 })
813 .detach();
814 }
815 RevealStrategy::NoFocus => {
816 this.update(&mut cx, |this, cx| {
817 this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
818 })
819 .ok()?;
820
821 cx.spawn(|mut cx| async move {
822 task_workspace
823 .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
824 .ok()
825 })
826 .detach();
827 }
828 RevealStrategy::Never => {}
829 }
830
831 Some(())
832 })
833 }
834
835 fn has_no_terminals(&self, cx: &WindowContext) -> bool {
836 self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
837 }
838
839 pub fn assistant_enabled(&self) -> bool {
840 self.assistant_enabled
841 }
842}
843
844pub fn new_terminal_pane(
845 workspace: WeakView<Workspace>,
846 project: Model<Project>,
847 zoomed: bool,
848 cx: &mut ViewContext<TerminalPanel>,
849) -> View<Pane> {
850 let is_local = project.read(cx).is_local();
851 let terminal_panel = cx.view().clone();
852 let pane = cx.new_view(|cx| {
853 let mut pane = Pane::new(
854 workspace.clone(),
855 project.clone(),
856 Default::default(),
857 None,
858 NewTerminal.boxed_clone(),
859 cx,
860 );
861 pane.set_zoomed(zoomed, cx);
862 pane.set_can_navigate(false, cx);
863 pane.display_nav_history_buttons(None);
864 pane.set_should_display_tab_bar(|_| true);
865 pane.set_zoom_out_on_close(false);
866
867 let terminal_panel_for_split_check = terminal_panel.clone();
868 pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| {
869 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
870 let current_pane = cx.view().clone();
871 let can_drag_away =
872 terminal_panel_for_split_check.update(cx, |terminal_panel, _| {
873 let current_panes = terminal_panel.center.panes();
874 !current_panes.contains(&&tab.pane)
875 || current_panes.len() > 1
876 || (tab.pane != current_pane || pane.items_len() > 1)
877 });
878 if can_drag_away {
879 let item = if tab.pane == current_pane {
880 pane.item_for_index(tab.ix)
881 } else {
882 tab.pane.read(cx).item_for_index(tab.ix)
883 };
884 if let Some(item) = item {
885 return item.downcast::<TerminalView>().is_some();
886 }
887 }
888 }
889 false
890 })));
891
892 let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
893 let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
894 pane.toolbar().update(cx, |toolbar, cx| {
895 toolbar.add_item(buffer_search_bar, cx);
896 toolbar.add_item(breadcrumbs, cx);
897 });
898
899 pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
900 if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
901 let this_pane = cx.view().clone();
902 let belongs_to_this_pane = tab.pane == this_pane;
903 let item = if belongs_to_this_pane {
904 pane.item_for_index(tab.ix)
905 } else {
906 tab.pane.read(cx).item_for_index(tab.ix)
907 };
908 if let Some(item) = item {
909 if item.downcast::<TerminalView>().is_some() {
910 let source = tab.pane.clone();
911 let item_id_to_move = item.item_id();
912
913 let new_pane = pane.drag_split_direction().and_then(|split_direction| {
914 terminal_panel.update(cx, |terminal_panel, cx| {
915 let is_zoomed = if terminal_panel.active_pane == this_pane {
916 pane.is_zoomed()
917 } else {
918 terminal_panel.active_pane.read(cx).is_zoomed()
919 };
920 let new_pane = new_terminal_pane(
921 workspace.clone(),
922 project.clone(),
923 is_zoomed,
924 cx,
925 );
926 terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
927 terminal_panel
928 .center
929 .split(&this_pane, &new_pane, split_direction)
930 .log_err()?;
931 Some(new_pane)
932 })
933 });
934
935 let destination;
936 let destination_index;
937 if let Some(new_pane) = new_pane {
938 destination_index = new_pane.read(cx).active_item_index();
939 destination = new_pane;
940 } else if belongs_to_this_pane {
941 return ControlFlow::Break(());
942 } else {
943 destination = cx.view().clone();
944 destination_index = pane.active_item_index();
945 }
946 // Destination pane may be the one currently updated, so defer the move.
947 cx.spawn(|_, mut cx| async move {
948 cx.update(|cx| {
949 move_item(
950 &source,
951 &destination,
952 item_id_to_move,
953 destination_index,
954 cx,
955 );
956 })
957 .ok();
958 })
959 .detach();
960 } else if let Some(project_path) = item.project_path(cx) {
961 if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
962 {
963 add_paths_to_terminal(pane, &[entry_path], cx);
964 }
965 }
966 }
967 } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
968 if let Some(entry_path) = project
969 .read(cx)
970 .path_for_entry(entry_id, cx)
971 .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
972 {
973 add_paths_to_terminal(pane, &[entry_path], cx);
974 }
975 } else if is_local {
976 if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
977 add_paths_to_terminal(pane, paths.paths(), cx);
978 }
979 }
980
981 ControlFlow::Break(())
982 });
983
984 pane
985 });
986
987 cx.subscribe(&pane, TerminalPanel::handle_pane_event)
988 .detach();
989 cx.observe(&pane, |_, _, cx| cx.notify()).detach();
990
991 pane
992}
993
994async fn wait_for_terminals_tasks(
995 terminals_for_task: Vec<(usize, View<Pane>, View<TerminalView>)>,
996 cx: &mut AsyncWindowContext,
997) {
998 let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
999 terminal
1000 .update(cx, |terminal_view, cx| {
1001 terminal_view
1002 .terminal()
1003 .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1004 })
1005 .ok()
1006 });
1007 let _: Vec<()> = join_all(pending_tasks).await;
1008}
1009
1010fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
1011 if let Some(terminal_view) = pane
1012 .active_item()
1013 .and_then(|item| item.downcast::<TerminalView>())
1014 {
1015 cx.focus_view(&terminal_view);
1016 let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1017 new_text.push(' ');
1018 terminal_view.update(cx, |terminal_view, cx| {
1019 terminal_view.terminal().update(cx, |terminal, _| {
1020 terminal.paste(&new_text);
1021 });
1022 });
1023 }
1024}
1025
1026impl EventEmitter<PanelEvent> for TerminalPanel {}
1027
1028impl Render for TerminalPanel {
1029 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1030 let mut registrar = DivRegistrar::new(
1031 |panel, cx| {
1032 panel
1033 .active_pane
1034 .read(cx)
1035 .toolbar()
1036 .read(cx)
1037 .item_of_type::<BufferSearchBar>()
1038 },
1039 cx,
1040 );
1041 BufferSearchBar::register(&mut registrar);
1042 let registrar = registrar.into_div();
1043 self.workspace
1044 .update(cx, |workspace, cx| {
1045 registrar.size_full().child(self.center.render(
1046 workspace.project(),
1047 &HashMap::default(),
1048 None,
1049 &self.active_pane,
1050 workspace.zoomed_item(),
1051 workspace.app_state(),
1052 cx,
1053 ))
1054 })
1055 .ok()
1056 .map(|div| {
1057 div.on_action({
1058 cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| {
1059 if let Some(pane) = terminal_panel.center.find_pane_in_direction(
1060 &terminal_panel.active_pane,
1061 action.0,
1062 cx,
1063 ) {
1064 cx.focus_view(&pane);
1065 } else {
1066 terminal_panel
1067 .workspace
1068 .update(cx, |workspace, cx| {
1069 workspace.activate_pane_in_direction(action.0, cx)
1070 })
1071 .ok();
1072 }
1073 })
1074 })
1075 .on_action(
1076 cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| {
1077 let panes = terminal_panel.center.panes();
1078 if let Some(ix) = panes
1079 .iter()
1080 .position(|pane| **pane == terminal_panel.active_pane)
1081 {
1082 let next_ix = (ix + 1) % panes.len();
1083 let next_pane = panes[next_ix].clone();
1084 cx.focus_view(&next_pane);
1085 }
1086 }),
1087 )
1088 .on_action(
1089 cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| {
1090 let panes = terminal_panel.center.panes();
1091 if let Some(ix) = panes
1092 .iter()
1093 .position(|pane| **pane == terminal_panel.active_pane)
1094 {
1095 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1096 let prev_pane = panes[prev_ix].clone();
1097 cx.focus_view(&prev_pane);
1098 }
1099 }),
1100 )
1101 .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| {
1102 let panes = terminal_panel.center.panes();
1103 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1104 cx.focus_view(&pane);
1105 } else {
1106 let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
1107 cx.spawn(|terminal_panel, mut cx| async move {
1108 if let Some(new_pane) = new_pane.await {
1109 terminal_panel
1110 .update(&mut cx, |terminal_panel, cx| {
1111 terminal_panel
1112 .center
1113 .split(
1114 &terminal_panel.active_pane,
1115 &new_pane,
1116 SplitDirection::Right,
1117 )
1118 .log_err();
1119 cx.focus_view(&new_pane);
1120 })
1121 .ok();
1122 }
1123 })
1124 .detach();
1125 }
1126 }))
1127 .on_action(cx.listener(
1128 |terminal_panel, action: &SwapPaneInDirection, cx| {
1129 if let Some(to) = terminal_panel
1130 .center
1131 .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx)
1132 .cloned()
1133 {
1134 terminal_panel
1135 .center
1136 .swap(&terminal_panel.active_pane.clone(), &to);
1137 cx.notify();
1138 }
1139 },
1140 ))
1141 })
1142 .unwrap_or_else(|| div())
1143 }
1144}
1145
1146impl FocusableView for TerminalPanel {
1147 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1148 self.active_pane.focus_handle(cx)
1149 }
1150}
1151
1152impl Panel for TerminalPanel {
1153 fn position(&self, cx: &WindowContext) -> DockPosition {
1154 match TerminalSettings::get_global(cx).dock {
1155 TerminalDockPosition::Left => DockPosition::Left,
1156 TerminalDockPosition::Bottom => DockPosition::Bottom,
1157 TerminalDockPosition::Right => DockPosition::Right,
1158 }
1159 }
1160
1161 fn position_is_valid(&self, _: DockPosition) -> bool {
1162 true
1163 }
1164
1165 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1166 settings::update_settings_file::<TerminalSettings>(
1167 self.fs.clone(),
1168 cx,
1169 move |settings, _| {
1170 let dock = match position {
1171 DockPosition::Left => TerminalDockPosition::Left,
1172 DockPosition::Bottom => TerminalDockPosition::Bottom,
1173 DockPosition::Right => TerminalDockPosition::Right,
1174 };
1175 settings.dock = Some(dock);
1176 },
1177 );
1178 }
1179
1180 fn size(&self, cx: &WindowContext) -> Pixels {
1181 let settings = TerminalSettings::get_global(cx);
1182 match self.position(cx) {
1183 DockPosition::Left | DockPosition::Right => {
1184 self.width.unwrap_or(settings.default_width)
1185 }
1186 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1187 }
1188 }
1189
1190 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1191 match self.position(cx) {
1192 DockPosition::Left | DockPosition::Right => self.width = size,
1193 DockPosition::Bottom => self.height = size,
1194 }
1195 self.serialize(cx);
1196 cx.notify();
1197 }
1198
1199 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1200 self.active_pane.read(cx).is_zoomed()
1201 }
1202
1203 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1204 for pane in self.center.panes() {
1205 pane.update(cx, |pane, cx| {
1206 pane.set_zoomed(zoomed, cx);
1207 })
1208 }
1209 cx.notify();
1210 }
1211
1212 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1213 if !active || !self.has_no_terminals(cx) {
1214 return;
1215 }
1216 cx.defer(|this, cx| {
1217 let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
1218 TerminalKind::Shell(default_working_directory(workspace, cx))
1219 }) else {
1220 return;
1221 };
1222
1223 this.add_terminal(kind, RevealStrategy::Never, cx)
1224 .detach_and_log_err(cx)
1225 })
1226 }
1227
1228 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
1229 let count = self
1230 .center
1231 .panes()
1232 .into_iter()
1233 .map(|pane| pane.read(cx).items_len())
1234 .sum::<usize>();
1235 if count == 0 {
1236 None
1237 } else {
1238 Some(count.to_string())
1239 }
1240 }
1241
1242 fn persistent_name() -> &'static str {
1243 "TerminalPanel"
1244 }
1245
1246 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1247 if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
1248 Some(IconName::Terminal)
1249 } else {
1250 None
1251 }
1252 }
1253
1254 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1255 Some("Terminal Panel")
1256 }
1257
1258 fn toggle_action(&self) -> Box<dyn gpui::Action> {
1259 Box::new(ToggleFocus)
1260 }
1261
1262 fn pane(&self) -> Option<View<Pane>> {
1263 Some(self.active_pane.clone())
1264 }
1265}
1266
1267struct InlineAssistTabBarButton {
1268 focus_handle: FocusHandle,
1269}
1270
1271impl Render for InlineAssistTabBarButton {
1272 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1273 let focus_handle = self.focus_handle.clone();
1274 IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1275 .icon_size(IconSize::Small)
1276 .on_click(cx.listener(|_, _, cx| {
1277 cx.dispatch_action(InlineAssist::default().boxed_clone());
1278 }))
1279 .tooltip(move |cx| {
1280 Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1281 })
1282 }
1283}
1284
1285fn retrieve_system_shell() -> Option<String> {
1286 #[cfg(not(target_os = "windows"))]
1287 {
1288 use anyhow::Context;
1289 use util::ResultExt;
1290
1291 std::env::var("SHELL")
1292 .context("Error finding SHELL in env.")
1293 .log_err()
1294 }
1295 // `alacritty_terminal` uses this as default on Windows. See:
1296 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
1297 #[cfg(target_os = "windows")]
1298 return Some("powershell".to_owned());
1299}
1300
1301#[cfg(target_os = "windows")]
1302fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
1303 match shell_type {
1304 WindowsShellType::Powershell => to_powershell_variable(input),
1305 WindowsShellType::Cmd => to_cmd_variable(input),
1306 WindowsShellType::Other => input,
1307 }
1308}
1309
1310#[cfg(target_os = "windows")]
1311fn to_windows_shell_type(shell: &str) -> WindowsShellType {
1312 if shell == "powershell"
1313 || shell.ends_with("powershell.exe")
1314 || shell == "pwsh"
1315 || shell.ends_with("pwsh.exe")
1316 {
1317 WindowsShellType::Powershell
1318 } else if shell == "cmd" || shell.ends_with("cmd.exe") {
1319 WindowsShellType::Cmd
1320 } else {
1321 // Someother shell detected, the user might install and use a
1322 // unix-like shell.
1323 WindowsShellType::Other
1324 }
1325}
1326
1327/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
1328#[inline]
1329#[cfg(target_os = "windows")]
1330fn to_cmd_variable(input: String) -> String {
1331 if let Some(var_str) = input.strip_prefix("${") {
1332 if var_str.find(':').is_none() {
1333 // If the input starts with "${", remove the trailing "}"
1334 format!("%{}%", &var_str[..var_str.len() - 1])
1335 } else {
1336 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1337 // which will result in the task failing to run in such cases.
1338 input
1339 }
1340 } else if let Some(var_str) = input.strip_prefix('$') {
1341 // If the input starts with "$", directly append to "$env:"
1342 format!("%{}%", var_str)
1343 } else {
1344 // If no prefix is found, return the input as is
1345 input
1346 }
1347}
1348
1349/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
1350#[inline]
1351#[cfg(target_os = "windows")]
1352fn to_powershell_variable(input: String) -> String {
1353 if let Some(var_str) = input.strip_prefix("${") {
1354 if var_str.find(':').is_none() {
1355 // If the input starts with "${", remove the trailing "}"
1356 format!("$env:{}", &var_str[..var_str.len() - 1])
1357 } else {
1358 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1359 // which will result in the task failing to run in such cases.
1360 input
1361 }
1362 } else if let Some(var_str) = input.strip_prefix('$') {
1363 // If the input starts with "$", directly append to "$env:"
1364 format!("$env:{}", var_str)
1365 } else {
1366 // If no prefix is found, return the input as is
1367 input
1368 }
1369}
1370
1371#[cfg(target_os = "windows")]
1372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1373enum WindowsShellType {
1374 Powershell,
1375 Cmd,
1376 Other,
1377}