1use std::{cmp, ops::ControlFlow, 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::KEY_VALUE_STORE;
12use futures::{channel::oneshot, future::join_all};
13use gpui::{
14 Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter,
15 ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled,
16 Task, WeakEntity, Window, actions,
17};
18use itertools::Itertools;
19use project::{Fs, Project, ProjectEntryId};
20use search::{BufferSearchBar, buffer_search::DivRegistrar};
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, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
32 MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal,
33 Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown,
34 SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
35 dock::{DockPosition, Panel, PanelEvent, PanelHandle},
36 item::SerializableItem,
37 move_active_item, move_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 pub(crate) width: Option<Pixels>,
83 pub(crate) height: Option<Pixels>,
84 pending_serialization: Task<Option<()>>,
85 pending_terminals_to_add: usize,
86 deferred_tasks: HashMap<TaskId, Task<()>>,
87 assistant_enabled: bool,
88 assistant_tab_bar_button: Option<AnyView>,
89 active: bool,
90}
91
92impl TerminalPanel {
93 pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
94 let project = workspace.project();
95 let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
96 let center = PaneGroup::new(pane.clone());
97 let terminal_panel = Self {
98 center,
99 active_pane: pane,
100 fs: workspace.app_state().fs.clone(),
101 workspace: workspace.weak_handle(),
102 pending_serialization: Task::ready(None),
103 width: None,
104 height: None,
105 pending_terminals_to_add: 0,
106 deferred_tasks: HashMap::default(),
107 assistant_enabled: false,
108 assistant_tab_bar_button: None,
109 active: false,
110 };
111 terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
112 terminal_panel
113 }
114
115 pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
116 self.assistant_enabled = enabled;
117 if enabled {
118 let focus_handle = self
119 .active_pane
120 .read(cx)
121 .active_item()
122 .map(|item| item.item_focus_handle(cx))
123 .unwrap_or(self.focus_handle(cx));
124 self.assistant_tab_bar_button = Some(
125 cx.new(move |_| InlineAssistTabBarButton { focus_handle })
126 .into(),
127 );
128 } else {
129 self.assistant_tab_bar_button = None;
130 }
131 for pane in self.center.panes() {
132 self.apply_tab_bar_buttons(pane, cx);
133 }
134 }
135
136 fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
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 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
145 return (None, None);
146 }
147 let focus_handle = pane.focus_handle(cx);
148 let right_children = h_flex()
149 .gap(DynamicSpacing::Base02.rems(cx))
150 .child(
151 PopoverMenu::new("terminal-tab-bar-popover-menu")
152 .trigger_with_tooltip(
153 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
154 Tooltip::text("New…"),
155 )
156 .anchor(Corner::TopRight)
157 .with_handle(pane.new_item_context_menu_handle.clone())
158 .menu(move |window, cx| {
159 let focus_handle = focus_handle.clone();
160 let menu = ContextMenu::build(window, cx, |menu, _, _| {
161 menu.context(focus_handle.clone())
162 .action(
163 "New Terminal",
164 workspace::NewTerminal.boxed_clone(),
165 )
166 // We want the focus to go back to terminal panel once task modal is dismissed,
167 // hence we focus that first. Otherwise, we'd end up without a focused element, as
168 // context menu will be gone the moment we spawn the modal.
169 .action(
170 "Spawn task",
171 zed_actions::Spawn::modal().boxed_clone(),
172 )
173 });
174
175 Some(menu)
176 }),
177 )
178 .children(assistant_tab_bar_button.clone())
179 .child(
180 PopoverMenu::new("terminal-pane-tab-bar-split")
181 .trigger_with_tooltip(
182 IconButton::new("terminal-pane-split", IconName::Split)
183 .icon_size(IconSize::Small),
184 Tooltip::text("Split Pane"),
185 )
186 .anchor(Corner::TopRight)
187 .with_handle(pane.split_item_context_menu_handle.clone())
188 .menu({
189 move |window, cx| {
190 ContextMenu::build(window, cx, |menu, _, _| {
191 menu.when_some(
192 split_context.clone(),
193 |menu, split_context| menu.context(split_context),
194 )
195 .action("Split Right", SplitRight.boxed_clone())
196 .action("Split Left", SplitLeft.boxed_clone())
197 .action("Split Up", SplitUp.boxed_clone())
198 .action("Split Down", SplitDown.boxed_clone())
199 })
200 .into()
201 }
202 }),
203 )
204 .child({
205 let zoomed = pane.is_zoomed();
206 IconButton::new("toggle_zoom", IconName::Maximize)
207 .icon_size(IconSize::Small)
208 .toggle_state(zoomed)
209 .selected_icon(IconName::Minimize)
210 .on_click(cx.listener(|pane, _, window, cx| {
211 pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
212 }))
213 .tooltip(move |window, cx| {
214 Tooltip::for_action(
215 if zoomed { "Zoom Out" } else { "Zoom In" },
216 &ToggleZoom,
217 window,
218 cx,
219 )
220 })
221 })
222 .into_any_element()
223 .into();
224 (None, right_children)
225 });
226 });
227 }
228
229 fn serialization_key(workspace: &Workspace) -> Option<String> {
230 workspace
231 .database_id()
232 .map(|id| i64::from(id).to_string())
233 .or(workspace.session_id())
234 .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
235 }
236
237 pub async fn load(
238 workspace: WeakEntity<Workspace>,
239 mut cx: AsyncWindowContext,
240 ) -> Result<Entity<Self>> {
241 let mut terminal_panel = None;
242
243 if let Some((database_id, serialization_key)) = workspace
244 .read_with(&cx, |workspace, _| {
245 workspace
246 .database_id()
247 .zip(TerminalPanel::serialization_key(workspace))
248 })
249 .ok()
250 .flatten()
251 && let Some(serialized_panel) = cx
252 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
253 .await
254 .log_err()
255 .flatten()
256 .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
257 .transpose()
258 .log_err()
259 .flatten()
260 && let Ok(serialized) = workspace
261 .update_in(&mut cx, |workspace, window, cx| {
262 deserialize_terminal_panel(
263 workspace.weak_handle(),
264 workspace.project().clone(),
265 database_id,
266 serialized_panel,
267 window,
268 cx,
269 )
270 })?
271 .await
272 {
273 terminal_panel = Some(serialized);
274 }
275
276 let terminal_panel = if let Some(panel) = terminal_panel {
277 panel
278 } else {
279 workspace.update_in(&mut cx, |workspace, window, cx| {
280 cx.new(|cx| TerminalPanel::new(workspace, window, cx))
281 })?
282 };
283
284 if let Some(workspace) = workspace.upgrade() {
285 workspace
286 .update(&mut cx, |workspace, _| {
287 workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
288 })
289 .ok();
290 }
291
292 // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
293 if let Some(workspace) = workspace.upgrade() {
294 let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
295 let alive_item_ids = terminal_panel
296 .read(cx)
297 .center
298 .panes()
299 .into_iter()
300 .flat_map(|pane| pane.read(cx).items())
301 .map(|item| item.item_id().as_u64() as ItemId)
302 .collect();
303 workspace.database_id().map(|workspace_id| {
304 TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
305 })
306 })?;
307 if let Some(task) = cleanup_task {
308 task.await.log_err();
309 }
310 }
311
312 if let Some(workspace) = workspace.upgrade() {
313 let should_focus = workspace
314 .update_in(&mut cx, |workspace, window, cx| {
315 workspace.active_item(cx).is_none()
316 && workspace
317 .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
318 })
319 .unwrap_or(false);
320
321 if should_focus {
322 terminal_panel
323 .update_in(&mut cx, |panel, window, cx| {
324 panel.active_pane.update(cx, |pane, cx| {
325 pane.focus_active_item(window, cx);
326 });
327 })
328 .ok();
329 }
330 }
331 Ok(terminal_panel)
332 }
333
334 fn handle_pane_event(
335 &mut self,
336 pane: &Entity<Pane>,
337 event: &pane::Event,
338 window: &mut Window,
339 cx: &mut Context<Self>,
340 ) {
341 match event {
342 pane::Event::ActivateItem { .. } => self.serialize(cx),
343 pane::Event::RemovedItem { .. } => self.serialize(cx),
344 pane::Event::Remove { focus_on_pane } => {
345 let pane_count_before_removal = self.center.panes().len();
346 let _removal_result = self.center.remove(pane);
347 if pane_count_before_removal == 1 {
348 self.center.first_pane().update(cx, |pane, cx| {
349 pane.set_zoomed(false, cx);
350 });
351 cx.emit(PanelEvent::Close);
352 } else if let Some(focus_on_pane) =
353 focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
354 {
355 focus_on_pane.focus_handle(cx).focus(window);
356 }
357 }
358 pane::Event::ZoomIn => {
359 for pane in self.center.panes() {
360 pane.update(cx, |pane, cx| {
361 pane.set_zoomed(true, cx);
362 })
363 }
364 cx.emit(PanelEvent::ZoomIn);
365 cx.notify();
366 }
367 pane::Event::ZoomOut => {
368 for pane in self.center.panes() {
369 pane.update(cx, |pane, cx| {
370 pane.set_zoomed(false, cx);
371 })
372 }
373 cx.emit(PanelEvent::ZoomOut);
374 cx.notify();
375 }
376 pane::Event::AddItem { item } => {
377 if let Some(workspace) = self.workspace.upgrade() {
378 workspace.update(cx, |workspace, cx| {
379 item.added_to_pane(workspace, pane.clone(), window, cx)
380 })
381 }
382 self.serialize(cx);
383 }
384 &pane::Event::Split {
385 direction,
386 clone_active_item,
387 } => {
388 if clone_active_item {
389 let fut = self.new_pane_with_cloned_active_terminal(window, cx);
390 let pane = pane.clone();
391 cx.spawn_in(window, async move |panel, cx| {
392 let Some(new_pane) = fut.await else {
393 return;
394 };
395 panel
396 .update_in(cx, |panel, window, cx| {
397 panel.center.split(&pane, &new_pane, direction).log_err();
398 window.focus(&new_pane.focus_handle(cx));
399 })
400 .ok();
401 })
402 .detach();
403 } else {
404 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
405 else {
406 return;
407 };
408 let Ok(project) = self
409 .workspace
410 .update(cx, |workspace, _| workspace.project().clone())
411 else {
412 return;
413 };
414 let new_pane =
415 new_terminal_pane(self.workspace.clone(), project, false, window, cx);
416 new_pane.update(cx, |pane, cx| {
417 pane.add_item(item, true, true, None, window, cx);
418 });
419 self.center.split(&pane, &new_pane, direction).log_err();
420 window.focus(&new_pane.focus_handle(cx));
421 }
422 }
423 pane::Event::Focus => {
424 self.active_pane = pane.clone();
425 }
426 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
427 self.serialize(cx);
428 }
429
430 _ => {}
431 }
432 }
433
434 fn new_pane_with_cloned_active_terminal(
435 &mut self,
436 window: &mut Window,
437 cx: &mut Context<Self>,
438 ) -> Task<Option<Entity<Pane>>> {
439 let Some(workspace) = self.workspace.upgrade() else {
440 return Task::ready(None);
441 };
442 let workspace = workspace.read(cx);
443 let database_id = workspace.database_id();
444 let weak_workspace = self.workspace.clone();
445 let project = workspace.project().clone();
446 let active_pane = &self.active_pane;
447 let terminal_view = active_pane
448 .read(cx)
449 .active_item()
450 .and_then(|item| item.downcast::<TerminalView>());
451 let working_directory = terminal_view
452 .as_ref()
453 .and_then(|terminal_view| {
454 terminal_view
455 .read(cx)
456 .terminal()
457 .read(cx)
458 .working_directory()
459 })
460 .or_else(|| default_working_directory(workspace, cx));
461 let is_zoomed = active_pane.read(cx).is_zoomed();
462 cx.spawn_in(window, async move |panel, cx| {
463 let terminal = project
464 .update(cx, |project, cx| match terminal_view {
465 Some(view) => project.clone_terminal(
466 &view.read(cx).terminal.clone(),
467 cx,
468 working_directory,
469 ),
470 None => project.create_terminal_shell(working_directory, cx),
471 })
472 .ok()?
473 .await
474 .log_err()?;
475
476 panel
477 .update_in(cx, move |terminal_panel, window, cx| {
478 let terminal_view = Box::new(cx.new(|cx| {
479 TerminalView::new(
480 terminal.clone(),
481 weak_workspace.clone(),
482 database_id,
483 project.downgrade(),
484 window,
485 cx,
486 )
487 }));
488 let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
489 terminal_panel.apply_tab_bar_buttons(&pane, cx);
490 pane.update(cx, |pane, cx| {
491 pane.add_item(terminal_view, true, true, None, window, cx);
492 });
493 Some(pane)
494 })
495 .ok()
496 .flatten()
497 })
498 }
499
500 pub fn open_terminal(
501 workspace: &mut Workspace,
502 action: &workspace::OpenTerminal,
503 window: &mut Window,
504 cx: &mut Context<Workspace>,
505 ) {
506 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
507 return;
508 };
509
510 terminal_panel
511 .update(cx, |panel, cx| {
512 panel.add_terminal_shell(
513 Some(action.working_directory.clone()),
514 RevealStrategy::Always,
515 window,
516 cx,
517 )
518 })
519 .detach_and_log_err(cx);
520 }
521
522 pub fn spawn_task(
523 &mut self,
524 task: &SpawnInTerminal,
525 window: &mut Window,
526 cx: &mut Context<Self>,
527 ) -> Task<Result<WeakEntity<Terminal>>> {
528 let Some(workspace) = self.workspace.upgrade() else {
529 return Task::ready(Err(anyhow!("failed to read workspace")));
530 };
531
532 let project = workspace.read(cx).project().read(cx);
533
534 if project.is_via_collab() {
535 return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
536 }
537
538 let remote_client = project.remote_client();
539 let is_windows = project.path_style(cx).is_windows();
540 let remote_shell = remote_client
541 .as_ref()
542 .and_then(|remote_client| remote_client.read(cx).shell());
543
544 let shell = if let Some(remote_shell) = remote_shell
545 && task.shell == Shell::System
546 {
547 Shell::Program(remote_shell)
548 } else {
549 task.shell.clone()
550 };
551
552 let builder = ShellBuilder::new(&shell, is_windows);
553 let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
554 let (command, args) = builder.build(task.command.clone(), &task.args);
555
556 let task = SpawnInTerminal {
557 command_label,
558 command: Some(command),
559 args,
560 ..task.clone()
561 };
562
563 if task.allow_concurrent_runs && task.use_new_terminal {
564 return self.spawn_in_new_terminal(task, window, cx);
565 }
566
567 let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
568 let Some(existing) = terminals_for_task.pop() else {
569 return self.spawn_in_new_terminal(task, window, cx);
570 };
571
572 let (existing_item_index, task_pane, existing_terminal) = existing;
573 if task.allow_concurrent_runs {
574 return self.replace_terminal(
575 task,
576 task_pane,
577 existing_item_index,
578 existing_terminal,
579 window,
580 cx,
581 );
582 }
583
584 let (tx, rx) = oneshot::channel();
585
586 self.deferred_tasks.insert(
587 task.id.clone(),
588 cx.spawn_in(window, async move |terminal_panel, cx| {
589 wait_for_terminals_tasks(terminals_for_task, cx).await;
590 let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
591 if task.use_new_terminal {
592 terminal_panel.spawn_in_new_terminal(task, window, cx)
593 } else {
594 terminal_panel.replace_terminal(
595 task,
596 task_pane,
597 existing_item_index,
598 existing_terminal,
599 window,
600 cx,
601 )
602 }
603 });
604 if let Ok(task) = task {
605 tx.send(task.await).ok();
606 }
607 }),
608 );
609
610 cx.spawn(async move |_, _| rx.await?)
611 }
612
613 fn spawn_in_new_terminal(
614 &mut self,
615 spawn_task: SpawnInTerminal,
616 window: &mut Window,
617 cx: &mut Context<Self>,
618 ) -> Task<Result<WeakEntity<Terminal>>> {
619 let reveal = spawn_task.reveal;
620 let reveal_target = spawn_task.reveal_target;
621 match reveal_target {
622 RevealTarget::Center => self
623 .workspace
624 .update(cx, |workspace, cx| {
625 Self::add_center_terminal(workspace, window, cx, |project, cx| {
626 project.create_terminal_task(spawn_task, cx)
627 })
628 })
629 .unwrap_or_else(|e| Task::ready(Err(e))),
630 RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
631 }
632 }
633
634 /// Create a new Terminal in the current working directory or the user's home directory
635 fn new_terminal(
636 workspace: &mut Workspace,
637 _: &workspace::NewTerminal,
638 window: &mut Window,
639 cx: &mut Context<Workspace>,
640 ) {
641 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
642 return;
643 };
644
645 terminal_panel
646 .update(cx, |this, cx| {
647 this.add_terminal_shell(
648 default_working_directory(workspace, cx),
649 RevealStrategy::Always,
650 window,
651 cx,
652 )
653 })
654 .detach_and_log_err(cx);
655 }
656
657 fn terminals_for_task(
658 &self,
659 label: &str,
660 cx: &mut App,
661 ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
662 let Some(workspace) = self.workspace.upgrade() else {
663 return Vec::new();
664 };
665
666 let pane_terminal_views = |pane: Entity<Pane>| {
667 pane.read(cx)
668 .items()
669 .enumerate()
670 .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
671 .filter_map(|(index, terminal_view)| {
672 let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
673 if &task_state.spawned_task.full_label == label {
674 Some((index, terminal_view))
675 } else {
676 None
677 }
678 })
679 .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
680 };
681
682 self.center
683 .panes()
684 .into_iter()
685 .cloned()
686 .flat_map(pane_terminal_views)
687 .chain(
688 workspace
689 .read(cx)
690 .panes()
691 .iter()
692 .cloned()
693 .flat_map(pane_terminal_views),
694 )
695 .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
696 .collect()
697 }
698
699 fn activate_terminal_view(
700 &self,
701 pane: &Entity<Pane>,
702 item_index: usize,
703 focus: bool,
704 window: &mut Window,
705 cx: &mut App,
706 ) {
707 pane.update(cx, |pane, cx| {
708 pane.activate_item(item_index, true, focus, window, cx)
709 })
710 }
711
712 pub fn add_center_terminal(
713 workspace: &mut Workspace,
714 window: &mut Window,
715 cx: &mut Context<Workspace>,
716 create_terminal: impl FnOnce(
717 &mut Project,
718 &mut Context<Project>,
719 ) -> Task<Result<Entity<Terminal>>>
720 + 'static,
721 ) -> Task<Result<WeakEntity<Terminal>>> {
722 if !is_enabled_in_workspace(workspace, cx) {
723 return Task::ready(Err(anyhow!(
724 "terminal not yet supported for remote projects"
725 )));
726 }
727 let project = workspace.project().downgrade();
728 cx.spawn_in(window, async move |workspace, cx| {
729 let terminal = project.update(cx, create_terminal)?.await?;
730
731 workspace.update_in(cx, |workspace, window, cx| {
732 let terminal_view = cx.new(|cx| {
733 TerminalView::new(
734 terminal.clone(),
735 workspace.weak_handle(),
736 workspace.database_id(),
737 workspace.project().downgrade(),
738 window,
739 cx,
740 )
741 });
742 workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
743 })?;
744 Ok(terminal.downgrade())
745 })
746 }
747
748 pub fn add_terminal_task(
749 &mut self,
750 task: SpawnInTerminal,
751 reveal_strategy: RevealStrategy,
752 window: &mut Window,
753 cx: &mut Context<Self>,
754 ) -> Task<Result<WeakEntity<Terminal>>> {
755 let workspace = self.workspace.clone();
756 cx.spawn_in(window, async move |terminal_panel, cx| {
757 if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
758 anyhow::bail!("terminal not yet supported for remote projects");
759 }
760 let pane = terminal_panel.update(cx, |terminal_panel, _| {
761 terminal_panel.pending_terminals_to_add += 1;
762 terminal_panel.active_pane.clone()
763 })?;
764 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
765 let terminal = project
766 .update(cx, |project, cx| project.create_terminal_task(task, cx))?
767 .await?;
768 let result = workspace.update_in(cx, |workspace, window, cx| {
769 let terminal_view = Box::new(cx.new(|cx| {
770 TerminalView::new(
771 terminal.clone(),
772 workspace.weak_handle(),
773 workspace.database_id(),
774 workspace.project().downgrade(),
775 window,
776 cx,
777 )
778 }));
779
780 match reveal_strategy {
781 RevealStrategy::Always => {
782 workspace.focus_panel::<Self>(window, cx);
783 }
784 RevealStrategy::NoFocus => {
785 workspace.open_panel::<Self>(window, cx);
786 }
787 RevealStrategy::Never => {}
788 }
789
790 pane.update(cx, |pane, cx| {
791 let focus = pane.has_focus(window, cx)
792 || matches!(reveal_strategy, RevealStrategy::Always);
793 pane.add_item(terminal_view, true, focus, None, window, cx);
794 });
795
796 Ok(terminal.downgrade())
797 })?;
798 terminal_panel.update(cx, |terminal_panel, cx| {
799 terminal_panel.pending_terminals_to_add =
800 terminal_panel.pending_terminals_to_add.saturating_sub(1);
801 terminal_panel.serialize(cx)
802 })?;
803 result
804 })
805 }
806
807 fn add_terminal_shell(
808 &mut self,
809 cwd: Option<PathBuf>,
810 reveal_strategy: RevealStrategy,
811 window: &mut Window,
812 cx: &mut Context<Self>,
813 ) -> Task<Result<WeakEntity<Terminal>>> {
814 let workspace = self.workspace.clone();
815
816 cx.spawn_in(window, async move |terminal_panel, cx| {
817 if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
818 anyhow::bail!("terminal not yet supported for collaborative projects");
819 }
820 let pane = terminal_panel.update(cx, |terminal_panel, _| {
821 terminal_panel.pending_terminals_to_add += 1;
822 terminal_panel.active_pane.clone()
823 })?;
824 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
825 let terminal = project
826 .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
827 .await;
828
829 match terminal {
830 Ok(terminal) => {
831 let result = workspace.update_in(cx, |workspace, window, cx| {
832 let terminal_view = Box::new(cx.new(|cx| {
833 TerminalView::new(
834 terminal.clone(),
835 workspace.weak_handle(),
836 workspace.database_id(),
837 workspace.project().downgrade(),
838 window,
839 cx,
840 )
841 }));
842
843 match reveal_strategy {
844 RevealStrategy::Always => {
845 workspace.focus_panel::<Self>(window, cx);
846 }
847 RevealStrategy::NoFocus => {
848 workspace.open_panel::<Self>(window, cx);
849 }
850 RevealStrategy::Never => {}
851 }
852
853 pane.update(cx, |pane, cx| {
854 let focus = pane.has_focus(window, cx)
855 || matches!(reveal_strategy, RevealStrategy::Always);
856 pane.add_item(terminal_view, true, focus, None, window, cx);
857 });
858
859 Ok(terminal.downgrade())
860 })?;
861 terminal_panel.update(cx, |terminal_panel, cx| {
862 terminal_panel.pending_terminals_to_add =
863 terminal_panel.pending_terminals_to_add.saturating_sub(1);
864 terminal_panel.serialize(cx)
865 })?;
866 result
867 }
868 Err(error) => {
869 pane.update_in(cx, |pane, window, cx| {
870 let focus = pane.has_focus(window, cx);
871 let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
872 error: error.to_string(),
873 focus_handle: cx.focus_handle(),
874 });
875 pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
876 })?;
877 Err(error)
878 }
879 }
880 })
881 }
882
883 fn serialize(&mut self, cx: &mut Context<Self>) {
884 let height = self.height;
885 let width = self.width;
886 let Some(serialization_key) = self
887 .workspace
888 .read_with(cx, |workspace, _| {
889 TerminalPanel::serialization_key(workspace)
890 })
891 .ok()
892 .flatten()
893 else {
894 return;
895 };
896 self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
897 cx.background_executor()
898 .timer(Duration::from_millis(50))
899 .await;
900 let terminal_panel = terminal_panel.upgrade()?;
901 let items = terminal_panel
902 .update(cx, |terminal_panel, cx| {
903 SerializedItems::WithSplits(serialize_pane_group(
904 &terminal_panel.center,
905 &terminal_panel.active_pane,
906 cx,
907 ))
908 })
909 .ok()?;
910 cx.background_spawn(
911 async move {
912 KEY_VALUE_STORE
913 .write_kvp(
914 serialization_key,
915 serde_json::to_string(&SerializedTerminalPanel {
916 items,
917 active_item_id: None,
918 height,
919 width,
920 })?,
921 )
922 .await?;
923 anyhow::Ok(())
924 }
925 .log_err(),
926 )
927 .await;
928 Some(())
929 });
930 }
931
932 fn replace_terminal(
933 &self,
934 spawn_task: SpawnInTerminal,
935 task_pane: Entity<Pane>,
936 terminal_item_index: usize,
937 terminal_to_replace: Entity<TerminalView>,
938 window: &mut Window,
939 cx: &mut Context<Self>,
940 ) -> Task<Result<WeakEntity<Terminal>>> {
941 let reveal = spawn_task.reveal;
942 let reveal_target = spawn_task.reveal_target;
943 let task_workspace = self.workspace.clone();
944 cx.spawn_in(window, async move |terminal_panel, cx| {
945 let project = terminal_panel.update(cx, |this, cx| {
946 this.workspace
947 .update(cx, |workspace, _| workspace.project().clone())
948 })??;
949 let new_terminal = project
950 .update(cx, |project, cx| {
951 project.create_terminal_task(spawn_task, cx)
952 })?
953 .await?;
954 terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
955 terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
956 })?;
957
958 match reveal {
959 RevealStrategy::Always => match reveal_target {
960 RevealTarget::Center => {
961 task_workspace.update_in(cx, |workspace, window, cx| {
962 let did_activate = workspace.activate_item(
963 &terminal_to_replace,
964 true,
965 true,
966 window,
967 cx,
968 );
969
970 anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
971
972 anyhow::Ok(())
973 })??;
974 }
975 RevealTarget::Dock => {
976 terminal_panel.update_in(cx, |terminal_panel, window, cx| {
977 terminal_panel.activate_terminal_view(
978 &task_pane,
979 terminal_item_index,
980 true,
981 window,
982 cx,
983 )
984 })?;
985
986 cx.spawn(async move |cx| {
987 task_workspace
988 .update_in(cx, |workspace, window, cx| {
989 workspace.focus_panel::<Self>(window, cx)
990 })
991 .ok()
992 })
993 .detach();
994 }
995 },
996 RevealStrategy::NoFocus => match reveal_target {
997 RevealTarget::Center => {
998 task_workspace.update_in(cx, |workspace, window, cx| {
999 workspace.active_pane().focus_handle(cx).focus(window);
1000 })?;
1001 }
1002 RevealTarget::Dock => {
1003 terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1004 terminal_panel.activate_terminal_view(
1005 &task_pane,
1006 terminal_item_index,
1007 false,
1008 window,
1009 cx,
1010 )
1011 })?;
1012
1013 cx.spawn(async move |cx| {
1014 task_workspace
1015 .update_in(cx, |workspace, window, cx| {
1016 workspace.open_panel::<Self>(window, cx)
1017 })
1018 .ok()
1019 })
1020 .detach();
1021 }
1022 },
1023 RevealStrategy::Never => {}
1024 }
1025
1026 Ok(new_terminal.downgrade())
1027 })
1028 }
1029
1030 fn has_no_terminals(&self, cx: &App) -> bool {
1031 self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1032 }
1033
1034 pub fn assistant_enabled(&self) -> bool {
1035 self.assistant_enabled
1036 }
1037
1038 fn is_enabled(&self, cx: &App) -> bool {
1039 self.workspace
1040 .upgrade()
1041 .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1042 }
1043
1044 fn activate_pane_in_direction(
1045 &mut self,
1046 direction: SplitDirection,
1047 window: &mut Window,
1048 cx: &mut Context<Self>,
1049 ) {
1050 if let Some(pane) = self
1051 .center
1052 .find_pane_in_direction(&self.active_pane, direction, cx)
1053 {
1054 window.focus(&pane.focus_handle(cx));
1055 } else {
1056 self.workspace
1057 .update(cx, |workspace, cx| {
1058 workspace.activate_pane_in_direction(direction, window, cx)
1059 })
1060 .ok();
1061 }
1062 }
1063
1064 fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1065 if let Some(to) = self
1066 .center
1067 .find_pane_in_direction(&self.active_pane, direction, cx)
1068 .cloned()
1069 {
1070 self.center.swap(&self.active_pane, &to);
1071 cx.notify();
1072 }
1073 }
1074
1075 fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1076 if self
1077 .center
1078 .move_to_border(&self.active_pane, direction)
1079 .unwrap()
1080 {
1081 cx.notify();
1082 }
1083 }
1084}
1085
1086fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1087 workspace.project().read(cx).supports_terminal(cx)
1088}
1089
1090pub fn new_terminal_pane(
1091 workspace: WeakEntity<Workspace>,
1092 project: Entity<Project>,
1093 zoomed: bool,
1094 window: &mut Window,
1095 cx: &mut Context<TerminalPanel>,
1096) -> Entity<Pane> {
1097 let is_local = project.read(cx).is_local();
1098 let terminal_panel = cx.entity();
1099 let pane = cx.new(|cx| {
1100 let mut pane = Pane::new(
1101 workspace.clone(),
1102 project.clone(),
1103 Default::default(),
1104 None,
1105 NewTerminal.boxed_clone(),
1106 false,
1107 window,
1108 cx,
1109 );
1110 pane.set_zoomed(zoomed, cx);
1111 pane.set_can_navigate(false, cx);
1112 pane.display_nav_history_buttons(None);
1113 pane.set_should_display_tab_bar(|_, _| true);
1114 pane.set_zoom_out_on_close(false);
1115
1116 let split_closure_terminal_panel = terminal_panel.downgrade();
1117 pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1118 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1119 let is_current_pane = tab.pane == cx.entity();
1120 let Some(can_drag_away) = split_closure_terminal_panel
1121 .read_with(cx, |terminal_panel, _| {
1122 let current_panes = terminal_panel.center.panes();
1123 !current_panes.contains(&&tab.pane)
1124 || current_panes.len() > 1
1125 || (!is_current_pane || pane.items_len() > 1)
1126 })
1127 .ok()
1128 else {
1129 return false;
1130 };
1131 if can_drag_away {
1132 let item = if is_current_pane {
1133 pane.item_for_index(tab.ix)
1134 } else {
1135 tab.pane.read(cx).item_for_index(tab.ix)
1136 };
1137 if let Some(item) = item {
1138 return item.downcast::<TerminalView>().is_some();
1139 }
1140 }
1141 }
1142 false
1143 })));
1144
1145 let buffer_search_bar = cx.new(|cx| {
1146 search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
1147 });
1148 let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1149 pane.toolbar().update(cx, |toolbar, cx| {
1150 toolbar.add_item(buffer_search_bar, window, cx);
1151 toolbar.add_item(breadcrumbs, window, cx);
1152 });
1153
1154 let drop_closure_project = project.downgrade();
1155 let drop_closure_terminal_panel = terminal_panel.downgrade();
1156 pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
1157 let Some(project) = drop_closure_project.upgrade() else {
1158 return ControlFlow::Break(());
1159 };
1160 if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
1161 let this_pane = cx.entity();
1162 let item = if tab.pane == this_pane {
1163 pane.item_for_index(tab.ix)
1164 } else {
1165 tab.pane.read(cx).item_for_index(tab.ix)
1166 };
1167 if let Some(item) = item {
1168 if item.downcast::<TerminalView>().is_some() {
1169 let source = tab.pane.clone();
1170 let item_id_to_move = item.item_id();
1171
1172 let Ok(new_split_pane) = pane
1173 .drag_split_direction()
1174 .map(|split_direction| {
1175 drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
1176 let is_zoomed = if terminal_panel.active_pane == this_pane {
1177 pane.is_zoomed()
1178 } else {
1179 terminal_panel.active_pane.read(cx).is_zoomed()
1180 };
1181 let new_pane = new_terminal_pane(
1182 workspace.clone(),
1183 project.clone(),
1184 is_zoomed,
1185 window,
1186 cx,
1187 );
1188 terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
1189 terminal_panel.center.split(
1190 &this_pane,
1191 &new_pane,
1192 split_direction,
1193 )?;
1194 anyhow::Ok(new_pane)
1195 })
1196 })
1197 .transpose()
1198 else {
1199 return ControlFlow::Break(());
1200 };
1201
1202 match new_split_pane.transpose() {
1203 // Source pane may be the one currently updated, so defer the move.
1204 Ok(Some(new_pane)) => cx
1205 .spawn_in(window, async move |_, cx| {
1206 cx.update(|window, cx| {
1207 move_item(
1208 &source,
1209 &new_pane,
1210 item_id_to_move,
1211 new_pane.read(cx).active_item_index(),
1212 true,
1213 window,
1214 cx,
1215 );
1216 })
1217 .ok();
1218 })
1219 .detach(),
1220 // If we drop into existing pane or current pane,
1221 // regular pane drop handler will take care of it,
1222 // using the right tab index for the operation.
1223 Ok(None) => return ControlFlow::Continue(()),
1224 err @ Err(_) => {
1225 err.log_err();
1226 return ControlFlow::Break(());
1227 }
1228 };
1229 } else if let Some(project_path) = item.project_path(cx)
1230 && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1231 {
1232 add_paths_to_terminal(pane, &[entry_path], window, cx);
1233 }
1234 }
1235 } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1236 let project = project.read(cx);
1237 let paths_to_add = selection
1238 .items()
1239 .map(|selected_entry| selected_entry.entry_id)
1240 .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1241 .filter_map(|project_path| project.absolute_path(&project_path, cx))
1242 .collect::<Vec<_>>();
1243 if !paths_to_add.is_empty() {
1244 add_paths_to_terminal(pane, &paths_to_add, window, cx);
1245 }
1246 } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1247 if let Some(entry_path) = project
1248 .read(cx)
1249 .path_for_entry(entry_id, cx)
1250 .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1251 {
1252 add_paths_to_terminal(pane, &[entry_path], window, cx);
1253 }
1254 } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1255 add_paths_to_terminal(pane, paths.paths(), window, cx);
1256 }
1257
1258 ControlFlow::Break(())
1259 });
1260
1261 pane
1262 });
1263
1264 cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1265 .detach();
1266 cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1267
1268 pane
1269}
1270
1271async fn wait_for_terminals_tasks(
1272 terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1273 cx: &mut AsyncApp,
1274) {
1275 let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1276 terminal
1277 .update(cx, |terminal_view, cx| {
1278 terminal_view
1279 .terminal()
1280 .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1281 })
1282 .ok()
1283 });
1284 join_all(pending_tasks).await;
1285}
1286
1287fn add_paths_to_terminal(
1288 pane: &mut Pane,
1289 paths: &[PathBuf],
1290 window: &mut Window,
1291 cx: &mut Context<Pane>,
1292) {
1293 if let Some(terminal_view) = pane
1294 .active_item()
1295 .and_then(|item| item.downcast::<TerminalView>())
1296 {
1297 window.focus(&terminal_view.focus_handle(cx));
1298 let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1299 new_text.push(' ');
1300 terminal_view.update(cx, |terminal_view, cx| {
1301 terminal_view.terminal().update(cx, |terminal, _| {
1302 terminal.paste(&new_text);
1303 });
1304 });
1305 }
1306}
1307
1308struct FailedToSpawnTerminal {
1309 error: String,
1310 focus_handle: FocusHandle,
1311}
1312
1313impl Focusable for FailedToSpawnTerminal {
1314 fn focus_handle(&self, _: &App) -> FocusHandle {
1315 self.focus_handle.clone()
1316 }
1317}
1318
1319impl Render for FailedToSpawnTerminal {
1320 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1321 let popover_menu = PopoverMenu::new("settings-popover")
1322 .trigger(
1323 IconButton::new("icon-button-popover", IconName::ChevronDown)
1324 .icon_size(IconSize::XSmall),
1325 )
1326 .menu(move |window, cx| {
1327 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1328 context_menu
1329 .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1330 .action(
1331 "Edit settings.json",
1332 zed_actions::OpenSettingsFile.boxed_clone(),
1333 )
1334 }))
1335 })
1336 .anchor(Corner::TopRight)
1337 .offset(gpui::Point {
1338 x: px(0.0),
1339 y: px(2.0),
1340 });
1341
1342 v_flex()
1343 .track_focus(&self.focus_handle)
1344 .size_full()
1345 .p_4()
1346 .items_center()
1347 .justify_center()
1348 .bg(cx.theme().colors().editor_background)
1349 .child(
1350 v_flex()
1351 .max_w_112()
1352 .items_center()
1353 .justify_center()
1354 .text_center()
1355 .child(Label::new("Failed to spawn terminal"))
1356 .child(
1357 Label::new(self.error.to_string())
1358 .size(LabelSize::Small)
1359 .color(Color::Muted)
1360 .mb_4(),
1361 )
1362 .child(SplitButton::new(
1363 ButtonLike::new("open-settings-ui")
1364 .child(Label::new("Edit Settings").size(LabelSize::Small))
1365 .on_click(|_, window, cx| {
1366 window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1367 }),
1368 popover_menu.into_any_element(),
1369 )),
1370 )
1371 }
1372}
1373
1374impl EventEmitter<()> for FailedToSpawnTerminal {}
1375
1376impl workspace::Item for FailedToSpawnTerminal {
1377 type Event = ();
1378
1379 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1380 SharedString::new_static("Failed to spawn terminal")
1381 }
1382}
1383
1384impl EventEmitter<PanelEvent> for TerminalPanel {}
1385
1386impl Render for TerminalPanel {
1387 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1388 let mut registrar = DivRegistrar::new(
1389 |panel, _, cx| {
1390 panel
1391 .active_pane
1392 .read(cx)
1393 .toolbar()
1394 .read(cx)
1395 .item_of_type::<BufferSearchBar>()
1396 },
1397 cx,
1398 );
1399 BufferSearchBar::register(&mut registrar);
1400 let registrar = registrar.into_div();
1401 self.workspace
1402 .update(cx, |workspace, cx| {
1403 registrar.size_full().child(self.center.render(
1404 workspace.zoomed_item(),
1405 &workspace::PaneRenderContext {
1406 follower_states: &HashMap::default(),
1407 active_call: workspace.active_call(),
1408 active_pane: &self.active_pane,
1409 app_state: workspace.app_state(),
1410 project: workspace.project(),
1411 workspace: &workspace.weak_handle(),
1412 },
1413 window,
1414 cx,
1415 ))
1416 })
1417 .ok()
1418 .map(|div| {
1419 div.on_action({
1420 cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1421 terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1422 })
1423 })
1424 .on_action({
1425 cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1426 terminal_panel.activate_pane_in_direction(
1427 SplitDirection::Right,
1428 window,
1429 cx,
1430 );
1431 })
1432 })
1433 .on_action({
1434 cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1435 terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1436 })
1437 })
1438 .on_action({
1439 cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1440 terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1441 })
1442 })
1443 .on_action(
1444 cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1445 let panes = terminal_panel.center.panes();
1446 if let Some(ix) = panes
1447 .iter()
1448 .position(|pane| **pane == terminal_panel.active_pane)
1449 {
1450 let next_ix = (ix + 1) % panes.len();
1451 window.focus(&panes[next_ix].focus_handle(cx));
1452 }
1453 }),
1454 )
1455 .on_action(cx.listener(
1456 |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1457 let panes = terminal_panel.center.panes();
1458 if let Some(ix) = panes
1459 .iter()
1460 .position(|pane| **pane == terminal_panel.active_pane)
1461 {
1462 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1463 window.focus(&panes[prev_ix].focus_handle(cx));
1464 }
1465 },
1466 ))
1467 .on_action(
1468 cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1469 let panes = terminal_panel.center.panes();
1470 if let Some(&pane) = panes.get(action.0) {
1471 window.focus(&pane.read(cx).focus_handle(cx));
1472 } else {
1473 let future =
1474 terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
1475 cx.spawn_in(window, async move |terminal_panel, cx| {
1476 if let Some(new_pane) = future.await {
1477 _ = terminal_panel.update_in(
1478 cx,
1479 |terminal_panel, window, cx| {
1480 terminal_panel
1481 .center
1482 .split(
1483 &terminal_panel.active_pane,
1484 &new_pane,
1485 SplitDirection::Right,
1486 )
1487 .log_err();
1488 let new_pane = new_pane.read(cx);
1489 window.focus(&new_pane.focus_handle(cx));
1490 },
1491 );
1492 }
1493 })
1494 .detach();
1495 }
1496 }),
1497 )
1498 .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1499 terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1500 }))
1501 .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1502 terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1503 }))
1504 .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1505 terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1506 }))
1507 .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1508 terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1509 }))
1510 .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1511 terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1512 }))
1513 .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1514 terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1515 }))
1516 .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1517 terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1518 }))
1519 .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1520 terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1521 }))
1522 .on_action(
1523 cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1524 let Some(&target_pane) =
1525 terminal_panel.center.panes().get(action.destination)
1526 else {
1527 return;
1528 };
1529 move_active_item(
1530 &terminal_panel.active_pane,
1531 target_pane,
1532 action.focus,
1533 true,
1534 window,
1535 cx,
1536 );
1537 }),
1538 )
1539 .on_action(cx.listener(
1540 |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1541 let source_pane = &terminal_panel.active_pane;
1542 if let Some(destination_pane) = terminal_panel
1543 .center
1544 .find_pane_in_direction(source_pane, action.direction, cx)
1545 {
1546 move_active_item(
1547 source_pane,
1548 destination_pane,
1549 action.focus,
1550 true,
1551 window,
1552 cx,
1553 );
1554 };
1555 },
1556 ))
1557 })
1558 .unwrap_or_else(|| div())
1559 }
1560}
1561
1562impl Focusable for TerminalPanel {
1563 fn focus_handle(&self, cx: &App) -> FocusHandle {
1564 self.active_pane.focus_handle(cx)
1565 }
1566}
1567
1568impl Panel for TerminalPanel {
1569 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1570 match TerminalSettings::get_global(cx).dock {
1571 TerminalDockPosition::Left => DockPosition::Left,
1572 TerminalDockPosition::Bottom => DockPosition::Bottom,
1573 TerminalDockPosition::Right => DockPosition::Right,
1574 }
1575 }
1576
1577 fn position_is_valid(&self, _: DockPosition) -> bool {
1578 true
1579 }
1580
1581 fn set_position(
1582 &mut self,
1583 position: DockPosition,
1584 _window: &mut Window,
1585 cx: &mut Context<Self>,
1586 ) {
1587 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1588 let dock = match position {
1589 DockPosition::Left => TerminalDockPosition::Left,
1590 DockPosition::Bottom => TerminalDockPosition::Bottom,
1591 DockPosition::Right => TerminalDockPosition::Right,
1592 };
1593 settings.terminal.get_or_insert_default().dock = Some(dock);
1594 });
1595 }
1596
1597 fn size(&self, window: &Window, cx: &App) -> Pixels {
1598 let settings = TerminalSettings::get_global(cx);
1599 match self.position(window, cx) {
1600 DockPosition::Left | DockPosition::Right => {
1601 self.width.unwrap_or(settings.default_width)
1602 }
1603 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1604 }
1605 }
1606
1607 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1608 match self.position(window, cx) {
1609 DockPosition::Left | DockPosition::Right => self.width = size,
1610 DockPosition::Bottom => self.height = size,
1611 }
1612 cx.notify();
1613 cx.defer_in(window, |this, _, cx| {
1614 this.serialize(cx);
1615 })
1616 }
1617
1618 fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1619 self.active_pane.read(cx).is_zoomed()
1620 }
1621
1622 fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1623 for pane in self.center.panes() {
1624 pane.update(cx, |pane, cx| {
1625 pane.set_zoomed(zoomed, cx);
1626 })
1627 }
1628 cx.notify();
1629 }
1630
1631 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1632 let old_active = self.active;
1633 self.active = active;
1634 if !active || old_active == active || !self.has_no_terminals(cx) {
1635 return;
1636 }
1637 cx.defer_in(window, |this, window, cx| {
1638 let Ok(kind) = this
1639 .workspace
1640 .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1641 else {
1642 return;
1643 };
1644
1645 this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1646 .detach_and_log_err(cx)
1647 })
1648 }
1649
1650 fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1651 let count = self
1652 .center
1653 .panes()
1654 .into_iter()
1655 .map(|pane| pane.read(cx).items_len())
1656 .sum::<usize>();
1657 if count == 0 {
1658 None
1659 } else {
1660 Some(count.to_string())
1661 }
1662 }
1663
1664 fn persistent_name() -> &'static str {
1665 "TerminalPanel"
1666 }
1667
1668 fn panel_key() -> &'static str {
1669 TERMINAL_PANEL_KEY
1670 }
1671
1672 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1673 if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1674 && TerminalSettings::get_global(cx).button
1675 {
1676 Some(IconName::TerminalAlt)
1677 } else {
1678 None
1679 }
1680 }
1681
1682 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1683 Some("Terminal Panel")
1684 }
1685
1686 fn toggle_action(&self) -> Box<dyn gpui::Action> {
1687 Box::new(ToggleFocus)
1688 }
1689
1690 fn pane(&self) -> Option<Entity<Pane>> {
1691 Some(self.active_pane.clone())
1692 }
1693
1694 fn activation_priority(&self) -> u32 {
1695 1
1696 }
1697}
1698
1699struct TerminalProvider(Entity<TerminalPanel>);
1700
1701impl workspace::TerminalProvider for TerminalProvider {
1702 fn spawn(
1703 &self,
1704 task: SpawnInTerminal,
1705 window: &mut Window,
1706 cx: &mut App,
1707 ) -> Task<Option<Result<ExitStatus>>> {
1708 let terminal_panel = self.0.clone();
1709 window.spawn(cx, async move |cx| {
1710 let terminal = terminal_panel
1711 .update_in(cx, |terminal_panel, window, cx| {
1712 terminal_panel.spawn_task(&task, window, cx)
1713 })
1714 .ok()?
1715 .await;
1716 match terminal {
1717 Ok(terminal) => {
1718 let exit_status = terminal
1719 .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1720 .ok()?
1721 .await?;
1722 Some(Ok(exit_status))
1723 }
1724 Err(e) => Some(Err(e)),
1725 }
1726 })
1727 }
1728}
1729
1730struct InlineAssistTabBarButton {
1731 focus_handle: FocusHandle,
1732}
1733
1734impl Render for InlineAssistTabBarButton {
1735 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1736 let focus_handle = self.focus_handle.clone();
1737 IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1738 .icon_size(IconSize::Small)
1739 .on_click(cx.listener(|_, _, window, cx| {
1740 window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1741 }))
1742 .tooltip(move |window, cx| {
1743 Tooltip::for_action_in(
1744 "Inline Assist",
1745 &InlineAssist::default(),
1746 &focus_handle,
1747 window,
1748 cx,
1749 )
1750 })
1751 }
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756 use std::num::NonZero;
1757
1758 use super::*;
1759 use gpui::{TestAppContext, UpdateGlobal as _};
1760 use pretty_assertions::assert_eq;
1761 use project::FakeFs;
1762 use settings::SettingsStore;
1763
1764 #[gpui::test]
1765 async fn test_spawn_an_empty_task(cx: &mut TestAppContext) {
1766 init_test(cx);
1767
1768 let fs = FakeFs::new(cx.executor());
1769 let project = Project::test(fs, [], cx).await;
1770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1771
1772 let (window_handle, terminal_panel) = workspace
1773 .update(cx, |workspace, window, cx| {
1774 let window_handle = window.window_handle();
1775 let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1776 (window_handle, terminal_panel)
1777 })
1778 .unwrap();
1779
1780 let task = window_handle
1781 .update(cx, |_, window, cx| {
1782 terminal_panel.update(cx, |terminal_panel, cx| {
1783 terminal_panel.spawn_task(&SpawnInTerminal::default(), window, cx)
1784 })
1785 })
1786 .unwrap();
1787
1788 let terminal = task.await.unwrap();
1789 let expected_shell = util::get_system_shell();
1790 terminal
1791 .update(cx, |terminal, _| {
1792 let task_metadata = terminal
1793 .task()
1794 .expect("When spawning a task, should have the task metadata")
1795 .spawned_task
1796 .clone();
1797 assert_eq!(task_metadata.env, HashMap::default());
1798 assert_eq!(task_metadata.cwd, None);
1799 assert_eq!(task_metadata.shell, task::Shell::System);
1800 assert_eq!(
1801 task_metadata.command,
1802 Some(expected_shell.clone()),
1803 "Empty tasks should spawn a -i shell"
1804 );
1805 assert_eq!(task_metadata.args, Vec::<String>::new());
1806 assert_eq!(
1807 task_metadata.command_label, expected_shell,
1808 "We show the shell launch for empty commands"
1809 );
1810 })
1811 .unwrap();
1812 }
1813
1814 #[gpui::test]
1815 async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1816 init_test(cx);
1817
1818 let fs = FakeFs::new(cx.executor());
1819 let project = Project::test(fs, [], cx).await;
1820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1821
1822 let (window_handle, terminal_panel) = workspace
1823 .update(cx, |workspace, window, cx| {
1824 let window_handle = window.window_handle();
1825 let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1826 (window_handle, terminal_panel)
1827 })
1828 .unwrap();
1829
1830 set_max_tabs(cx, Some(3));
1831
1832 for _ in 0..5 {
1833 let task = window_handle
1834 .update(cx, |_, window, cx| {
1835 terminal_panel.update(cx, |panel, cx| {
1836 panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1837 })
1838 })
1839 .unwrap();
1840 task.await.unwrap();
1841 }
1842
1843 cx.run_until_parked();
1844
1845 let item_count =
1846 terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1847
1848 assert_eq!(
1849 item_count, 5,
1850 "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1851 );
1852 }
1853
1854 // A complex Unix command won't be properly parsed by the Windows terminal hence omit the test there.
1855 #[cfg(unix)]
1856 #[gpui::test]
1857 async fn test_spawn_script_like_task(cx: &mut TestAppContext) {
1858 init_test(cx);
1859
1860 let fs = FakeFs::new(cx.executor());
1861 let project = Project::test(fs, [], cx).await;
1862 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1863
1864 let (window_handle, terminal_panel) = workspace
1865 .update(cx, |workspace, window, cx| {
1866 let window_handle = window.window_handle();
1867 let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1868 (window_handle, terminal_panel)
1869 })
1870 .unwrap();
1871
1872 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();
1873
1874 let expected_cwd = PathBuf::from("/some/work");
1875 let task = window_handle
1876 .update(cx, |_, window, cx| {
1877 terminal_panel.update(cx, |terminal_panel, cx| {
1878 terminal_panel.spawn_task(
1879 &SpawnInTerminal {
1880 command: Some(user_command.clone()),
1881 cwd: Some(expected_cwd.clone()),
1882 ..SpawnInTerminal::default()
1883 },
1884 window,
1885 cx,
1886 )
1887 })
1888 })
1889 .unwrap();
1890
1891 let terminal = task.await.unwrap();
1892 let shell = util::get_system_shell();
1893 terminal
1894 .update(cx, |terminal, _| {
1895 let task_metadata = terminal
1896 .task()
1897 .expect("When spawning a task, should have the task metadata")
1898 .spawned_task
1899 .clone();
1900 assert_eq!(task_metadata.env, HashMap::default());
1901 assert_eq!(task_metadata.cwd, Some(expected_cwd));
1902 assert_eq!(task_metadata.shell, task::Shell::System);
1903 assert_eq!(task_metadata.command, Some(shell.clone()));
1904 assert_eq!(
1905 task_metadata.args,
1906 vec!["-i".to_string(), "-c".to_string(), user_command.clone(),],
1907 "Use command should have been moved into the arguments, as we're spawning a new -i shell",
1908 );
1909 assert_eq!(
1910 task_metadata.command_label,
1911 format!("{shell} {interactive}-c '{user_command}'", interactive = if cfg!(windows) {""} else {"-i "}),
1912 "We want to show to the user the entire command spawned");
1913 })
1914 .unwrap();
1915 }
1916
1917 #[gpui::test]
1918 async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1919 init_test(cx);
1920
1921 cx.update(|cx| {
1922 SettingsStore::update_global(cx, |store, cx| {
1923 store.update_user_settings(cx, |settings| {
1924 settings.terminal.get_or_insert_default().project.shell =
1925 Some(settings::Shell::Program("asdf".to_owned()));
1926 });
1927 });
1928 });
1929
1930 let fs = FakeFs::new(cx.executor());
1931 let project = Project::test(fs, [], cx).await;
1932 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
1933
1934 let (window_handle, terminal_panel) = workspace
1935 .update(cx, |workspace, window, cx| {
1936 let window_handle = window.window_handle();
1937 let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
1938 (window_handle, terminal_panel)
1939 })
1940 .unwrap();
1941
1942 window_handle
1943 .update(cx, |_, window, cx| {
1944 terminal_panel.update(cx, |terminal_panel, cx| {
1945 terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1946 })
1947 })
1948 .unwrap()
1949 .await
1950 .unwrap_err();
1951
1952 window_handle
1953 .update(cx, |_, _, cx| {
1954 terminal_panel.update(cx, |terminal_panel, cx| {
1955 assert!(
1956 terminal_panel
1957 .active_pane
1958 .read(cx)
1959 .items()
1960 .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1961 "should spawn `FailedToSpawnTerminal` pane"
1962 );
1963 })
1964 })
1965 .unwrap();
1966 }
1967
1968 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
1969 cx.update_global(|store: &mut SettingsStore, cx| {
1970 store.update_user_settings(cx, |settings| {
1971 settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
1972 });
1973 });
1974 }
1975
1976 pub fn init_test(cx: &mut TestAppContext) {
1977 cx.update(|cx| {
1978 let store = SettingsStore::test(cx);
1979 cx.set_global(store);
1980 theme::init(theme::LoadThemes::JustBase, cx);
1981 client::init_settings(cx);
1982 language::init(cx);
1983 Project::init_settings(cx);
1984 workspace::init_settings(cx);
1985 editor::init(cx);
1986 crate::init(cx);
1987 });
1988 }
1989}