Files
oak-gpui/crates/repl/src/repl_sessions_ui.rs
T
Finn Evers f393138711 Fix keybind hints flickering in certain scenarios (#40927)
Closes #39172

This refactors when we resolve UI keybindings in an effort to reduce
flickering whilst painting these: Previously, we would always resolve
these upon creating the binding. This could lead to cases where the
corresponding context was not yet available and no binding could be
resolved, even if the binding was then available on the next presented
frame. Following that, on the next rerender of whatever requested this
keybinding, the keybind for that context would then be found, we would
render that and then also win a layout shift in that process, as we went
from nothing rendered to something rendered between these frames.

With these changes, this now happens less often, because we only look
for the keybinding once the context can actually be resolved in the
window.

| Before | After | 
| --- | --- |
|
https://github.com/user-attachments/assets/adebf8ac-217d-4c7f-ae5a-bab3aa0b0ee8
|
https://github.com/user-attachments/assets/70a82b4b-488f-4a9f-94d7-b6d0a49aada9
|

Also reduced cloning in the keymap editor in this process, since that
requiered changing due to this anyway.

Release Notes:

- Fixed some cases where keybinds would appear with a slight delay,
causing a flicker in the process
2025-10-22 19:52:38 +00:00

283 lines
9.1 KiB
Rust

use editor::Editor;
use gpui::{
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, actions,
prelude::*,
};
use project::ProjectItem as _;
use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};
use util::ResultExt as _;
use workspace::item::ItemEvent;
use workspace::{Workspace, item::Item};
use crate::jupyter_settings::JupyterSettings;
use crate::repl_store::ReplStore;
actions!(
repl,
[
/// Runs the current cell and advances to the next one.
Run,
/// Runs the current cell without advancing.
RunInPlace,
/// Clears all outputs in the REPL.
ClearOutputs,
/// Opens the REPL sessions panel.
Sessions,
/// Interrupts the currently running kernel.
Interrupt,
/// Shuts down the current kernel.
Shutdown,
/// Restarts the current kernel.
Restart,
/// Refreshes the list of available kernelspecs.
RefreshKernelspecs
]
);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &Sessions, window, cx| {
let existing = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ReplSessionsPage>());
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let repl_sessions_page = ReplSessionsPage::new(window, cx);
workspace.add_item_to_active_pane(
Box::new(repl_sessions_page),
None,
true,
window,
cx,
)
}
});
workspace.register_action(|_workspace, _: &RefreshKernelspecs, _, cx| {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store.refresh_kernelspecs(cx).detach();
});
});
},
)
.detach();
cx.observe_new(
move |editor: &mut Editor, window, cx: &mut Context<Editor>| {
let Some(window) = window else {
return;
};
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
return;
}
cx.defer_in(window, |editor, window, cx| {
let workspace = Workspace::for_window(window, cx);
let project = workspace.map(|workspace| workspace.read(cx).project().clone());
let is_local_project = project
.as_ref()
.map(|project| project.read(cx).is_local())
.unwrap_or(false);
if !is_local_project {
return;
}
let buffer = editor.buffer().read(cx).as_singleton();
let language = buffer
.as_ref()
.and_then(|buffer| buffer.read(cx).language());
let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
let editor_handle = cx.entity().downgrade();
if let Some(language) = language
&& language.name() == "Python".into()
&& let (Some(project_path), Some(project)) = (project_path, project)
{
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
.detach_and_log_err(cx);
});
}
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Run, window, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::run(editor_handle.clone(), true, window, cx).log_err();
}
})
.detach();
editor
.register_action({
move |_: &RunInPlace, window, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::run(editor_handle.clone(), false, window, cx).log_err();
}
})
.detach();
});
},
)
.detach();
}
pub struct ReplSessionsPage {
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
}
impl ReplSessionsPage {
pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()),
cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| {
cx.notify()
}),
];
Self {
focus_handle,
_subscriptions: subscriptions,
}
})
}
}
impl EventEmitter<ItemEvent> for ReplSessionsPage {}
impl Focusable for ReplSessionsPage {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ReplSessionsPage {
type Event = ItemEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"REPL Sessions".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("REPL Session Started")
}
fn show_toolbar(&self) -> bool {
false
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}
impl Render for ReplSessionsPage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let store = ReplStore::global(cx);
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
(
store
.pure_jupyter_kernel_specifications()
.cloned()
.collect::<Vec<_>>(),
store.sessions().cloned().collect::<Vec<_>>(),
)
});
// When there are no kernel specifications, show a link to the Zed docs explaining how to
// install kernels. It can be assumed they don't have a running kernel if we have no
// specifications.
if kernel_specifications.is_empty() {
let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
return ReplSessionsContainer::new("No Jupyter Kernels Available")
.child(Label::new(instructions))
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("install-kernels")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Install Kernels"))
.on_click(move |_, _, cx| {
cx.open_url(
"https://zed.dev/docs/repl#language-specific-instructions",
)
}),
),
);
}
// When there are no sessions, show the command to run code in an editor
if sessions.is_empty() {
let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
v_flex()
.child(Label::new(instructions))
.child(KeyBinding::for_action(&Run, cx)),
);
}
ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
}
}
#[derive(IntoElement)]
struct ReplSessionsContainer {
title: SharedString,
children: Vec<AnyElement>,
}
impl ReplSessionsContainer {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
children: Vec::new(),
}
}
}
impl ParentElement for ReplSessionsContainer {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for ReplSessionsContainer {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
v_flex()
.p_4()
.gap_2()
.size_full()
.child(Label::new(self.title).size(LabelSize::Large))
.children(self.children)
}
}