
- Implement restart kernel functionality - Clean up shutdown process to properly drop messaging and exit status tasks - Refactor kernel state handling for better consistency Closes #16037 Release Notes: - repl: Added restart kernel action - repl: Fixed issue with shutting down kernels that are in a failure state
382 lines
15 KiB
Rust
382 lines
15 KiB
Rust
use std::time::Duration;
|
|
|
|
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
|
|
use repl::{
|
|
ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
|
|
SessionSupport,
|
|
};
|
|
use ui::{
|
|
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
|
|
Tooltip,
|
|
};
|
|
|
|
use gpui::ElementId;
|
|
use util::ResultExt;
|
|
|
|
use crate::QuickActionBar;
|
|
|
|
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
|
|
|
|
struct ReplMenuState {
|
|
tooltip: SharedString,
|
|
icon: IconName,
|
|
icon_color: Color,
|
|
icon_is_animating: bool,
|
|
popover_disabled: bool,
|
|
indicator: Option<Indicator>,
|
|
|
|
status: KernelStatus,
|
|
kernel_name: SharedString,
|
|
kernel_language: SharedString,
|
|
// TODO: Persist rotation state so the
|
|
// icon doesn't reset on every state change
|
|
// current_delta: Duration,
|
|
}
|
|
|
|
impl QuickActionBar {
|
|
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return None;
|
|
}
|
|
|
|
let editor = self.active_editor()?;
|
|
|
|
let has_nonempty_selection = {
|
|
editor.update(cx, |this, cx| {
|
|
this.selections
|
|
.count()
|
|
.ne(&0)
|
|
.then(|| {
|
|
let latest = this.selections.newest_display(cx);
|
|
!latest.is_empty()
|
|
})
|
|
.unwrap_or_default()
|
|
})
|
|
};
|
|
|
|
let session = repl::session(editor.downgrade(), cx);
|
|
let session = match session {
|
|
SessionSupport::ActiveSession(session) => session,
|
|
SessionSupport::Inactive(spec) => {
|
|
let spec = *spec;
|
|
return self.render_repl_launch_menu(spec, cx);
|
|
}
|
|
SessionSupport::RequiresSetup(language) => {
|
|
return self.render_repl_setup(&language, cx);
|
|
}
|
|
SessionSupport::Unsupported => return None,
|
|
};
|
|
|
|
let menu_state = session_state(session.clone(), cx);
|
|
|
|
let id = "repl-menu".to_string();
|
|
|
|
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
|
|
|
|
let editor = editor.downgrade();
|
|
let dropdown_menu = PopoverMenu::new(element_id("menu"))
|
|
.menu(move |cx| {
|
|
let editor = editor.clone();
|
|
let session = session.clone();
|
|
ContextMenu::build(cx, move |menu, cx| {
|
|
let menu_state = session_state(session, cx);
|
|
let status = menu_state.status;
|
|
let editor = editor.clone();
|
|
|
|
menu.map(|menu| {
|
|
if status.is_connected() {
|
|
let status = status.clone();
|
|
menu.custom_row(move |_cx| {
|
|
h_flex()
|
|
.child(
|
|
Label::new(format!(
|
|
"kernel: {} ({})",
|
|
menu_state.kernel_name.clone(),
|
|
menu_state.kernel_language.clone()
|
|
))
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.into_any_element()
|
|
})
|
|
.custom_row(move |_cx| {
|
|
h_flex()
|
|
.child(
|
|
Label::new(status.clone().to_string())
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.into_any_element()
|
|
})
|
|
} else {
|
|
let status = status.clone();
|
|
menu.custom_row(move |_cx| {
|
|
h_flex()
|
|
.child(
|
|
Label::new(format!("{}...", status.clone().to_string()))
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.into_any_element()
|
|
})
|
|
}
|
|
})
|
|
.separator()
|
|
.custom_entry(
|
|
move |_cx| {
|
|
Label::new(if has_nonempty_selection {
|
|
"Run Selection"
|
|
} else {
|
|
"Run Line"
|
|
})
|
|
.into_any_element()
|
|
},
|
|
{
|
|
let editor = editor.clone();
|
|
move |cx| {
|
|
repl::run(editor.clone(), true, cx).log_err();
|
|
}
|
|
},
|
|
)
|
|
.custom_entry(
|
|
move |_cx| {
|
|
Label::new("Interrupt")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Error)
|
|
.into_any_element()
|
|
},
|
|
{
|
|
let editor = editor.clone();
|
|
move |cx| {
|
|
repl::interrupt(editor.clone(), cx);
|
|
}
|
|
},
|
|
)
|
|
.custom_entry(
|
|
move |_cx| {
|
|
Label::new("Clear Outputs")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.into_any_element()
|
|
},
|
|
{
|
|
let editor = editor.clone();
|
|
move |cx| {
|
|
repl::clear_outputs(editor.clone(), cx);
|
|
}
|
|
},
|
|
)
|
|
.separator()
|
|
.link(
|
|
"Change Kernel",
|
|
Box::new(zed_actions::OpenBrowser {
|
|
url: format!("{}#change-kernel", ZED_REPL_DOCUMENTATION),
|
|
}),
|
|
)
|
|
.custom_entry(
|
|
move |_cx| {
|
|
Label::new("Shut Down Kernel")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Error)
|
|
.into_any_element()
|
|
},
|
|
{
|
|
let editor = editor.clone();
|
|
move |cx| {
|
|
repl::shutdown(editor.clone(), cx);
|
|
}
|
|
},
|
|
)
|
|
.custom_entry(
|
|
move |_cx| {
|
|
Label::new("Restart Kernel")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Error)
|
|
.into_any_element()
|
|
},
|
|
{
|
|
let editor = editor.clone();
|
|
move |cx| {
|
|
repl::restart(editor.clone(), cx);
|
|
}
|
|
},
|
|
)
|
|
.separator()
|
|
.action("View Sessions", Box::new(repl::Sessions))
|
|
// TODO: Add shut down all kernels action
|
|
// .action("Shut Down all Kernels", Box::new(gpui::NoAction))
|
|
})
|
|
.into()
|
|
})
|
|
.trigger(
|
|
ButtonLike::new_rounded_right(element_id("dropdown"))
|
|
.child(
|
|
Icon::new(IconName::ChevronDownSmall)
|
|
.size(IconSize::XSmall)
|
|
.color(Color::Muted),
|
|
)
|
|
.tooltip(move |cx| Tooltip::text("REPL Menu", cx))
|
|
.width(rems(1.).into())
|
|
.disabled(menu_state.popover_disabled),
|
|
);
|
|
|
|
let button = ButtonLike::new_rounded_left("toggle_repl_icon")
|
|
.child(if menu_state.icon_is_animating {
|
|
Icon::new(menu_state.icon)
|
|
.color(menu_state.icon_color)
|
|
.with_animation(
|
|
"arrow-circle",
|
|
Animation::new(Duration::from_secs(5)).repeat(),
|
|
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
|
)
|
|
.into_any_element()
|
|
} else {
|
|
IconWithIndicator::new(
|
|
Icon::new(IconName::ReplNeutral).color(menu_state.icon_color),
|
|
menu_state.indicator,
|
|
)
|
|
.indicator_border_color(Some(cx.theme().colors().toolbar_background))
|
|
.into_any_element()
|
|
})
|
|
.size(ButtonSize::Compact)
|
|
.style(ButtonStyle::Subtle)
|
|
.tooltip(move |cx| Tooltip::text(menu_state.tooltip.clone(), cx))
|
|
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
|
.into_any_element();
|
|
|
|
Some(
|
|
h_flex()
|
|
.child(button)
|
|
.child(dropdown_menu)
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
|
|
pub fn render_repl_launch_menu(
|
|
&self,
|
|
kernel_specification: KernelSpecification,
|
|
_cx: &mut ViewContext<Self>,
|
|
) -> Option<AnyElement> {
|
|
let tooltip: SharedString =
|
|
SharedString::from(format!("Start REPL for {}", kernel_specification.name));
|
|
|
|
Some(
|
|
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
|
.size(ButtonSize::Compact)
|
|
.icon_color(Color::Muted)
|
|
.style(ButtonStyle::Subtle)
|
|
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
|
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
|
|
pub fn render_repl_setup(
|
|
&self,
|
|
language: &str,
|
|
_cx: &mut ViewContext<Self>,
|
|
) -> Option<AnyElement> {
|
|
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
|
|
Some(
|
|
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
|
.size(ButtonSize::Compact)
|
|
.icon_color(Color::Muted)
|
|
.style(ButtonStyle::Subtle)
|
|
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
|
.on_click(|_, cx| cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)))
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
|
|
let session = session.read(cx);
|
|
|
|
let kernel_name: SharedString = session.kernel_specification.name.clone().into();
|
|
let kernel_language: SharedString = session
|
|
.kernel_specification
|
|
.kernelspec
|
|
.language
|
|
.clone()
|
|
.into();
|
|
|
|
let fill_fields = || {
|
|
ReplMenuState {
|
|
tooltip: "Nothing running".into(),
|
|
icon: IconName::ReplNeutral,
|
|
icon_color: Color::Default,
|
|
icon_is_animating: false,
|
|
popover_disabled: false,
|
|
indicator: None,
|
|
kernel_name: kernel_name.clone(),
|
|
kernel_language: kernel_language.clone(),
|
|
// todo!(): Technically not shutdown, but indeterminate
|
|
status: KernelStatus::Shutdown,
|
|
// current_delta: Duration::default(),
|
|
}
|
|
};
|
|
|
|
let menu_state = match &session.kernel {
|
|
Kernel::Restarting => ReplMenuState {
|
|
tooltip: format!("Restarting {}", kernel_name).into(),
|
|
icon_is_animating: true,
|
|
popover_disabled: true,
|
|
icon_color: Color::Muted,
|
|
indicator: Some(Indicator::dot().color(Color::Muted)),
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
Kernel::RunningKernel(kernel) => match &kernel.execution_state {
|
|
ExecutionState::Idle => ReplMenuState {
|
|
tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
|
|
indicator: Some(Indicator::dot().color(Color::Success)),
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
ExecutionState::Busy => ReplMenuState {
|
|
tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
|
|
icon_is_animating: true,
|
|
popover_disabled: false,
|
|
indicator: None,
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
},
|
|
Kernel::StartingKernel(_) => ReplMenuState {
|
|
tooltip: format!("{} is starting", kernel_name).into(),
|
|
icon_is_animating: true,
|
|
popover_disabled: true,
|
|
icon_color: Color::Muted,
|
|
indicator: Some(Indicator::dot().color(Color::Muted)),
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
Kernel::ErroredLaunch(e) => ReplMenuState {
|
|
tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
|
|
popover_disabled: false,
|
|
indicator: Some(Indicator::dot().color(Color::Error)),
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
Kernel::ShuttingDown => ReplMenuState {
|
|
tooltip: format!("{} is shutting down", kernel_name).into(),
|
|
popover_disabled: true,
|
|
icon_color: Color::Muted,
|
|
indicator: Some(Indicator::dot().color(Color::Muted)),
|
|
status: session.kernel.status(),
|
|
..fill_fields()
|
|
},
|
|
Kernel::Shutdown => ReplMenuState {
|
|
tooltip: "Nothing running".into(),
|
|
icon: IconName::ReplNeutral,
|
|
icon_color: Color::Default,
|
|
icon_is_animating: false,
|
|
popover_disabled: false,
|
|
indicator: None,
|
|
status: KernelStatus::Shutdown,
|
|
..fill_fields()
|
|
},
|
|
};
|
|
|
|
menu_state
|
|
}
|