Detailed changes
@@ -7120,6 +7120,7 @@ name = "gpui_macros"
version = "0.1.0"
dependencies = [
"gpui",
+ "heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
@@ -8162,6 +8163,7 @@ dependencies = [
"anyhow",
"command_palette_hooks",
"editor",
+ "fuzzy",
"gpui",
"language",
"project",
@@ -16827,6 +16829,7 @@ dependencies = [
"component",
"documented",
"gpui",
+ "gpui_macros",
"icons",
"itertools 0.14.0",
"menu",
@@ -676,6 +676,7 @@
{
"bindings": {
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
+ // Only available in debug builds: opens an element inspector for development.
"ctrl-alt-i": "dev::ToggleInspector"
}
},
@@ -736,6 +736,7 @@
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
"cmd-shift-c": "collab_panel::ToggleFocus",
+ // Only available in debug builds: opens an element inspector for development.
"cmd-alt-i": "dev::ToggleInspector"
}
},
@@ -1,5 +1,6 @@
use std::ops::Range;
use std::path::Path;
+use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
@@ -915,8 +916,8 @@ impl AgentPanel {
open_rules_library(
self.language_registry.clone(),
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
- Arc::new(|| {
- Box::new(SlashCommandCompletionProvider::new(
+ Rc::new(|| {
+ Rc::new(SlashCommandCompletionProvider::new(
Arc::new(SlashCommandWorkingSet::default()),
None,
None,
@@ -1289,7 +1289,7 @@ mod tests {
.map(Entity::downgrade)
});
window.focus(&editor.focus_handle(cx));
- editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.downgrade(),
context_store.downgrade(),
None,
@@ -28,6 +28,7 @@ use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;
+use std::rc::Rc;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
@@ -890,7 +891,7 @@ impl PromptEditor<BufferCodegen> {
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
- editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
@@ -1061,7 +1062,7 @@ impl PromptEditor<TerminalCodegen> {
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
- editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
+use std::rc::Rc;
use std::sync::Arc;
use crate::agent_model_selector::{AgentModelSelector, ModelType};
@@ -121,7 +122,7 @@ pub(crate) fn create_editor(
let editor_entity = editor.downgrade();
editor.update(cx, |editor, _| {
- editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace,
context_store,
Some(thread_store),
@@ -51,6 +51,7 @@ use std::{
cmp,
ops::Range,
path::{Path, PathBuf},
+ rc::Rc,
sync::Arc,
time::Duration,
};
@@ -234,7 +235,7 @@ impl ContextEditor {
editor.set_show_breakpoints(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Box::new(completion_provider)));
+ editor.set_completion_provider(Some(Rc::new(completion_provider)));
editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
editor.set_collaboration_hub(Box::new(project.clone()));
@@ -112,7 +112,7 @@ impl MessageEditor {
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Box::new(MessageEditorCompletionProvider(this))));
+ editor.set_completion_provider(Some(Rc::new(MessageEditorCompletionProvider(this))));
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)
.auto_replace_emoji_shortcode
@@ -72,7 +72,7 @@ impl Console {
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
+ editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
editor
});
@@ -1,9 +1,9 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior,
- ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, UniformListScrollHandle,
- div, px, uniform_list,
+ AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
+ Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
};
+use gpui::{AsyncWindowContext, WeakEntity};
use language::Buffer;
use language::CodeLabel;
use markdown::{Markdown, MarkdownElement};
@@ -50,11 +50,12 @@ impl CodeContextMenu {
pub fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
if self.visible() {
match self {
- CodeContextMenu::Completions(menu) => menu.select_first(provider, cx),
+ CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
}
true
@@ -66,11 +67,12 @@ impl CodeContextMenu {
pub fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
if self.visible() {
match self {
- CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx),
+ CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
}
true
@@ -82,11 +84,12 @@ impl CodeContextMenu {
pub fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
if self.visible() {
match self {
- CodeContextMenu::Completions(menu) => menu.select_next(provider, cx),
+ CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
}
true
@@ -98,11 +101,12 @@ impl CodeContextMenu {
pub fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
if self.visible() {
match self {
- CodeContextMenu::Completions(menu) => menu.select_last(provider, cx),
+ CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
}
true
@@ -290,6 +294,7 @@ impl CompletionsMenu {
fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) {
let index = if self.scroll_handle.y_flipped() {
@@ -297,40 +302,56 @@ impl CompletionsMenu {
} else {
0
};
- self.update_selection_index(index, provider, cx);
+ self.update_selection_index(index, provider, window, cx);
}
- fn select_last(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {
+ fn select_last(
+ &mut self,
+ provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
let index = if self.scroll_handle.y_flipped() {
0
} else {
self.entries.borrow().len() - 1
};
- self.update_selection_index(index, provider, cx);
+ self.update_selection_index(index, provider, window, cx);
}
- fn select_prev(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {
+ fn select_prev(
+ &mut self,
+ provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
let index = if self.scroll_handle.y_flipped() {
self.next_match_index()
} else {
self.prev_match_index()
};
- self.update_selection_index(index, provider, cx);
+ self.update_selection_index(index, provider, window, cx);
}
- fn select_next(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {
+ fn select_next(
+ &mut self,
+ provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
let index = if self.scroll_handle.y_flipped() {
self.prev_match_index()
} else {
self.next_match_index()
};
- self.update_selection_index(index, provider, cx);
+ self.update_selection_index(index, provider, window, cx);
}
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) {
if self.selected_item != match_index {
@@ -338,6 +359,9 @@ impl CompletionsMenu {
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
+ if let Some(provider) = provider {
+ self.handle_selection_changed(provider, window, cx);
+ }
cx.notify();
}
}
@@ -358,6 +382,21 @@ impl CompletionsMenu {
}
}
+ fn handle_selection_changed(
+ &self,
+ provider: &dyn CompletionProvider,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let entries = self.entries.borrow();
+ let entry = if self.selected_item < entries.len() {
+ Some(&entries[self.selected_item])
+ } else {
+ None
+ };
+ provider.selection_changed(entry, window, cx);
+ }
+
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
@@ -753,7 +792,13 @@ impl CompletionsMenu {
});
}
- pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
+ pub async fn filter(
+ &mut self,
+ query: Option<&str>,
+ provider: Option<Rc<dyn CompletionProvider>>,
+ editor: WeakEntity<Editor>,
+ cx: &mut AsyncWindowContext,
+ ) {
let mut matches = if let Some(query) = query {
fuzzy::match_strings(
&self.match_candidates,
@@ -761,7 +806,7 @@ impl CompletionsMenu {
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
- executor,
+ cx.background_executor().clone(),
)
.await
} else {
@@ -822,6 +867,28 @@ impl CompletionsMenu {
self.selected_item = 0;
// This keeps the display consistent when y_flipped.
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
+
+ if let Some(provider) = provider {
+ cx.update(|window, cx| {
+ // Since this is async, it's possible the menu has been closed and possibly even
+ // another opened. `provider.selection_changed` should not be called in this case.
+ let this_menu_still_active = editor
+ .read_with(cx, |editor, _cx| {
+ if let Some(CodeContextMenu::Completions(completions_menu)) =
+ editor.context_menu.borrow().as_ref()
+ {
+ completions_menu.id == self.id
+ } else {
+ false
+ }
+ })
+ .unwrap_or(false);
+ if this_menu_still_active {
+ self.handle_selection_changed(&*provider, window, cx);
+ }
+ })
+ .ok();
+ }
}
}
@@ -77,7 +77,7 @@ use futures::{
FutureExt,
future::{self, Shared, join},
};
-use fuzzy::StringMatchCandidate;
+use fuzzy::{StringMatch, StringMatchCandidate};
use ::git::blame::BlameEntry;
use ::git::{Restore, blame::ParsedCommitMessage};
@@ -912,7 +912,7 @@ pub struct Editor {
// TODO: make this a access method
pub project: Option<Entity<Project>>,
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
- completion_provider: Option<Box<dyn CompletionProvider>>,
+ completion_provider: Option<Rc<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Entity<BlinkManager>,
show_cursor_names: bool,
@@ -1755,7 +1755,7 @@ impl Editor {
soft_wrap_mode_override,
diagnostics_max_severity,
hard_wrap: None,
- completion_provider: project.clone().map(|project| Box::new(project) as _),
+ completion_provider: project.clone().map(|project| Rc::new(project) as _),
semantics_provider: project.clone().map(|project| Rc::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project,
@@ -2374,7 +2374,7 @@ impl Editor {
self.custom_context_menu = Some(Box::new(f))
}
- pub fn set_completion_provider(&mut self, provider: Option<Box<dyn CompletionProvider>>) {
+ pub fn set_completion_provider(&mut self, provider: Option<Rc<dyn CompletionProvider>>) {
self.completion_provider = provider;
}
@@ -2684,9 +2684,10 @@ impl Editor {
drop(context_menu);
let query = Self::completion_query(buffer, cursor_position);
- cx.spawn(async move |this, cx| {
+ let completion_provider = self.completion_provider.clone();
+ cx.spawn_in(window, async move |this, cx| {
completion_menu
- .filter(query.as_deref(), cx.background_executor().clone())
+ .filter(query.as_deref(), completion_provider, this.clone(), cx)
.await;
this.update(cx, |this, cx| {
@@ -4960,15 +4961,16 @@ impl Editor {
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
..buffer_snapshot.point_to_offset(max_word_search);
- let provider = self
- .completion_provider
- .as_ref()
- .filter(|_| !ignore_completion_provider);
+ let provider = if ignore_completion_provider {
+ None
+ } else {
+ self.completion_provider.clone()
+ };
let skip_digits = query
.as_ref()
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
- let (mut words, provided_completions) = match provider {
+ let (mut words, provided_completions) = match &provider {
Some(provider) => {
let completions = provider.completions(
position.excerpt_id,
@@ -5071,7 +5073,9 @@ impl Editor {
} else {
None
},
- cx.background_executor().clone(),
+ provider,
+ editor.clone(),
+ cx,
)
.await;
@@ -8651,6 +8655,11 @@ impl Editor {
let context_menu = self.context_menu.borrow_mut().take();
self.stale_inline_completion_in_menu.take();
self.update_visible_inline_completion(window, cx);
+ if let Some(CodeContextMenu::Completions(_)) = &context_menu {
+ if let Some(completion_provider) = &self.completion_provider {
+ completion_provider.selection_changed(None, window, cx);
+ }
+ }
context_menu
}
@@ -11353,7 +11362,7 @@ impl Editor {
.context_menu
.borrow_mut()
.as_mut()
- .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
+ .map(|menu| menu.select_first(self.completion_provider.as_deref(), window, cx))
.unwrap_or(false)
{
return;
@@ -11477,7 +11486,7 @@ impl Editor {
.context_menu
.borrow_mut()
.as_mut()
- .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
+ .map(|menu| menu.select_last(self.completion_provider.as_deref(), window, cx))
.unwrap_or(false)
{
return;
@@ -11532,44 +11541,44 @@ impl Editor {
pub fn context_menu_first(
&mut self,
_: &ContextMenuFirst,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
- context_menu.select_first(self.completion_provider.as_deref(), cx);
+ context_menu.select_first(self.completion_provider.as_deref(), window, cx);
}
}
pub fn context_menu_prev(
&mut self,
_: &ContextMenuPrevious,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
- context_menu.select_prev(self.completion_provider.as_deref(), cx);
+ context_menu.select_prev(self.completion_provider.as_deref(), window, cx);
}
}
pub fn context_menu_next(
&mut self,
_: &ContextMenuNext,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
- context_menu.select_next(self.completion_provider.as_deref(), cx);
+ context_menu.select_next(self.completion_provider.as_deref(), window, cx);
}
}
pub fn context_menu_last(
&mut self,
_: &ContextMenuLast,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
- context_menu.select_last(self.completion_provider.as_deref(), cx);
+ context_menu.select_last(self.completion_provider.as_deref(), window, cx);
}
}
@@ -19615,6 +19624,8 @@ pub trait CompletionProvider {
cx: &mut Context<Editor>,
) -> bool;
+ fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {}
+
fn sort_completions(&self) -> bool {
true
}
@@ -22,7 +22,7 @@ test-support = [
"wayland",
"x11",
]
-inspector = []
+inspector = ["gpui_macros/inspector"]
leak-detection = ["backtrace"]
runtime_shaders = []
macos-blade = [
@@ -8,7 +8,7 @@ use std::{
#[derive(Debug)]
pub(crate) struct BoundsTree<U>
where
- U: Default + Clone + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
root: Option<usize>,
nodes: Vec<Node<U>>,
@@ -17,7 +17,14 @@ where
impl<U> BoundsTree<U>
where
- U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
+ U: Clone
+ + Debug
+ + PartialEq
+ + PartialOrd
+ + Add<U, Output = U>
+ + Sub<Output = U>
+ + Half
+ + Default,
{
pub fn clear(&mut self) {
self.root = None;
@@ -174,7 +181,7 @@ where
impl<U> Default for BoundsTree<U>
where
- U: Default + Clone + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
fn default() -> Self {
BoundsTree {
@@ -188,7 +195,7 @@ where
#[derive(Debug, Clone)]
enum Node<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
Leaf {
bounds: Bounds<U>,
@@ -204,7 +211,7 @@ where
impl<U> Node<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
fn bounds(&self) -> &Bounds<U> {
match self {
@@ -76,9 +76,9 @@ pub trait Along {
JsonSchema,
Hash,
)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
-pub struct Point<T: Default + Clone + Debug> {
+pub struct Point<T: Clone + Debug + Default + PartialEq> {
/// The x coordinate of the point.
pub x: T,
/// The y coordinate of the point.
@@ -104,11 +104,11 @@ pub struct Point<T: Default + Clone + Debug> {
/// assert_eq!(p.x, 10);
/// assert_eq!(p.y, 20);
/// ```
-pub const fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> {
+pub const fn point<T: Clone + Debug + Default + PartialEq>(x: T, y: T) -> Point<T> {
Point { x, y }
}
-impl<T: Clone + Debug + Default> Point<T> {
+impl<T: Clone + Debug + Default + PartialEq> Point<T> {
/// Creates a new `Point` with the specified `x` and `y` coordinates.
///
/// # Arguments
@@ -145,7 +145,7 @@ impl<T: Clone + Debug + Default> Point<T> {
/// let p_float = p.map(|coord| coord as f32);
/// assert_eq!(p_float, Point { x: 3.0, y: 4.0 });
/// ```
- pub fn map<U: Clone + Default + Debug>(&self, f: impl Fn(T) -> U) -> Point<U> {
+ pub fn map<U: Clone + Debug + Default + PartialEq>(&self, f: impl Fn(T) -> U) -> Point<U> {
Point {
x: f(self.x.clone()),
y: f(self.y.clone()),
@@ -153,7 +153,7 @@ impl<T: Clone + Debug + Default> Point<T> {
}
}
-impl<T: Clone + Debug + Default> Along for Point<T> {
+impl<T: Clone + Debug + Default + PartialEq> Along for Point<T> {
type Unit = T;
fn along(&self, axis: Axis) -> T {
@@ -177,7 +177,7 @@ impl<T: Clone + Debug + Default> Along for Point<T> {
}
}
-impl<T: Clone + Debug + Default + Negate> Negate for Point<T> {
+impl<T: Clone + Debug + Default + PartialEq + Negate> Negate for Point<T> {
fn negate(self) -> Self {
self.map(Negate::negate)
}
@@ -222,7 +222,7 @@ impl Point<Pixels> {
impl<T> Point<T>
where
- T: Sub<T, Output = T> + Debug + Clone + Default,
+ T: Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{
/// Get the position of this point, relative to the given origin
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
@@ -235,7 +235,7 @@ where
impl<T, Rhs> Mul<Rhs> for Point<T>
where
- T: Mul<Rhs, Output = T> + Clone + Default + Debug,
+ T: Mul<Rhs, Output = T> + Clone + Debug + Default + PartialEq,
Rhs: Clone + Debug,
{
type Output = Point<T>;
@@ -250,7 +250,7 @@ where
impl<T, S> MulAssign<S> for Point<T>
where
- T: Clone + Mul<S, Output = T> + Default + Debug,
+ T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
fn mul_assign(&mut self, rhs: S) {
@@ -261,7 +261,7 @@ where
impl<T, S> Div<S> for Point<T>
where
- T: Div<S, Output = T> + Clone + Default + Debug,
+ T: Div<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
type Output = Self;
@@ -276,7 +276,7 @@ where
impl<T> Point<T>
where
- T: PartialOrd + Clone + Default + Debug,
+ T: PartialOrd + Clone + Debug + Default + PartialEq,
{
/// Returns a new point with the maximum values of each dimension from `self` and `other`.
///
@@ -369,7 +369,7 @@ where
}
}
-impl<T: Clone + Default + Debug> Clone for Point<T> {
+impl<T: Clone + Debug + Default + PartialEq> Clone for Point<T> {
fn clone(&self) -> Self {
Self {
x: self.x.clone(),
@@ -378,7 +378,7 @@ impl<T: Clone + Default + Debug> Clone for Point<T> {
}
}
-impl<T: Default + Clone + Debug + Display> Display for Point<T> {
+impl<T: Clone + Debug + Default + PartialEq + Display> Display for Point<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
@@ -389,16 +389,16 @@ impl<T: Default + Clone + Debug + Display> Display for Point<T> {
/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`.
/// It is commonly used to specify dimensions for elements in a UI, such as a window or element.
#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
-pub struct Size<T: Clone + Default + Debug> {
+pub struct Size<T: Clone + Debug + Default + PartialEq> {
/// The width component of the size.
pub width: T,
/// The height component of the size.
pub height: T,
}
-impl<T: Clone + Default + Debug> Size<T> {
+impl<T: Clone + Debug + Default + PartialEq> Size<T> {
/// Create a new Size, a synonym for [`size`]
pub fn new(width: T, height: T) -> Self {
size(width, height)
@@ -422,14 +422,14 @@ impl<T: Clone + Default + Debug> Size<T> {
/// ```
pub const fn size<T>(width: T, height: T) -> Size<T>
where
- T: Clone + Default + Debug,
+ T: Clone + Debug + Default + PartialEq,
{
Size { width, height }
}
impl<T> Size<T>
where
- T: Clone + Default + Debug,
+ T: Clone + Debug + Default + PartialEq,
{
/// Applies a function to the width and height of the size, producing a new `Size<U>`.
///
@@ -451,7 +451,7 @@ where
/// ```
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Size<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
Size {
width: f(self.width.clone()),
@@ -462,7 +462,7 @@ where
impl<T> Size<T>
where
- T: Clone + Default + Debug + Half,
+ T: Clone + Debug + Default + PartialEq + Half,
{
/// Compute the center point of the size.g
pub fn center(&self) -> Point<T> {
@@ -502,7 +502,7 @@ impl Size<Pixels> {
impl<T> Along for Size<T>
where
- T: Clone + Default + Debug,
+ T: Clone + Debug + Default + PartialEq,
{
type Unit = T;
@@ -530,7 +530,7 @@ where
impl<T> Size<T>
where
- T: PartialOrd + Clone + Default + Debug,
+ T: PartialOrd + Clone + Debug + Default + PartialEq,
{
/// Returns a new `Size` with the maximum width and height from `self` and `other`.
///
@@ -595,7 +595,7 @@ where
impl<T> Sub for Size<T>
where
- T: Sub<Output = T> + Clone + Default + Debug,
+ T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Size<T>;
@@ -609,7 +609,7 @@ where
impl<T> Add for Size<T>
where
- T: Add<Output = T> + Clone + Default + Debug,
+ T: Add<Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Size<T>;
@@ -623,8 +623,8 @@ where
impl<T, Rhs> Mul<Rhs> for Size<T>
where
- T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
- Rhs: Clone + Default + Debug,
+ T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
+ Rhs: Clone + Debug + Default + PartialEq,
{
type Output = Size<Rhs>;
@@ -638,7 +638,7 @@ where
impl<T, S> MulAssign<S> for Size<T>
where
- T: Mul<S, Output = T> + Clone + Default + Debug,
+ T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
fn mul_assign(&mut self, rhs: S) {
@@ -647,24 +647,24 @@ where
}
}
-impl<T> Eq for Size<T> where T: Eq + Default + Debug + Clone {}
+impl<T> Eq for Size<T> where T: Eq + Clone + Debug + Default + PartialEq {}
impl<T> Debug for Size<T>
where
- T: Clone + Default + Debug,
+ T: Clone + Debug + Default + PartialEq,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Size {{ {:?} Γ {:?} }}", self.width, self.height)
}
}
-impl<T: Default + Clone + Debug + Display> Display for Size<T> {
+impl<T: Clone + Debug + Default + PartialEq + Display> Display for Size<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} Γ {}", self.width, self.height)
}
}
-impl<T: Clone + Default + Debug> From<Point<T>> for Size<T> {
+impl<T: Clone + Debug + Default + PartialEq> From<Point<T>> for Size<T> {
fn from(point: Point<T>) -> Self {
Self {
width: point.x,
@@ -746,7 +746,7 @@ impl Size<Length> {
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[refineable(Debug)]
#[repr(C)]
-pub struct Bounds<T: Clone + Default + Debug> {
+pub struct Bounds<T: Clone + Debug + Default + PartialEq> {
/// The origin point of this area.
pub origin: Point<T>,
/// The size of the rectangle.
@@ -754,7 +754,10 @@ pub struct Bounds<T: Clone + Default + Debug> {
}
/// Create a bounds with the given origin and size
-pub fn bounds<T: Clone + Default + Debug>(origin: Point<T>, size: Size<T>) -> Bounds<T> {
+pub fn bounds<T: Clone + Debug + Default + PartialEq>(
+ origin: Point<T>,
+ size: Size<T>,
+) -> Bounds<T> {
Bounds { origin, size }
}
@@ -790,7 +793,7 @@ impl Bounds<Pixels> {
impl<T> Bounds<T>
where
- T: Clone + Debug + Default,
+ T: Clone + Debug + Default + PartialEq,
{
/// Creates a new `Bounds` with the specified origin and size.
///
@@ -809,7 +812,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Sub<Output = T> + Default,
+ T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
{
/// Constructs a `Bounds` from two corner points: the top left and bottom right corners.
///
@@ -875,7 +878,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Sub<T, Output = T> + Default + Half,
+ T: Sub<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
{
/// Creates a new bounds centered at the given point.
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
@@ -889,7 +892,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + PartialOrd + Add<T, Output = T> + Default,
+ T: PartialOrd + Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{
/// Checks if this `Bounds` intersects with another `Bounds`.
///
@@ -937,7 +940,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Add<T, Output = T> + Default + Half,
+ T: Add<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
{
/// Returns the center point of the bounds.
///
@@ -970,7 +973,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Add<T, Output = T> + Default,
+ T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{
/// Calculates the half perimeter of a rectangle defined by the bounds.
///
@@ -997,7 +1000,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Add<T, Output = T> + Sub<Output = T> + Default,
+ T: Add<T, Output = T> + Sub<Output = T> + Clone + Debug + Default + PartialEq,
{
/// Dilates the bounds by a specified amount in all directions.
///
@@ -1048,7 +1051,13 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Neg<Output = T> + Default,
+ T: Add<T, Output = T>
+ + Sub<T, Output = T>
+ + Neg<Output = T>
+ + Clone
+ + Debug
+ + Default
+ + PartialEq,
{
/// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated.
///
@@ -1058,7 +1067,9 @@ where
}
}
-impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
+impl<T: PartialOrd + Add<T, Output = T> + Sub<Output = T> + Clone + Debug + Default + PartialEq>
+ Bounds<T>
+{
/// Calculates the intersection of two `Bounds` objects.
///
/// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect,
@@ -1140,7 +1151,7 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
impl<T> Bounds<T>
where
- T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Default,
+ T: Add<T, Output = T> + Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{
/// Computes the space available within outer bounds.
pub fn space_within(&self, outer: &Self) -> Edges<T> {
@@ -1155,9 +1166,9 @@ where
impl<T, Rhs> Mul<Rhs> for Bounds<T>
where
- T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
+ T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
Point<T>: Mul<Rhs, Output = Point<Rhs>>,
- Rhs: Clone + Default + Debug,
+ Rhs: Clone + Debug + Default + PartialEq,
{
type Output = Bounds<Rhs>;
@@ -1171,7 +1182,7 @@ where
impl<T, S> MulAssign<S> for Bounds<T>
where
- T: Mul<S, Output = T> + Clone + Default + Debug,
+ T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
fn mul_assign(&mut self, rhs: S) {
@@ -1183,7 +1194,7 @@ where
impl<T, S> Div<S> for Bounds<T>
where
Size<T>: Div<S, Output = Size<T>>,
- T: Div<S, Output = T> + Default + Clone + Debug,
+ T: Div<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
type Output = Self;
@@ -1198,7 +1209,7 @@ where
impl<T> Add<Point<T>> for Bounds<T>
where
- T: Add<T, Output = T> + Default + Clone + Debug,
+ T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Self;
@@ -1212,7 +1223,7 @@ where
impl<T> Sub<Point<T>> for Bounds<T>
where
- T: Sub<T, Output = T> + Default + Clone + Debug,
+ T: Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Self;
@@ -1226,7 +1237,7 @@ where
impl<T> Bounds<T>
where
- T: Add<T, Output = T> + Clone + Default + Debug,
+ T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{
/// Returns the top edge of the bounds.
///
@@ -1365,7 +1376,7 @@ where
impl<T> Bounds<T>
where
- T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug,
+ T: Add<T, Output = T> + PartialOrd + Clone + Debug + Default + PartialEq,
{
/// Checks if the given point is within the bounds.
///
@@ -1472,7 +1483,7 @@ where
/// ```
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
Bounds {
origin: self.origin.map(&f),
@@ -1531,7 +1542,7 @@ where
impl<T> Bounds<T>
where
- T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug + Sub<T, Output = T>,
+ T: Add<T, Output = T> + Sub<T, Output = T> + PartialOrd + Clone + Debug + Default + PartialEq,
{
/// Convert a point to the coordinate space defined by this Bounds
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
@@ -1545,7 +1556,7 @@ where
/// # Returns
///
/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area.
-impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> {
+impl<T: PartialOrd + Clone + Debug + Default + PartialEq> Bounds<T> {
/// Checks if the bounds represent an empty area.
///
/// # Returns
@@ -1556,7 +1567,7 @@ impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> {
}
}
-impl<T: Default + Clone + Debug + Display + Add<T, Output = T>> Display for Bounds<T> {
+impl<T: Clone + Debug + Default + PartialEq + Display + Add<T, Output = T>> Display for Bounds<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
@@ -1651,7 +1662,7 @@ impl Bounds<DevicePixels> {
}
}
-impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
+impl<T: Copy + Clone + Debug + Default + PartialEq> Copy for Bounds<T> {}
/// Represents the edges of a box in a 2D space, such as padding or margin.
///
@@ -1674,9 +1685,9 @@ impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
/// assert_eq!(edges.left, 40.0);
/// ```
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
-pub struct Edges<T: Clone + Default + Debug> {
+pub struct Edges<T: Clone + Debug + Default + PartialEq> {
/// The size of the top edge.
pub top: T,
/// The size of the right edge.
@@ -1689,7 +1700,7 @@ pub struct Edges<T: Clone + Default + Debug> {
impl<T> Mul for Edges<T>
where
- T: Mul<Output = T> + Clone + Default + Debug,
+ T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Self;
@@ -1705,7 +1716,7 @@ where
impl<T, S> MulAssign<S> for Edges<T>
where
- T: Mul<S, Output = T> + Clone + Default + Debug,
+ T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
fn mul_assign(&mut self, rhs: S) {
@@ -1716,9 +1727,9 @@ where
}
}
-impl<T: Clone + Default + Debug + Copy> Copy for Edges<T> {}
+impl<T: Clone + Debug + Default + PartialEq + Copy> Copy for Edges<T> {}
-impl<T: Clone + Default + Debug> Edges<T> {
+impl<T: Clone + Debug + Default + PartialEq> Edges<T> {
/// Constructs `Edges` where all sides are set to the same specified value.
///
/// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized
@@ -1776,7 +1787,7 @@ impl<T: Clone + Default + Debug> Edges<T> {
/// ```
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Edges<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
Edges {
top: f(&self.top),
@@ -2151,9 +2162,9 @@ impl Corner {
///
/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
-pub struct Corners<T: Clone + Default + Debug> {
+pub struct Corners<T: Clone + Debug + Default + PartialEq> {
/// The value associated with the top left corner.
pub top_left: T,
/// The value associated with the top right corner.
@@ -2166,7 +2177,7 @@ pub struct Corners<T: Clone + Default + Debug> {
impl<T> Corners<T>
where
- T: Clone + Default + Debug,
+ T: Clone + Debug + Default + PartialEq,
{
/// Constructs `Corners` where all sides are set to the same specified value.
///
@@ -2319,7 +2330,7 @@ impl Corners<Pixels> {
}
}
-impl<T: Div<f32, Output = T> + Ord + Clone + Default + Debug> Corners<T> {
+impl<T: Div<f32, Output = T> + Ord + Clone + Debug + Default + PartialEq> Corners<T> {
/// Clamps corner radii to be less than or equal to half the shortest side of a quad.
///
/// # Arguments
@@ -2340,7 +2351,7 @@ impl<T: Div<f32, Output = T> + Ord + Clone + Default + Debug> Corners<T> {
}
}
-impl<T: Clone + Default + Debug> Corners<T> {
+impl<T: Clone + Debug + Default + PartialEq> Corners<T> {
/// Applies a function to each field of the `Corners`, producing a new `Corners<U>`.
///
/// This method allows for converting a `Corners<T>` to a `Corners<U>` by specifying a closure
@@ -2375,7 +2386,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
/// ```
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Corners<U>
where
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
Corners {
top_left: f(&self.top_left),
@@ -2388,7 +2399,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
impl<T> Mul for Corners<T>
where
- T: Mul<Output = T> + Clone + Default + Debug,
+ T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
{
type Output = Self;
@@ -2404,7 +2415,7 @@ where
impl<T, S> MulAssign<S> for Corners<T>
where
- T: Mul<S, Output = T> + Clone + Default + Debug,
+ T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone,
{
fn mul_assign(&mut self, rhs: S) {
@@ -2415,7 +2426,7 @@ where
}
}
-impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
+impl<T> Copy for Corners<T> where T: Copy + Clone + Debug + Default + PartialEq {}
impl From<f32> for Corners<Pixels> {
fn from(val: f32) -> Self {
@@ -3427,7 +3438,7 @@ impl Default for DefiniteLength {
}
/// A length that can be defined in pixels, rems, percent of parent, or auto.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, PartialEq)]
pub enum Length {
/// A definite length specified either in pixels, rems, or as a fraction of the parent's size.
Definite(DefiniteLength),
@@ -3772,7 +3783,7 @@ impl IsZero for Length {
}
}
-impl<T: IsZero + Debug + Clone + Default> IsZero for Point<T> {
+impl<T: IsZero + Clone + Debug + Default + PartialEq> IsZero for Point<T> {
fn is_zero(&self) -> bool {
self.x.is_zero() && self.y.is_zero()
}
@@ -3780,14 +3791,14 @@ impl<T: IsZero + Debug + Clone + Default> IsZero for Point<T> {
impl<T> IsZero for Size<T>
where
- T: IsZero + Default + Debug + Clone,
+ T: IsZero + Clone + Debug + Default + PartialEq,
{
fn is_zero(&self) -> bool {
self.width.is_zero() || self.height.is_zero()
}
}
-impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> {
+impl<T: IsZero + Clone + Debug + Default + PartialEq> IsZero for Bounds<T> {
fn is_zero(&self) -> bool {
self.size.is_zero()
}
@@ -3795,7 +3806,7 @@ impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> {
impl<T> IsZero for Corners<T>
where
- T: IsZero + Clone + Default + Debug,
+ T: IsZero + Clone + Debug + Default + PartialEq,
{
fn is_zero(&self) -> bool {
self.top_left.is_zero()
@@ -221,3 +221,34 @@ mod conditional {
}
}
}
+
+/// Provides definitions used by `#[derive_inspector_reflection]`.
+#[cfg(any(feature = "inspector", debug_assertions))]
+pub mod inspector_reflection {
+ use std::any::Any;
+
+ /// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name,
+ /// documentation, and ability to invoke the function.
+ #[derive(Clone, Copy)]
+ pub struct FunctionReflection<T> {
+ /// The name of the function
+ pub name: &'static str,
+ /// The method
+ pub function: fn(Box<dyn Any>) -> Box<dyn Any>,
+ /// Documentation for the function
+ pub documentation: Option<&'static str>,
+ /// `PhantomData` for the type of the argument and result
+ pub _type: std::marker::PhantomData<T>,
+ }
+
+ impl<T: 'static> FunctionReflection<T> {
+ /// Invoke this method on a value and return the result.
+ pub fn invoke(&self, value: T) -> T {
+ let boxed = Box::new(value) as Box<dyn Any>;
+ let result = (self.function)(boxed);
+ *result
+ .downcast::<T>()
+ .expect("Type mismatch in reflection invoke")
+ }
+ }
+}
@@ -679,7 +679,7 @@ pub(crate) struct PathId(pub(crate) usize);
/// A line made up of a series of vertices and control points.
#[derive(Clone, Debug)]
-pub struct Path<P: Clone + Default + Debug> {
+pub struct Path<P: Clone + Debug + Default + PartialEq> {
pub(crate) id: PathId,
order: DrawOrder,
pub(crate) bounds: Bounds<P>,
@@ -812,7 +812,7 @@ impl From<Path<ScaledPixels>> for Primitive {
#[derive(Clone, Debug)]
#[repr(C)]
-pub(crate) struct PathVertex<P: Clone + Default + Debug> {
+pub(crate) struct PathVertex<P: Clone + Debug + Default + PartialEq> {
pub(crate) xy_position: Point<P>,
pub(crate) st_position: Point<f32>,
pub(crate) content_mask: ContentMask<P>,
@@ -140,7 +140,7 @@ impl ObjectFit {
/// The CSS styling that can be applied to an element via the `Styled` trait
#[derive(Clone, Refineable, Debug)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Style {
/// What layout strategy should be used?
pub display: Display,
@@ -286,7 +286,7 @@ pub enum Visibility {
}
/// The possible values of the box-shadow property
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct BoxShadow {
/// What color should the shadow have?
pub color: Hsla,
@@ -332,7 +332,7 @@ pub enum TextAlign {
/// The properties that can be used to style text in GPUI
#[derive(Refineable, Clone, Debug, PartialEq)]
-#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
+#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextStyle {
/// The color of the text
pub color: Hsla,
@@ -794,7 +794,7 @@ pub struct StrikethroughStyle {
}
/// The kinds of fill that can be applied to a shape.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum Fill {
/// A solid color fill.
Color(Background),
@@ -14,6 +14,10 @@ const ELLIPSIS: SharedString = SharedString::new_static("β¦");
/// A trait for elements that can be styled.
/// Use this to opt-in to a utility CSS-like styling API.
+#[cfg_attr(
+ any(feature = "inspector", debug_assertions),
+ gpui_macros::derive_inspector_reflection
+)]
pub trait Styled: Sized {
/// Returns a reference to the style memory of this element.
fn style(&mut self) -> &mut StyleRefinement;
@@ -359,7 +359,7 @@ impl ToTaffy<taffy::style::LengthPercentage> for AbsoluteLength {
impl<T, T2> From<TaffyPoint<T>> for Point<T2>
where
T: Into<T2>,
- T2: Clone + Default + Debug,
+ T2: Clone + Debug + Default + PartialEq,
{
fn from(point: TaffyPoint<T>) -> Point<T2> {
Point {
@@ -371,7 +371,7 @@ where
impl<T, T2> From<Point<T>> for TaffyPoint<T2>
where
- T: Into<T2> + Clone + Default + Debug,
+ T: Into<T2> + Clone + Debug + Default + PartialEq,
{
fn from(val: Point<T>) -> Self {
TaffyPoint {
@@ -383,7 +383,7 @@ where
impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
where
- T: ToTaffy<U> + Clone + Default + Debug,
+ T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{
fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
TaffySize {
@@ -395,7 +395,7 @@ where
impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
where
- T: ToTaffy<U> + Clone + Default + Debug,
+ T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{
fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
TaffyRect {
@@ -410,7 +410,7 @@ where
impl<T, U> From<TaffySize<T>> for Size<U>
where
T: Into<U>,
- U: Clone + Default + Debug,
+ U: Clone + Debug + Default + PartialEq,
{
fn from(taffy_size: TaffySize<T>) -> Self {
Size {
@@ -422,7 +422,7 @@ where
impl<T, U> From<Size<T>> for TaffySize<U>
where
- T: Into<U> + Clone + Default + Debug,
+ T: Into<U> + Clone + Debug + Default + PartialEq,
{
fn from(size: Size<T>) -> Self {
TaffySize {
@@ -979,7 +979,7 @@ pub(crate) struct DispatchEventResult {
/// to leave room to support more complex shapes in the future.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)]
-pub struct ContentMask<P: Clone + Default + Debug> {
+pub struct ContentMask<P: Clone + Debug + Default + PartialEq> {
/// The bounds
pub bounds: Bounds<P>,
}
@@ -8,16 +8,20 @@ license = "Apache-2.0"
[lints]
workspace = true
+[features]
+inspector = []
+
[lib]
path = "src/gpui_macros.rs"
proc-macro = true
doctest = true
[dependencies]
+heck.workspace = true
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
-gpui.workspace = true
+gpui = { workspace = true, features = ["inspector"] }
@@ -0,0 +1,307 @@
+//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods
+//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4.
+
+use heck::ToSnakeCase as _;
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{
+ Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type,
+ parse_macro_input, parse_quote,
+ visit_mut::{self, VisitMut},
+};
+
+pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
+ let mut item = parse_macro_input!(input as Item);
+
+ // First, expand any macros in the trait
+ match &mut item {
+ Item::Trait(trait_item) => {
+ let mut expander = MacroExpander;
+ expander.visit_item_trait_mut(trait_item);
+ }
+ _ => {
+ return syn::Error::new_spanned(
+ quote!(#item),
+ "#[derive_inspector_reflection] can only be applied to traits",
+ )
+ .to_compile_error()
+ .into();
+ }
+ }
+
+ // Now process the expanded trait
+ match item {
+ Item::Trait(trait_item) => generate_reflected_trait(trait_item),
+ _ => unreachable!(),
+ }
+}
+
+fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream {
+ let trait_name = &trait_item.ident;
+ let vis = &trait_item.vis;
+
+ // Determine if we're being called from within the gpui crate
+ let call_site = Span::call_site();
+ let inspector_reflection_path = if is_called_from_gpui_crate(call_site) {
+ quote! { crate::inspector_reflection }
+ } else {
+ quote! { ::gpui::inspector_reflection }
+ };
+
+ // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self
+ let mut method_infos = Vec::new();
+
+ for item in &trait_item.items {
+ if let TraitItem::Fn(method) = item {
+ let method_name = &method.sig.ident;
+
+ // Check if method has self or mut self receiver
+ let has_valid_self_receiver = method
+ .sig
+ .inputs
+ .iter()
+ .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none()));
+
+ // Check if method returns Self
+ let returns_self = match &method.sig.output {
+ ReturnType::Type(_, ty) => {
+ matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self"))
+ }
+ ReturnType::Default => false,
+ };
+
+ // Check if method has exactly one parameter (self or mut self)
+ let param_count = method.sig.inputs.len();
+
+ // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self
+ // This includes methods with default implementations
+ if has_valid_self_receiver && returns_self && param_count == 1 {
+ // Extract documentation and cfg attributes
+ let doc = extract_doc_comment(&method.attrs);
+ let cfg_attrs = extract_cfg_attributes(&method.attrs);
+ method_infos.push((method_name.clone(), doc, cfg_attrs));
+ }
+ }
+ }
+
+ // Generate the reflection module name
+ let reflection_mod_name = Ident::new(
+ &format!("{}_reflection", trait_name.to_string().to_snake_case()),
+ trait_name.span(),
+ );
+
+ // Generate wrapper functions for each method
+ // These wrappers use type erasure to allow runtime invocation
+ let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| {
+ let wrapper_name = Ident::new(
+ &format!("__wrapper_{}", method_name),
+ method_name.span(),
+ );
+ quote! {
+ #(#cfg_attrs)*
+ fn #wrapper_name<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
+ if let Ok(concrete) = value.downcast::<T>() {
+ Box::new(concrete.#method_name())
+ } else {
+ panic!("Type mismatch in reflection wrapper");
+ }
+ }
+ }
+ });
+
+ // Generate method info entries
+ let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| {
+ let method_name_str = method_name.to_string();
+ let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span());
+ let doc_expr = match doc {
+ Some(doc_str) => quote! { Some(#doc_str) },
+ None => quote! { None },
+ };
+ quote! {
+ #(#cfg_attrs)*
+ #inspector_reflection_path::FunctionReflection {
+ name: #method_name_str,
+ function: #wrapper_name::<T>,
+ documentation: #doc_expr,
+ _type: ::std::marker::PhantomData,
+ }
+ }
+ });
+
+ // Generate the complete output
+ let output = quote! {
+ #trait_item
+
+ /// Implements function reflection
+ #vis mod #reflection_mod_name {
+ use super::*;
+
+ #(#wrapper_functions)*
+
+ /// Get all reflectable methods for a concrete type implementing the trait
+ pub fn methods<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
+ vec![
+ #(#method_info_entries),*
+ ]
+ }
+
+ /// Find a method by name for a concrete type implementing the trait
+ pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
+ methods::<T>().into_iter().find(|m| m.name == name)
+ }
+ }
+ };
+
+ TokenStream::from(output)
+}
+
+fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
+ let mut doc_lines = Vec::new();
+
+ for attr in attrs {
+ if attr.path().is_ident("doc") {
+ if let Meta::NameValue(meta) = &attr.meta {
+ if let Expr::Lit(expr_lit) = &meta.value {
+ if let Lit::Str(lit_str) = &expr_lit.lit {
+ let line = lit_str.value();
+ let line = line.strip_prefix(' ').unwrap_or(&line);
+ doc_lines.push(line.to_string());
+ }
+ }
+ }
+ }
+ }
+
+ if doc_lines.is_empty() {
+ None
+ } else {
+ Some(doc_lines.join("\n"))
+ }
+}
+
+fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
+ attrs
+ .iter()
+ .filter(|attr| attr.path().is_ident("cfg"))
+ .cloned()
+ .collect()
+}
+
+fn is_called_from_gpui_crate(_span: Span) -> bool {
+ // Check if we're being called from within the gpui crate by examining the call site
+ // This is a heuristic approach - we check if the current crate name is "gpui"
+ std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui")
+}
+
+struct MacroExpander;
+
+impl VisitMut for MacroExpander {
+ fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) {
+ let mut expanded_items = Vec::new();
+ let mut items_to_keep = Vec::new();
+
+ for item in trait_item.items.drain(..) {
+ match item {
+ TraitItem::Macro(macro_item) => {
+ // Try to expand known macros
+ if let Some(expanded) = try_expand_macro(¯o_item) {
+ expanded_items.extend(expanded);
+ } else {
+ // Keep unknown macros as-is
+ items_to_keep.push(TraitItem::Macro(macro_item));
+ }
+ }
+ other => {
+ items_to_keep.push(other);
+ }
+ }
+ }
+
+ // Rebuild the items list with expanded content first, then original items
+ trait_item.items = expanded_items;
+ trait_item.items.extend(items_to_keep);
+
+ // Continue visiting
+ visit_mut::visit_item_trait_mut(self, trait_item);
+ }
+}
+
+fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
+ let path = ¯o_item.mac.path;
+
+ // Check if this is one of our known style macros
+ let macro_name = path_to_string(path);
+
+ // Handle the known macros by calling their implementations
+ match macro_name.as_str() {
+ "gpui_macros::style_helpers" | "style_helpers" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::margin_style_methods" | "margin_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::padding_style_methods" | "padding_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::position_style_methods" | "position_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::border_style_methods" | "border_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
+ let tokens = macro_item.mac.tokens.clone();
+ let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
+ parse_expanded_items(expanded)
+ }
+ _ => None,
+ }
+}
+
+fn path_to_string(path: &Path) -> String {
+ path.segments
+ .iter()
+ .map(|seg| seg.ident.to_string())
+ .collect::<Vec<_>>()
+ .join("::")
+}
+
+fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
+ let tokens = TokenStream2::from(expanded);
+
+ // Try to parse the expanded tokens as trait items
+ // We need to wrap them in a dummy trait to parse properly
+ let dummy_trait: ItemTrait = parse_quote! {
+ trait Dummy {
+ #tokens
+ }
+ };
+
+ Some(dummy_trait.items)
+}
@@ -6,6 +6,9 @@ mod register_action;
mod styles;
mod test;
+#[cfg(any(feature = "inspector", debug_assertions))]
+mod derive_inspector_reflection;
+
use proc_macro::TokenStream;
use syn::{DeriveInput, Ident};
@@ -178,6 +181,28 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
}
+/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides
+/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`.
+/// This is used by the inspector so that it can use the builder methods in `Styled` and
+/// `StyledExt`.
+///
+/// The generated module will have the name `<snake_case_trait_name>_reflection` and contain the
+/// following functions:
+///
+/// ```ignore
+/// pub fn methods::<T: TheTrait + 'static>() -> Vec<gpui::inspector_reflection::FunctionReflection<T>>;
+///
+/// pub fn find_method::<T: TheTrait + 'static>() -> Option<gpui::inspector_reflection::FunctionReflection<T>>;
+/// ```
+///
+/// The `invoke` method on `FunctionReflection` will run the method. `FunctionReflection` also
+/// provides the method's documentation.
+#[cfg(any(feature = "inspector", debug_assertions))]
+#[proc_macro_attribute]
+pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
+ derive_inspector_reflection::derive_inspector_reflection(_args, input)
+}
+
pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option<Ident> {
match &ast.data {
syn::Data::Struct(data_struct) => data_struct
@@ -0,0 +1,148 @@
+//! This code was generated using Zed Agent with Claude Opus 4.
+
+use gpui_macros::derive_inspector_reflection;
+
+#[derive_inspector_reflection]
+trait Transform: Clone {
+ /// Doubles the value
+ fn double(self) -> Self;
+
+ /// Triples the value
+ fn triple(self) -> Self;
+
+ /// Increments the value by one
+ ///
+ /// This method has a default implementation
+ fn increment(self) -> Self {
+ // Default implementation
+ self.add_one()
+ }
+
+ /// Quadruples the value by doubling twice
+ fn quadruple(self) -> Self {
+ // Default implementation with mut self
+ self.double().double()
+ }
+
+ // These methods will be filtered out:
+ #[allow(dead_code)]
+ fn add(&self, other: &Self) -> Self;
+ #[allow(dead_code)]
+ fn set_value(&mut self, value: i32);
+ #[allow(dead_code)]
+ fn get_value(&self) -> i32;
+
+ /// Adds one to the value
+ fn add_one(self) -> Self;
+
+ /// cfg attributes are respected
+ #[cfg(all())]
+ fn cfg_included(self) -> Self;
+
+ #[cfg(any())]
+ fn cfg_omitted(self) -> Self;
+}
+
+#[derive(Debug, Clone, PartialEq)]
+struct Number(i32);
+
+impl Transform for Number {
+ fn double(self) -> Self {
+ Number(self.0 * 2)
+ }
+
+ fn triple(self) -> Self {
+ Number(self.0 * 3)
+ }
+
+ fn add(&self, other: &Self) -> Self {
+ Number(self.0 + other.0)
+ }
+
+ fn set_value(&mut self, value: i32) {
+ self.0 = value;
+ }
+
+ fn get_value(&self) -> i32 {
+ self.0
+ }
+
+ fn add_one(self) -> Self {
+ Number(self.0 + 1)
+ }
+
+ fn cfg_included(self) -> Self {
+ Number(self.0)
+ }
+}
+
+#[test]
+fn test_derive_inspector_reflection() {
+ use transform_reflection::*;
+
+ // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
+ let methods = methods::<Number>();
+
+ assert_eq!(methods.len(), 6);
+ let method_names: Vec<_> = methods.iter().map(|m| m.name).collect();
+ assert!(method_names.contains(&"double"));
+ assert!(method_names.contains(&"triple"));
+ assert!(method_names.contains(&"increment"));
+ assert!(method_names.contains(&"quadruple"));
+ assert!(method_names.contains(&"add_one"));
+ assert!(method_names.contains(&"cfg_included"));
+
+ // Invoke methods by name
+ let num = Number(5);
+
+ let doubled = find_method::<Number>("double").unwrap().invoke(num.clone());
+ assert_eq!(doubled, Number(10));
+
+ let tripled = find_method::<Number>("triple").unwrap().invoke(num.clone());
+ assert_eq!(tripled, Number(15));
+
+ let incremented = find_method::<Number>("increment")
+ .unwrap()
+ .invoke(num.clone());
+ assert_eq!(incremented, Number(6));
+
+ let quadrupled = find_method::<Number>("quadruple")
+ .unwrap()
+ .invoke(num.clone());
+ assert_eq!(quadrupled, Number(20));
+
+ // Try to invoke a non-existent method
+ let result = find_method::<Number>("nonexistent");
+ assert!(result.is_none());
+
+ // Chain operations
+ let num = Number(10);
+ let result = find_method::<Number>("double")
+ .map(|m| m.invoke(num))
+ .and_then(|n| find_method::<Number>("increment").map(|m| m.invoke(n)))
+ .and_then(|n| find_method::<Number>("triple").map(|m| m.invoke(n)));
+
+ assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63
+
+ // Test documentationumentation capture
+ let double_method = find_method::<Number>("double").unwrap();
+ assert_eq!(double_method.documentation, Some("Doubles the value"));
+
+ let triple_method = find_method::<Number>("triple").unwrap();
+ assert_eq!(triple_method.documentation, Some("Triples the value"));
+
+ let increment_method = find_method::<Number>("increment").unwrap();
+ assert_eq!(
+ increment_method.documentation,
+ Some("Increments the value by one\n\nThis method has a default implementation")
+ );
+
+ let quadruple_method = find_method::<Number>("quadruple").unwrap();
+ assert_eq!(
+ quadruple_method.documentation,
+ Some("Quadruples the value by doubling twice")
+ );
+
+ let add_one_method = find_method::<Number>("add_one").unwrap();
+ assert_eq!(add_one_method.documentation, Some("Adds one to the value"));
+}
@@ -15,6 +15,7 @@ path = "src/inspector_ui.rs"
anyhow.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
+fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
@@ -23,6 +24,6 @@ serde_json_lenient.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
-workspace.workspace = true
workspace-hack.workspace = true
+workspace.workspace = true
zed_actions.workspace = true
@@ -1,8 +1,6 @@
# Inspector
-This is a tool for inspecting and manipulating rendered elements in Zed. It is
-only available in debug builds. Use the `dev::ToggleInspector` action to toggle
-inspector mode and click on UI elements to inspect them.
+This is a tool for inspecting and manipulating rendered elements in Zed. It is only available in debug builds. Use the `dev::ToggleInspector` action to toggle inspector mode and click on UI elements to inspect them.
# Current features
@@ -10,44 +8,72 @@ inspector mode and click on UI elements to inspect them.
* Temporary manipulation of the selected element.
-* Layout info and JSON-based style manipulation for `Div`.
+* Layout info for `Div`.
+
+* Both Rust and JSON-based style manipulation of `Div` style. The rust style editor only supports argumentless `Styled` and `StyledExt` method calls.
* Navigation to code that constructed the element.
# Known bugs
-* The style inspector buffer will leak memory over time due to building up
-history on each change of inspected element. Instead of using `Project` to
-create it, should just directly build the `Buffer` and `File` each time the inspected element changes.
+## JSON style editor undo history doesn't get reset
+
+The JSON style editor appends to its undo stack on every change of the active inspected element.
+
+I attempted to fix it by creating a new buffer and setting the buffer associated with the `json_style_buffer` entity. Unfortunately this doesn't work because the language server uses the `version: clock::Global` to figure out the changes, so would need some way to start the new buffer's text at that version.
+
+```
+ json_style_buffer.update(cx, |json_style_buffer, cx| {
+ let language = json_style_buffer.language().cloned();
+ let file = json_style_buffer.file().cloned();
+
+ *json_style_buffer = Buffer::local("", cx);
+
+ json_style_buffer.set_language(language, cx);
+ if let Some(file) = file {
+ json_style_buffer.file_updated(file, cx);
+ }
+ });
+```
# Future features
-* Info and manipulation of element types other than `Div`.
+* Action and keybinding for entering pick mode.
* Ability to highlight current element after it's been picked.
+* Info and manipulation of element types other than `Div`.
+
* Indicate when the picked element has disappeared.
+* To inspect elements that disappear, it would be helpful to be able to pause the UI.
+
* Hierarchy view?
-## Better manipulation than JSON
+## Methods that take arguments in Rust style editor
-The current approach is not easy to move back to the code. Possibilities:
+Could use TreeSitter to parse out the fluent style method chain and arguments. Tricky part of this is completions - ideally the Rust Analyzer already being used by the developer's Zed would be used.
-* Editable list of style attributes to apply.
+## Edit original code in Rust style editor
-* Rust buffer of code that does a very lenient parse to get the style attributes. Some options:
+Two approaches:
- - Take all the identifier-like tokens and use them if they are the name of an attribute. A custom completion provider in a buffer could be used.
+1. Open an excerpt of the original file.
- - Use TreeSitter to parse out the fluent style method chain. With this approach the buffer could even be the actual code file. Tricky part of this is LSP - ideally the LSP already being used by the developer's Zed would be used.
+2. Communicate with the Zed process that has the repo open - it would send the code for the element. This seems like a lot of work, but would be very nice for rapid development, and it would allow use of rust analyzer.
-## Source locations
+With both approaches, would need to record the buffer version and use that when referring to source locations, since editing elements can cause code layout shift.
+
+## Source location UI improvements
* Mode to navigate to source code on every element change while picking.
* Tracking of more source locations - currently the source location is often in a ui compoenent. Ideally this would have a way for the components to indicate that they are probably not the source location the user is looking for.
+ - Could have `InspectorElementId` be `Vec<(ElementId, Option<Location>)>`, but if there are multiple code paths that construct the same element this would cause them to be considered different.
+
+ - Probably better to have a separate `Vec<Option<Location>>` that uses the same indices as `GlobalElementId`.
+
## Persistent modification
Currently, element modifications disappear when picker mode is started. Handling this well is tricky. Potential features:
@@ -60,9 +86,11 @@ Currently, element modifications disappear when picker mode is started. Handling
* The code should probably distinguish the data that is provided by the element and the modifications from the inspector. Currently these are conflated in element states.
+If support is added for editing original code, then the logical selector in this case would be just matches of the source path.
+
# Code cleanups
-## Remove special side pane rendering
+## Consider removing special side pane rendering
Currently the inspector has special rendering in the UI, but maybe it could just be a workspace item.
@@ -1,26 +1,64 @@
-use anyhow::Result;
-use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
+use anyhow::{Result, anyhow};
+use editor::{Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MultiBuffer};
+use fuzzy::StringMatch;
use gpui::{
- AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
- Window,
+ AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
+ StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
};
-use language::Buffer;
use language::language_settings::SoftWrap;
-use project::{Project, ProjectPath};
+use language::{
+ Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
+ DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
+};
+use project::lsp_store::CompletionDocumentation;
+use project::{Completion, CompletionSource, Project, ProjectPath};
+use std::cell::RefCell;
+use std::fmt::Write as _;
+use std::ops::Range;
use std::path::Path;
-use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
+use std::rc::Rc;
+use std::sync::LazyLock;
+use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
+use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this
/// matches the name used in the generated schemas.
-const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
+const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
pub(crate) struct DivInspector {
+ state: State,
project: Entity<Project>,
inspector_id: Option<InspectorElementId>,
- state: Option<DivInspectorState>,
- style_buffer: Option<Entity<Buffer>>,
- style_editor: Option<Entity<Editor>>,
- last_error: Option<SharedString>,
+ inspector_state: Option<DivInspectorState>,
+ /// Value of `DivInspectorState.base_style` when initially picked.
+ initial_style: StyleRefinement,
+ /// Portion of `initial_style` that can't be converted to rust code.
+ unconvertible_style: StyleRefinement,
+ /// Edits the user has made to the json buffer: `json_editor - (unconvertible_style + rust_editor)`.
+ json_style_overrides: StyleRefinement,
+ /// Error to display from parsing the json, or if serialization errors somehow occur.
+ json_style_error: Option<SharedString>,
+ /// Currently selected completion.
+ rust_completion: Option<String>,
+ /// Range that will be replaced by the completion if selected.
+ rust_completion_replace_range: Option<Range<Anchor>>,
+}
+
+enum State {
+ Loading,
+ BuffersLoaded {
+ rust_style_buffer: Entity<Buffer>,
+ json_style_buffer: Entity<Buffer>,
+ },
+ Ready {
+ rust_style_buffer: Entity<Buffer>,
+ rust_style_editor: Entity<Editor>,
+ json_style_buffer: Entity<Buffer>,
+ json_style_editor: Entity<Editor>,
+ },
+ LoadError {
+ message: SharedString,
+ },
}
impl DivInspector {
@@ -29,136 +67,178 @@ impl DivInspector {
window: &mut Window,
cx: &mut Context<Self>,
) -> DivInspector {
- // Open the buffer once, so it can then be used for each editor.
+ // Open the buffers once, so they can then be used for each editor.
cx.spawn_in(window, {
+ let languages = project.read(cx).languages().clone();
let project = project.clone();
- async move |this, cx| Self::open_style_buffer(project, this, cx).await
+ async move |this, cx| {
+ // Open the JSON style buffer in the inspector-specific project, so that it runs the
+ // JSON language server.
+ let json_style_buffer =
+ Self::create_buffer_in_project(ZED_INSPECTOR_STYLE_JSON, &project, cx).await;
+
+ // Create Rust style buffer without adding it to the project / buffer_store, so that
+ // Rust Analyzer doesn't get started for it.
+ let rust_language_result = languages.language_for_name("Rust").await;
+ let rust_style_buffer = rust_language_result.and_then(|rust_language| {
+ cx.new(|cx| Buffer::local("", cx).with_language(rust_language, cx))
+ });
+
+ match json_style_buffer.and_then(|json_style_buffer| {
+ rust_style_buffer
+ .map(|rust_style_buffer| (json_style_buffer, rust_style_buffer))
+ }) {
+ Ok((json_style_buffer, rust_style_buffer)) => {
+ this.update_in(cx, |this, window, cx| {
+ this.state = State::BuffersLoaded {
+ json_style_buffer: json_style_buffer,
+ rust_style_buffer: rust_style_buffer,
+ };
+
+ // Initialize editors immediately instead of waiting for
+ // `update_inspected_element`. This avoids continuing to show
+ // "Loading..." until the user moves the mouse to a different element.
+ if let Some(id) = this.inspector_id.take() {
+ let inspector_state =
+ window.with_inspector_state(Some(&id), cx, |state, _window| {
+ state.clone()
+ });
+ if let Some(inspector_state) = inspector_state {
+ this.update_inspected_element(&id, inspector_state, window, cx);
+ cx.notify();
+ }
+ }
+ })
+ .ok();
+ }
+ Err(err) => {
+ this.update(cx, |this, _cx| {
+ this.state = State::LoadError {
+ message: format!(
+ "Failed to create buffers for style editing: {err}"
+ )
+ .into(),
+ };
+ })
+ .ok();
+ }
+ }
+ }
})
.detach();
DivInspector {
+ state: State::Loading,
project,
inspector_id: None,
- state: None,
- style_buffer: None,
- style_editor: None,
- last_error: None,
+ inspector_state: None,
+ initial_style: StyleRefinement::default(),
+ unconvertible_style: StyleRefinement::default(),
+ json_style_overrides: StyleRefinement::default(),
+ rust_completion: None,
+ rust_completion_replace_range: None,
+ json_style_error: None,
}
}
- async fn open_style_buffer(
- project: Entity<Project>,
- this: WeakEntity<DivInspector>,
- cx: &mut AsyncWindowContext,
- ) -> Result<()> {
- let worktree = project
- .update(cx, |project, cx| {
- project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
- })?
- .await?;
-
- let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
- worktree_id: worktree.id(),
- path: Path::new("").into(),
- })?;
-
- let style_buffer = project
- .update(cx, |project, cx| project.open_path(project_path, cx))?
- .await?
- .1;
-
- project.update(cx, |project, cx| {
- project.register_buffer_with_language_servers(&style_buffer, cx)
- })?;
-
- this.update_in(cx, |this, window, cx| {
- this.style_buffer = Some(style_buffer);
- if let Some(id) = this.inspector_id.clone() {
- let state =
- window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
- if let Some(state) = state {
- this.update_inspected_element(&id, state, window, cx);
- cx.notify();
- }
- }
- })?;
-
- Ok(())
- }
-
pub fn update_inspected_element(
&mut self,
id: &InspectorElementId,
- state: DivInspectorState,
+ inspector_state: DivInspectorState,
window: &mut Window,
cx: &mut Context<Self>,
) {
- let base_style_json = serde_json::to_string_pretty(&state.base_style);
- self.state = Some(state);
+ let style = (*inspector_state.base_style).clone();
+ self.inspector_state = Some(inspector_state);
if self.inspector_id.as_ref() == Some(id) {
return;
- } else {
- self.inspector_id = Some(id.clone());
}
- let Some(style_buffer) = self.style_buffer.clone() else {
- return;
- };
- let base_style_json = match base_style_json {
- Ok(base_style_json) => base_style_json,
- Err(err) => {
- self.style_editor = None;
- self.last_error =
- Some(format!("Failed to convert base_style to JSON: {err}").into());
- return;
+ self.inspector_id = Some(id.clone());
+ self.initial_style = style.clone();
+
+ let (rust_style_buffer, json_style_buffer) = match &self.state {
+ State::BuffersLoaded {
+ rust_style_buffer,
+ json_style_buffer,
}
+ | State::Ready {
+ rust_style_buffer,
+ json_style_buffer,
+ ..
+ } => (rust_style_buffer.clone(), json_style_buffer.clone()),
+ State::Loading | State::LoadError { .. } => return,
};
- self.last_error = None;
- style_buffer.update(cx, |style_buffer, cx| {
- style_buffer.set_text(base_style_json, cx)
- });
+ let json_style_editor = self.create_editor(json_style_buffer.clone(), window, cx);
+ let rust_style_editor = self.create_editor(rust_style_buffer.clone(), window, cx);
- let style_editor = cx.new(|cx| {
- let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
- let mut editor = Editor::new(
- EditorMode::full(),
- multi_buffer,
- Some(self.project.clone()),
- window,
- cx,
- );
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor.set_show_line_numbers(false, cx);
- editor.set_show_code_actions(false, cx);
- editor.set_show_breakpoints(false, cx);
- editor.set_show_git_diff_gutter(false, cx);
- editor.set_show_runnables(false, cx);
- editor.set_show_edit_predictions(Some(false), window, cx);
- editor
+ rust_style_editor.update(cx, {
+ let div_inspector = cx.entity();
+ |rust_style_editor, _cx| {
+ rust_style_editor.set_completion_provider(Some(Rc::new(
+ RustStyleCompletionProvider { div_inspector },
+ )));
+ }
});
- cx.subscribe_in(&style_editor, window, {
+ let rust_style = match self.reset_style_editors(&rust_style_buffer, &json_style_buffer, cx)
+ {
+ Ok(rust_style) => {
+ self.json_style_error = None;
+ rust_style
+ }
+ Err(err) => {
+ self.json_style_error = Some(format!("{err}").into());
+ return;
+ }
+ };
+
+ cx.subscribe_in(&json_style_editor, window, {
let id = id.clone();
+ let rust_style_buffer = rust_style_buffer.clone();
move |this, editor, event: &EditorEvent, window, cx| match event {
EditorEvent::BufferEdited => {
- let base_style_json = editor.read(cx).text(cx);
- match serde_json_lenient::from_str(&base_style_json) {
- Ok(new_base_style) => {
+ let style_json = editor.read(cx).text(cx);
+ match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
+ Ok(new_style) => {
+ let (rust_style, _) = this.style_from_rust_buffer_snapshot(
+ &rust_style_buffer.read(cx).snapshot(),
+ );
+
+ let mut unconvertible_plus_rust = this.unconvertible_style.clone();
+ unconvertible_plus_rust.refine(&rust_style);
+
+ // The serialization of `DefiniteLength::Fraction` does not perfectly
+ // roundtrip because with f32, `(x / 100.0 * 100.0) == x` is not always
+ // true (such as for `p_1_3`). This can cause these values to
+ // erroneously appear in `json_style_overrides` since they are not
+ // perfectly equal. Roundtripping before `subtract` fixes this.
+ unconvertible_plus_rust =
+ serde_json::to_string(&unconvertible_plus_rust)
+ .ok()
+ .and_then(|json| {
+ serde_json_lenient::from_str_lenient(&json).ok()
+ })
+ .unwrap_or(unconvertible_plus_rust);
+
+ this.json_style_overrides =
+ new_style.subtract(&unconvertible_plus_rust);
+
window.with_inspector_state::<DivInspectorState, _>(
Some(&id),
cx,
- |state, _window| {
- if let Some(state) = state.as_mut() {
- *state.base_style = new_base_style;
+ |inspector_state, _window| {
+ if let Some(inspector_state) = inspector_state.as_mut() {
+ *inspector_state.base_style = new_style;
}
},
);
window.refresh();
- this.last_error = None;
+ this.json_style_error = None;
}
- Err(err) => this.last_error = Some(err.to_string().into()),
+ Err(err) => this.json_style_error = Some(err.to_string().into()),
}
}
_ => {}
@@ -166,7 +246,262 @@ impl DivInspector {
})
.detach();
- self.style_editor = Some(style_editor);
+ cx.subscribe(&rust_style_editor, {
+ let json_style_buffer = json_style_buffer.clone();
+ let rust_style_buffer = rust_style_buffer.clone();
+ move |this, _editor, event: &EditorEvent, cx| match event {
+ EditorEvent::BufferEdited => {
+ this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
+ }
+ _ => {}
+ }
+ })
+ .detach();
+
+ self.unconvertible_style = style.subtract(&rust_style);
+ self.json_style_overrides = StyleRefinement::default();
+ self.state = State::Ready {
+ rust_style_buffer,
+ rust_style_editor,
+ json_style_buffer,
+ json_style_editor,
+ };
+ }
+
+ fn reset_style(&mut self, cx: &mut App) {
+ match &self.state {
+ State::Ready {
+ rust_style_buffer,
+ json_style_buffer,
+ ..
+ } => {
+ if let Err(err) = self.reset_style_editors(
+ &rust_style_buffer.clone(),
+ &json_style_buffer.clone(),
+ cx,
+ ) {
+ self.json_style_error = Some(format!("{err}").into());
+ } else {
+ self.json_style_error = None;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn reset_style_editors(
+ &self,
+ rust_style_buffer: &Entity<Buffer>,
+ json_style_buffer: &Entity<Buffer>,
+ cx: &mut App,
+ ) -> Result<StyleRefinement> {
+ let json_text = match serde_json::to_string_pretty(&self.initial_style) {
+ Ok(json_text) => json_text,
+ Err(err) => {
+ return Err(anyhow!("Failed to convert style to JSON: {err}"));
+ }
+ };
+
+ let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style);
+ rust_style_buffer.update(cx, |rust_style_buffer, cx| {
+ rust_style_buffer.set_text(rust_code, cx);
+ let snapshot = rust_style_buffer.snapshot();
+ let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
+ Self::set_rust_buffer_diagnostics(
+ unrecognized_ranges,
+ rust_style_buffer,
+ &snapshot,
+ cx,
+ );
+ });
+ json_style_buffer.update(cx, |json_style_buffer, cx| {
+ json_style_buffer.set_text(json_text, cx);
+ });
+
+ Ok(rust_style)
+ }
+
+ fn handle_rust_completion_selection_change(
+ &mut self,
+ rust_completion: Option<String>,
+ cx: &mut Context<Self>,
+ ) {
+ self.rust_completion = rust_completion;
+ if let State::Ready {
+ rust_style_buffer,
+ json_style_buffer,
+ ..
+ } = &self.state
+ {
+ self.update_json_style_from_rust(
+ &json_style_buffer.clone(),
+ &rust_style_buffer.clone(),
+ cx,
+ );
+ }
+ }
+
+ fn update_json_style_from_rust(
+ &mut self,
+ json_style_buffer: &Entity<Buffer>,
+ rust_style_buffer: &Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) {
+ let rust_style = rust_style_buffer.update(cx, |rust_style_buffer, cx| {
+ let snapshot = rust_style_buffer.snapshot();
+ let (rust_style, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
+ Self::set_rust_buffer_diagnostics(
+ unrecognized_ranges,
+ rust_style_buffer,
+ &snapshot,
+ cx,
+ );
+ rust_style
+ });
+
+ // Preserve parts of the json style which do not come from the unconvertible style or rust
+ // style. This way user edits to the json style are preserved when they are not overridden
+ // by the rust style.
+ //
+ // This results in a behavior where user changes to the json style that do overlap with the
+ // rust style will get set to the rust style when the user edits the rust style. It would be
+ // possible to update the rust style when the json style changes, but this is undesirable
+ // as the user may be working on the actual code in the rust style.
+ let mut new_style = self.unconvertible_style.clone();
+ new_style.refine(&self.json_style_overrides);
+ let new_style = new_style.refined(rust_style);
+
+ match serde_json::to_string_pretty(&new_style) {
+ Ok(json) => {
+ json_style_buffer.update(cx, |json_style_buffer, cx| {
+ json_style_buffer.set_text(json, cx);
+ });
+ }
+ Err(err) => {
+ self.json_style_error = Some(err.to_string().into());
+ }
+ }
+ }
+
+ fn style_from_rust_buffer_snapshot(
+ &self,
+ snapshot: &BufferSnapshot,
+ ) -> (StyleRefinement, Vec<Range<Anchor>>) {
+ let method_names = if let Some((completion, completion_range)) = self
+ .rust_completion
+ .as_ref()
+ .zip(self.rust_completion_replace_range.as_ref())
+ {
+ let before_text = snapshot
+ .text_for_range(0..completion_range.start.to_offset(&snapshot))
+ .collect::<String>();
+ let after_text = snapshot
+ .text_for_range(
+ completion_range.end.to_offset(&snapshot)
+ ..snapshot.clip_offset(usize::MAX, Bias::Left),
+ )
+ .collect::<String>();
+ let mut method_names = split_str_with_ranges(&before_text, is_not_identifier_char)
+ .into_iter()
+ .map(|(range, name)| (Some(range), name.to_string()))
+ .collect::<Vec<_>>();
+ method_names.push((None, completion.clone()));
+ method_names.extend(
+ split_str_with_ranges(&after_text, is_not_identifier_char)
+ .into_iter()
+ .map(|(range, name)| (Some(range), name.to_string())),
+ );
+ method_names
+ } else {
+ split_str_with_ranges(&snapshot.text(), is_not_identifier_char)
+ .into_iter()
+ .map(|(range, name)| (Some(range), name.to_string()))
+ .collect::<Vec<_>>()
+ };
+
+ let mut style = StyleRefinement::default();
+ let mut unrecognized_ranges = Vec::new();
+ for (range, name) in method_names {
+ if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
+ style = method.invoke(style);
+ } else if let Some(range) = range {
+ unrecognized_ranges
+ .push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
+ }
+ }
+
+ (style, unrecognized_ranges)
+ }
+
+ fn set_rust_buffer_diagnostics(
+ unrecognized_ranges: Vec<Range<Anchor>>,
+ rust_style_buffer: &mut Buffer,
+ snapshot: &BufferSnapshot,
+ cx: &mut Context<Buffer>,
+ ) {
+ let diagnostic_entries = unrecognized_ranges
+ .into_iter()
+ .enumerate()
+ .map(|(ix, range)| DiagnosticEntry {
+ range,
+ diagnostic: Diagnostic {
+ message: "unrecognized".to_string(),
+ severity: DiagnosticSeverity::WARNING,
+ is_primary: true,
+ group_id: ix,
+ ..Default::default()
+ },
+ });
+ let diagnostics = DiagnosticSet::from_sorted_entries(diagnostic_entries, snapshot);
+ rust_style_buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
+ }
+
+ async fn create_buffer_in_project(
+ path: impl AsRef<Path>,
+ project: &Entity<Project>,
+ cx: &mut AsyncWindowContext,
+ ) -> Result<Entity<Buffer>> {
+ let worktree = project
+ .update(cx, |project, cx| project.create_worktree(path, false, cx))?
+ .await?;
+
+ let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
+ worktree_id: worktree.id(),
+ path: Path::new("").into(),
+ })?;
+
+ let buffer = project
+ .update(cx, |project, cx| project.open_path(project_path, cx))?
+ .await?
+ .1;
+
+ Ok(buffer)
+ }
+
+ fn create_editor(
+ &self,
+ buffer: Entity<Buffer>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Entity<Editor> {
+ cx.new(|cx| {
+ let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+ let mut editor = Editor::new(
+ EditorMode::full(),
+ multi_buffer,
+ Some(self.project.clone()),
+ window,
+ cx,
+ );
+ editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor.set_show_line_numbers(false, cx);
+ editor.set_show_code_actions(false, cx);
+ editor.set_show_breakpoints(false, cx);
+ editor.set_show_git_diff_gutter(false, cx);
+ editor.set_show_runnables(false, cx);
+ editor.set_show_edit_predictions(Some(false), window, cx);
+ editor
+ })
}
}
@@ -175,49 +510,223 @@ impl Render for DivInspector {
v_flex()
.size_full()
.gap_2()
- .when_some(self.state.as_ref(), |this, state| {
+ .when_some(self.inspector_state.as_ref(), |this, inspector_state| {
this.child(
v_flex()
.child(Label::new("Layout").size(LabelSize::Large))
- .child(render_layout_state(state, cx)),
+ .child(render_layout_state(inspector_state, cx)),
)
})
- .when_some(self.style_editor.as_ref(), |this, style_editor| {
- this.child(
- v_flex()
- .gap_2()
- .child(Label::new("Style").size(LabelSize::Large))
- .child(div().h_128().child(style_editor.clone()))
- .when_some(self.last_error.as_ref(), |this, last_error| {
- this.child(
- div()
- .w_full()
- .border_1()
- .border_color(Color::Error.color(cx))
- .child(Label::new(last_error)),
+ .map(|this| match &self.state {
+ State::Loading | State::BuffersLoaded { .. } => {
+ this.child(Label::new("Loading..."))
+ }
+ State::LoadError { message } => this.child(
+ div()
+ .w_full()
+ .border_1()
+ .border_color(Color::Error.color(cx))
+ .child(Label::new(message)),
+ ),
+ State::Ready {
+ rust_style_editor,
+ json_style_editor,
+ ..
+ } => this
+ .child(
+ v_flex()
+ .gap_2()
+ .child(
+ h_flex()
+ .justify_between()
+ .child(Label::new("Rust Style").size(LabelSize::Large))
+ .child(
+ IconButton::new("reset-style", IconName::Eraser)
+ .tooltip(Tooltip::text("Reset style"))
+ .on_click(cx.listener(|this, _, _window, cx| {
+ this.reset_style(cx);
+ })),
+ ),
)
- }),
- )
- })
- .when_none(&self.style_editor, |this| {
- this.child(Label::new("Loading..."))
+ .child(div().h_64().child(rust_style_editor.clone())),
+ )
+ .child(
+ v_flex()
+ .gap_2()
+ .child(Label::new("JSON Style").size(LabelSize::Large))
+ .child(div().h_128().child(json_style_editor.clone()))
+ .when_some(self.json_style_error.as_ref(), |this, last_error| {
+ this.child(
+ div()
+ .w_full()
+ .border_1()
+ .border_color(Color::Error.color(cx))
+ .child(Label::new(last_error)),
+ )
+ }),
+ ),
})
.into_any_element()
}
}
-fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
+fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
v_flex()
- .child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
+ .child(
+ div()
+ .text_ui(cx)
+ .child(format!("Bounds: {}", inspector_state.bounds)),
+ )
.child(
div()
.id("content-size")
.text_ui(cx)
.tooltip(Tooltip::text("Size of the element's children"))
- .child(if state.content_size != state.bounds.size {
- format!("Content size: {}", state.content_size)
- } else {
- "".to_string()
- }),
+ .child(
+ if inspector_state.content_size != inspector_state.bounds.size {
+ format!("Content size: {}", inspector_state.content_size)
+ } else {
+ "".to_string()
+ },
+ ),
)
}
+
+static STYLE_METHODS: LazyLock<Vec<(Box<StyleRefinement>, FunctionReflection<StyleRefinement>)>> =
+ LazyLock::new(|| {
+ // Include StyledExt methods first so that those methods take precedence.
+ styled_ext_reflection::methods::<StyleRefinement>()
+ .into_iter()
+ .chain(styled_reflection::methods::<StyleRefinement>())
+ .map(|method| (Box::new(method.invoke(StyleRefinement::default())), method))
+ .collect()
+ });
+
+fn guess_rust_code_from_style(goal_style: &StyleRefinement) -> (String, StyleRefinement) {
+ let mut subset_methods = Vec::new();
+ for (style, method) in STYLE_METHODS.iter() {
+ if goal_style.is_superset_of(style) {
+ subset_methods.push(method);
+ }
+ }
+
+ let mut code = "fn build() -> Div {\n div()".to_string();
+ let mut style = StyleRefinement::default();
+ for method in subset_methods {
+ let before_change = style.clone();
+ style = method.invoke(style);
+ if before_change != style {
+ let _ = write!(code, "\n .{}()", &method.name);
+ }
+ }
+ code.push_str("\n}");
+
+ (code, style)
+}
+
+fn is_not_identifier_char(c: char) -> bool {
+ !c.is_alphanumeric() && c != '_'
+}
+
+struct RustStyleCompletionProvider {
+ div_inspector: Entity<DivInspector>,
+}
+
+impl CompletionProvider for RustStyleCompletionProvider {
+ fn completions(
+ &self,
+ _excerpt_id: ExcerptId,
+ buffer: &Entity<Buffer>,
+ position: Anchor,
+ _: editor::CompletionContext,
+ _window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> Task<Result<Option<Vec<project::Completion>>>> {
+ let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
+ else {
+ return Task::ready(Ok(Some(Vec::new())));
+ };
+
+ self.div_inspector.update(cx, |div_inspector, _cx| {
+ div_inspector.rust_completion_replace_range = Some(replace_range.clone());
+ });
+
+ Task::ready(Ok(Some(
+ STYLE_METHODS
+ .iter()
+ .map(|(_, method)| Completion {
+ replace_range: replace_range.clone(),
+ new_text: format!(".{}()", method.name),
+ label: CodeLabel::plain(method.name.to_string(), None),
+ icon_path: None,
+ documentation: method.documentation.map(|documentation| {
+ CompletionDocumentation::MultiLineMarkdown(documentation.into())
+ }),
+ source: CompletionSource::Custom,
+ insert_text_mode: None,
+ confirm: None,
+ })
+ .collect(),
+ )))
+ }
+
+ fn resolve_completions(
+ &self,
+ _buffer: Entity<Buffer>,
+ _completion_indices: Vec<usize>,
+ _completions: Rc<RefCell<Box<[Completion]>>>,
+ _cx: &mut Context<Editor>,
+ ) -> Task<Result<bool>> {
+ Task::ready(Ok(true))
+ }
+
+ fn is_completion_trigger(
+ &self,
+ buffer: &Entity<language::Buffer>,
+ position: language::Anchor,
+ _: &str,
+ _: bool,
+ cx: &mut Context<Editor>,
+ ) -> bool {
+ completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()
+ }
+
+ fn selection_changed(&self, mat: Option<&StringMatch>, _window: &mut Window, cx: &mut App) {
+ let div_inspector = self.div_inspector.clone();
+ let rust_completion = mat.as_ref().map(|mat| mat.string.clone());
+ cx.defer(move |cx| {
+ div_inspector.update(cx, |div_inspector, cx| {
+ div_inspector.handle_rust_completion_selection_change(rust_completion, cx);
+ });
+ });
+ }
+
+ fn sort_completions(&self) -> bool {
+ false
+ }
+}
+
+fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
+ let point = anchor.to_point(&snapshot);
+ let offset = point.to_offset(&snapshot);
+ let line_start = Point::new(point.row, 0).to_offset(&snapshot);
+ let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot);
+ let mut lines = snapshot.text_for_range(line_start..line_end).lines();
+ let line = lines.next()?;
+
+ let start_in_line = &line[..offset - line_start]
+ .rfind(|c| is_not_identifier_char(c) && c != '.')
+ .map(|ix| ix + 1)
+ .unwrap_or(0);
+ let end_in_line = &line[offset - line_start..]
+ .rfind(|c| is_not_identifier_char(c) && c != '(' && c != ')')
+ .unwrap_or(line_end - line_start);
+
+ if end_in_line > start_in_line {
+ let replace_start = snapshot.anchor_before(line_start + start_in_line);
+ let replace_end = snapshot.anchor_before(line_start + end_in_line);
+ Some(replace_start..replace_end)
+ } else {
+ None
+ }
+}
@@ -24,7 +24,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
});
});
- // Project used for editor buffers + LSP support
+ // Project used for editor buffers with LSP support
let project = project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
@@ -57,14 +57,12 @@ fn render_inspector(
let colors = cx.theme().colors();
let inspector_id = inspector.active_element_id();
v_flex()
- .id("gpui-inspector")
.size_full()
.bg(colors.panel_background)
.text_color(colors.text)
.font(ui_font)
.border_l_1()
.border_color(colors.border)
- .overflow_y_scroll()
.child(
h_flex()
.p_2()
@@ -89,6 +87,8 @@ fn render_inspector(
)
.child(
v_flex()
+ .id("gpui-inspector-content")
+ .overflow_y_scroll()
.p_2()
.gap_2()
.when_some(inspector_id, |this, inspector_id| {
@@ -101,26 +101,32 @@ fn render_inspector(
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
let source_location = inspector_id.path.source_location;
+ // For unknown reasons, for some elements the path is absolute.
+ let source_location_string = source_location.to_string();
+ let source_location_string = source_location_string
+ .strip_prefix(env!("ZED_REPO_DIR"))
+ .and_then(|s| s.strip_prefix("/"))
+ .map(|s| s.to_string())
+ .unwrap_or(source_location_string);
+
v_flex()
.child(Label::new("Element ID").size(LabelSize::Large))
- .when(inspector_id.instance_id != 0, |this| {
- this.child(
- div()
- .id("instance-id")
- .text_ui(cx)
- .tooltip(Tooltip::text(
- "Disambiguates elements from the same source location",
- ))
- .child(format!("Instance {}", inspector_id.instance_id)),
- )
- })
+ .child(
+ div()
+ .id("instance-id")
+ .text_ui(cx)
+ .tooltip(Tooltip::text(
+ "Disambiguates elements from the same source location",
+ ))
+ .child(format!("Instance {}", inspector_id.instance_id)),
+ )
.child(
div()
.id("source-location")
.text_ui(cx)
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.underline()
- .child(format!("{}", source_location))
+ .child(source_location_string)
.tooltip(Tooltip::text("Click to open by running zed cli"))
.on_click(move |_, _window, cx| {
cx.background_spawn(open_zed_source_location(source_location))
@@ -131,7 +137,7 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
div()
.id("global-id")
.text_ui(cx)
- .min_h_12()
+ .min_h_20()
.tooltip(Tooltip::text(
"GlobalElementId of the nearest ancestor with an ID",
))
@@ -66,7 +66,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
})
.collect();
- // Create trait bound that each wrapped type must implement Clone // & Default
+ // Create trait bound that each wrapped type must implement Clone
let type_param_bounds: Vec<_> = wrapped_types
.iter()
.map(|ty| {
@@ -273,6 +273,116 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
})
.collect();
+ let refineable_is_superset_conditions: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+ let is_optional = is_optional_field(field);
+
+ if is_refineable {
+ quote! {
+ if !self.#name.is_superset_of(&refinement.#name) {
+ return false;
+ }
+ }
+ } else if is_optional {
+ quote! {
+ if refinement.#name.is_some() && &self.#name != &refinement.#name {
+ return false;
+ }
+ }
+ } else {
+ quote! {
+ if let Some(refinement_value) = &refinement.#name {
+ if &self.#name != refinement_value {
+ return false;
+ }
+ }
+ }
+ }
+ })
+ .collect();
+
+ let refinement_is_superset_conditions: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+
+ if is_refineable {
+ quote! {
+ if !self.#name.is_superset_of(&refinement.#name) {
+ return false;
+ }
+ }
+ } else {
+ quote! {
+ if refinement.#name.is_some() && &self.#name != &refinement.#name {
+ return false;
+ }
+ }
+ }
+ })
+ .collect();
+
+ let refineable_subtract_assignments: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+ let is_optional = is_optional_field(field);
+
+ if is_refineable {
+ quote! {
+ #name: self.#name.subtract(&refinement.#name),
+ }
+ } else if is_optional {
+ quote! {
+ #name: if &self.#name == &refinement.#name {
+ None
+ } else {
+ self.#name.clone()
+ },
+ }
+ } else {
+ quote! {
+ #name: if let Some(refinement_value) = &refinement.#name {
+ if &self.#name == refinement_value {
+ None
+ } else {
+ Some(self.#name.clone())
+ }
+ } else {
+ Some(self.#name.clone())
+ },
+ }
+ }
+ })
+ .collect();
+
+ let refinement_subtract_assignments: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+
+ if is_refineable {
+ quote! {
+ #name: self.#name.subtract(&refinement.#name),
+ }
+ } else {
+ quote! {
+ #name: if &self.#name == &refinement.#name {
+ None
+ } else {
+ self.#name.clone()
+ },
+ }
+ }
+ })
+ .collect();
+
let mut derive_stream = quote! {};
for trait_to_derive in refinement_traits_to_derive {
derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
@@ -303,6 +413,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refineable_refined_assignments )*
self
}
+
+ fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
+ {
+ #( #refineable_is_superset_conditions )*
+ true
+ }
+
+ fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
+ {
+ #refinement_ident {
+ #( #refineable_subtract_assignments )*
+ }
+ }
}
impl #impl_generics Refineable for #refinement_ident #ty_generics
@@ -318,6 +441,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refinement_refined_assignments )*
self
}
+
+ fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
+ {
+ #( #refinement_is_superset_conditions )*
+ true
+ }
+
+ fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
+ {
+ #refinement_ident {
+ #( #refinement_subtract_assignments )*
+ }
+ }
}
impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics
@@ -1,23 +1,120 @@
pub use derive_refineable::Refineable;
+/// A trait for types that can be refined with partial updates.
+///
+/// The `Refineable` trait enables hierarchical configuration patterns where a base configuration
+/// can be selectively overridden by refinements. This is particularly useful for styling and
+/// settings, and theme hierarchies.
+///
+/// # Derive Macro
+///
+/// The `#[derive(Refineable)]` macro automatically generates a companion refinement type and
+/// implements this trait. For a struct `Style`, it creates `StyleRefinement` where each field is
+/// wrapped appropriately:
+///
+/// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type
+/// (e.g., `Bar` becomes `BarRefinement`)
+/// - **Optional fields** (`Option<T>`): Remain as `Option<T>`
+/// - **Regular fields**: Become `Option<T>`
+///
+/// ## Example
+///
+/// ```rust
+/// #[derive(Refineable, Clone, Default)]
+/// struct Example {
+/// color: String,
+/// font_size: Option<u32>,
+/// #[refineable]
+/// margin: Margin,
+/// }
+///
+/// #[derive(Refineable, Clone, Default)]
+/// struct Margin {
+/// top: u32,
+/// left: u32,
+/// }
+///
+///
+/// fn example() {
+/// let mut example = Example::default();
+/// let refinement = ExampleRefinement {
+/// color: Some("red".to_string()),
+/// font_size: None,
+/// margin: MarginRefinement {
+/// top: Some(10),
+/// left: None,
+/// },
+/// };
+///
+/// base_style.refine(&refinement);
+/// }
+/// ```
+///
+/// This generates `ExampleRefinement` with:
+/// - `color: Option<String>`
+/// - `font_size: Option<u32>` (unchanged)
+/// - `margin: MarginRefinement`
+///
+/// ## Attributes
+///
+/// The derive macro supports these attributes on the struct:
+/// - `#[refineable(Debug)]`: Implements `Debug` for the refinement type
+/// - `#[refineable(Serialize)]`: Derives `Serialize` which skips serializing `None`
+/// - `#[refineable(OtherTrait)]`: Derives additional traits on the refinement type
+///
+/// Fields can be marked with:
+/// - `#[refineable]`: Field is itself refineable (uses nested refinement type)
pub trait Refineable: Clone {
type Refinement: Refineable<Refinement = Self::Refinement> + IsEmpty + Default;
+ /// Applies the given refinement to this instance, modifying it in place.
+ ///
+ /// Only non-empty values in the refinement are applied.
+ ///
+ /// * For refineable fields, this recursively calls `refine`.
+ /// * For other fields, the value is replaced if present in the refinement.
fn refine(&mut self, refinement: &Self::Refinement);
+
+ /// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling
+ /// `refine` on it.
fn refined(self, refinement: Self::Refinement) -> Self;
+
+ /// Creates an instance from a cascade by merging all refinements atop the default value.
fn from_cascade(cascade: &Cascade<Self>) -> Self
where
Self: Default + Sized,
{
Self::default().refined(cascade.merged())
}
+
+ /// Returns `true` if this instance would contain all values from the refinement.
+ ///
+ /// For refineable fields, this recursively checks `is_superset_of`. For other fields, this
+ /// checks if the refinement's `Some` values match this instance's values.
+ fn is_superset_of(&self, refinement: &Self::Refinement) -> bool;
+
+ /// Returns a refinement that represents the difference between this instance and the given
+ /// refinement.
+ ///
+ /// For refineable fields, this recursively calls `subtract`. For other fields, the field is
+ /// `None` if the field's value is equal to the refinement.
+ fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement;
}
pub trait IsEmpty {
- /// When `true`, indicates that use applying this refinement does nothing.
+ /// Returns `true` if applying this refinement would have no effect.
fn is_empty(&self) -> bool;
}
+/// A cascade of refinements that can be merged in priority order.
+///
+/// A cascade maintains a sequence of optional refinements where later entries
+/// take precedence over earlier ones. The first slot (index 0) is always the
+/// base refinement and is guaranteed to be present.
+///
+/// This is useful for implementing configuration hierarchies like CSS cascading,
+/// where styles from different sources (user agent, user, author) are combined
+/// with specific precedence rules.
pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
impl<S: Refineable + Default> Default for Cascade<S> {
@@ -26,23 +123,43 @@ impl<S: Refineable + Default> Default for Cascade<S> {
}
}
+/// A handle to a specific slot in a cascade.
+///
+/// Slots are used to identify specific positions in the cascade where
+/// refinements can be set or updated.
#[derive(Copy, Clone)]
pub struct CascadeSlot(usize);
impl<S: Refineable + Default> Cascade<S> {
+ /// Reserves a new slot in the cascade and returns a handle to it.
+ ///
+ /// The new slot is initially empty (`None`) and can be populated later
+ /// using `set()`.
pub fn reserve(&mut self) -> CascadeSlot {
self.0.push(None);
CascadeSlot(self.0.len() - 1)
}
+ /// Returns a mutable reference to the base refinement (slot 0).
+ ///
+ /// The base refinement is always present and serves as the foundation
+ /// for the cascade.
pub fn base(&mut self) -> &mut S::Refinement {
self.0[0].as_mut().unwrap()
}
+ /// Sets the refinement for a specific slot in the cascade.
+ ///
+ /// Setting a slot to `None` effectively removes it from consideration
+ /// during merging.
pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
self.0[slot.0] = refinement
}
+ /// Merges all refinements in the cascade into a single refinement.
+ ///
+ /// Refinements are applied in order, with later slots taking precedence.
+ /// Empty slots (`None`) are skipped during merging.
pub fn merged(&self) -> S::Refinement {
let mut merged = self.0[0].clone().unwrap();
for refinement in self.0.iter().skip(1).flatten() {
@@ -15,6 +15,7 @@ use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel;
use rope::Rope;
use settings::Settings;
+use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
@@ -70,7 +71,7 @@ pub trait InlineAssistDelegate {
pub fn open_rules_library(
language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
+ make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
prompt_to_select: Option<PromptId>,
cx: &mut App,
) -> Task<Result<WindowHandle<RulesLibrary>>> {
@@ -146,7 +147,7 @@ pub struct RulesLibrary {
picker: Entity<Picker<RulePickerDelegate>>,
pending_load: Task<()>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
+ make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
_subscriptions: Vec<Subscription>,
}
@@ -349,7 +350,7 @@ impl RulesLibrary {
store: Entity<PromptStore>,
language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
+ make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
rule_to_select: Option<PromptId>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -17,6 +17,7 @@ chrono.workspace = true
component.workspace = true
documented.workspace = true
gpui.workspace = true
+gpui_macros.workspace = true
icons.workspace = true
itertools.workspace = true
menu.workspace = true
@@ -18,6 +18,7 @@ fn elevated_borderless<E: Styled>(this: E, cx: &mut App, index: ElevationIndex)
}
/// Extends [`gpui::Styled`] with Zed-specific styling methods.
+#[cfg_attr(debug_assertions, gpui_macros::derive_inspector_reflection)]
pub trait StyledExt: Styled + Sized {
/// Horizontally stacks elements.
///
@@ -1213,6 +1213,28 @@ pub fn word_consists_of_emojis(s: &str) -> bool {
prev_end == s.len()
}
+/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike
+/// `str::split`, this is not generic on pattern types and does not return an `Iterator`.
+pub fn split_str_with_ranges(s: &str, pat: impl Fn(char) -> bool) -> Vec<(Range<usize>, &str)> {
+ let mut result = Vec::new();
+ let mut start = 0;
+
+ for (i, ch) in s.char_indices() {
+ if pat(ch) {
+ if i > start {
+ result.push((start..i, &s[start..i]));
+ }
+ start = i + ch.len_utf8();
+ }
+ }
+
+ if s.len() > start {
+ result.push((start..s.len(), &s[start..s.len()]));
+ }
+
+ result
+}
+
pub fn default<D: Default>() -> D {
Default::default()
}
@@ -1639,4 +1661,20 @@ Line 3"#
"θΏζ―δ»\nδΉ ι’\nη¬"
);
}
+
+ #[test]
+ fn test_split_with_ranges() {
+ let input = "hi";
+ let result = split_str_with_ranges(input, |c| c == ' ');
+
+ assert_eq!(result.len(), 1);
+ assert_eq!(result[0], (0..2, "hi"));
+
+ let input = "hΓ©lloπ¦world";
+ let result = split_str_with_ranges(input, |c| c == 'π¦');
+
+ assert_eq!(result.len(), 2);
+ assert_eq!(result[0], (0..6, "hΓ©llo")); // 'Γ©' is 2 bytes
+ assert_eq!(result[1], (10..15, "world")); // 'π¦' is 4 bytes
+ }
}