terminal_ui: Terminal failed to spawn UI (#40246)

Co-authored-by: Piotr piotr@zed.dev
Co-authored-by: Lukas lukas@zed.dev
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Cameron Mcloughlin
2025-10-19 15:26:41 +01:00
committed by GitHub
co-authored by Piotr piotr@zed.dev Lukas lukas@zed.dev Lukas Wirth Gaauwe Rombouts Danilo Leal
parent 197d244378
commit 57387812dc
+182 -39
View File
@@ -22,8 +22,8 @@ use settings::{Settings, TerminalDockPosition};
use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
use terminal::{Terminal, terminal_settings::TerminalSettings};
use ui::{
ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip,
prelude::*,
ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt};
use workspace::{
@@ -35,7 +35,6 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
item::SerializableItem,
move_active_item, move_item, pane,
ui::IconName,
};
use anyhow::{Result, anyhow};
@@ -813,6 +812,7 @@ impl TerminalPanel {
cx: &mut Context<Self>,
) -> Task<Result<WeakEntity<Terminal>>> {
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |terminal_panel, cx| {
if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
anyhow::bail!("terminal not yet supported for collaborative projects");
@@ -824,43 +824,59 @@ impl TerminalPanel {
let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
let terminal = project
.update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
.await?;
let result = workspace.update_in(cx, |workspace, window, cx| {
let terminal_view = Box::new(cx.new(|cx| {
TerminalView::new(
terminal.clone(),
workspace.weak_handle(),
workspace.database_id(),
workspace.project().downgrade(),
window,
cx,
)
}));
.await;
match reveal_strategy {
RevealStrategy::Always => {
workspace.focus_panel::<Self>(window, cx);
}
RevealStrategy::NoFocus => {
workspace.open_panel::<Self>(window, cx);
}
RevealStrategy::Never => {}
match terminal {
Ok(terminal) => {
let result = workspace.update_in(cx, |workspace, window, cx| {
let terminal_view = Box::new(cx.new(|cx| {
TerminalView::new(
terminal.clone(),
workspace.weak_handle(),
workspace.database_id(),
workspace.project().downgrade(),
window,
cx,
)
}));
match reveal_strategy {
RevealStrategy::Always => {
workspace.focus_panel::<Self>(window, cx);
}
RevealStrategy::NoFocus => {
workspace.open_panel::<Self>(window, cx);
}
RevealStrategy::Never => {}
}
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
Ok(terminal.downgrade())
})?;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.pending_terminals_to_add =
terminal_panel.pending_terminals_to_add.saturating_sub(1);
terminal_panel.serialize(cx)
})?;
result
}
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
Ok(terminal.downgrade())
})?;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.pending_terminals_to_add =
terminal_panel.pending_terminals_to_add.saturating_sub(1);
terminal_panel.serialize(cx)
})?;
result
Err(error) => {
pane.update_in(cx, |pane, window, cx| {
let focus = pane.has_focus(window, cx);
let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
error: error.to_string(),
focus_handle: cx.focus_handle(),
});
pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
})?;
Err(error)
}
}
})
}
@@ -1288,6 +1304,82 @@ fn add_paths_to_terminal(
}
}
struct FailedToSpawnTerminal {
error: String,
focus_handle: FocusHandle,
}
impl Focusable for FailedToSpawnTerminal {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for FailedToSpawnTerminal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let popover_menu = PopoverMenu::new("settings-popover")
.trigger(
IconButton::new("icon-button-popover", IconName::ChevronDown)
.icon_size(IconSize::XSmall),
)
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action("Open Settings", zed_actions::OpenSettings.boxed_clone())
.action(
"Edit settings.json",
zed_actions::OpenSettingsFile.boxed_clone(),
)
}))
})
.anchor(Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
.track_focus(&self.focus_handle)
.size_full()
.p_4()
.items_center()
.justify_center()
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.max_w_112()
.items_center()
.justify_center()
.text_center()
.child(Label::new("Failed to spawn terminal"))
.child(
Label::new(self.error.to_string())
.size(LabelSize::Small)
.color(Color::Muted)
.mb_4(),
)
.child(SplitButton::new(
ButtonLike::new("open-settings-ui")
.child(Label::new("Edit Settings").size(LabelSize::Small))
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
}),
popover_menu.into_any_element(),
)),
)
}
}
impl EventEmitter<()> for FailedToSpawnTerminal {}
impl workspace::Item for FailedToSpawnTerminal {
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
SharedString::new_static("Failed to spawn terminal")
}
}
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
@@ -1657,7 +1749,7 @@ impl Render for InlineAssistTabBarButton {
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use gpui::{TestAppContext, UpdateGlobal as _};
use pretty_assertions::assert_eq;
use project::FakeFs;
use settings::SettingsStore;
@@ -1775,6 +1867,57 @@ mod tests {
.unwrap();
}
#[gpui::test]
async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.terminal.get_or_insert_default().project.shell =
Some(settings::Shell::Program("asdf".to_owned()));
});
});
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
let (window_handle, terminal_panel) = workspace
.update(cx, |workspace, window, cx| {
let window_handle = window.window_handle();
let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
(window_handle, terminal_panel)
})
.unwrap();
window_handle
.update(cx, |_, window, cx| {
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
})
})
.unwrap()
.await
.unwrap_err();
window_handle
.update(cx, |_, _, cx| {
terminal_panel.update(cx, |terminal_panel, cx| {
assert!(
terminal_panel
.active_pane
.read(cx)
.items()
.any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
"should spawn `FailedToSpawnTerminal` pane"
);
})
})
.unwrap();
}
pub fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);