Add REPL dropdown menu to toolbar (#14493)

TODO: 


- [x] Actions run from menu not firing
- [x] Menu differentiates idle and busy for running kernel

Menu States:
- [x] No session && no support known

No session && no kernel installed for languages of known support
- (TODO after) Intro to REPL
- [x] Link to docs

No session but can start one
- [x] Start REPL
- (TODO after) More info -> Docs?

Yes Session

- [x] Info: Kernel name, language
  example: chatlab-3.7-adsf87fsa (Python)
  example: condapy-3.7 (Python)
- [x] Change Kernel -> https://zed.dev/docs/repl#change-kernel
- ---
- [x] Run
- [x] Interrupt
- [x] Clear Outputs
- ---
- [x] Shutdown


(Release notes left empty as the change will be documented in the REPL
release!)

Reserved for a follow on PR:

```
- [ ] Status should update when the menu is open (missing `cx.notify`?)
- [ ] Shutdown all kernels action
- [ ] Restart action
- [ ] [Default kernel changed - restart (this kernel) to apply] // todo!(kyle): need some kind of state thing that says if this has happened
```


Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Nate Butler 2024-07-15 14:55:49 -04:00 committed by GitHub
parent 1856320516
commit fa3d29087d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 489 additions and 71 deletions

View file

@ -19,8 +19,10 @@ gpui.workspace = true
search.workspace = true
settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
repl.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -304,7 +304,7 @@ impl Render for QuickActionBar {
.child(
h_flex()
.gap(Spacing::Medium.rems(cx))
.children(search_button)
.children(self.render_repl_menu(cx))
.when(
AssistantSettings::get_global(cx).enabled
&& AssistantSettings::get_global(cx).button,
@ -314,7 +314,11 @@ impl Render for QuickActionBar {
.child(
h_flex()
.gap(Spacing::Medium.rems(cx))
.children(self.render_repl_menu(cx))
.children(search_button),
)
.child(
h_flex()
.gap(Spacing::Medium.rems(cx))
.children(editor_selections_dropdown)
.child(editor_settings_dropdown),
)

View file

@ -1,9 +1,16 @@
use gpui::AnyElement;
use std::time::Duration;
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation};
use repl::{
ExecutionState, JupyterSettings, Kernel, KernelSpecification, RuntimePanel, Session,
SessionSupport,
ExecutionState, JupyterSettings, Kernel, KernelSpecification, RuntimePanel, SessionSupport,
};
use ui::{prelude::*, ButtonLike, IconWithIndicator, IntoElement, Tooltip};
use ui::{
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
Tooltip,
};
use gpui::ElementId;
use util::ResultExt;
use crate::QuickActionBar;
@ -25,6 +32,19 @@ impl QuickActionBar {
return None;
};
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_panel.update(cx, |repl_panel, cx| {
repl_panel.session(editor.downgrade(), cx)
});
@ -32,6 +52,7 @@ impl QuickActionBar {
let session = match session {
SessionSupport::ActiveSession(session) => session.read(cx),
SessionSupport::Inactive(spec) => {
let spec = *spec;
return self.render_repl_launch_menu(spec, cx);
}
SessionSupport::RequiresSetup(language) => {
@ -48,34 +69,270 @@ impl QuickActionBar {
.clone()
.into();
let tooltip = |session: &Session| match &session.kernel {
Kernel::RunningKernel(kernel) => match &kernel.execution_state {
ExecutionState::Idle => {
format!("Run code on {} ({})", kernel_name, kernel_language)
struct ReplMenuState {
tooltip: SharedString,
icon: IconName,
icon_color: Color,
icon_is_animating: bool,
popover_disabled: bool,
indicator: Option<Indicator>,
// TODO: Persist rotation state so the
// icon doesn't reset on every state change
// current_delta: Duration,
}
impl Default for ReplMenuState {
fn default() -> Self {
Self {
tooltip: "Nothing running".into(),
icon: IconName::ReplNeutral,
icon_color: Color::Default,
icon_is_animating: false,
popover_disabled: false,
indicator: None,
// current_delta: Duration::default(),
}
ExecutionState::Busy => format!("Interrupt {} ({})", kernel_name, kernel_language),
}
}
let menu_state = match &session.kernel {
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)),
..Default::default()
},
ExecutionState::Busy => ReplMenuState {
tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
icon_is_animating: true,
popover_disabled: false,
indicator: None,
..Default::default()
},
},
Kernel::StartingKernel(_) => format!("{} is starting", kernel_name),
Kernel::ErroredLaunch(e) => format!("Error with kernel {}: {}", kernel_name, e),
Kernel::ShuttingDown => format!("{} is shutting down", kernel_name),
Kernel::Shutdown => "Nothing running".to_string(),
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)),
..Default::default()
},
Kernel::ErroredLaunch(e) => ReplMenuState {
tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
popover_disabled: false,
indicator: Some(Indicator::dot().color(Color::Error)),
..Default::default()
},
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)),
..Default::default()
},
Kernel::Shutdown => ReplMenuState::default(),
};
let tooltip_text: SharedString = SharedString::from(tooltip(&session).clone());
let id = "repl-menu".to_string();
let button = ButtonLike::new("toggle_repl_icon")
.child(
IconWithIndicator::new(Icon::new(IconName::Play), Some(session.kernel.dot()))
.indicator_border_color(Some(cx.theme().colors().border)),
)
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
let kernel = &session.kernel;
let status_borrow = &kernel.status();
let status = status_borrow.clone();
let panel_clone = repl_panel.clone();
let editor_clone = editor.downgrade();
let dropdown_menu = PopoverMenu::new(element_id("menu"))
.menu(move |cx| {
let kernel_name = kernel_name.clone();
let kernel_language = kernel_language.clone();
let status = status.clone();
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
ContextMenu::build(cx, move |menu, _cx| {
let editor_clone = editor_clone.clone();
let panel_clone = panel_clone.clone();
let kernel_name = kernel_name.clone();
let kernel_language = kernel_language.clone();
let status = status.clone();
menu.when_else(
status.is_connected(),
|running| {
let status = status.clone();
running
.custom_row(move |_cx| {
h_flex()
.child(
Label::new(format!(
"kernel: {} ({})",
kernel_name.clone(),
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()
})
},
|not_running| {
let status = status.clone();
not_running.custom_row(move |_cx| {
h_flex()
.child(
Label::new(format!("{}...", status.clone().to_string()))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
})
},
)
.separator()
// Run
.custom_entry(
move |_cx| {
Label::new(if has_nonempty_selection {
"Run Selection"
} else {
"Run Line"
})
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.run(editor_clone.clone(), cx).log_err();
});
}
},
)
// Interrupt
.custom_entry(
move |_cx| {
Label::new("Interrupt")
.size(LabelSize::Small)
.color(Color::Error)
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.interrupt(editor_clone, cx);
});
}
},
)
// Clear Outputs
.custom_entry(
move |_cx| {
Label::new("Clear Outputs")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.clear_outputs(editor_clone, cx);
});
}
},
)
.separator()
.link(
"Change Kernel",
Box::new(zed_actions::OpenBrowser {
url: format!("{}#change-kernel", ZED_REPL_DOCUMENTATION),
}),
)
// TODO: Add Restart action
// .action("Restart", Box::new(gpui::NoAction))
// Shut down kernel
.custom_entry(
move |_cx| {
Label::new("Shut Down Kernel")
.size(LabelSize::Small)
.color(Color::Error)
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.shutdown(editor_clone, cx);
});
}
},
)
// .separator()
// 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(tooltip_text.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.tooltip(move |cx| Tooltip::text(menu_state.tooltip.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.into_any_element();
Some(button)
Some(
h_flex()
.child(button)
.child(dropdown_menu)
.into_any_element(),
)
}
pub fn render_repl_launch_menu(
@ -87,7 +344,7 @@ impl QuickActionBar {
SharedString::from(format!("Start REPL for {}", kernel_specification.name));
Some(
IconButton::new("toggle_repl_icon", IconName::Play)
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
@ -104,12 +361,12 @@ impl QuickActionBar {
) -> Option<AnyElement> {
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
Some(
IconButton::new("toggle_repl_icon", IconName::Play)
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(ZED_REPL_DOCUMENTATION))
.on_click(|_, cx| cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)))
.into_any_element(),
)
}