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 item = if tab.pane == this_pane {
903 pane.item_for_index(tab.ix)
904 } else {
905 tab.pane.read(cx).item_for_index(tab.ix)
906 };
907 if let Some(item) = item {
908 if item.downcast::<TerminalView>().is_some() {
909 let source = tab.pane.clone();
910 let item_id_to_move = item.item_id();
911
912 let new_split_pane = pane
913 .drag_split_direction()
914 .map(|split_direction| {
915 terminal_panel.update(cx, |terminal_panel, cx| {
916 let is_zoomed = if terminal_panel.active_pane == this_pane {
917 pane.is_zoomed()
918 } else {
919 terminal_panel.active_pane.read(cx).is_zoomed()
920 };
921 let new_pane = new_terminal_pane(
922 workspace.clone(),
923 project.clone(),
924 is_zoomed,
925 cx,
926 );
927 terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
928 terminal_panel.center.split(
929 &this_pane,
930 &new_pane,
931 split_direction,
932 )?;
933 anyhow::Ok(new_pane)
934 })
935 })
936 .transpose();
937
938 match new_split_pane {
939 // Source pane may be the one currently updated, so defer the move.
940 Ok(Some(new_pane)) => cx
941 .spawn(|_, mut cx| async move {
942 cx.update(|cx| {
943 move_item(
944 &source,
945 &new_pane,
946 item_id_to_move,
947 new_pane.read(cx).active_item_index(),
948 cx,
949 );
950 })
951 .ok();
952 })
953 .detach(),
954 // If we drop into existing pane or current pane,
955 // regular pane drop handler will take care of it,
956 // using the right tab index for the operation.
957 Ok(None) => return ControlFlow::Continue(()),
958 err @ Err(_) => {
959 err.log_err();
960 return ControlFlow::Break(());
961 }
962 };
963 } else if let Some(project_path) = item.project_path(cx) {
964 if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
965 {
966 add_paths_to_terminal(pane, &[entry_path], cx);
967 }
968 }
969 }
970 } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
971 if let Some(entry_path) = project
972 .read(cx)
973 .path_for_entry(entry_id, cx)
974 .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
975 {
976 add_paths_to_terminal(pane, &[entry_path], cx);
977 }
978 } else if is_local {
979 if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
980 add_paths_to_terminal(pane, paths.paths(), cx);
981 }
982 }
983
984 ControlFlow::Break(())
985 });
986
987 pane
988 });
989
990 cx.subscribe(&pane, TerminalPanel::handle_pane_event)
991 .detach();
992 cx.observe(&pane, |_, _, cx| cx.notify()).detach();
993
994 pane
995}
996
997async fn wait_for_terminals_tasks(
998 terminals_for_task: Vec<(usize, View<Pane>, View<TerminalView>)>,
999 cx: &mut AsyncWindowContext,
1000) {
1001 let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1002 terminal
1003 .update(cx, |terminal_view, cx| {
1004 terminal_view
1005 .terminal()
1006 .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1007 })
1008 .ok()
1009 });
1010 let _: Vec<()> = join_all(pending_tasks).await;
1011}
1012
1013fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
1014 if let Some(terminal_view) = pane
1015 .active_item()
1016 .and_then(|item| item.downcast::<TerminalView>())
1017 {
1018 cx.focus_view(&terminal_view);
1019 let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1020 new_text.push(' ');
1021 terminal_view.update(cx, |terminal_view, cx| {
1022 terminal_view.terminal().update(cx, |terminal, _| {
1023 terminal.paste(&new_text);
1024 });
1025 });
1026 }
1027}
1028
1029impl EventEmitter<PanelEvent> for TerminalPanel {}
1030
1031impl Render for TerminalPanel {
1032 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1033 let mut registrar = DivRegistrar::new(
1034 |panel, cx| {
1035 panel
1036 .active_pane
1037 .read(cx)
1038 .toolbar()
1039 .read(cx)
1040 .item_of_type::<BufferSearchBar>()
1041 },
1042 cx,
1043 );
1044 BufferSearchBar::register(&mut registrar);
1045 let registrar = registrar.into_div();
1046 self.workspace
1047 .update(cx, |workspace, cx| {
1048 registrar.size_full().child(self.center.render(
1049 workspace.project(),
1050 &HashMap::default(),
1051 None,
1052 &self.active_pane,
1053 workspace.zoomed_item(),
1054 workspace.app_state(),
1055 cx,
1056 ))
1057 })
1058 .ok()
1059 .map(|div| {
1060 div.on_action({
1061 cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| {
1062 if let Some(pane) = terminal_panel.center.find_pane_in_direction(
1063 &terminal_panel.active_pane,
1064 action.0,
1065 cx,
1066 ) {
1067 cx.focus_view(&pane);
1068 } else {
1069 terminal_panel
1070 .workspace
1071 .update(cx, |workspace, cx| {
1072 workspace.activate_pane_in_direction(action.0, cx)
1073 })
1074 .ok();
1075 }
1076 })
1077 })
1078 .on_action(
1079 cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| {
1080 let panes = terminal_panel.center.panes();
1081 if let Some(ix) = panes
1082 .iter()
1083 .position(|pane| **pane == terminal_panel.active_pane)
1084 {
1085 let next_ix = (ix + 1) % panes.len();
1086 let next_pane = panes[next_ix].clone();
1087 cx.focus_view(&next_pane);
1088 }
1089 }),
1090 )
1091 .on_action(
1092 cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| {
1093 let panes = terminal_panel.center.panes();
1094 if let Some(ix) = panes
1095 .iter()
1096 .position(|pane| **pane == terminal_panel.active_pane)
1097 {
1098 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1099 let prev_pane = panes[prev_ix].clone();
1100 cx.focus_view(&prev_pane);
1101 }
1102 }),
1103 )
1104 .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| {
1105 let panes = terminal_panel.center.panes();
1106 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1107 cx.focus_view(&pane);
1108 } else {
1109 let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
1110 cx.spawn(|terminal_panel, mut cx| async move {
1111 if let Some(new_pane) = new_pane.await {
1112 terminal_panel
1113 .update(&mut cx, |terminal_panel, cx| {
1114 terminal_panel
1115 .center
1116 .split(
1117 &terminal_panel.active_pane,
1118 &new_pane,
1119 SplitDirection::Right,
1120 )
1121 .log_err();
1122 cx.focus_view(&new_pane);
1123 })
1124 .ok();
1125 }
1126 })
1127 .detach();
1128 }
1129 }))
1130 .on_action(cx.listener(
1131 |terminal_panel, action: &SwapPaneInDirection, cx| {
1132 if let Some(to) = terminal_panel
1133 .center
1134 .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx)
1135 .cloned()
1136 {
1137 terminal_panel
1138 .center
1139 .swap(&terminal_panel.active_pane.clone(), &to);
1140 cx.notify();
1141 }
1142 },
1143 ))
1144 })
1145 .unwrap_or_else(|| div())
1146 }
1147}
1148
1149impl FocusableView for TerminalPanel {
1150 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1151 self.active_pane.focus_handle(cx)
1152 }
1153}
1154
1155impl Panel for TerminalPanel {
1156 fn position(&self, cx: &WindowContext) -> DockPosition {
1157 match TerminalSettings::get_global(cx).dock {
1158 TerminalDockPosition::Left => DockPosition::Left,
1159 TerminalDockPosition::Bottom => DockPosition::Bottom,
1160 TerminalDockPosition::Right => DockPosition::Right,
1161 }
1162 }
1163
1164 fn position_is_valid(&self, _: DockPosition) -> bool {
1165 true
1166 }
1167
1168 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1169 settings::update_settings_file::<TerminalSettings>(
1170 self.fs.clone(),
1171 cx,
1172 move |settings, _| {
1173 let dock = match position {
1174 DockPosition::Left => TerminalDockPosition::Left,
1175 DockPosition::Bottom => TerminalDockPosition::Bottom,
1176 DockPosition::Right => TerminalDockPosition::Right,
1177 };
1178 settings.dock = Some(dock);
1179 },
1180 );
1181 }
1182
1183 fn size(&self, cx: &WindowContext) -> Pixels {
1184 let settings = TerminalSettings::get_global(cx);
1185 match self.position(cx) {
1186 DockPosition::Left | DockPosition::Right => {
1187 self.width.unwrap_or(settings.default_width)
1188 }
1189 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1190 }
1191 }
1192
1193 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1194 match self.position(cx) {
1195 DockPosition::Left | DockPosition::Right => self.width = size,
1196 DockPosition::Bottom => self.height = size,
1197 }
1198 self.serialize(cx);
1199 cx.notify();
1200 }
1201
1202 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1203 self.active_pane.read(cx).is_zoomed()
1204 }
1205
1206 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1207 for pane in self.center.panes() {
1208 pane.update(cx, |pane, cx| {
1209 pane.set_zoomed(zoomed, cx);
1210 })
1211 }
1212 cx.notify();
1213 }
1214
1215 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1216 if !active || !self.has_no_terminals(cx) {
1217 return;
1218 }
1219 cx.defer(|this, cx| {
1220 let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
1221 TerminalKind::Shell(default_working_directory(workspace, cx))
1222 }) else {
1223 return;
1224 };
1225
1226 this.add_terminal(kind, RevealStrategy::Never, cx)
1227 .detach_and_log_err(cx)
1228 })
1229 }
1230
1231 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
1232 let count = self
1233 .center
1234 .panes()
1235 .into_iter()
1236 .map(|pane| pane.read(cx).items_len())
1237 .sum::<usize>();
1238 if count == 0 {
1239 None
1240 } else {
1241 Some(count.to_string())
1242 }
1243 }
1244
1245 fn persistent_name() -> &'static str {
1246 "TerminalPanel"
1247 }
1248
1249 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1250 if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
1251 Some(IconName::Terminal)
1252 } else {
1253 None
1254 }
1255 }
1256
1257 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1258 Some("Terminal Panel")
1259 }
1260
1261 fn toggle_action(&self) -> Box<dyn gpui::Action> {
1262 Box::new(ToggleFocus)
1263 }
1264
1265 fn pane(&self) -> Option<View<Pane>> {
1266 Some(self.active_pane.clone())
1267 }
1268}
1269
1270struct InlineAssistTabBarButton {
1271 focus_handle: FocusHandle,
1272}
1273
1274impl Render for InlineAssistTabBarButton {
1275 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1276 let focus_handle = self.focus_handle.clone();
1277 IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1278 .icon_size(IconSize::Small)
1279 .on_click(cx.listener(|_, _, cx| {
1280 cx.dispatch_action(InlineAssist::default().boxed_clone());
1281 }))
1282 .tooltip(move |cx| {
1283 Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1284 })
1285 }
1286}
1287
1288fn retrieve_system_shell() -> Option<String> {
1289 #[cfg(not(target_os = "windows"))]
1290 {
1291 use anyhow::Context;
1292 use util::ResultExt;
1293
1294 std::env::var("SHELL")
1295 .context("Error finding SHELL in env.")
1296 .log_err()
1297 }
1298 // `alacritty_terminal` uses this as default on Windows. See:
1299 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
1300 #[cfg(target_os = "windows")]
1301 return Some("powershell".to_owned());
1302}
1303
1304#[cfg(target_os = "windows")]
1305fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
1306 match shell_type {
1307 WindowsShellType::Powershell => to_powershell_variable(input),
1308 WindowsShellType::Cmd => to_cmd_variable(input),
1309 WindowsShellType::Other => input,
1310 }
1311}
1312
1313#[cfg(target_os = "windows")]
1314fn to_windows_shell_type(shell: &str) -> WindowsShellType {
1315 if shell == "powershell"
1316 || shell.ends_with("powershell.exe")
1317 || shell == "pwsh"
1318 || shell.ends_with("pwsh.exe")
1319 {
1320 WindowsShellType::Powershell
1321 } else if shell == "cmd" || shell.ends_with("cmd.exe") {
1322 WindowsShellType::Cmd
1323 } else {
1324 // Someother shell detected, the user might install and use a
1325 // unix-like shell.
1326 WindowsShellType::Other
1327 }
1328}
1329
1330/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
1331#[inline]
1332#[cfg(target_os = "windows")]
1333fn to_cmd_variable(input: String) -> String {
1334 if let Some(var_str) = input.strip_prefix("${") {
1335 if var_str.find(':').is_none() {
1336 // If the input starts with "${", remove the trailing "}"
1337 format!("%{}%", &var_str[..var_str.len() - 1])
1338 } else {
1339 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1340 // which will result in the task failing to run in such cases.
1341 input
1342 }
1343 } else if let Some(var_str) = input.strip_prefix('$') {
1344 // If the input starts with "$", directly append to "$env:"
1345 format!("%{}%", var_str)
1346 } else {
1347 // If no prefix is found, return the input as is
1348 input
1349 }
1350}
1351
1352/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
1353#[inline]
1354#[cfg(target_os = "windows")]
1355fn to_powershell_variable(input: String) -> String {
1356 if let Some(var_str) = input.strip_prefix("${") {
1357 if var_str.find(':').is_none() {
1358 // If the input starts with "${", remove the trailing "}"
1359 format!("$env:{}", &var_str[..var_str.len() - 1])
1360 } else {
1361 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1362 // which will result in the task failing to run in such cases.
1363 input
1364 }
1365 } else if let Some(var_str) = input.strip_prefix('$') {
1366 // If the input starts with "$", directly append to "$env:"
1367 format!("$env:{}", var_str)
1368 } else {
1369 // If no prefix is found, return the input as is
1370 input
1371 }
1372}
1373
1374#[cfg(target_os = "windows")]
1375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1376enum WindowsShellType {
1377 Powershell,
1378 Cmd,
1379 Other,
1380}