Add a live Rust style editor to inspector to edit a sequence of no-argument style modifiers (#31443)
Editing JSON styles is not very helpful for bringing style changes back to the actual code. This PR adds a buffer that pretends to be Rust, applying any style attribute identifiers it finds. Also supports completions with display of documentation. The effect of the currently selected completion is previewed. Warning diagnostics appear on any unrecognized identifier. https://github.com/user-attachments/assets/af39ff0a-26a5-4835-a052-d8f642b2080c Adds a `#[derive_inspector_reflection]` macro which allows these methods to be enumerated and called by their name. The macro code changes were 95% generated by Zed Agent + Opus 4. Release Notes: * Added an element inspector for development. On debug builds, `dev::ToggleInspector` will open a pane allowing inspecting of element info and modifying styles.
This commit is contained in:
parent
6253b95f82
commit
649072d140
35 changed files with 1778 additions and 316 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -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"] }
|
||||
|
|
307
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal file
307
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal file
|
@ -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
|
||||
|
|
148
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal file
148
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal file
|
@ -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,32 +67,402 @@ 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>,
|
||||
pub fn update_inspected_element(
|
||||
&mut self,
|
||||
id: &InspectorElementId,
|
||||
inspector_state: DivInspectorState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let style = (*inspector_state.base_style).clone();
|
||||
self.inspector_state = Some(inspector_state);
|
||||
|
||||
if self.inspector_id.as_ref() == Some(id) {
|
||||
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,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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 },
|
||||
)));
|
||||
}
|
||||
});
|
||||
|
||||
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 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,
|
||||
|inspector_state, _window| {
|
||||
if let Some(inspector_state) = inspector_state.as_mut() {
|
||||
*inspector_state.base_style = new_style;
|
||||
}
|
||||
},
|
||||
);
|
||||
window.refresh();
|
||||
this.json_style_error = None;
|
||||
}
|
||||
Err(err) => this.json_style_error = Some(err.to_string().into()),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
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<()> {
|
||||
) -> Result<Entity<Buffer>> {
|
||||
let worktree = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
|
||||
})?
|
||||
.update(cx, |project, cx| project.create_worktree(path, false, cx))?
|
||||
.await?;
|
||||
|
||||
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
|
||||
|
@ -62,66 +470,22 @@ impl DivInspector {
|
|||
path: Path::new("").into(),
|
||||
})?;
|
||||
|
||||
let style_buffer = project
|
||||
let 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(())
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub fn update_inspected_element(
|
||||
&mut self,
|
||||
id: &InspectorElementId,
|
||||
state: DivInspectorState,
|
||||
fn create_editor(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let base_style_json = serde_json::to_string_pretty(&state.base_style);
|
||||
self.state = Some(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.last_error = None;
|
||||
|
||||
style_buffer.update(cx, |style_buffer, cx| {
|
||||
style_buffer.set_text(base_style_json, cx)
|
||||
});
|
||||
|
||||
let style_editor = cx.new(|cx| {
|
||||
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
|
||||
) -> Entity<Editor> {
|
||||
cx.new(|cx| {
|
||||
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
|
@ -137,36 +501,7 @@ impl DivInspector {
|
|||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
cx.subscribe_in(&style_editor, window, {
|
||||
let id = id.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) => {
|
||||
window.with_inspector_state::<DivInspectorState, _>(
|
||||
Some(&id),
|
||||
cx,
|
||||
|state, _window| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
*state.base_style = new_base_style;
|
||||
}
|
||||
},
|
||||
);
|
||||
window.refresh();
|
||||
this.last_error = None;
|
||||
}
|
||||
Err(err) => this.last_error = Some(err.to_string().into()),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.style_editor = Some(style_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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue