Compare commits

...

7 Commits

Author SHA1 Message Date
Kyle Kelley
80c2f9dc6c 💄 2024-12-10 14:59:52 -08:00
Kyle Kelley
baf90b31c9 wip
now the repl menu is keeping state around, but not watching that the editor changed (?)

There's a disconnect between selected kernel and the kernel that's running in the editor.
2024-12-10 14:59:52 -08:00
Kyle Kelley
e8e66428f0 downgrade editor 2024-12-10 14:59:52 -08:00
Kyle Kelley
946ef41822 wip 2024-12-10 14:59:52 -08:00
Kyle Kelley
0135d1ae27 Refer to the simplified session state struct as such 2024-12-10 14:59:52 -08:00
Kyle Kelley
feee7b3f27 turn repl menu into a view 2024-12-10 14:59:52 -08:00
Kyle Kelley
99840ce814 carving out a repl menu with grouping that sticks 2024-12-10 14:59:52 -08:00
5 changed files with 311 additions and 185 deletions

1
Cargo.lock generated
View File

@@ -16012,7 +16012,6 @@ dependencies = [
"outline_panel",
"parking_lot",
"paths",
"picker",
"profiling",
"project",
"project_panel",

View File

@@ -2,27 +2,29 @@ use crate::kernels::KernelSpecification;
use crate::repl_store::ReplStore;
use crate::KERNEL_DOCS_URL;
use editor::Editor;
use gpui::DismissEvent;
use gpui::FontWeight;
use gpui::WeakView;
use picker::Picker;
use picker::PickerDelegate;
use project::WorktreeId;
use ui::ButtonLike;
use ui::Tooltip;
use std::sync::Arc;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle};
type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
pub type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
#[derive(IntoElement)]
pub struct KernelSelector<T: PopoverTrigger> {
pub struct KernelSelector {
handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
on_select: OnSelect,
trigger: T,
editor: WeakView<Editor>,
info_text: Option<SharedString>,
worktree_id: WorktreeId,
}
@@ -32,6 +34,7 @@ pub struct KernelPickerDelegate {
filtered_kernels: Vec<KernelSpecification>,
selected_kernelspec: Option<KernelSpecification>,
on_select: OnSelect,
group: Group,
}
// Helper function to truncate long paths
@@ -44,12 +47,15 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
}
}
impl<T: PopoverTrigger> KernelSelector<T> {
pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
impl KernelSelector {
pub fn new(
worktree_id: WorktreeId,
editor: WeakView<Editor>,
_cx: &mut ViewContext<Self>,
) -> Self {
KernelSelector {
on_select,
editor,
handle: None,
trigger,
info_text: None,
worktree_id,
}
@@ -66,6 +72,14 @@ impl<T: PopoverTrigger> KernelSelector<T> {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Group {
All,
Jupyter,
Python,
Remote,
}
impl PickerDelegate for KernelPickerDelegate {
type ListItem = ListItem;
@@ -204,6 +218,75 @@ impl PickerDelegate for KernelPickerDelegate {
)
}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
let mode = Group::All;
Some(
h_flex()
.child(
div()
.id("all")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::All, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("All"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::All, cx);
})),
)
.child(
div()
.id("jupyter")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Jupyter, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Jupyter"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Jupyter, cx);
})),
)
.child(
div()
.id("python")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Python, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Python"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Python, cx);
})),
)
.child(
div()
.id("remote")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Remote, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Remote"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Remote, cx);
})),
)
.into_any_element(),
)
}
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
Some(
h_flex()
@@ -225,8 +308,29 @@ impl PickerDelegate for KernelPickerDelegate {
}
}
impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
impl KernelPickerDelegate {
fn new(
on_select: OnSelect,
kernels: Vec<KernelSpecification>,
selected_kernelspec: Option<KernelSpecification>,
) -> Self {
Self {
on_select,
all_kernels: kernels.clone(),
filtered_kernels: kernels,
group: Group::All,
selected_kernelspec,
}
}
fn set_group(&mut self, group: Group, cx: &mut ViewContext<Picker<Self>>) {
self.group = group;
cx.notify();
}
}
impl Render for KernelSelector {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let store = ReplStore::global(cx).read(cx);
let all_kernels: Vec<KernelSpecification> = store
@@ -235,13 +339,18 @@ impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
.collect();
let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
let current_kernel_name = selected_kernelspec.as_ref().map(|spec| spec.name()).clone();
let delegate = KernelPickerDelegate {
on_select: self.on_select,
all_kernels: all_kernels.clone(),
filtered_kernels: all_kernels,
selected_kernelspec,
};
let editor = self.editor.clone();
let on_select: OnSelect = Box::new(move |kernelspec, cx| {
crate::assign_kernelspec(kernelspec, editor.clone(), cx).ok();
});
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
PopoverMenuHandle::default();
let delegate =
KernelPickerDelegate::new(on_select, all_kernels, selected_kernelspec.clone());
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
@@ -252,8 +361,42 @@ impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
PopoverMenu::new("kernel-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.trigger(
ButtonLike::new("kernel-selector")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(if let Some(name) = current_kernel_name {
name
} else {
SharedString::from("Select Kernel")
})
.size(LabelSize::Small)
.color(if selected_kernelspec.is_some() {
Color::Default
} else {
Color::Placeholder
})
.into_any_element(),
),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
.with_handle(menu_handle)
}
}

