1use std::{cmp, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration};
2
3use crate::{
4 TerminalView, default_working_directory,
5 persistence::{
6 SerializedItems, SerializedTerminalPanel, deserialize_terminal_panel, serialize_pane_group,
7 },
8};
9use breadcrumbs::Breadcrumbs;
10use collections::HashMap;
11use db::kvp::KeyValueStore;
12use futures::{channel::oneshot, future::join_all};
13use gpui::{
14 Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter,
15 FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, WeakEntity,
16 Window, actions,
17};
18use itertools::Itertools;
19use project::{Fs, Project};
20
21use settings::{Settings, TerminalDockPosition};
22use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
23use terminal::{Terminal, terminal_settings::TerminalSettings};
24use ui::{
25 ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
26 Tooltip, prelude::*,
27};
28use util::{ResultExt, TryFutureExt};
29use workspace::{
30 ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
31 ActivatePaneUp, ActivatePreviousPane, DraggedTab, ItemId, MoveItemToPane,
32 MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane,
33 PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown,
34 SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
35 dock::{DockPosition, Panel, PanelEvent, PanelHandle},
36 item::SerializableItem,
37 move_active_item, pane,
38};
39
40use anyhow::{Result, anyhow};
41use zed_actions::assistant::InlineAssist;
42
43const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
44
45actions!(
46 terminal_panel,
47 [
48 /// Toggles the terminal panel.
49 Toggle,
50 /// Toggles focus on the terminal panel.
51 ToggleFocus
52 ]
53);
54
55pub fn init(cx: &mut App) {
56 cx.observe_new(
57 |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
58 workspace.register_action(TerminalPanel::new_terminal);
59 workspace.register_action(TerminalPanel::open_terminal);
60 workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
61 if is_enabled_in_workspace(workspace, cx) {
62 workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
63 }
64 });
65 workspace.register_action(|workspace, _: &Toggle, window, cx| {
66 if is_enabled_in_workspace(workspace, cx) {
67 if !workspace.toggle_panel_focus::<TerminalPanel>(window, cx) {
68 workspace.close_panel::<TerminalPanel>(window, cx);
69 }
70 }
71 });
72 },
73 )
74 .detach();
75}
76
77pub struct TerminalPanel {
78 pub(crate) active_pane: Entity<Pane>,
79 pub(crate) center: PaneGroup,
80 fs: Arc<dyn Fs>,
81 workspace: WeakEntity<Workspace>,
82 pending_serialization: Task<Option<()>>,
83 pending_terminals_to_add: usize,
84 deferred_tasks: HashMap<TaskId, Task<()>>,
85 assistant_enabled: bool,
86 assistant_tab_bar_button: Option<AnyView>,
87 active: bool,
88}
89
90impl TerminalPanel {
91 pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
92 let project = workspace.project();
93 let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
94 let center = PaneGroup::new(pane.clone());
95 let terminal_panel = Self {
96 center,
97 active_pane: pane,
98 fs: workspace.app_state().fs.clone(),
99 workspace: workspace.weak_handle(),
100 pending_serialization: Task::ready(None),
101 pending_terminals_to_add: 0,
102 deferred_tasks: HashMap::default(),
103 assistant_enabled: false,
104 assistant_tab_bar_button: None,
105 active: false,
106 };
107 terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
108 terminal_panel
109 }
110
111 pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
112 self.assistant_enabled = enabled;
113 if enabled {
114 let focus_handle = self
115 .active_pane
116 .read(cx)
117 .active_item()
118 .map(|item| item.item_focus_handle(cx))
119 .unwrap_or(self.focus_handle(cx));
120 self.assistant_tab_bar_button = Some(
121 cx.new(move |_| InlineAssistTabBarButton { focus_handle })
122 .into(),
123 );
124 } else {
125 self.assistant_tab_bar_button = None;
126 }
127 for pane in self.center.panes() {
128 self.apply_tab_bar_buttons(pane, cx);
129 }
130 }
131
132 pub(crate) fn apply_tab_bar_buttons(
133 &self,
134 terminal_pane: &Entity<Pane>,
135 cx: &mut Context<Self>,
136 ) {
137 let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
138 terminal_pane.update(cx, |pane, cx| {
139 pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
140 let split_context = pane
141 .active_item()
142 .and_then(|item| item.downcast::<TerminalView>())
143 .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
144 let has_focused_rename_editor = pane
145 .active_item()
146 .and_then(|item| item.downcast::<TerminalView>())
147 .is_some_and(|view| view.read(cx).rename_editor_is_focused(window, cx));
148 if !pane.has_focus(window, cx)
149 && !pane.context_menu_focused(window, cx)
150 && !has_focused_rename_editor
151 {
152 return (None, None);
153 }
154 let focus_handle = pane.focus_handle(cx);
155 let right_children = h_flex()
156 .gap(DynamicSpacing::Base02.rems(cx))
157 .child(
158 PopoverMenu::new("terminal-tab-bar-popover-menu")
159 .trigger_with_tooltip(
160 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
161 Tooltip::text("Newβ¦"),
162 )
163 .anchor(Corner::TopRight)
164 .with_handle(pane.new_item_context_menu_handle.clone())
165 .menu(move |window, cx| {
166 let focus_handle = focus_handle.clone();
167 let menu = ContextMenu::build(window, cx, |menu, _, _| {
168 menu.context(focus_handle.clone())
169 .action(
170 "New Terminal",
171 workspace::NewTerminal::default().boxed_clone(),
172 )
173 // We want the focus to go back to terminal panel once task modal is dismissed,
174 // hence we focus that first. Otherwise, we'd end up without a focused element, as
175 // context menu will be gone the moment we spawn the modal.
176 .action(
177 "Spawn Task",
178 zed_actions::Spawn::modal().boxed_clone(),
179 )
180 });
181
182 Some(menu)
183 }),
184 )
185 .children(assistant_tab_bar_button.clone())
186 .child(
187 PopoverMenu::new("terminal-pane-tab-bar-split")
188 .trigger_with_tooltip(
189 IconButton::new("terminal-pane-split", IconName::Split)
190 .icon_size(IconSize::Small),
191 Tooltip::text("Split Pane"),
192 )
193 .anchor(Corner::TopRight)
194 .with_handle(pane.split_item_context_menu_handle.clone())
195 .menu({
196 move |window, cx| {
197 ContextMenu::build(window, cx, |menu, _, _| {
198 menu.when_some(
199 split_context.clone(),
200 |menu, split_context| menu.context(split_context),
201 )
202 .action("Split Right", SplitRight::default().boxed_clone())
203 .action("Split Left", SplitLeft::default().boxed_clone())
204 .action("Split Up", SplitUp::default().boxed_clone())
205 .action("Split Down", SplitDown::default().boxed_clone())
206 })
207 .into()
208 }
209 }),
210 )
211 .child({
212 let zoomed = pane.is_zoomed();
213 IconButton::new("toggle_zoom", IconName::Maximize)
214 .icon_size(IconSize::Small)
215 .toggle_state(zoomed)
216 .selected_icon(IconName::Minimize)
217 .on_click(cx.listener(|pane, _, window, cx| {
218 pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
219 }))
220 .tooltip(move |_window, cx| {
221 Tooltip::for_action(
222 if zoomed { "Zoom Out" } else { "Zoom In" },
223 &ToggleZoom,
224 cx,
225 )
226 })
227 })
228 .into_any_element()
229 .into();
230 (None, right_children)
231 });
232 });
233 }
234
235 fn serialization_key(workspace: &Workspace) -> Option<String> {
236 workspace
237 .database_id()
238 .map(|id| i64::from(id).to_string())
239 .or(workspace.session_id())
240 .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
241 }
242
243 pub async fn load(
244 workspace: WeakEntity<Workspace>,
245 mut cx: AsyncWindowContext,
246 ) -> Result<Entity<Self>> {
247 let mut terminal_panel = None;
248
249 if let Some((database_id, serialization_key, kvp)) = workspace
250 .read_with(&cx, |workspace, cx| {
251 workspace
252 .database_id()
253 .zip(TerminalPanel::serialization_key(workspace))
254 .map(|(id, key)| (id, key, KeyValueStore::global(cx)))
255 })
256 .ok()
257 .flatten()
258 && let Some(serialized_panel) = cx
259 .background_spawn(async move { kvp.read_kvp(&serialization_key) })
260 .await
261 .log_err()
262 .flatten()
263 .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
264 .transpose()
265 .log_err()
266 .flatten()
267 && let Ok(serialized) = workspace
268 .update_in(&mut cx, |workspace, window, cx| {
269 deserialize_terminal_panel(
270 workspace.weak_handle(),
271 workspace.project().clone(),
272 database_id,
273 serialized_panel,
274 window,
275 cx,
276 )
277 })?
278 .await
279 {
280 terminal_panel = Some(serialized);
281 }
282
283 let terminal_panel = if let Some(panel) = terminal_panel {
284 panel
285 } else {
286 workspace.update_in(&mut cx, |workspace, window, cx| {
287 cx.new(|cx| TerminalPanel::new(workspace, window, cx))
288 })?
289 };
290
291 if let Some(workspace) = workspace.upgrade() {
292 workspace.update(&mut cx, |workspace, _| {
293 workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
294 });
295 }
296
297 // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
298 if let Some(workspace) = workspace.upgrade() {
299 let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
300 let alive_item_ids = terminal_panel
301 .read(cx)
302 .center
303 .panes()
304 .into_iter()
305 .flat_map(|pane| pane.read(cx).items())
306 .map(|item| item.item_id().as_u64() as ItemId)
307 .collect();
308 workspace.database_id().map(|workspace_id| {
309 TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
310 })
311 })?;
312 if let Some(task) = cleanup_task {
313 task.await.log_err();
314 }
315 }
316
317 if let Some(workspace) = workspace.upgrade() {
318 let should_focus = workspace
319 .update_in(&mut cx, |workspace, window, cx| {
320 workspace.active_item(cx).is_none()
321 && workspace
322 .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
323 })
324 .unwrap_or(false);
325
326 if should_focus {
327 terminal_panel
328 .update_in(&mut cx, |panel, window, cx| {
329 panel.active_pane.update(cx, |pane, cx| {
330 pane.focus_active_item(window, cx);
331 });
332 })
333 .ok();
334 }
335 }
336 Ok(terminal_panel)
337 }
338
339 fn handle_pane_event(
340 &mut self,
341 pane: &Entity<Pane>,
342 event: &pane::Event,
343 window: &mut Window,
344 cx: &mut Context<Self>,
345 ) {
346 match event {
347 pane::Event::ActivateItem { .. } => self.serialize(cx),
348 pane::Event::RemovedItem { .. } => self.serialize(cx),
349 pane::Event::Remove { focus_on_pane } => {
350 let pane_count_before_removal = self.center.panes().len();
351 let _removal_result = self.center.remove(pane, cx);
352 if pane_count_before_removal == 1 {
353 self.center.first_pane().update(cx, |pane, cx| {
354 pane.set_zoomed(false, cx);
355 });
356 cx.emit(PanelEvent::Close);
357 } else if let Some(focus_on_pane) =
358 focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
359 {
360 focus_on_pane.focus_handle(cx).focus(window, cx);
361 }
362 }
363 pane::Event::ZoomIn => {
364 for pane in self.center.panes() {
365 pane.update(cx, |pane, cx| {
366 pane.set_zoomed(true, cx);
367 })
368 }
369 cx.emit(PanelEvent::ZoomIn);
370 cx.notify();
371 }
372 pane::Event::ZoomOut => {
373 for pane in self.center.panes() {
374 pane.update(cx, |pane, cx| {
375 pane.set_zoomed(false, cx);
376 })
377 }
378 cx.emit(PanelEvent::ZoomOut);
379 cx.notify();
380 }
381 pane::Event::AddItem { item } => {
382 if let Some(workspace) = self.workspace.upgrade() {
383 workspace.update(cx, |workspace, cx| {
384 item.added_to_pane(workspace, pane.clone(), window, cx)
385 })
386 }
387 self.serialize(cx);
388 }
389 &pane::Event::Split { direction, mode } => {
390 match mode {
391 SplitMode::ClonePane | SplitMode::EmptyPane => {
392 let clone = matches!(mode, SplitMode::ClonePane);
393 let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
394 let pane = pane.clone();
395 cx.spawn_in(window, async move |panel, cx| {
396 let Some(new_pane) = new_pane.await else {
397 return;
398 };
399 panel
400 .update_in(cx, |panel, window, cx| {
401 panel.center.split(&pane, &new_pane, direction, cx);
402 window.focus(&new_pane.focus_handle(cx), cx);
403 })
404 .ok();
405 })
406 .detach();
407 }
408 SplitMode::MovePane => {
409 let Some(item) =
410 pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
411 else {
412 return;
413 };
414 let Ok(project) = self
415 .workspace
416 .update(cx, |workspace, _| workspace.project().clone())
417 else {
418 return;
419 };
420 let new_pane =
421 new_terminal_pane(self.workspace.clone(), project, false, window, cx);
422 new_pane.update(cx, |pane, cx| {
423 pane.add_item(item, true, true, None, window, cx);
424 });
425 self.center.split(&pane, &new_pane, direction, cx);
426 window.focus(&new_pane.focus_handle(cx), cx);
427 }
428 };
429 }
430 pane::Event::Focus => {
431 self.active_pane = pane.clone();
432 }
433 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
434 self.serialize(cx);
435 }
436
437 _ => {}
438 }
439 }
440
441 fn new_pane_with_active_terminal(
442 &mut self,
443 clone: bool,
444 window: &mut Window,
445 cx: &mut Context<Self>,
446 ) -> Task<Option<Entity<Pane>>> {
447 let Some(workspace) = self.workspace.upgrade() else {
448 return Task::ready(None);
449 };
450 let workspace = workspace.read(cx);
451 let database_id = workspace.database_id();
452 let weak_workspace = self.workspace.clone();
453 let project = workspace.project().clone();
454 let active_pane = &self.active_pane;
455 let terminal_view = if clone {
456 active_pane
457 .read(cx)
458 .active_item()
459 .and_then(|item| item.downcast::<TerminalView>())
460 } else {
461 None
462 };
463 let working_directory = if clone {
464 terminal_view
465 .as_ref()
466 .and_then(|terminal_view| {
467 terminal_view
468 .read(cx)
469 .terminal()
470 .read(cx)
471 .working_directory()
472 })
473 .or_else(|| default_working_directory(workspace, cx))
474 } else {
475 default_working_directory(workspace, cx)
476 };
477
478 let is_zoomed = if clone {
479 active_pane.read(cx).is_zoomed()
480 } else {
481 false
482 };
483 cx.spawn_in(window, async move |panel, cx| {
484 let terminal = project
485 .update(cx, |project, cx| match terminal_view {
486 Some(view) => project.clone_terminal(
487 &view.read(cx).terminal.clone(),
488 cx,
489 working_directory,
490 ),
491 None => project.create_terminal_shell(working_directory, cx),
492 })
493 .await
494 .log_err()?;
495
496 panel
497 .update_in(cx, move |terminal_panel, window, cx| {
498 let terminal_view = Box::new(cx.new(|cx| {
499 TerminalView::new(
500 terminal.clone(),
501 weak_workspace.clone(),
502 database_id,
503 project.downgrade(),
504 window,
505 cx,
506 )
507 }));
508 let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
509 terminal_panel.apply_tab_bar_buttons(&pane, cx);
510 pane.update(cx, |pane, cx| {
511 pane.add_item(terminal_view, true, true, None, window, cx);
512 });
513 Some(pane)
514 })
515 .ok()
516 .flatten()
517 })
518 }
519
520 pub fn open_terminal(
521 workspace: &mut Workspace,
522 action: &workspace::OpenTerminal,
523 window: &mut Window,
524 cx: &mut Context<Workspace>,
525 ) {
526 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
527 return;
528 };
529
530 terminal_panel
531 .update(cx, |panel, cx| {
532 if action.local {
533 panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
534 } else {
535 panel.add_terminal_shell(
536 Some(action.working_directory.clone()),
537 RevealStrategy::Always,
538 window,
539 cx,
540 )
541 }
542 })
543 .detach_and_log_err(cx);
544 }
545
546 pub fn spawn_task(
547 &mut self,
548 task: &SpawnInTerminal,
549 window: &mut Window,
550 cx: &mut Context<Self>,
551 ) -> Task<Result<WeakEntity<Terminal>>> {
552 let Some(workspace) = self.workspace.upgrade() else {
553 return Task::ready(Err(anyhow!("failed to read workspace")));
554 };
555
556 let project = workspace.read(cx).project().read(cx);
557
558 if project.is_via_collab() {
559 return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
560 }
561
562 let remote_client = project.remote_client();
563 let is_windows = project.path_style(cx).is_windows();
564 let remote_shell = remote_client
565 .as_ref()
566 .and_then(|remote_client| remote_client.read(cx).shell());
567
568 let shell = if let Some(remote_shell) = remote_shell
569 && task.shell == Shell::System
570 {
571 Shell::Program(remote_shell)
572 } else {
573 task.shell.clone()
574 };
575
576 let task = prepare_task_for_spawn(task, &shell, is_windows);
577
578 if task.allow_concurrent_runs && task.use_new_terminal {
579 return self.spawn_in_new_terminal(task, window, cx);
580 }
581
582 let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
583 let Some(existing) = terminals_for_task.pop() else {
584 return self.spawn_in_new_terminal(task, window, cx);
585 };
586
587 let (existing_item_index, task_pane, existing_terminal) = existing;
588 if task.allow_concurrent_runs {
589 return self.replace_terminal(
590 task,
591 task_pane,
592 existing_item_index,
593 existing_terminal,
594 window,
595 cx,
596 );
597 }
598
599 let (tx, rx) = oneshot::channel();
600
601 self.deferred_tasks.insert(
602 task.id.clone(),
603 cx.spawn_in(window, async move |terminal_panel, cx| {
604 wait_for_terminals_tasks(terminals_for_task, cx).await;
605 let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
606 if task.use_new_terminal {
607 terminal_panel.spawn_in_new_terminal(task, window, cx)
608 } else {
609 terminal_panel.replace_terminal(
610 task,
611 task_pane,
612 existing_item_index,
613 existing_terminal,
614 window,
615 cx,
616 )
617 }
618 });
619 if let Ok(task) = task {
620 tx.send(task.await).ok();
621 }
622 }),
623 );
624
625 cx.spawn(async move |_, _| rx.await?)
626 }
627
628 fn spawn_in_new_terminal(
629 &mut self,
630 spawn_task: SpawnInTerminal,
631 window: &mut Window,
632 cx: &mut Context<Self>,
633 ) -> Task<Result<WeakEntity<Terminal>>> {
634 let reveal = spawn_task.reveal;
635 let reveal_target = spawn_task.reveal_target;
636 match reveal_target {
637 RevealTarget::Center => self
638 .workspace
639 .update(cx, |workspace, cx| {
640 Self::add_center_terminal(workspace, window, cx, |project, cx| {
641 project.create_terminal_task(spawn_task, cx)
642 })
643 })
644 .unwrap_or_else(|e| Task::ready(Err(e))),
645 RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
646 }
647 }
648
649 /// Create a new Terminal in the current working directory or the user's home directory
650 fn new_terminal(
651 workspace: &mut Workspace,
652 action: &workspace::NewTerminal,
653 window: &mut Window,
654 cx: &mut Context<Workspace>,
655 ) {
656 let center_pane = workspace.active_pane();
657 let center_pane_has_focus = center_pane.focus_handle(cx).contains_focused(window, cx);
658 let active_center_item_is_terminal = center_pane
659 .read(cx)
660 .active_item()
661 .is_some_and(|item| item.downcast::<TerminalView>().is_some());
662
663 if center_pane_has_focus && active_center_item_is_terminal {
664 let working_directory = default_working_directory(workspace, cx);
665 let local = action.local;
666 Self::add_center_terminal(workspace, window, cx, move |project, cx| {
667 if local {
668 project.create_local_terminal(cx)
669 } else {
670 project.create_terminal_shell(working_directory, cx)
671 }
672 })
673 .detach_and_log_err(cx);
674 return;
675 }
676
677 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
678 return;
679 };
680
681 terminal_panel
682 .update(cx, |this, cx| {
683 if action.local {
684 this.add_local_terminal_shell(RevealStrategy::Always, window, cx)
685 } else {
686 this.add_terminal_shell(
687 default_working_directory(workspace, cx),
688 RevealStrategy::Always,
689 window,
690 cx,
691 )
692 }
693 })
694 .detach_and_log_err(cx);
695 }
696
697 fn terminals_for_task(
698 &self,
699 label: &str,
700 cx: &mut App,
701 ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
702 let Some(workspace) = self.workspace.upgrade() else {
703 return Vec::new();
704 };
705
706 let pane_terminal_views = |pane: Entity<Pane>| {
707 pane.read(cx)
708 .items()
709 .enumerate()
710 .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
711 .filter_map(|(index, terminal_view)| {
712 let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
713 if &task_state.spawned_task.full_label == label {
714 Some((index, terminal_view))
715 } else {
716 None
717 }
718 })
719 .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
720 };
721
722 self.center
723 .panes()
724 .into_iter()
725 .cloned()
726 .flat_map(pane_terminal_views)
727 .chain(
728 workspace
729 .read(cx)
730 .panes()
731 .iter()
732 .cloned()
733 .flat_map(pane_terminal_views),
734 )
735 .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
736 .collect()
737 }
738
739 fn activate_terminal_view(
740 &self,
741 pane: &Entity<Pane>,
742 item_index: usize,
743 focus: bool,
744 window: &mut Window,
745 cx: &mut App,
746 ) {
747 pane.update(cx, |pane, cx| {
748 pane.activate_item(item_index, true, focus, window, cx)
749 })
750 }
751
752 pub fn add_center_terminal(
753 workspace: &mut Workspace,
754 window: &mut Window,
755 cx: &mut Context<Workspace>,
756 create_terminal: impl FnOnce(
757 &mut Project,
758 &mut Context<Project>,
759 ) -> Task<Result<Entity<Terminal>>>
760 + 'static,
761 ) -> Task<Result<WeakEntity<Terminal>>> {
762 if !is_enabled_in_workspace(workspace, cx) {
763 return Task::ready(Err(anyhow!(
764 "terminal not yet supported for remote projects"
765 )));
766 }
767 let project = workspace.project().downgrade();
768 cx.spawn_in(window, async move |workspace, cx| {
769 let terminal = project.update(cx, create_terminal)?.await?;
770
771 workspace.update_in(cx, |workspace, window, cx| {
772 let terminal_view = cx.new(|cx| {
773 TerminalView::new(
774 terminal.clone(),
775 workspace.weak_handle(),
776 workspace.database_id(),
777 workspace.project().downgrade(),
778 window,
779 cx,
780 )
781 });
782 workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
783 })?;
784 Ok(terminal.downgrade())
785 })
786 }
787
788 pub fn add_terminal_task(
789 &mut self,
790 task: SpawnInTerminal,
791 reveal_strategy: RevealStrategy,
792 window: &mut Window,
793 cx: &mut Context<Self>,
794 ) -> Task<Result<WeakEntity<Terminal>>> {
795 let workspace = self.workspace.clone();
796 cx.spawn_in(window, async move |terminal_panel, cx| {
797 if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
798 anyhow::bail!("terminal not yet supported for remote projects");
799 }
800 let pane = terminal_panel.update(cx, |terminal_panel, _| {
801 terminal_panel.pending_terminals_to_add += 1;
802 terminal_panel.active_pane.clone()
803 })?;
804 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
805 let terminal = project
806 .update(cx, |project, cx| project.create_terminal_task(task, cx))
807 .await?;
808 let result = workspace.update_in(cx, |workspace, window, cx| {
809 let terminal_view = Box::new(cx.new(|cx| {
810 TerminalView::new(
811 terminal.clone(),
812 workspace.weak_handle(),
813 workspace.database_id(),
814 workspace.project().downgrade(),
815 window,
816 cx,
817 )
818 }));
819
820 match reveal_strategy {
821 RevealStrategy::Always => {
822 workspace.focus_panel::<Self>(window, cx);
823 }
824 RevealStrategy::NoFocus => {
825 workspace.open_panel::<Self>(window, cx);
826 }
827 RevealStrategy::Never => {}
828 }
829
830 pane.update(cx, |pane, cx| {
831 let focus = matches!(reveal_strategy, RevealStrategy::Always);
832 pane.add_item(terminal_view, true, focus, None, window, cx);
833 });
834
835 Ok(terminal.downgrade())
836 })?;
837 terminal_panel.update(cx, |terminal_panel, cx| {
838 terminal_panel.pending_terminals_to_add =
839 terminal_panel.pending_terminals_to_add.saturating_sub(1);
840 terminal_panel.serialize(cx)
841 })?;
842 result
843 })
844 }
845
846 fn add_terminal_shell(
847 &mut self,
848 cwd: Option<PathBuf>,
849 reveal_strategy: RevealStrategy,
850 window: &mut Window,
851 cx: &mut Context<Self>,
852 ) -> Task<Result<WeakEntity<Terminal>>> {
853 self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx)
854 }
855
856 fn add_local_terminal_shell(
857 &mut self,
858 reveal_strategy: RevealStrategy,
859 window: &mut Window,
860 cx: &mut Context<Self>,
861 ) -> Task<Result<WeakEntity<Terminal>>> {
862 self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx)
863 }
864
865 fn add_terminal_shell_internal(
866 &mut self,
867 force_local: bool,
868 cwd: Option<PathBuf>,
869 reveal_strategy: RevealStrategy,
870 window: &mut Window,
871 cx: &mut Context<Self>,
872 ) -> Task<Result<WeakEntity<Terminal>>> {
873 let workspace = self.workspace.clone();
874
875 cx.spawn_in(window, async move |terminal_panel, cx| {
876 if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
877 anyhow::bail!("terminal not yet supported for collaborative projects");
878 }
879 let pane = terminal_panel.update(cx, |terminal_panel, _| {
880 terminal_panel.pending_terminals_to_add += 1;
881 terminal_panel.active_pane.clone()
882 })?;
883 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
884 let terminal = if force_local {
885 project
886 .update(cx, |project, cx| project.create_local_terminal(cx))
887 .await
888 } else {
889 project
890 .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))
891 .await
892 };
893
894 match terminal {
895 Ok(terminal) => {
896 let result = workspace.update_in(cx, |workspace, window, cx| {
897 let terminal_view = Box::new(cx.new(|cx| {
898 TerminalView::new(
899 terminal.clone(),
900 workspace.weak_handle(),
901 workspace.database_id(),
902 workspace.project().downgrade(),
903 window,
904 cx,
905 )
906 }));
907
908 match reveal_strategy {
909 RevealStrategy::Always => {
910 workspace.focus_panel::<Self>(window, cx);
911 }
912 RevealStrategy::NoFocus => {
913 workspace.open_panel::<Self>(window, cx);
914 }
915 RevealStrategy::Never => {}
916 }
917
918 pane.update(cx, |pane, cx| {
919 let focus = matches!(reveal_strategy, RevealStrategy::Always);
920 pane.add_item(terminal_view, true, focus, None, window, cx);
921 });
922
923 Ok(terminal.downgrade())
924 })?;
925 terminal_panel.update(cx, |terminal_panel, cx| {
926 terminal_panel.pending_terminals_to_add =
927 terminal_panel.pending_terminals_to_add.saturating_sub(1);
928 terminal_panel.serialize(cx)
929 })?;
930 result
931 }
932 Err(error) => {
933 pane.update_in(cx, |pane, window, cx| {
934 let focus = pane.has_focus(window, cx);
935 let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
936 error: error.to_string(),
937 focus_handle: cx.focus_handle(),
938 });
939 pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
940 })?;
941 Err(error)
942 }
943 }
944 })
945 }
946
947 fn serialize(&mut self, cx: &mut Context<Self>) {
948 let Some(serialization_key) = self
949 .workspace
950 .read_with(cx, |workspace, _| {
951 TerminalPanel::serialization_key(workspace)
952 })
953 .ok()
954 .flatten()
955 else {
956 return;
957 };
958 let kvp = KeyValueStore::global(cx);
959 self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
960 cx.background_executor()
961 .timer(Duration::from_millis(50))
962 .await;
963 let terminal_panel = terminal_panel.upgrade()?;
964 let items = terminal_panel.update(cx, |terminal_panel, cx| {
965 SerializedItems::WithSplits(serialize_pane_group(
966 &terminal_panel.center,
967 &terminal_panel.active_pane,
968 cx,
969 ))
970 });
971 cx.background_spawn(
972 async move {
973 kvp.write_kvp(
974 serialization_key,
975 serde_json::to_string(&SerializedTerminalPanel {
976 items,
977 active_item_id: None,
978 })?,
979 )
980 .await?;
981 anyhow::Ok(())
982 }
983 .log_err(),
984 )
985 .await;
986 Some(())
987 });
988 }
989
990 fn replace_terminal(
991 &self,
992 spawn_task: SpawnInTerminal,
993 task_pane: Entity<Pane>,
994 terminal_item_index: usize,
995 terminal_to_replace: Entity<TerminalView>,
996 window: &mut Window,
997 cx: &mut Context<Self>,
998 ) -> Task<Result<WeakEntity<Terminal>>> {
999 let reveal = spawn_task.reveal;
1000 let task_workspace = self.workspace.clone();
1001 cx.spawn_in(window, async move |terminal_panel, cx| {
1002 let project = terminal_panel.update(cx, |this, cx| {
1003 this.workspace
1004 .update(cx, |workspace, _| workspace.project().clone())
1005 })??;
1006 let new_terminal = project
1007 .update(cx, |project, cx| {
1008 project.create_terminal_task(spawn_task, cx)
1009 })
1010 .await?;
1011 terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
1012 terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
1013 })?;
1014
1015 let reveal_target = terminal_panel.update(cx, |panel, _| {
1016 if panel.center.panes().iter().any(|p| **p == task_pane) {
1017 RevealTarget::Dock
1018 } else {
1019 RevealTarget::Center
1020 }
1021 })?;
1022
1023 match reveal {
1024 RevealStrategy::Always => match reveal_target {
1025 RevealTarget::Center => {
1026 task_workspace.update_in(cx, |workspace, window, cx| {
1027 let did_activate = workspace.activate_item(
1028 &terminal_to_replace,
1029 true,
1030 true,
1031 window,
1032 cx,
1033 );
1034
1035 anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
1036
1037 anyhow::Ok(())
1038 })??;
1039 }
1040 RevealTarget::Dock => {
1041 terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1042 terminal_panel.activate_terminal_view(
1043 &task_pane,
1044 terminal_item_index,
1045 true,
1046 window,
1047 cx,
1048 )
1049 })?;
1050
1051 cx.spawn(async move |cx| {
1052 task_workspace
1053 .update_in(cx, |workspace, window, cx| {
1054 workspace.focus_panel::<Self>(window, cx)
1055 })
1056 .ok()
1057 })
1058 .detach();
1059 }
1060 },
1061 RevealStrategy::NoFocus => match reveal_target {
1062 RevealTarget::Center => {
1063 task_workspace.update_in(cx, |workspace, window, cx| {
1064 workspace.active_pane().focus_handle(cx).focus(window, cx);
1065 })?;
1066 }
1067 RevealTarget::Dock => {
1068 terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1069 terminal_panel.activate_terminal_view(
1070 &task_pane,
1071 terminal_item_index,
1072 false,
1073 window,
1074 cx,
1075 )
1076 })?;
1077
1078 cx.spawn(async move |cx| {
1079 task_workspace
1080 .update_in(cx, |workspace, window, cx| {
1081 workspace.open_panel::<Self>(window, cx)
1082 })
1083 .ok()
1084 })
1085 .detach();
1086 }
1087 },
1088 RevealStrategy::Never => {}
1089 }
1090
1091 Ok(new_terminal.downgrade())
1092 })
1093 }
1094
1095 fn has_no_terminals(&self, cx: &App) -> bool {
1096 self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1097 }
1098
1099 pub fn assistant_enabled(&self) -> bool {
1100 self.assistant_enabled
1101 }
1102
1103 /// Returns all panes in the terminal panel.
1104 pub fn panes(&self) -> Vec<&Entity<Pane>> {
1105 self.center.panes()
1106 }
1107
1108 /// Returns all non-empty terminal selections from all terminal views in all panes.
1109 pub fn terminal_selections(&self, cx: &App) -> Vec<String> {
1110 self.center
1111 .panes()
1112 .iter()
1113 .flat_map(|pane| {
1114 pane.read(cx).items().filter_map(|item| {
1115 let terminal_view = item.downcast::<crate::TerminalView>()?;
1116 terminal_view
1117 .read(cx)
1118 .terminal()
1119 .read(cx)
1120 .last_content
1121 .selection_text
1122 .clone()
1123 .filter(|text| !text.is_empty())
1124 })
1125 })
1126 .collect()
1127 }
1128
1129 fn is_enabled(&self, cx: &App) -> bool {
1130 self.workspace
1131 .upgrade()
1132 .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1133 }
1134
1135 fn activate_pane_in_direction(
1136 &mut self,
1137 direction: SplitDirection,
1138 window: &mut Window,
1139 cx: &mut Context<Self>,
1140 ) {
1141 if let Some(pane) = self
1142 .center
1143 .find_pane_in_direction(&self.active_pane, direction, cx)
1144 {
1145 window.focus(&pane.focus_handle(cx), cx);
1146 } else {
1147 self.workspace
1148 .update(cx, |workspace, cx| {
1149 workspace.activate_pane_in_direction(direction, window, cx)
1150 })
1151 .ok();
1152 }
1153 }
1154
1155 fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1156 if let Some(to) = self
1157 .center
1158 .find_pane_in_direction(&self.active_pane, direction, cx)
1159 .cloned()
1160 {
1161 self.center.swap(&self.active_pane, &to, cx);
1162 cx.notify();
1163 }
1164 }
1165
1166 fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1167 if self
1168 .center
1169 .move_to_border(&self.active_pane, direction, cx)
1170 .unwrap()
1171 {
1172 cx.notify();
1173 }
1174 }
1175}
1176
1177/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
1178/// based on the shell configuration. This is a pure function that can be tested
1179/// without spawning actual terminals.
1180pub fn prepare_task_for_spawn(
1181 task: &SpawnInTerminal,
1182 shell: &Shell,
1183 is_windows: bool,
1184) -> SpawnInTerminal {
1185 let builder = ShellBuilder::new(shell, is_windows);
1186 let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
1187 let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
1188
1189 SpawnInTerminal {
1190 command_label,
1191 command: Some(command),
1192 args,
1193 ..task.clone()
1194 }
1195}
1196
1197fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1198 workspace.project().read(cx).supports_terminal(cx)
1199}
1200
1201pub fn new_terminal_pane(
1202 workspace: WeakEntity<Workspace>,
1203 project: Entity<Project>,
1204 zoomed: bool,
1205 window: &mut Window,
1206 cx: &mut Context<TerminalPanel>,
1207) -> Entity<Pane> {
1208 let terminal_panel = cx.entity();
1209 let pane = cx.new(|cx| {
1210 let mut pane = Pane::new(
1211 workspace.clone(),
1212 project.clone(),
1213 Default::default(),
1214 None,
1215 workspace::NewTerminal::default().boxed_clone(),
1216 false,
1217 window,
1218 cx,
1219 );
1220 pane.set_zoomed(zoomed, cx);
1221 pane.set_can_navigate(false, cx);
1222 pane.display_nav_history_buttons(None);
1223 pane.set_should_display_tab_bar(|_, _| true);
1224 pane.set_zoom_out_on_close(false);
1225
1226 let split_closure_terminal_panel = terminal_panel.downgrade();
1227 pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1228 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1229 let is_current_pane = tab.pane == cx.entity();
1230 let Some(can_drag_away) = split_closure_terminal_panel
1231 .read_with(cx, |terminal_panel, _| {
1232 let current_panes = terminal_panel.center.panes();
1233 !current_panes.contains(&&tab.pane)
1234 || current_panes.len() > 1
1235 || (!is_current_pane || pane.items_len() > 1)
1236 })
1237 .ok()
1238 else {
1239 return false;
1240 };
1241 if can_drag_away {
1242 let item = if is_current_pane {
1243 pane.item_for_index(tab.ix)
1244 } else {
1245 tab.pane.read(cx).item_for_index(tab.ix)
1246 };
1247 if let Some(item) = item {
1248 return item.downcast::<TerminalView>().is_some();
1249 }
1250 }
1251 }
1252 false
1253 })));
1254
1255 let toolbar = pane.toolbar().clone();
1256 if let Some(callbacks) = cx.try_global::<workspace::PaneSearchBarCallbacks>() {
1257 let languages = Some(project.read(cx).languages().clone());
1258 (callbacks.setup_search_bar)(languages, &toolbar, window, cx);
1259 }
1260 let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1261 toolbar.update(cx, |toolbar, cx| {
1262 toolbar.add_item(breadcrumbs, window, cx);
1263 });
1264
1265 pane
1266 });
1267
1268 cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1269 .detach();
1270 cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1271
1272 pane
1273}
1274
1275async fn wait_for_terminals_tasks(
1276 terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1277 cx: &mut AsyncApp,
1278) {
1279 let pending_tasks = terminals_for_task.iter().map(|(_, _, terminal)| {
1280 terminal.update(cx, |terminal_view, cx| {
1281 terminal_view
1282 .terminal()
1283 .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1284 })
1285 });
1286 join_all(pending_tasks).await;
1287}
1288
1289struct FailedToSpawnTerminal {
1290 error: String,
1291 focus_handle: FocusHandle,
1292}
1293
1294impl Focusable for FailedToSpawnTerminal {
1295 fn focus_handle(&self, _: &App) -> FocusHandle {
1296 self.focus_handle.clone()
1297 }
1298}
1299
1300impl Render for FailedToSpawnTerminal {
1301 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1302 let popover_menu = PopoverMenu::new("settings-popover")
1303 .trigger(
1304 IconButton::new("icon-button-popover", IconName::ChevronDown)
1305 .icon_size(IconSize::XSmall),
1306 )
1307 .menu(move |window, cx| {
1308 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1309 context_menu
1310 .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1311 .action(
1312 "Edit settings.json",
1313 zed_actions::OpenSettingsFile.boxed_clone(),
1314 )
1315 }))
1316 })
1317 .anchor(Corner::TopRight)
1318 .offset(gpui::Point {
1319 x: px(0.0),
1320 y: px(2.0),
1321 });
1322
1323 v_flex()
1324 .track_focus(&self.focus_handle)
1325 .size_full()
1326 .p_4()
1327 .items_center()
1328 .justify_center()
1329 .bg(cx.theme().colors().editor_background)
1330 .child(
1331 v_flex()
1332 .max_w_112()
1333 .items_center()
1334 .justify_center()
1335 .text_center()
1336 .child(Label::new("Failed to spawn terminal"))
1337 .child(
1338 Label::new(self.error.to_string())
1339 .size(LabelSize::Small)
1340 .color(Color::Muted)
1341 .mb_4(),
1342 )
1343 .child(SplitButton::new(
1344 ButtonLike::new("open-settings-ui")
1345 .child(Label::new("Edit Settings").size(LabelSize::Small))
1346 .on_click(|_, window, cx| {
1347 window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1348 }),
1349 popover_menu.into_any_element(),
1350 )),
1351 )
1352 }
1353}
1354
1355impl EventEmitter<()> for FailedToSpawnTerminal {}
1356
1357impl workspace::Item for FailedToSpawnTerminal {
1358 type Event = ();
1359
1360 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1361 SharedString::new_static("Failed to spawn terminal")
1362 }
1363}
1364
1365impl EventEmitter<PanelEvent> for TerminalPanel {}
1366
1367impl Render for TerminalPanel {
1368 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1369 let registrar = cx
1370 .try_global::<workspace::PaneSearchBarCallbacks>()
1371 .map(|callbacks| {
1372 (callbacks.wrap_div_with_search_actions)(div(), self.active_pane.clone())
1373 })
1374 .unwrap_or_else(div);
1375 self.workspace
1376 .update(cx, |workspace, cx| {
1377 registrar.size_full().child(self.center.render(
1378 workspace.zoomed_item(),
1379 &workspace::PaneRenderContext {
1380 follower_states: &HashMap::default(),
1381 active_call: workspace.active_call(),
1382 active_pane: &self.active_pane,
1383 app_state: workspace.app_state(),
1384 project: workspace.project(),
1385 workspace: &workspace.weak_handle(),
1386 },
1387 window,
1388 cx,
1389 ))
1390 })
1391 .ok()
1392 .map(|div| {
1393 div.on_action({
1394 cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1395 terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1396 })
1397 })
1398 .on_action({
1399 cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1400 terminal_panel.activate_pane_in_direction(
1401 SplitDirection::Right,
1402 window,
1403 cx,
1404 );
1405 })
1406 })
1407 .on_action({
1408 cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1409 terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1410 })
1411 })
1412 .on_action({
1413 cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1414 terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1415 })
1416 })
1417 .on_action(
1418 cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1419 let panes = terminal_panel.center.panes();
1420 if let Some(ix) = panes
1421 .iter()
1422 .position(|pane| **pane == terminal_panel.active_pane)
1423 {
1424 let next_ix = (ix + 1) % panes.len();
1425 window.focus(&panes[next_ix].focus_handle(cx), cx);
1426 }
1427 }),
1428 )
1429 .on_action(cx.listener(
1430 |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1431 let panes = terminal_panel.center.panes();
1432 if let Some(ix) = panes
1433 .iter()
1434 .position(|pane| **pane == terminal_panel.active_pane)
1435 {
1436 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1437 window.focus(&panes[prev_ix].focus_handle(cx), cx);
1438 }
1439 },
1440 ))
1441 .on_action(
1442 cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1443 let panes = terminal_panel.center.panes();
1444 if let Some(&pane) = panes.get(action.0) {
1445 window.focus(&pane.read(cx).focus_handle(cx), cx);
1446 } else {
1447 let future =
1448 terminal_panel.new_pane_with_active_terminal(true, window, cx);
1449 cx.spawn_in(window, async move |terminal_panel, cx| {
1450 if let Some(new_pane) = future.await {
1451 _ = terminal_panel.update_in(
1452 cx,
1453 |terminal_panel, window, cx| {
1454 terminal_panel.center.split(
1455 &terminal_panel.active_pane,
1456 &new_pane,
1457 SplitDirection::Right,
1458 cx,
1459 );
1460 let new_pane = new_pane.read(cx);
1461 window.focus(&new_pane.focus_handle(cx), cx);
1462 },
1463 );
1464 }
1465 })
1466 .detach();
1467 }
1468 }),
1469 )
1470 .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1471 terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1472 }))
1473 .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1474 terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1475 }))
1476 .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1477 terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1478 }))
1479 .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1480 terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1481 }))
1482 .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1483 terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1484 }))
1485 .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1486 terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1487 }))
1488 .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1489 terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1490 }))
1491 .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1492 terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1493 }))
1494 .on_action(
1495 cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1496 let Some(&target_pane) =
1497 terminal_panel.center.panes().get(action.destination)
1498 else {
1499 return;
1500 };
1501 move_active_item(
1502 &terminal_panel.active_pane,
1503 target_pane,
1504 action.focus,
1505 true,
1506 window,
1507 cx,
1508 );
1509 }),
1510 )
1511 .on_action(cx.listener(
1512 |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1513 let source_pane = &terminal_panel.active_pane;
1514 if let Some(destination_pane) = terminal_panel
1515 .center
1516 .find_pane_in_direction(source_pane, action.direction, cx)
1517 {
1518 move_active_item(
1519 source_pane,
1520 destination_pane,
1521 action.focus,
1522 true,
1523 window,
1524 cx,
1525 );
1526 };
1527 },
1528 ))
1529 })
1530 .unwrap_or_else(|| div())
1531 }
1532}
1533
1534impl Focusable for TerminalPanel {
1535 fn focus_handle(&self, cx: &App) -> FocusHandle {
1536 self.active_pane.focus_handle(cx)
1537 }
1538}
1539
1540impl Panel for TerminalPanel {
1541 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1542 match TerminalSettings::get_global(cx).dock {
1543 TerminalDockPosition::Left => DockPosition::Left,
1544 TerminalDockPosition::Bottom => DockPosition::Bottom,
1545 TerminalDockPosition::Right => DockPosition::Right,
1546 }
1547 }
1548
1549 fn position_is_valid(&self, _: DockPosition) -> bool {
1550 true
1551 }
1552
1553 fn set_position(
1554 &mut self,
1555 position: DockPosition,
1556 _window: &mut Window,
1557 cx: &mut Context<Self>,
1558 ) {
1559 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1560 let dock = match position {
1561 DockPosition::Left => TerminalDockPosition::Left,
1562 DockPosition::Bottom => TerminalDockPosition::Bottom,
1563 DockPosition::Right => TerminalDockPosition::Right,
1564 };
1565 settings.terminal.get_or_insert_default().dock = Some(dock);
1566 });
1567 }
1568
1569 fn default_size(&self, window: &Window, cx: &App) -> Pixels {
1570 let settings = TerminalSettings::get_global(cx);
1571 match self.position(window, cx) {
1572 DockPosition::Left | DockPosition::Right => settings.default_width,
1573 DockPosition::Bottom => settings.default_height,
1574 }
1575 }
1576
1577 fn supports_flexible_size(&self) -> bool {
1578 true
1579 }
1580
1581 fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
1582 TerminalSettings::get_global(cx).flexible
1583 }
1584
1585 fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
1586 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1587 settings.terminal.get_or_insert_default().flexible = Some(flexible);
1588 });
1589 }
1590
1591 fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1592 self.active_pane.read(cx).is_zoomed()
1593 }
1594
1595 fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1596 for pane in self.center.panes() {
1597 pane.update(cx, |pane, cx| {
1598 pane.set_zoomed(zoomed, cx);
1599 })
1600 }
1601 cx.notify();
1602 }
1603
1604 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1605 let old_active = self.active;
1606 self.active = active;
1607 if !active || old_active == active || !self.has_no_terminals(cx) {
1608 return;
1609 }
1610 cx.defer_in(window, |this, window, cx| {
1611 let Ok(kind) = this
1612 .workspace
1613 .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1614 else {
1615 return;
1616 };
1617
1618 this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1619 .detach_and_log_err(cx)
1620 })
1621 }
1622
1623 fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1624 if !TerminalSettings::get_global(cx).show_count_badge {
1625 return None;
1626 }
1627 let count = self
1628 .center
1629 .panes()
1630 .into_iter()
1631 .map(|pane| pane.read(cx).items_len())
1632 .sum::<usize>();
1633 if count == 0 {
1634 None
1635 } else {
1636 Some(count.to_string())
1637 }
1638 }
1639
1640 fn persistent_name() -> &'static str {
1641 "TerminalPanel"
1642 }
1643
1644 fn panel_key() -> &'static str {
1645 TERMINAL_PANEL_KEY
1646 }
1647
1648 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1649 if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1650 && TerminalSettings::get_global(cx).button
1651 {
1652 Some(IconName::TerminalAlt)
1653 } else {
1654 None
1655 }
1656 }
1657
1658 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1659 Some("Terminal Panel")
1660 }
1661
1662 fn toggle_action(&self) -> Box<dyn gpui::Action> {
1663 Box::new(Toggle)
1664 }
1665
1666 fn pane(&self) -> Option<Entity<Pane>> {
1667 Some(self.active_pane.clone())
1668 }
1669
1670 fn activation_priority(&self) -> u32 {
1671 2
1672 }
1673}
1674
1675struct TerminalProvider(Entity<TerminalPanel>);
1676
1677impl workspace::TerminalProvider for TerminalProvider {
1678 fn spawn(
1679 &self,
1680 task: SpawnInTerminal,
1681 window: &mut Window,
1682 cx: &mut App,
1683 ) -> Task<Option<Result<ExitStatus>>> {
1684 let terminal_panel = self.0.clone();
1685 window.spawn(cx, async move |cx| {
1686 let terminal = terminal_panel
1687 .update_in(cx, |terminal_panel, window, cx| {
1688 terminal_panel.spawn_task(&task, window, cx)
1689 })
1690 .ok()?
1691 .await;
1692 match terminal {
1693 Ok(terminal) => {
1694 let exit_status = terminal
1695 .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1696 .ok()?
1697 .await?;
1698 Some(Ok(exit_status))
1699 }
1700 Err(e) => Some(Err(e)),
1701 }
1702 })
1703 }
1704}
1705
1706struct InlineAssistTabBarButton {
1707 focus_handle: FocusHandle,
1708}
1709
1710impl Render for InlineAssistTabBarButton {
1711 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1712 let focus_handle = self.focus_handle.clone();
1713 IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1714 .icon_size(IconSize::Small)
1715 .on_click(cx.listener(|_, _, window, cx| {
1716 window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1717 }))
1718 .tooltip(move |_window, cx| {
1719 Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1720 })
1721 }
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726 use std::num::NonZero;
1727
1728 use super::*;
1729 use gpui::{TestAppContext, UpdateGlobal as _};
1730 use pretty_assertions::assert_eq;
1731 use project::FakeFs;
1732 use settings::SettingsStore;
1733 use workspace::MultiWorkspace;
1734
1735 #[test]
1736 fn test_prepare_empty_task() {
1737 let input = SpawnInTerminal::default();
1738 let shell = Shell::System;
1739
1740 let result = prepare_task_for_spawn(&input, &shell, false);
1741
1742 let expected_shell = util::get_system_shell();
1743 assert_eq!(result.env, HashMap::default());
1744 assert_eq!(result.cwd, None);
1745 assert_eq!(result.shell, Shell::System);
1746 assert_eq!(
1747 result.command,
1748 Some(expected_shell.clone()),
1749 "Empty tasks should spawn a -i shell"
1750 );
1751 assert_eq!(result.args, Vec::<String>::new());
1752 assert_eq!(
1753 result.command_label, expected_shell,
1754 "We show the shell launch for empty commands"
1755 );
1756 }
1757
1758 #[gpui::test]
1759 async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1760 cx.executor().allow_parking();
1761 init_test(cx);
1762
1763 let fs = FakeFs::new(cx.executor());
1764 let project = Project::test(fs, [], cx).await;
1765 let window_handle =
1766 cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1767
1768 let terminal_panel = window_handle
1769 .update(cx, |multi_workspace, window, cx| {
1770 multi_workspace.workspace().update(cx, |workspace, cx| {
1771 cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1772 })
1773 })
1774 .unwrap();
1775
1776 set_max_tabs(cx, Some(3));
1777
1778 for _ in 0..5 {
1779 let task = window_handle
1780 .update(cx, |_, window, cx| {
1781 terminal_panel.update(cx, |panel, cx| {
1782 panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1783 })
1784 })
1785 .unwrap();
1786 task.await.unwrap();
1787 }
1788
1789 cx.run_until_parked();
1790
1791 let item_count =
1792 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1793
1794 assert_eq!(
1795 item_count, 5,
1796 "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1797 );
1798 }
1799
1800 #[cfg(unix)]
1801 #[test]
1802 fn test_prepare_script_like_task() {
1803 let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string();
1804 let expected_cwd = PathBuf::from("/some/work");
1805
1806 let input = SpawnInTerminal {
1807 command: Some(user_command.clone()),
1808 cwd: Some(expected_cwd.clone()),
1809 ..SpawnInTerminal::default()
1810 };
1811 let shell = Shell::System;
1812
1813 let result = prepare_task_for_spawn(&input, &shell, false);
1814
1815 let system_shell = util::get_system_shell();
1816 assert_eq!(result.env, HashMap::default());
1817 assert_eq!(result.cwd, Some(expected_cwd));
1818 assert_eq!(result.shell, Shell::System);
1819 assert_eq!(result.command, Some(system_shell.clone()));
1820 assert_eq!(
1821 result.args,
1822 vec!["-i".to_string(), "-c".to_string(), user_command.clone()],
1823 "User command should have been moved into the arguments, as we're spawning a new -i shell",
1824 );
1825 assert_eq!(
1826 result.command_label,
1827 format!(
1828 "{system_shell} {interactive}-c '{user_command}'",
1829 interactive = if cfg!(windows) { "" } else { "-i " }
1830 ),
1831 "We want to show to the user the entire command spawned"
1832 );
1833 }
1834
1835 #[gpui::test]
1836 async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1837 cx.executor().allow_parking();
1838 init_test(cx);
1839
1840 cx.update(|cx| {
1841 SettingsStore::update_global(cx, |store, cx| {
1842 store.update_user_settings(cx, |settings| {
1843 settings.terminal.get_or_insert_default().project.shell =
1844 Some(settings::Shell::Program("__nonexistent_shell__".to_owned()));
1845 });
1846 });
1847 });
1848
1849 let fs = FakeFs::new(cx.executor());
1850 let project = Project::test(fs, [], cx).await;
1851 let window_handle =
1852 cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1853
1854 let terminal_panel = window_handle
1855 .update(cx, |multi_workspace, window, cx| {
1856 multi_workspace.workspace().update(cx, |workspace, cx| {
1857 cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1858 })
1859 })
1860 .unwrap();
1861
1862 window_handle
1863 .update(cx, |_, window, cx| {
1864 terminal_panel.update(cx, |terminal_panel, cx| {
1865 terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1866 })
1867 })
1868 .unwrap()
1869 .await
1870 .unwrap_err();
1871
1872 window_handle
1873 .update(cx, |_, _, cx| {
1874 terminal_panel.update(cx, |terminal_panel, cx| {
1875 assert!(
1876 terminal_panel
1877 .active_pane
1878 .read(cx)
1879 .items()
1880 .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1881 "should spawn `FailedToSpawnTerminal` pane"
1882 );
1883 })
1884 })
1885 .unwrap();
1886 }
1887
1888 #[gpui::test]
1889 async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) {
1890 cx.executor().allow_parking();
1891 init_test(cx);
1892
1893 let fs = FakeFs::new(cx.executor());
1894 let project = Project::test(fs, [], cx).await;
1895 let window_handle =
1896 cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1897
1898 let terminal_panel = window_handle
1899 .update(cx, |multi_workspace, window, cx| {
1900 multi_workspace.workspace().update(cx, |workspace, cx| {
1901 cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1902 })
1903 })
1904 .unwrap();
1905
1906 let result = window_handle
1907 .update(cx, |_, window, cx| {
1908 terminal_panel.update(cx, |terminal_panel, cx| {
1909 terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
1910 })
1911 })
1912 .unwrap()
1913 .await;
1914
1915 assert!(
1916 result.is_ok(),
1917 "local terminal should successfully create in local project"
1918 );
1919 }
1920
1921 async fn init_workspace_with_panel(
1922 cx: &mut TestAppContext,
1923 ) -> (gpui::WindowHandle<MultiWorkspace>, Entity<TerminalPanel>) {
1924 let fs = FakeFs::new(cx.executor());
1925 let project = Project::test(fs, [], cx).await;
1926 let window_handle =
1927 cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1928
1929 let terminal_panel = window_handle
1930 .update(cx, |multi_workspace, window, cx| {
1931 multi_workspace.workspace().update(cx, |workspace, cx| {
1932 let panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1933 workspace.add_panel(panel.clone(), window, cx);
1934 panel
1935 })
1936 })
1937 .expect("Failed to initialize workspace with terminal panel");
1938
1939 (window_handle, terminal_panel)
1940 }
1941
1942 #[gpui::test]
1943 async fn test_new_terminal_opens_in_panel_by_default(cx: &mut TestAppContext) {
1944 cx.executor().allow_parking();
1945 init_test(cx);
1946
1947 let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
1948
1949 let panel_items_before =
1950 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1951 let center_items_before = window_handle
1952 .read_with(cx, |multi_workspace, cx| {
1953 multi_workspace
1954 .workspace()
1955 .read(cx)
1956 .active_pane()
1957 .read(cx)
1958 .items_len()
1959 })
1960 .expect("Failed to read center pane items");
1961
1962 window_handle
1963 .update(cx, |multi_workspace, window, cx| {
1964 multi_workspace.workspace().update(cx, |workspace, cx| {
1965 TerminalPanel::new_terminal(
1966 workspace,
1967 &workspace::NewTerminal::default(),
1968 window,
1969 cx,
1970 );
1971 })
1972 })
1973 .expect("Failed to dispatch new_terminal");
1974
1975 cx.run_until_parked();
1976
1977 let panel_items_after =
1978 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1979 let center_items_after = window_handle
1980 .read_with(cx, |multi_workspace, cx| {
1981 multi_workspace
1982 .workspace()
1983 .read(cx)
1984 .active_pane()
1985 .read(cx)
1986 .items_len()
1987 })
1988 .expect("Failed to read center pane items");
1989
1990 assert_eq!(
1991 panel_items_after,
1992 panel_items_before + 1,
1993 "Terminal should be added to the panel when no center terminal is focused"
1994 );
1995 assert_eq!(
1996 center_items_after, center_items_before,
1997 "Center pane should not gain a new terminal"
1998 );
1999 }
2000
2001 #[gpui::test]
2002 async fn test_new_terminal_opens_in_center_when_center_terminal_focused(
2003 cx: &mut TestAppContext,
2004 ) {
2005 cx.executor().allow_parking();
2006 init_test(cx);
2007
2008 let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2009
2010 window_handle
2011 .update(cx, |multi_workspace, window, cx| {
2012 multi_workspace.workspace().update(cx, |workspace, cx| {
2013 TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2014 project.create_terminal_shell(None, cx)
2015 })
2016 })
2017 })
2018 .expect("Failed to update workspace")
2019 .await
2020 .expect("Failed to create center terminal");
2021 cx.run_until_parked();
2022
2023 let center_items_before = window_handle
2024 .read_with(cx, |multi_workspace, cx| {
2025 multi_workspace
2026 .workspace()
2027 .read(cx)
2028 .active_pane()
2029 .read(cx)
2030 .items_len()
2031 })
2032 .expect("Failed to read center pane items");
2033 assert_eq!(center_items_before, 1, "Center pane should have 1 terminal");
2034
2035 window_handle
2036 .update(cx, |multi_workspace, window, cx| {
2037 multi_workspace.workspace().update(cx, |workspace, cx| {
2038 let active_item = workspace
2039 .active_pane()
2040 .read(cx)
2041 .active_item()
2042 .expect("Center pane should have an active item");
2043 let terminal_view = active_item
2044 .downcast::<TerminalView>()
2045 .expect("Active center item should be a TerminalView");
2046 window.focus(&terminal_view.focus_handle(cx), cx);
2047 })
2048 })
2049 .expect("Failed to focus terminal view");
2050 cx.run_until_parked();
2051
2052 let panel_items_before =
2053 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2054
2055 window_handle
2056 .update(cx, |multi_workspace, window, cx| {
2057 multi_workspace.workspace().update(cx, |workspace, cx| {
2058 TerminalPanel::new_terminal(
2059 workspace,
2060 &workspace::NewTerminal::default(),
2061 window,
2062 cx,
2063 );
2064 })
2065 })
2066 .expect("Failed to dispatch new_terminal");
2067 cx.run_until_parked();
2068
2069 let center_items_after = window_handle
2070 .read_with(cx, |multi_workspace, cx| {
2071 multi_workspace
2072 .workspace()
2073 .read(cx)
2074 .active_pane()
2075 .read(cx)
2076 .items_len()
2077 })
2078 .expect("Failed to read center pane items");
2079 let panel_items_after =
2080 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2081
2082 assert_eq!(
2083 center_items_after,
2084 center_items_before + 1,
2085 "New terminal should be added to the center pane"
2086 );
2087 assert_eq!(
2088 panel_items_after, panel_items_before,
2089 "Terminal panel should not gain a new terminal"
2090 );
2091 }
2092
2093 #[gpui::test]
2094 async fn test_new_terminal_opens_in_panel_when_panel_focused(cx: &mut TestAppContext) {
2095 cx.executor().allow_parking();
2096 init_test(cx);
2097
2098 let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2099
2100 window_handle
2101 .update(cx, |_, window, cx| {
2102 terminal_panel.update(cx, |panel, cx| {
2103 panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2104 })
2105 })
2106 .expect("Failed to update workspace")
2107 .await
2108 .expect("Failed to create panel terminal");
2109 cx.run_until_parked();
2110
2111 window_handle
2112 .update(cx, |_, window, cx| {
2113 window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2114 })
2115 .expect("Failed to focus terminal panel");
2116 cx.run_until_parked();
2117
2118 let panel_items_before =
2119 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2120
2121 let center_items_before = window_handle
2122 .read_with(cx, |multi_workspace, cx| {
2123 multi_workspace
2124 .workspace()
2125 .read(cx)
2126 .active_pane()
2127 .read(cx)
2128 .items_len()
2129 })
2130 .expect("Failed to read center pane items");
2131
2132 window_handle
2133 .update(cx, |multi_workspace, window, cx| {
2134 multi_workspace.workspace().update(cx, |workspace, cx| {
2135 TerminalPanel::new_terminal(
2136 workspace,
2137 &workspace::NewTerminal::default(),
2138 window,
2139 cx,
2140 );
2141 })
2142 })
2143 .expect("Failed to dispatch new_terminal");
2144 cx.run_until_parked();
2145
2146 let panel_items_after =
2147 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2148 let center_items_after = window_handle
2149 .read_with(cx, |multi_workspace, cx| {
2150 multi_workspace
2151 .workspace()
2152 .read(cx)
2153 .active_pane()
2154 .read(cx)
2155 .items_len()
2156 })
2157 .expect("Failed to read center pane items");
2158
2159 assert_eq!(
2160 panel_items_after,
2161 panel_items_before + 1,
2162 "New terminal should be added to the panel when panel is focused"
2163 );
2164 assert_eq!(
2165 center_items_after, center_items_before,
2166 "Center pane should not gain a new terminal"
2167 );
2168 }
2169
2170 #[gpui::test]
2171 async fn test_new_local_terminal_opens_in_center_when_center_terminal_focused(
2172 cx: &mut TestAppContext,
2173 ) {
2174 cx.executor().allow_parking();
2175 init_test(cx);
2176
2177 let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2178
2179 window_handle
2180 .update(cx, |multi_workspace, window, cx| {
2181 multi_workspace.workspace().update(cx, |workspace, cx| {
2182 TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2183 project.create_terminal_shell(None, cx)
2184 })
2185 })
2186 })
2187 .expect("Failed to update workspace")
2188 .await
2189 .expect("Failed to create center terminal");
2190 cx.run_until_parked();
2191
2192 window_handle
2193 .update(cx, |multi_workspace, window, cx| {
2194 multi_workspace.workspace().update(cx, |workspace, cx| {
2195 let active_item = workspace
2196 .active_pane()
2197 .read(cx)
2198 .active_item()
2199 .expect("Center pane should have an active item");
2200 let terminal_view = active_item
2201 .downcast::<TerminalView>()
2202 .expect("Active center item should be a TerminalView");
2203 window.focus(&terminal_view.focus_handle(cx), cx);
2204 })
2205 })
2206 .expect("Failed to focus terminal view");
2207 cx.run_until_parked();
2208
2209 let center_items_before = window_handle
2210 .read_with(cx, |multi_workspace, cx| {
2211 multi_workspace
2212 .workspace()
2213 .read(cx)
2214 .active_pane()
2215 .read(cx)
2216 .items_len()
2217 })
2218 .expect("Failed to read center pane items");
2219 let panel_items_before =
2220 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2221
2222 window_handle
2223 .update(cx, |multi_workspace, window, cx| {
2224 multi_workspace.workspace().update(cx, |workspace, cx| {
2225 TerminalPanel::new_terminal(
2226 workspace,
2227 &workspace::NewTerminal { local: true },
2228 window,
2229 cx,
2230 );
2231 })
2232 })
2233 .expect("Failed to dispatch new_terminal with local=true");
2234 cx.run_until_parked();
2235
2236 let center_items_after = window_handle
2237 .read_with(cx, |multi_workspace, cx| {
2238 multi_workspace
2239 .workspace()
2240 .read(cx)
2241 .active_pane()
2242 .read(cx)
2243 .items_len()
2244 })
2245 .expect("Failed to read center pane items");
2246 let panel_items_after =
2247 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2248
2249 assert_eq!(
2250 center_items_after,
2251 center_items_before + 1,
2252 "New local terminal should be added to the center pane"
2253 );
2254 assert_eq!(
2255 panel_items_after, panel_items_before,
2256 "Terminal panel should not gain a new terminal"
2257 );
2258 }
2259
2260 #[gpui::test]
2261 async fn test_new_terminal_opens_in_panel_when_panel_focused_and_center_has_terminal(
2262 cx: &mut TestAppContext,
2263 ) {
2264 cx.executor().allow_parking();
2265 init_test(cx);
2266
2267 let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2268
2269 window_handle
2270 .update(cx, |multi_workspace, window, cx| {
2271 multi_workspace.workspace().update(cx, |workspace, cx| {
2272 TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2273 project.create_terminal_shell(None, cx)
2274 })
2275 })
2276 })
2277 .expect("Failed to update workspace")
2278 .await
2279 .expect("Failed to create center terminal");
2280 cx.run_until_parked();
2281
2282 window_handle
2283 .update(cx, |_, window, cx| {
2284 terminal_panel.update(cx, |panel, cx| {
2285 panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2286 })
2287 })
2288 .expect("Failed to update workspace")
2289 .await
2290 .expect("Failed to create panel terminal");
2291 cx.run_until_parked();
2292
2293 window_handle
2294 .update(cx, |_, window, cx| {
2295 window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2296 })
2297 .expect("Failed to focus terminal panel");
2298 cx.run_until_parked();
2299
2300 let panel_items_before =
2301 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2302 let center_items_before = window_handle
2303 .read_with(cx, |multi_workspace, cx| {
2304 multi_workspace
2305 .workspace()
2306 .read(cx)
2307 .active_pane()
2308 .read(cx)
2309 .items_len()
2310 })
2311 .expect("Failed to read center pane items");
2312
2313 window_handle
2314 .update(cx, |multi_workspace, window, cx| {
2315 multi_workspace.workspace().update(cx, |workspace, cx| {
2316 TerminalPanel::new_terminal(
2317 workspace,
2318 &workspace::NewTerminal::default(),
2319 window,
2320 cx,
2321 );
2322 })
2323 })
2324 .expect("Failed to dispatch new_terminal");
2325 cx.run_until_parked();
2326
2327 let panel_items_after =
2328 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2329 let center_items_after = window_handle
2330 .read_with(cx, |multi_workspace, cx| {
2331 multi_workspace
2332 .workspace()
2333 .read(cx)
2334 .active_pane()
2335 .read(cx)
2336 .items_len()
2337 })
2338 .expect("Failed to read center pane items");
2339
2340 assert_eq!(
2341 panel_items_after,
2342 panel_items_before + 1,
2343 "New terminal should go to panel when panel is focused, even if center has a terminal"
2344 );
2345 assert_eq!(
2346 center_items_after, center_items_before,
2347 "Center pane should not gain a new terminal when panel is focused"
2348 );
2349 }
2350
2351 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
2352 cx.update_global(|store: &mut SettingsStore, cx| {
2353 store.update_user_settings(cx, |settings| {
2354 settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
2355 });
2356 });
2357 }
2358
2359 pub fn init_test(cx: &mut TestAppContext) {
2360 cx.update(|cx| {
2361 let store = SettingsStore::test(cx);
2362 cx.set_global(store);
2363 theme_settings::init(theme::LoadThemes::JustBase, cx);
2364 editor::init(cx);
2365 crate::init(cx);
2366 });
2367 }
2368}