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