View File

@@ -80,7 +80,6 @@ outline.workspace = true
outline_panel.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
profiling.workspace = true
project.workspace = true
project_panel.workspace = true

View File

@@ -13,6 +13,8 @@ use gpui::{
Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
};
use repl::worktree_id_for_editor;
use repl_menu::ReplMenu;
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{
@@ -33,6 +35,7 @@ pub struct QuickActionBar {
toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
workspace: WeakView<Workspace>,
repl_menu: Option<View<ReplMenu>>,
}
impl QuickActionBar {
@@ -49,6 +52,7 @@ impl QuickActionBar {
toggle_selections_handle: Default::default(),
toggle_settings_handle: Default::default(),
workspace: workspace.weak_handle(),
repl_menu: None,
};
this.apply_settings(cx);
cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
@@ -350,7 +354,7 @@ impl Render for QuickActionBar {
h_flex()
.id("quick action bar")
.gap(DynamicSpacing::Base06.rems(cx))
.children(self.render_repl_menu(cx))
.children(self.repl_menu.clone())
.children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
.children(search_button)
.when(
@@ -425,7 +429,15 @@ impl ToolbarItemView for QuickActionBar {
if let Some(active_item) = active_pane_item {
self._inlay_hints_enabled_subscription.take();
if let Some(editor) = active_item.downcast::<Editor>() {
let editor = active_item.downcast::<Editor>();
let work_tree_id = active_item
.downcast::<Editor>()
.and_then(|editor| worktree_id_for_editor(editor.downgrade(), cx));
if let (Some(editor), Some(work_tree_id)) = (editor, work_tree_id) {
self.repl_menu =
Some(cx.new_view(|cx| ReplMenu::new(work_tree_id, editor.downgrade(), cx)));
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
self._inlay_hints_enabled_subscription =
@@ -441,6 +453,8 @@ impl ToolbarItemView for QuickActionBar {
cx.notify()
}
}));
} else {
self.repl_menu = None
}
}
self.get_toolbar_item_location()

View File

@@ -1,24 +1,22 @@
use std::time::Duration;
use gpui::ElementId;
use editor::Editor;
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
use picker::Picker;
use gpui::{ElementId, WeakView};
use project::WorktreeId;
use repl::{
components::{KernelPickerDelegate, KernelSelector},
worktree_id_for_editor, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
components::KernelSelector, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
KernelStatus, Session, SessionSupport,
};
use ui::{
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
PopoverMenuHandle, Tooltip,
Tooltip,
};
use util::ResultExt;
use super::QuickActionBar;
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
struct ReplMenuState {
struct ReplSessionState {
tooltip: SharedString,
icon: IconName,
icon_color: Color,
@@ -31,47 +29,73 @@ struct ReplMenuState {
kernel_language: SharedString,
}
impl QuickActionBar {
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
pub struct ReplMenu {
active_editor: WeakView<Editor>,
kernel_menu: View<KernelSelector>,
}
impl ReplMenu {
pub fn new(
work_tree_id: WorktreeId,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
kernel_menu: cx.new_view(|cx| KernelSelector::new(work_tree_id, editor.clone(), cx)),
active_editor: editor.clone(),
}
}
}
impl Render for ReplMenu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if !JupyterSettings::enabled(cx) {
return None;
return div().into_any_element();
}
let editor = self.active_editor()?;
let editor = self.active_editor.clone();
let is_local_project = editor
.read(cx)
.workspace()
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
.upgrade()
.as_ref()
.map(|editor| {
editor
.read(cx)
.workspace()
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
.unwrap_or(false)
})
.unwrap_or(false);
if !is_local_project {
return None;
return div().into_any_element();
}
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()
})
editor
.update(cx, |this, cx| {
this.selections
.count()
.ne(&0)
.then(|| {
let latest = this.selections.newest_display(cx);
!latest.is_empty()
})
.unwrap_or_default()
})
.unwrap_or(false)
};
let session = repl::session(editor.downgrade(), cx);
let session = repl::session(editor.clone(), cx);
let session = match session {
SessionSupport::ActiveSession(session) => session,
SessionSupport::Inactive(spec) => {
return self.render_repl_launch_menu(spec, cx);
return self.render_repl_launch_menu(spec, cx).into_any_element();
}
SessionSupport::RequiresSetup(language) => {
return self.render_repl_setup(&language.0, cx);
return self.render_repl_setup(&language.0, cx).into_any_element();
}
SessionSupport::Unsupported => return None,
SessionSupport::Unsupported => return div().into_any_element(),
};
let menu_state = session_state(session.clone(), cx);
@@ -80,7 +104,7 @@ impl QuickActionBar {
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
let editor = editor.downgrade();
let editor = editor.clone();
let dropdown_menu = PopoverMenu::new(element_id("menu"))
.menu(move |cx| {
let editor = editor.clone();
@@ -245,138 +269,85 @@ impl QuickActionBar {
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.into_any_element();
Some(
h_flex()
.child(self.render_kernel_selector(cx))
.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(
h_flex()
.child(self.render_kernel_selector(cx))
.child(
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_kernel_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let editor = if let Some(editor) = self.active_editor() {
editor
} else {
return div().into_any_element();
};
let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else {
return div().into_any_element();
};
let session = repl::session(editor.downgrade(), cx);
let current_kernelspec = match session {
SessionSupport::ActiveSession(view) => Some(view.read(cx).kernel_specification.clone()),
SessionSupport::Inactive(kernel_specification) => Some(kernel_specification),
SessionSupport::RequiresSetup(_language_name) => None,
SessionSupport::Unsupported => None,
};
let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name());
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
PopoverMenuHandle::default();
KernelSelector::new(
{
Box::new(move |kernelspec, cx| {
repl::assign_kernelspec(kernelspec, editor.downgrade(), cx).ok();
})
},
worktree_id,
ButtonLike::new("kernel-selector")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(if let Some(name) = current_kernel_name {
name
} else {
SharedString::from("Select Kernel")
})
.size(LabelSize::Small)
.color(if current_kernelspec.is_some() {
Color::Default
} else {
Color::Placeholder
})
.into_any_element(),
),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
)
.with_handle(menu_handle.clone())
.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(
h_flex()
.child(self.render_kernel_selector(cx))
.child(
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(),
)
h_flex()
.child(self.kernel_menu.clone())
.child(button)
.child(dropdown_menu)
.into_any_element()
}
}
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
impl ReplMenu {
pub fn render_repl_launch_menu(
&self,
kernel_specification: KernelSpecification,
_cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let tooltip: SharedString =
SharedString::from(format!("Start REPL for {}", kernel_specification.name()));
h_flex().child(self.kernel_menu.clone()).child(
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 {}))),
)
}
pub fn render_repl_setup(&self, language: &str, _cx: &mut ViewContext<Self>) -> AnyElement {
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
h_flex()
.child(self.kernel_menu.clone())
.child(
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()
}
}
// struct KernelMenu {
// menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>,
// editor: WeakView<Editor>,
// }
// impl KernelMenu {
// pub fn new(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Self {
// Self {
// editor,
// menu_handle,
// }
// }
// }
// impl Render for KernelMenu {
// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// let Some(worktree_id) = worktree_id_for_editor(self.editor.clone(), cx) else {
// return div().into_any_element();
// };
// KernelSelector::new(self.editor.clone(), worktree_id)
// .with_handle(self.menu_handle.clone())
// .into_any_element()
// }
// }
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState {
let session = session.read(cx);
let kernel_name = session.kernel_specification.name();
let kernel_language: SharedString = session.kernel_specification.language();
let fill_fields = || {
ReplMenuState {
ReplSessionState {
tooltip: "Nothing running".into(),
icon: IconName::ReplNeutral,
icon_color: Color::Default,
@@ -392,7 +363,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
};
match &session.kernel {
Kernel::Restarting => ReplMenuState {
Kernel::Restarting => ReplSessionState {
tooltip: format!("Restarting {}", kernel_name).into(),
icon_is_animating: true,
popover_disabled: true,
@@ -402,13 +373,13 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
..fill_fields()
},
Kernel::RunningKernel(kernel) => match &kernel.execution_state() {
ExecutionState::Idle => ReplMenuState {
ExecutionState::Idle => ReplSessionState {
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 {
ExecutionState::Busy => ReplSessionState {
tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
icon_is_animating: true,
popover_disabled: false,
@@ -417,7 +388,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
..fill_fields()
},
},
Kernel::StartingKernel(_) => ReplMenuState {
Kernel::StartingKernel(_) => ReplSessionState {
tooltip: format!("{} is starting", kernel_name).into(),
icon_is_animating: true,
popover_disabled: true,
@@ -426,14 +397,14 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
status: session.kernel.status(),
..fill_fields()
},
Kernel::ErroredLaunch(e) => ReplMenuState {
Kernel::ErroredLaunch(e) => ReplSessionState {
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 {
Kernel::ShuttingDown => ReplSessionState {
tooltip: format!("{} is shutting down", kernel_name).into(),
popover_disabled: true,
icon_color: Color::Muted,
@@ -441,7 +412,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
status: session.kernel.status(),
..fill_fields()
},
Kernel::Shutdown => ReplMenuState {
Kernel::Shutdown => ReplSessionState {
tooltip: "Nothing running".into(),
icon: IconName::ReplNeutral,
icon_color: Color::Default,