Support built-in Zed prompts for all platforms (#26201)

This pull request does two things:

1. Adds a setting to force Zed to use the built-in prompts, instead of
the system provided ones. I've personally found the system prompts on
macOS often fail to respond to keyboard input, are slow to render
initially, and don't match Zed's style.
2. Makes the previously Linux-only Zed provided prompts available to
everybody using the above setting.

Release Notes:
- Added support for a built-in prompting system, regardless of platform.
Use the new `use_system_prompts` setting to control whether to use the
system provided prompts or Zed's built-in system. Note that on Linux,
this setting has no effect, as Linux doesn't have a system prompting
mechanism.
This commit is contained in:
Ryan Hawkins 2025-03-18 22:27:09 -06:00 committed by GitHub
parent 382f9f6151
commit 0f5a3afe94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 99 additions and 21 deletions

View file

@ -0,0 +1,24 @@
[package]
name = "ui_prompt"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/ui_prompt.rs"
[features]
default = []
[dependencies]
gpui.workspace = true
markdown.workspace = true
menu.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -0,0 +1,202 @@
use gpui::{
div, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
Refineable, Render, RenderablePromptHandle, SharedString, Styled, TextStyleRefinement, Window,
};
use markdown::{Markdown, MarkdownStyle};
use settings::{Settings, SettingsStore};
use theme::ThemeSettings;
use ui::{
h_flex, v_flex, ActiveTheme, ButtonCommon, ButtonStyle, Clickable, ElevationIndex,
FluentBuilder, LabelSize, StyledExt, TintColor,
};
use workspace::WorkspaceSettings;
pub fn init(cx: &mut App) {
process_settings(cx);
cx.observe_global::<SettingsStore>(process_settings)
.detach();
}
fn process_settings(cx: &mut App) {
let settings = WorkspaceSettings::get_global(cx);
if settings.use_system_prompts && cfg!(not(any(target_os = "linux", target_os = "freebsd"))) {
cx.reset_prompt_builder();
} else {
cx.set_prompt_builder(zed_prompt_renderer);
}
}
/// Use this function in conjunction with [App::set_prompt_builder] to force
/// GPUI to use the internal prompt system.
fn zed_prompt_renderer(
level: PromptLevel,
message: &str,
detail: Option<&str>,
actions: &[&str],
handle: PromptHandle,
window: &mut Window,
cx: &mut App,
) -> RenderablePromptHandle {
let renderer = cx.new({
|cx| ZedPromptRenderer {
_level: level,
message: message.to_string(),
actions: actions.iter().map(ToString::to_string).collect(),
focus: cx.focus_handle(),
active_action_id: 0,
detail: detail.filter(|text| !text.is_empty()).map(|text| {
cx.new(|cx| {
let settings = ThemeSettings::get_global(cx);
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_size: Some(settings.ui_font_size(cx).into()),
color: Some(ui::Color::Muted.color(cx)),
..Default::default()
});
let markdown_style = MarkdownStyle {
base_text_style,
selection_background_color: { cx.theme().players().local().selection },
..Default::default()
};
Markdown::new(SharedString::new(text), markdown_style, None, None, cx)
})
}),
}
});
handle.with_view(renderer, window, cx)
}
pub struct ZedPromptRenderer {
_level: PromptLevel,
message: String,
actions: Vec<String>,
focus: FocusHandle,
active_action_id: usize,
detail: Option<Entity<Markdown>>,
}
impl ZedPromptRenderer {
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(PromptResponse(self.active_action_id));
}
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
if let Some(ix) = self.actions.iter().position(|a| a == "Cancel") {
cx.emit(PromptResponse(ix));
}
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.active_action_id = self.actions.len().saturating_sub(1);
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
self.active_action_id = 0;
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
if self.active_action_id > 0 {
self.active_action_id -= 1;
} else {
self.active_action_id = self.actions.len().saturating_sub(1);
}
cx.notify();
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.active_action_id = (self.active_action_id + 1) % self.actions.len();
cx.notify();
}
}
impl Render for ZedPromptRenderer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let font_family = settings.ui_font.family.clone();
let prompt = v_flex()
.key_context("Prompt")
.cursor_default()
.track_focus(&self.focus)
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.elevation_3(cx)
.w_72()
.overflow_hidden()
.p_4()
.gap_4()
.font_family(font_family)
.child(
div()
.w_full()
.font_weight(FontWeight::BOLD)
.child(self.message.clone())
.text_color(ui::Color::Default.color(cx)),
)
.children(
self.detail
.clone()
.map(|detail| div().w_full().text_xs().child(detail)),
)
.child(h_flex().justify_end().gap_2().children(
self.actions.iter().enumerate().rev().map(|(ix, action)| {
ui::Button::new(ix, action.clone())
.label_size(LabelSize::Large)
.style(ButtonStyle::Filled)
.when(ix == self.active_action_id, |el| {
el.style(ButtonStyle::Tinted(TintColor::Accent))
})
.layer(ElevationIndex::ModalSurface)
.on_click(cx.listener(move |_, _, _window, cx| {
cx.emit(PromptResponse(ix));
}))
}),
));
div().size_full().occlude().child(
div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
}
}
impl EventEmitter<PromptResponse> for ZedPromptRenderer {}
impl Focusable for ZedPromptRenderer {
fn focus_handle(&self, _: &crate::App) -> FocusHandle {
self.focus.clone()
}
}