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