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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
|
@ -8162,6 +8163,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
"editor",
|
"editor",
|
||||||
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"project",
|
"project",
|
||||||
|
@ -16827,6 +16829,7 @@ dependencies = [
|
||||||
"component",
|
"component",
|
||||||
"documented",
|
"documented",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"gpui_macros",
|
||||||
"icons",
|
"icons",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"menu",
|
"menu",
|
||||||
|
|
|
@ -676,6 +676,7 @@
|
||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||||
|
// Only available in debug builds: opens an element inspector for development.
|
||||||
"ctrl-alt-i": "dev::ToggleInspector"
|
"ctrl-alt-i": "dev::ToggleInspector"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -736,6 +736,7 @@
|
||||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||||
// TODO: Move this to a dock open action
|
// TODO: Move this to a dock open action
|
||||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||||
|
// Only available in debug builds: opens an element inspector for development.
|
||||||
"cmd-alt-i": "dev::ToggleInspector"
|
"cmd-alt-i": "dev::ToggleInspector"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -915,8 +916,8 @@ impl AgentPanel {
|
||||||
open_rules_library(
|
open_rules_library(
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
|
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
|
||||||
Arc::new(|| {
|
Rc::new(|| {
|
||||||
Box::new(SlashCommandCompletionProvider::new(
|
Rc::new(SlashCommandCompletionProvider::new(
|
||||||
Arc::new(SlashCommandWorkingSet::default()),
|
Arc::new(SlashCommandWorkingSet::default()),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -1289,7 +1289,7 @@ mod tests {
|
||||||
.map(Entity::downgrade)
|
.map(Entity::downgrade)
|
||||||
});
|
});
|
||||||
window.focus(&editor.focus_handle(cx));
|
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(),
|
workspace.downgrade(),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -28,6 +28,7 @@ use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::utils::WithRemSize;
|
use ui::utils::WithRemSize;
|
||||||
|
@ -890,7 +891,7 @@ impl PromptEditor<BufferCodegen> {
|
||||||
|
|
||||||
let prompt_editor_entity = prompt_editor.downgrade();
|
let prompt_editor_entity = prompt_editor.downgrade();
|
||||||
prompt_editor.update(cx, |editor, _| {
|
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(),
|
workspace.clone(),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
|
@ -1061,7 +1062,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
|
|
||||||
let prompt_editor_entity = prompt_editor.downgrade();
|
let prompt_editor_entity = prompt_editor.downgrade();
|
||||||
prompt_editor.update(cx, |editor, _| {
|
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(),
|
workspace.clone(),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||||
|
@ -121,7 +122,7 @@ pub(crate) fn create_editor(
|
||||||
|
|
||||||
let editor_entity = editor.downgrade();
|
let editor_entity = editor.downgrade();
|
||||||
editor.update(cx, |editor, _| {
|
editor.update(cx, |editor, _| {
|
||||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
context_store,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
|
|
|
@ -51,6 +51,7 @@ use std::{
|
||||||
cmp,
|
cmp,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
@ -234,7 +235,7 @@ impl ContextEditor {
|
||||||
editor.set_show_breakpoints(false, cx);
|
editor.set_show_breakpoints(false, cx);
|
||||||
editor.set_show_wrap_guides(false, cx);
|
editor.set_show_wrap_guides(false, cx);
|
||||||
editor.set_show_indent_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_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
|
||||||
editor.set_collaboration_hub(Box::new(project.clone()));
|
editor.set_collaboration_hub(Box::new(project.clone()));
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ impl MessageEditor {
|
||||||
editor.set_show_gutter(false, cx);
|
editor.set_show_gutter(false, cx);
|
||||||
editor.set_show_wrap_guides(false, cx);
|
editor.set_show_wrap_guides(false, cx);
|
||||||
editor.set_show_indent_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(
|
editor.set_auto_replace_emoji_shortcode(
|
||||||
MessageEditorSettings::get_global(cx)
|
MessageEditorSettings::get_global(cx)
|
||||||
.auto_replace_emoji_shortcode
|
.auto_replace_emoji_shortcode
|
||||||
|
|
|
@ -72,7 +72,7 @@ impl Console {
|
||||||
editor.set_show_gutter(false, cx);
|
editor.set_show_gutter(false, cx);
|
||||||
editor.set_show_wrap_guides(false, cx);
|
editor.set_show_wrap_guides(false, cx);
|
||||||
editor.set_show_indent_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
|
editor
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior,
|
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
|
||||||
ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, UniformListScrollHandle,
|
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
|
||||||
div, px, uniform_list,
|
|
||||||
};
|
};
|
||||||
|
use gpui::{AsyncWindowContext, WeakEntity};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language::CodeLabel;
|
use language::CodeLabel;
|
||||||
use markdown::{Markdown, MarkdownElement};
|
use markdown::{Markdown, MarkdownElement};
|
||||||
|
@ -50,11 +50,12 @@ impl CodeContextMenu {
|
||||||
pub fn select_first(
|
pub fn select_first(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.visible() {
|
if self.visible() {
|
||||||
match self {
|
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),
|
CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -66,11 +67,12 @@ impl CodeContextMenu {
|
||||||
pub fn select_prev(
|
pub fn select_prev(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.visible() {
|
if self.visible() {
|
||||||
match self {
|
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),
|
CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -82,11 +84,12 @@ impl CodeContextMenu {
|
||||||
pub fn select_next(
|
pub fn select_next(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.visible() {
|
if self.visible() {
|
||||||
match self {
|
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),
|
CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -98,11 +101,12 @@ impl CodeContextMenu {
|
||||||
pub fn select_last(
|
pub fn select_last(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.visible() {
|
if self.visible() {
|
||||||
match self {
|
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),
|
CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -290,6 +294,7 @@ impl CompletionsMenu {
|
||||||
fn select_first(
|
fn select_first(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
let index = if self.scroll_handle.y_flipped() {
|
let index = if self.scroll_handle.y_flipped() {
|
||||||
|
@ -297,40 +302,56 @@ impl CompletionsMenu {
|
||||||
} else {
|
} else {
|
||||||
0
|
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() {
|
let index = if self.scroll_handle.y_flipped() {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
self.entries.borrow().len() - 1
|
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() {
|
let index = if self.scroll_handle.y_flipped() {
|
||||||
self.next_match_index()
|
self.next_match_index()
|
||||||
} else {
|
} else {
|
||||||
self.prev_match_index()
|
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() {
|
let index = if self.scroll_handle.y_flipped() {
|
||||||
self.prev_match_index()
|
self.prev_match_index()
|
||||||
} else {
|
} else {
|
||||||
self.next_match_index()
|
self.next_match_index()
|
||||||
};
|
};
|
||||||
self.update_selection_index(index, provider, cx);
|
self.update_selection_index(index, provider, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_selection_index(
|
fn update_selection_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
match_index: usize,
|
match_index: usize,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
if self.selected_item != match_index {
|
if self.selected_item != match_index {
|
||||||
|
@ -338,6 +359,9 @@ impl CompletionsMenu {
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||||
self.resolve_visible_completions(provider, cx);
|
self.resolve_visible_completions(provider, cx);
|
||||||
|
if let Some(provider) = provider {
|
||||||
|
self.handle_selection_changed(provider, window, cx);
|
||||||
|
}
|
||||||
cx.notify();
|
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(
|
pub fn resolve_visible_completions(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
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 {
|
let mut matches = if let Some(query) = query {
|
||||||
fuzzy::match_strings(
|
fuzzy::match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
|
@ -761,7 +806,7 @@ impl CompletionsMenu {
|
||||||
query.chars().any(|c| c.is_uppercase()),
|
query.chars().any(|c| c.is_uppercase()),
|
||||||
100,
|
100,
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor,
|
cx.background_executor().clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
|
@ -822,6 +867,28 @@ impl CompletionsMenu {
|
||||||
self.selected_item = 0;
|
self.selected_item = 0;
|
||||||
// This keeps the display consistent when y_flipped.
|
// This keeps the display consistent when y_flipped.
|
||||||
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
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,
|
FutureExt,
|
||||||
future::{self, Shared, join},
|
future::{self, Shared, join},
|
||||||
};
|
};
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
|
|
||||||
use ::git::blame::BlameEntry;
|
use ::git::blame::BlameEntry;
|
||||||
use ::git::{Restore, blame::ParsedCommitMessage};
|
use ::git::{Restore, blame::ParsedCommitMessage};
|
||||||
|
@ -912,7 +912,7 @@ pub struct Editor {
|
||||||
// TODO: make this a access method
|
// TODO: make this a access method
|
||||||
pub project: Option<Entity<Project>>,
|
pub project: Option<Entity<Project>>,
|
||||||
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
|
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
|
||||||
completion_provider: Option<Box<dyn CompletionProvider>>,
|
completion_provider: Option<Rc<dyn CompletionProvider>>,
|
||||||
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||||
blink_manager: Entity<BlinkManager>,
|
blink_manager: Entity<BlinkManager>,
|
||||||
show_cursor_names: bool,
|
show_cursor_names: bool,
|
||||||
|
@ -1755,7 +1755,7 @@ impl Editor {
|
||||||
soft_wrap_mode_override,
|
soft_wrap_mode_override,
|
||||||
diagnostics_max_severity,
|
diagnostics_max_severity,
|
||||||
hard_wrap: None,
|
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 _),
|
semantics_provider: project.clone().map(|project| Rc::new(project) as _),
|
||||||
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
||||||
project,
|
project,
|
||||||
|
@ -2374,7 +2374,7 @@ impl Editor {
|
||||||
self.custom_context_menu = Some(Box::new(f))
|
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;
|
self.completion_provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2684,9 +2684,10 @@ impl Editor {
|
||||||
drop(context_menu);
|
drop(context_menu);
|
||||||
|
|
||||||
let query = Self::completion_query(buffer, cursor_position);
|
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
|
completion_menu
|
||||||
.filter(query.as_deref(), cx.background_executor().clone())
|
.filter(query.as_deref(), completion_provider, this.clone(), cx)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -4960,15 +4961,16 @@ impl Editor {
|
||||||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||||
..buffer_snapshot.point_to_offset(max_word_search);
|
..buffer_snapshot.point_to_offset(max_word_search);
|
||||||
|
|
||||||
let provider = self
|
let provider = if ignore_completion_provider {
|
||||||
.completion_provider
|
None
|
||||||
.as_ref()
|
} else {
|
||||||
.filter(|_| !ignore_completion_provider);
|
self.completion_provider.clone()
|
||||||
|
};
|
||||||
let skip_digits = query
|
let skip_digits = query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
|
.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) => {
|
Some(provider) => {
|
||||||
let completions = provider.completions(
|
let completions = provider.completions(
|
||||||
position.excerpt_id,
|
position.excerpt_id,
|
||||||
|
@ -5071,7 +5073,9 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
cx.background_executor().clone(),
|
provider,
|
||||||
|
editor.clone(),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -8651,6 +8655,11 @@ impl Editor {
|
||||||
let context_menu = self.context_menu.borrow_mut().take();
|
let context_menu = self.context_menu.borrow_mut().take();
|
||||||
self.stale_inline_completion_in_menu.take();
|
self.stale_inline_completion_in_menu.take();
|
||||||
self.update_visible_inline_completion(window, cx);
|
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
|
context_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11353,7 +11362,7 @@ impl Editor {
|
||||||
.context_menu
|
.context_menu
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.as_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)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -11477,7 +11486,7 @@ impl Editor {
|
||||||
.context_menu
|
.context_menu
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.as_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)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -11532,44 +11541,44 @@ impl Editor {
|
||||||
pub fn context_menu_first(
|
pub fn context_menu_first(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ContextMenuFirst,
|
_: &ContextMenuFirst,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
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(
|
pub fn context_menu_prev(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ContextMenuPrevious,
|
_: &ContextMenuPrevious,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
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(
|
pub fn context_menu_next(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ContextMenuNext,
|
_: &ContextMenuNext,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
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(
|
pub fn context_menu_last(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ContextMenuLast,
|
_: &ContextMenuLast,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
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>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool;
|
) -> bool;
|
||||||
|
|
||||||
|
fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {}
|
||||||
|
|
||||||
fn sort_completions(&self) -> bool {
|
fn sort_completions(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ test-support = [
|
||||||
"wayland",
|
"wayland",
|
||||||
"x11",
|
"x11",
|
||||||
]
|
]
|
||||||
inspector = []
|
inspector = ["gpui_macros/inspector"]
|
||||||
leak-detection = ["backtrace"]
|
leak-detection = ["backtrace"]
|
||||||
runtime_shaders = []
|
runtime_shaders = []
|
||||||
macos-blade = [
|
macos-blade = [
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::{
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct BoundsTree<U>
|
pub(crate) struct BoundsTree<U>
|
||||||
where
|
where
|
||||||
U: Default + Clone + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
root: Option<usize>,
|
root: Option<usize>,
|
||||||
nodes: Vec<Node<U>>,
|
nodes: Vec<Node<U>>,
|
||||||
|
@ -17,7 +17,14 @@ where
|
||||||
|
|
||||||
impl<U> BoundsTree<U>
|
impl<U> BoundsTree<U>
|
||||||
where
|
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) {
|
pub fn clear(&mut self) {
|
||||||
self.root = None;
|
self.root = None;
|
||||||
|
@ -174,7 +181,7 @@ where
|
||||||
|
|
||||||
impl<U> Default for BoundsTree<U>
|
impl<U> Default for BoundsTree<U>
|
||||||
where
|
where
|
||||||
U: Default + Clone + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
BoundsTree {
|
BoundsTree {
|
||||||
|
@ -188,7 +195,7 @@ where
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Node<U>
|
enum Node<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Leaf {
|
Leaf {
|
||||||
bounds: Bounds<U>,
|
bounds: Bounds<U>,
|
||||||
|
@ -204,7 +211,7 @@ where
|
||||||
|
|
||||||
impl<U> Node<U>
|
impl<U> Node<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn bounds(&self) -> &Bounds<U> {
|
fn bounds(&self) -> &Bounds<U> {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -76,9 +76,9 @@ pub trait Along {
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
Hash,
|
Hash,
|
||||||
)]
|
)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct Point<T: Default + Clone + Debug> {
|
pub struct Point<T: Clone + Debug + Default + PartialEq> {
|
||||||
/// The x coordinate of the point.
|
/// The x coordinate of the point.
|
||||||
pub x: T,
|
pub x: T,
|
||||||
/// The y coordinate of the point.
|
/// 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.x, 10);
|
||||||
/// assert_eq!(p.y, 20);
|
/// 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 }
|
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.
|
/// Creates a new `Point` with the specified `x` and `y` coordinates.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -145,7 +145,7 @@ impl<T: Clone + Debug + Default> Point<T> {
|
||||||
/// let p_float = p.map(|coord| coord as f32);
|
/// let p_float = p.map(|coord| coord as f32);
|
||||||
/// assert_eq!(p_float, Point { x: 3.0, y: 4.0 });
|
/// 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 {
|
Point {
|
||||||
x: f(self.x.clone()),
|
x: f(self.x.clone()),
|
||||||
y: f(self.y.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;
|
type Unit = T;
|
||||||
|
|
||||||
fn along(&self, axis: Axis) -> 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 {
|
fn negate(self) -> Self {
|
||||||
self.map(Negate::negate)
|
self.map(Negate::negate)
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@ impl Point<Pixels> {
|
||||||
|
|
||||||
impl<T> Point<T>
|
impl<T> Point<T>
|
||||||
where
|
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
|
/// Get the position of this point, relative to the given origin
|
||||||
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
|
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
|
||||||
|
@ -235,7 +235,7 @@ where
|
||||||
|
|
||||||
impl<T, Rhs> Mul<Rhs> for Point<T>
|
impl<T, Rhs> Mul<Rhs> for Point<T>
|
||||||
where
|
where
|
||||||
T: Mul<Rhs, Output = T> + Clone + Default + Debug,
|
T: Mul<Rhs, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
Rhs: Clone + Debug,
|
Rhs: Clone + Debug,
|
||||||
{
|
{
|
||||||
type Output = Point<T>;
|
type Output = Point<T>;
|
||||||
|
@ -250,7 +250,7 @@ where
|
||||||
|
|
||||||
impl<T, S> MulAssign<S> for Point<T>
|
impl<T, S> MulAssign<S> for Point<T>
|
||||||
where
|
where
|
||||||
T: Clone + Mul<S, Output = T> + Default + Debug,
|
T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
fn mul_assign(&mut self, rhs: S) {
|
fn mul_assign(&mut self, rhs: S) {
|
||||||
|
@ -261,7 +261,7 @@ where
|
||||||
|
|
||||||
impl<T, S> Div<S> for Point<T>
|
impl<T, S> Div<S> for Point<T>
|
||||||
where
|
where
|
||||||
T: Div<S, Output = T> + Clone + Default + Debug,
|
T: Div<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
@ -276,7 +276,7 @@ where
|
||||||
|
|
||||||
impl<T> Point<T>
|
impl<T> Point<T>
|
||||||
where
|
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`.
|
/// 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 {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: self.x.clone(),
|
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "({}, {})", self.x, self.y)
|
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`.
|
/// 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.
|
/// 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)]
|
#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct Size<T: Clone + Default + Debug> {
|
pub struct Size<T: Clone + Debug + Default + PartialEq> {
|
||||||
/// The width component of the size.
|
/// The width component of the size.
|
||||||
pub width: T,
|
pub width: T,
|
||||||
/// The height component of the size.
|
/// The height component of the size.
|
||||||
pub height: T,
|
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`]
|
/// Create a new Size, a synonym for [`size`]
|
||||||
pub fn new(width: T, height: T) -> Self {
|
pub fn new(width: T, height: T) -> Self {
|
||||||
size(width, height)
|
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>
|
pub const fn size<T>(width: T, height: T) -> Size<T>
|
||||||
where
|
where
|
||||||
T: Clone + Default + Debug,
|
T: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Size { width, height }
|
Size { width, height }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Size<T>
|
impl<T> Size<T>
|
||||||
where
|
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>`.
|
/// 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>
|
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Size<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Size {
|
Size {
|
||||||
width: f(self.width.clone()),
|
width: f(self.width.clone()),
|
||||||
|
@ -462,7 +462,7 @@ where
|
||||||
|
|
||||||
impl<T> Size<T>
|
impl<T> Size<T>
|
||||||
where
|
where
|
||||||
T: Clone + Default + Debug + Half,
|
T: Clone + Debug + Default + PartialEq + Half,
|
||||||
{
|
{
|
||||||
/// Compute the center point of the size.g
|
/// Compute the center point of the size.g
|
||||||
pub fn center(&self) -> Point<T> {
|
pub fn center(&self) -> Point<T> {
|
||||||
|
@ -502,7 +502,7 @@ impl Size<Pixels> {
|
||||||
|
|
||||||
impl<T> Along for Size<T>
|
impl<T> Along for Size<T>
|
||||||
where
|
where
|
||||||
T: Clone + Default + Debug,
|
T: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Unit = T;
|
type Unit = T;
|
||||||
|
|
||||||
|
@ -530,7 +530,7 @@ where
|
||||||
|
|
||||||
impl<T> Size<T>
|
impl<T> Size<T>
|
||||||
where
|
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`.
|
/// 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>
|
impl<T> Sub for Size<T>
|
||||||
where
|
where
|
||||||
T: Sub<Output = T> + Clone + Default + Debug,
|
T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Size<T>;
|
type Output = Size<T>;
|
||||||
|
|
||||||
|
@ -609,7 +609,7 @@ where
|
||||||
|
|
||||||
impl<T> Add for Size<T>
|
impl<T> Add for Size<T>
|
||||||
where
|
where
|
||||||
T: Add<Output = T> + Clone + Default + Debug,
|
T: Add<Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Size<T>;
|
type Output = Size<T>;
|
||||||
|
|
||||||
|
@ -623,8 +623,8 @@ where
|
||||||
|
|
||||||
impl<T, Rhs> Mul<Rhs> for Size<T>
|
impl<T, Rhs> Mul<Rhs> for Size<T>
|
||||||
where
|
where
|
||||||
T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
|
T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
|
||||||
Rhs: Clone + Default + Debug,
|
Rhs: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Size<Rhs>;
|
type Output = Size<Rhs>;
|
||||||
|
|
||||||
|
@ -638,7 +638,7 @@ where
|
||||||
|
|
||||||
impl<T, S> MulAssign<S> for Size<T>
|
impl<T, S> MulAssign<S> for Size<T>
|
||||||
where
|
where
|
||||||
T: Mul<S, Output = T> + Clone + Default + Debug,
|
T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
fn mul_assign(&mut self, rhs: S) {
|
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>
|
impl<T> Debug for Size<T>
|
||||||
where
|
where
|
||||||
T: Clone + Default + Debug,
|
T: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height)
|
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{} × {}", self.width, self.height)
|
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 {
|
fn from(point: Point<T>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
width: point.x,
|
width: point.x,
|
||||||
|
@ -746,7 +746,7 @@ impl Size<Length> {
|
||||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||||
#[refineable(Debug)]
|
#[refineable(Debug)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct Bounds<T: Clone + Default + Debug> {
|
pub struct Bounds<T: Clone + Debug + Default + PartialEq> {
|
||||||
/// The origin point of this area.
|
/// The origin point of this area.
|
||||||
pub origin: Point<T>,
|
pub origin: Point<T>,
|
||||||
/// The size of the rectangle.
|
/// 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
|
/// 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 }
|
Bounds { origin, size }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -790,7 +793,7 @@ impl Bounds<Pixels> {
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
where
|
||||||
T: Clone + Debug + Default,
|
T: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
/// Creates a new `Bounds` with the specified origin and size.
|
/// Creates a new `Bounds` with the specified origin and size.
|
||||||
///
|
///
|
||||||
|
@ -809,7 +812,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Constructs a `Bounds` from two corner points: the top left and bottom right corners.
|
||||||
///
|
///
|
||||||
|
@ -875,7 +878,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Creates a new bounds centered at the given point.
|
||||||
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
|
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
|
||||||
|
@ -889,7 +892,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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`.
|
/// Checks if this `Bounds` intersects with another `Bounds`.
|
||||||
///
|
///
|
||||||
|
@ -937,7 +940,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Returns the center point of the bounds.
|
||||||
///
|
///
|
||||||
|
@ -970,7 +973,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Calculates the half perimeter of a rectangle defined by the bounds.
|
||||||
///
|
///
|
||||||
|
@ -997,7 +1000,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Dilates the bounds by a specified amount in all directions.
|
||||||
///
|
///
|
||||||
|
@ -1048,7 +1051,13 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// 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.
|
/// Calculates the intersection of two `Bounds` objects.
|
||||||
///
|
///
|
||||||
/// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect,
|
/// 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>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Computes the space available within outer bounds.
|
||||||
pub fn space_within(&self, outer: &Self) -> Edges<T> {
|
pub fn space_within(&self, outer: &Self) -> Edges<T> {
|
||||||
|
@ -1155,9 +1166,9 @@ where
|
||||||
|
|
||||||
impl<T, Rhs> Mul<Rhs> for Bounds<T>
|
impl<T, Rhs> Mul<Rhs> for Bounds<T>
|
||||||
where
|
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>>,
|
Point<T>: Mul<Rhs, Output = Point<Rhs>>,
|
||||||
Rhs: Clone + Default + Debug,
|
Rhs: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Bounds<Rhs>;
|
type Output = Bounds<Rhs>;
|
||||||
|
|
||||||
|
@ -1171,7 +1182,7 @@ where
|
||||||
|
|
||||||
impl<T, S> MulAssign<S> for Bounds<T>
|
impl<T, S> MulAssign<S> for Bounds<T>
|
||||||
where
|
where
|
||||||
T: Mul<S, Output = T> + Clone + Default + Debug,
|
T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
fn mul_assign(&mut self, rhs: S) {
|
fn mul_assign(&mut self, rhs: S) {
|
||||||
|
@ -1183,7 +1194,7 @@ where
|
||||||
impl<T, S> Div<S> for Bounds<T>
|
impl<T, S> Div<S> for Bounds<T>
|
||||||
where
|
where
|
||||||
Size<T>: Div<S, Output = Size<T>>,
|
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,
|
S: Clone,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
@ -1198,7 +1209,7 @@ where
|
||||||
|
|
||||||
impl<T> Add<Point<T>> for Bounds<T>
|
impl<T> Add<Point<T>> for Bounds<T>
|
||||||
where
|
where
|
||||||
T: Add<T, Output = T> + Default + Clone + Debug,
|
T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
@ -1212,7 +1223,7 @@ where
|
||||||
|
|
||||||
impl<T> Sub<Point<T>> for Bounds<T>
|
impl<T> Sub<Point<T>> for Bounds<T>
|
||||||
where
|
where
|
||||||
T: Sub<T, Output = T> + Default + Clone + Debug,
|
T: Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
@ -1226,7 +1237,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// Returns the top edge of the bounds.
|
||||||
///
|
///
|
||||||
|
@ -1365,7 +1376,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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.
|
/// 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>
|
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Bounds {
|
Bounds {
|
||||||
origin: self.origin.map(&f),
|
origin: self.origin.map(&f),
|
||||||
|
@ -1531,7 +1542,7 @@ where
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
where
|
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
|
/// Convert a point to the coordinate space defined by this Bounds
|
||||||
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
|
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
|
||||||
|
@ -1545,7 +1556,7 @@ where
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area.
|
/// 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.
|
/// Checks if the bounds represent an empty area.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # 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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
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.
|
/// 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);
|
/// assert_eq!(edges.left, 40.0);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
|
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct Edges<T: Clone + Default + Debug> {
|
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
|
||||||
/// The size of the top edge.
|
/// The size of the top edge.
|
||||||
pub top: T,
|
pub top: T,
|
||||||
/// The size of the right edge.
|
/// The size of the right edge.
|
||||||
|
@ -1689,7 +1700,7 @@ pub struct Edges<T: Clone + Default + Debug> {
|
||||||
|
|
||||||
impl<T> Mul for Edges<T>
|
impl<T> Mul for Edges<T>
|
||||||
where
|
where
|
||||||
T: Mul<Output = T> + Clone + Default + Debug,
|
T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
@ -1705,7 +1716,7 @@ where
|
||||||
|
|
||||||
impl<T, S> MulAssign<S> for Edges<T>
|
impl<T, S> MulAssign<S> for Edges<T>
|
||||||
where
|
where
|
||||||
T: Mul<S, Output = T> + Clone + Default + Debug,
|
T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
fn mul_assign(&mut self, rhs: S) {
|
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.
|
/// 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
|
/// 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>
|
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Edges<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Edges {
|
Edges {
|
||||||
top: f(&self.top),
|
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`.
|
/// 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)]
|
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[repr(C)]
|
#[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.
|
/// The value associated with the top left corner.
|
||||||
pub top_left: T,
|
pub top_left: T,
|
||||||
/// The value associated with the top right corner.
|
/// The value associated with the top right corner.
|
||||||
|
@ -2166,7 +2177,7 @@ pub struct Corners<T: Clone + Default + Debug> {
|
||||||
|
|
||||||
impl<T> Corners<T>
|
impl<T> Corners<T>
|
||||||
where
|
where
|
||||||
T: Clone + Default + Debug,
|
T: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
/// Constructs `Corners` where all sides are set to the same specified value.
|
/// 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.
|
/// Clamps corner radii to be less than or equal to half the shortest side of a quad.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # 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>`.
|
/// 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
|
/// 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>
|
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Corners<U>
|
||||||
where
|
where
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
Corners {
|
Corners {
|
||||||
top_left: f(&self.top_left),
|
top_left: f(&self.top_left),
|
||||||
|
@ -2388,7 +2399,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
|
||||||
|
|
||||||
impl<T> Mul for Corners<T>
|
impl<T> Mul for Corners<T>
|
||||||
where
|
where
|
||||||
T: Mul<Output = T> + Clone + Default + Debug,
|
T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
@ -2404,7 +2415,7 @@ where
|
||||||
|
|
||||||
impl<T, S> MulAssign<S> for Corners<T>
|
impl<T, S> MulAssign<S> for Corners<T>
|
||||||
where
|
where
|
||||||
T: Mul<S, Output = T> + Clone + Default + Debug,
|
T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
fn mul_assign(&mut self, rhs: S) {
|
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> {
|
impl From<f32> for Corners<Pixels> {
|
||||||
fn from(val: f32) -> Self {
|
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.
|
/// A length that can be defined in pixels, rems, percent of parent, or auto.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum Length {
|
pub enum Length {
|
||||||
/// A definite length specified either in pixels, rems, or as a fraction of the parent's size.
|
/// A definite length specified either in pixels, rems, or as a fraction of the parent's size.
|
||||||
Definite(DefiniteLength),
|
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 {
|
fn is_zero(&self) -> bool {
|
||||||
self.x.is_zero() && self.y.is_zero()
|
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>
|
impl<T> IsZero for Size<T>
|
||||||
where
|
where
|
||||||
T: IsZero + Default + Debug + Clone,
|
T: IsZero + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn is_zero(&self) -> bool {
|
fn is_zero(&self) -> bool {
|
||||||
self.width.is_zero() || self.height.is_zero()
|
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 {
|
fn is_zero(&self) -> bool {
|
||||||
self.size.is_zero()
|
self.size.is_zero()
|
||||||
}
|
}
|
||||||
|
@ -3795,7 +3806,7 @@ impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> {
|
||||||
|
|
||||||
impl<T> IsZero for Corners<T>
|
impl<T> IsZero for Corners<T>
|
||||||
where
|
where
|
||||||
T: IsZero + Clone + Default + Debug,
|
T: IsZero + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn is_zero(&self) -> bool {
|
fn is_zero(&self) -> bool {
|
||||||
self.top_left.is_zero()
|
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.
|
/// A line made up of a series of vertices and control points.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Path<P: Clone + Default + Debug> {
|
pub struct Path<P: Clone + Debug + Default + PartialEq> {
|
||||||
pub(crate) id: PathId,
|
pub(crate) id: PathId,
|
||||||
order: DrawOrder,
|
order: DrawOrder,
|
||||||
pub(crate) bounds: Bounds<P>,
|
pub(crate) bounds: Bounds<P>,
|
||||||
|
@ -812,7 +812,7 @@ impl From<Path<ScaledPixels>> for Primitive {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[repr(C)]
|
#[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) xy_position: Point<P>,
|
||||||
pub(crate) st_position: Point<f32>,
|
pub(crate) st_position: Point<f32>,
|
||||||
pub(crate) content_mask: ContentMask<P>,
|
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
|
/// The CSS styling that can be applied to an element via the `Styled` trait
|
||||||
#[derive(Clone, Refineable, Debug)]
|
#[derive(Clone, Refineable, Debug)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
/// What layout strategy should be used?
|
/// What layout strategy should be used?
|
||||||
pub display: Display,
|
pub display: Display,
|
||||||
|
@ -286,7 +286,7 @@ pub enum Visibility {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The possible values of the box-shadow property
|
/// The possible values of the box-shadow property
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct BoxShadow {
|
pub struct BoxShadow {
|
||||||
/// What color should the shadow have?
|
/// What color should the shadow have?
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
|
@ -332,7 +332,7 @@ pub enum TextAlign {
|
||||||
|
|
||||||
/// The properties that can be used to style text in GPUI
|
/// The properties that can be used to style text in GPUI
|
||||||
#[derive(Refineable, Clone, Debug, PartialEq)]
|
#[derive(Refineable, Clone, Debug, PartialEq)]
|
||||||
#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
|
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct TextStyle {
|
pub struct TextStyle {
|
||||||
/// The color of the text
|
/// The color of the text
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
|
@ -794,7 +794,7 @@ pub struct StrikethroughStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The kinds of fill that can be applied to a shape.
|
/// 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 {
|
pub enum Fill {
|
||||||
/// A solid color fill.
|
/// A solid color fill.
|
||||||
Color(Background),
|
Color(Background),
|
||||||
|
|
|
@ -14,6 +14,10 @@ const ELLIPSIS: SharedString = SharedString::new_static("…");
|
||||||
|
|
||||||
/// A trait for elements that can be styled.
|
/// A trait for elements that can be styled.
|
||||||
/// Use this to opt-in to a utility CSS-like styling API.
|
/// 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 {
|
pub trait Styled: Sized {
|
||||||
/// Returns a reference to the style memory of this element.
|
/// Returns a reference to the style memory of this element.
|
||||||
fn style(&mut self) -> &mut StyleRefinement;
|
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>
|
impl<T, T2> From<TaffyPoint<T>> for Point<T2>
|
||||||
where
|
where
|
||||||
T: Into<T2>,
|
T: Into<T2>,
|
||||||
T2: Clone + Default + Debug,
|
T2: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn from(point: TaffyPoint<T>) -> Point<T2> {
|
fn from(point: TaffyPoint<T>) -> Point<T2> {
|
||||||
Point {
|
Point {
|
||||||
|
@ -371,7 +371,7 @@ where
|
||||||
|
|
||||||
impl<T, T2> From<Point<T>> for TaffyPoint<T2>
|
impl<T, T2> From<Point<T>> for TaffyPoint<T2>
|
||||||
where
|
where
|
||||||
T: Into<T2> + Clone + Default + Debug,
|
T: Into<T2> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn from(val: Point<T>) -> Self {
|
fn from(val: Point<T>) -> Self {
|
||||||
TaffyPoint {
|
TaffyPoint {
|
||||||
|
@ -383,7 +383,7 @@ where
|
||||||
|
|
||||||
impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
|
impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
|
||||||
where
|
where
|
||||||
T: ToTaffy<U> + Clone + Default + Debug,
|
T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
|
fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
|
||||||
TaffySize {
|
TaffySize {
|
||||||
|
@ -395,7 +395,7 @@ where
|
||||||
|
|
||||||
impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
|
impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
|
||||||
where
|
where
|
||||||
T: ToTaffy<U> + Clone + Default + Debug,
|
T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
|
fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
|
||||||
TaffyRect {
|
TaffyRect {
|
||||||
|
@ -410,7 +410,7 @@ where
|
||||||
impl<T, U> From<TaffySize<T>> for Size<U>
|
impl<T, U> From<TaffySize<T>> for Size<U>
|
||||||
where
|
where
|
||||||
T: Into<U>,
|
T: Into<U>,
|
||||||
U: Clone + Default + Debug,
|
U: Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn from(taffy_size: TaffySize<T>) -> Self {
|
fn from(taffy_size: TaffySize<T>) -> Self {
|
||||||
Size {
|
Size {
|
||||||
|
@ -422,7 +422,7 @@ where
|
||||||
|
|
||||||
impl<T, U> From<Size<T>> for TaffySize<U>
|
impl<T, U> From<Size<T>> for TaffySize<U>
|
||||||
where
|
where
|
||||||
T: Into<U> + Clone + Default + Debug,
|
T: Into<U> + Clone + Debug + Default + PartialEq,
|
||||||
{
|
{
|
||||||
fn from(size: Size<T>) -> Self {
|
fn from(size: Size<T>) -> Self {
|
||||||
TaffySize {
|
TaffySize {
|
||||||
|
|
|
@ -979,7 +979,7 @@ pub(crate) struct DispatchEventResult {
|
||||||
/// to leave room to support more complex shapes in the future.
|
/// to leave room to support more complex shapes in the future.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct ContentMask<P: Clone + Default + Debug> {
|
pub struct ContentMask<P: Clone + Debug + Default + PartialEq> {
|
||||||
/// The bounds
|
/// The bounds
|
||||||
pub bounds: Bounds<P>,
|
pub bounds: Bounds<P>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,20 @@ license = "Apache-2.0"
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
inspector = []
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/gpui_macros.rs"
|
path = "src/gpui_macros.rs"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
doctest = true
|
doctest = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
heck.workspace = true
|
||||||
proc-macro2.workspace = true
|
proc-macro2.workspace = true
|
||||||
quote.workspace = true
|
quote.workspace = true
|
||||||
syn.workspace = true
|
syn.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[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 styles;
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
|
mod derive_inspector_reflection;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use syn::{DeriveInput, Ident};
|
use syn::{DeriveInput, Ident};
|
||||||
|
|
||||||
|
@ -178,6 +181,28 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
test::test(args, function)
|
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> {
|
pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option<Ident> {
|
||||||
match &ast.data {
|
match &ast.data {
|
||||||
syn::Data::Struct(data_struct) => data_struct
|
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
|
anyhow.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
@ -23,6 +24,6 @@ serde_json_lenient.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
# Inspector
|
# Inspector
|
||||||
|
|
||||||
This is a tool for inspecting and manipulating rendered elements in Zed. It is
|
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.
|
||||||
only available in debug builds. Use the `dev::ToggleInspector` action to toggle
|
|
||||||
inspector mode and click on UI elements to inspect them.
|
|
||||||
|
|
||||||
# Current features
|
# Current features
|
||||||
|
|
||||||
|
@ -10,44 +8,72 @@ inspector mode and click on UI elements to inspect them.
|
||||||
|
|
||||||
* Temporary manipulation of the selected element.
|
* 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.
|
* Navigation to code that constructed the element.
|
||||||
|
|
||||||
# Known bugs
|
# Known bugs
|
||||||
|
|
||||||
* The style inspector buffer will leak memory over time due to building up
|
## JSON style editor undo history doesn't get reset
|
||||||
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.
|
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
|
# 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.
|
* 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.
|
* 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?
|
* 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.
|
* 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.
|
* 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
|
## Persistent modification
|
||||||
|
|
||||||
Currently, element modifications disappear when picker mode is started. Handling this well is tricky. Potential features:
|
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.
|
* 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
|
# 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.
|
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 anyhow::{Result, anyhow};
|
||||||
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
|
use editor::{Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MultiBuffer};
|
||||||
|
use fuzzy::StringMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
|
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
|
||||||
Window,
|
StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
|
||||||
use language::language_settings::SoftWrap;
|
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 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
|
/// Path used for unsaved buffer that contains style json. To support the json language server, this
|
||||||
/// matches the name used in the generated schemas.
|
/// 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 {
|
pub(crate) struct DivInspector {
|
||||||
|
state: State,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
inspector_id: Option<InspectorElementId>,
|
inspector_id: Option<InspectorElementId>,
|
||||||
state: Option<DivInspectorState>,
|
inspector_state: Option<DivInspectorState>,
|
||||||
style_buffer: Option<Entity<Buffer>>,
|
/// Value of `DivInspectorState.base_style` when initially picked.
|
||||||
style_editor: Option<Entity<Editor>>,
|
initial_style: StyleRefinement,
|
||||||
last_error: Option<SharedString>,
|
/// 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 {
|
impl DivInspector {
|
||||||
|
@ -29,32 +67,402 @@ impl DivInspector {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> DivInspector {
|
) -> 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, {
|
cx.spawn_in(window, {
|
||||||
|
let languages = project.read(cx).languages().clone();
|
||||||
let project = project.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();
|
.detach();
|
||||||
|
|
||||||
DivInspector {
|
DivInspector {
|
||||||
|
state: State::Loading,
|
||||||
project,
|
project,
|
||||||
inspector_id: None,
|
inspector_id: None,
|
||||||
state: None,
|
inspector_state: None,
|
||||||
style_buffer: None,
|
initial_style: StyleRefinement::default(),
|
||||||
style_editor: None,
|
unconvertible_style: StyleRefinement::default(),
|
||||||
last_error: None,
|
json_style_overrides: StyleRefinement::default(),
|
||||||
|
rust_completion: None,
|
||||||
|
rust_completion_replace_range: None,
|
||||||
|
json_style_error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn open_style_buffer(
|
pub fn update_inspected_element(
|
||||||
project: Entity<Project>,
|
&mut self,
|
||||||
this: WeakEntity<DivInspector>,
|
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,
|
cx: &mut AsyncWindowContext,
|
||||||
) -> Result<()> {
|
) -> Result<Entity<Buffer>> {
|
||||||
let worktree = project
|
let worktree = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| project.create_worktree(path, false, cx))?
|
||||||
project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
|
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
|
||||||
|
@ -62,66 +470,22 @@ impl DivInspector {
|
||||||
path: Path::new("").into(),
|
path: Path::new("").into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let style_buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.open_path(project_path, cx))?
|
.update(cx, |project, cx| project.open_path(project_path, cx))?
|
||||||
.await?
|
.await?
|
||||||
.1;
|
.1;
|
||||||
|
|
||||||
project.update(cx, |project, cx| {
|
Ok(buffer)
|
||||||
project.register_buffer_with_language_servers(&style_buffer, cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.style_buffer = Some(style_buffer);
|
|
||||||
if let Some(id) = this.inspector_id.clone() {
|
|
||||||
let state =
|
|
||||||
window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
|
|
||||||
if let Some(state) = state {
|
|
||||||
this.update_inspected_element(&id, state, window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_inspected_element(
|
fn create_editor(
|
||||||
&mut self,
|
&self,
|
||||||
id: &InspectorElementId,
|
buffer: Entity<Buffer>,
|
||||||
state: DivInspectorState,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) -> Entity<Editor> {
|
||||||
let base_style_json = serde_json::to_string_pretty(&state.base_style);
|
cx.new(|cx| {
|
||||||
self.state = Some(state);
|
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
|
||||||
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));
|
|
||||||
let mut editor = Editor::new(
|
let mut editor = Editor::new(
|
||||||
EditorMode::full(),
|
EditorMode::full(),
|
||||||
multi_buffer,
|
multi_buffer,
|
||||||
|
@ -137,36 +501,7 @@ impl DivInspector {
|
||||||
editor.set_show_runnables(false, cx);
|
editor.set_show_runnables(false, cx);
|
||||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||||
editor
|
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()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when_some(self.state.as_ref(), |this, state| {
|
.when_some(self.inspector_state.as_ref(), |this, inspector_state| {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(Label::new("Layout").size(LabelSize::Large))
|
.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| {
|
.map(|this| match &self.state {
|
||||||
this.child(
|
State::Loading | State::BuffersLoaded { .. } => {
|
||||||
v_flex()
|
this.child(Label::new("Loading..."))
|
||||||
.gap_2()
|
}
|
||||||
.child(Label::new("Style").size(LabelSize::Large))
|
State::LoadError { message } => this.child(
|
||||||
.child(div().h_128().child(style_editor.clone()))
|
div()
|
||||||
.when_some(self.last_error.as_ref(), |this, last_error| {
|
.w_full()
|
||||||
this.child(
|
.border_1()
|
||||||
div()
|
.border_color(Color::Error.color(cx))
|
||||||
.w_full()
|
.child(Label::new(message)),
|
||||||
.border_1()
|
),
|
||||||
.border_color(Color::Error.color(cx))
|
State::Ready {
|
||||||
.child(Label::new(last_error)),
|
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);
|
||||||
|
})),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}),
|
.child(div().h_64().child(rust_style_editor.clone())),
|
||||||
)
|
)
|
||||||
})
|
.child(
|
||||||
.when_none(&self.style_editor, |this| {
|
v_flex()
|
||||||
this.child(Label::new("Loading..."))
|
.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()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
|
fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
|
.child(
|
||||||
|
div()
|
||||||
|
.text_ui(cx)
|
||||||
|
.child(format!("Bounds: {}", inspector_state.bounds)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id("content-size")
|
.id("content-size")
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.tooltip(Tooltip::text("Size of the element's children"))
|
.tooltip(Tooltip::text("Size of the element's children"))
|
||||||
.child(if state.content_size != state.bounds.size {
|
.child(
|
||||||
format!("Content size: {}", state.content_size)
|
if inspector_state.content_size != inspector_state.bounds.size {
|
||||||
} else {
|
format!("Content size: {}", inspector_state.content_size)
|
||||||
"".to_string()
|
} 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(
|
let project = project::Project::local(
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
app_state.node_runtime.clone(),
|
app_state.node_runtime.clone(),
|
||||||
|
@ -57,14 +57,12 @@ fn render_inspector(
|
||||||
let colors = cx.theme().colors();
|
let colors = cx.theme().colors();
|
||||||
let inspector_id = inspector.active_element_id();
|
let inspector_id = inspector.active_element_id();
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("gpui-inspector")
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(colors.panel_background)
|
.bg(colors.panel_background)
|
||||||
.text_color(colors.text)
|
.text_color(colors.text)
|
||||||
.font(ui_font)
|
.font(ui_font)
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_color(colors.border)
|
.border_color(colors.border)
|
||||||
.overflow_y_scroll()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_2()
|
.p_2()
|
||||||
|
@ -89,6 +87,8 @@ fn render_inspector(
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.id("gpui-inspector-content")
|
||||||
|
.overflow_y_scroll()
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when_some(inspector_id, |this, inspector_id| {
|
.when_some(inspector_id, |this, inspector_id| {
|
||||||
|
@ -101,26 +101,32 @@ fn render_inspector(
|
||||||
|
|
||||||
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
|
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
|
||||||
let source_location = inspector_id.path.source_location;
|
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()
|
v_flex()
|
||||||
.child(Label::new("Element ID").size(LabelSize::Large))
|
.child(Label::new("Element ID").size(LabelSize::Large))
|
||||||
.when(inspector_id.instance_id != 0, |this| {
|
.child(
|
||||||
this.child(
|
div()
|
||||||
div()
|
.id("instance-id")
|
||||||
.id("instance-id")
|
.text_ui(cx)
|
||||||
.text_ui(cx)
|
.tooltip(Tooltip::text(
|
||||||
.tooltip(Tooltip::text(
|
"Disambiguates elements from the same source location",
|
||||||
"Disambiguates elements from the same source location",
|
))
|
||||||
))
|
.child(format!("Instance {}", inspector_id.instance_id)),
|
||||||
.child(format!("Instance {}", inspector_id.instance_id)),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id("source-location")
|
.id("source-location")
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||||
.underline()
|
.underline()
|
||||||
.child(format!("{}", source_location))
|
.child(source_location_string)
|
||||||
.tooltip(Tooltip::text("Click to open by running zed cli"))
|
.tooltip(Tooltip::text("Click to open by running zed cli"))
|
||||||
.on_click(move |_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
cx.background_spawn(open_zed_source_location(source_location))
|
cx.background_spawn(open_zed_source_location(source_location))
|
||||||
|
@ -131,7 +137,7 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
|
||||||
div()
|
div()
|
||||||
.id("global-id")
|
.id("global-id")
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.min_h_12()
|
.min_h_20()
|
||||||
.tooltip(Tooltip::text(
|
.tooltip(Tooltip::text(
|
||||||
"GlobalElementId of the nearest ancestor with an ID",
|
"GlobalElementId of the nearest ancestor with an ID",
|
||||||
))
|
))
|
||||||
|
|
|
@ -66,7 +66,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
|
||||||
})
|
})
|
||||||
.collect();
|
.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
|
let type_param_bounds: Vec<_> = wrapped_types
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ty| {
|
.map(|ty| {
|
||||||
|
@ -273,6 +273,116 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
|
||||||
})
|
})
|
||||||
.collect();
|
.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! {};
|
let mut derive_stream = quote! {};
|
||||||
for trait_to_derive in refinement_traits_to_derive {
|
for trait_to_derive in refinement_traits_to_derive {
|
||||||
derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
|
derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
|
||||||
|
@ -303,6 +413,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
|
||||||
#( #refineable_refined_assignments )*
|
#( #refineable_refined_assignments )*
|
||||||
self
|
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
|
impl #impl_generics Refineable for #refinement_ident #ty_generics
|
||||||
|
@ -318,6 +441,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
|
||||||
#( #refinement_refined_assignments )*
|
#( #refinement_refined_assignments )*
|
||||||
self
|
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
|
impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics
|
||||||
|
|
|
@ -1,23 +1,120 @@
|
||||||
pub use derive_refineable::Refineable;
|
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 {
|
pub trait Refineable: Clone {
|
||||||
type Refinement: Refineable<Refinement = Self::Refinement> + IsEmpty + Default;
|
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);
|
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;
|
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
|
fn from_cascade(cascade: &Cascade<Self>) -> Self
|
||||||
where
|
where
|
||||||
Self: Default + Sized,
|
Self: Default + Sized,
|
||||||
{
|
{
|
||||||
Self::default().refined(cascade.merged())
|
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 {
|
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;
|
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>>);
|
pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
|
||||||
|
|
||||||
impl<S: Refineable + Default> Default for Cascade<S> {
|
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)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct CascadeSlot(usize);
|
pub struct CascadeSlot(usize);
|
||||||
|
|
||||||
impl<S: Refineable + Default> Cascade<S> {
|
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 {
|
pub fn reserve(&mut self) -> CascadeSlot {
|
||||||
self.0.push(None);
|
self.0.push(None);
|
||||||
CascadeSlot(self.0.len() - 1)
|
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 {
|
pub fn base(&mut self) -> &mut S::Refinement {
|
||||||
self.0[0].as_mut().unwrap()
|
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>) {
|
pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
|
||||||
self.0[slot.0] = 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 {
|
pub fn merged(&self) -> S::Refinement {
|
||||||
let mut merged = self.0[0].clone().unwrap();
|
let mut merged = self.0[0].clone().unwrap();
|
||||||
for refinement in self.0.iter().skip(1).flatten() {
|
for refinement in self.0.iter().skip(1).flatten() {
|
||||||
|
|
|
@ -15,6 +15,7 @@ use picker::{Picker, PickerDelegate};
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -70,7 +71,7 @@ pub trait InlineAssistDelegate {
|
||||||
pub fn open_rules_library(
|
pub fn open_rules_library(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
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>,
|
prompt_to_select: Option<PromptId>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<WindowHandle<RulesLibrary>>> {
|
) -> Task<Result<WindowHandle<RulesLibrary>>> {
|
||||||
|
@ -146,7 +147,7 @@ pub struct RulesLibrary {
|
||||||
picker: Entity<Picker<RulePickerDelegate>>,
|
picker: Entity<Picker<RulePickerDelegate>>,
|
||||||
pending_load: Task<()>,
|
pending_load: Task<()>,
|
||||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
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>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +350,7 @@ impl RulesLibrary {
|
||||||
store: Entity<PromptStore>,
|
store: Entity<PromptStore>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
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>,
|
rule_to_select: Option<PromptId>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|
|
@ -17,6 +17,7 @@ chrono.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
documented.workspace = true
|
documented.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui_macros.workspace = true
|
||||||
icons.workspace = true
|
icons.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
menu.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.
|
/// Extends [`gpui::Styled`] with Zed-specific styling methods.
|
||||||
|
#[cfg_attr(debug_assertions, gpui_macros::derive_inspector_reflection)]
|
||||||
pub trait StyledExt: Styled + Sized {
|
pub trait StyledExt: Styled + Sized {
|
||||||
/// Horizontally stacks elements.
|
/// Horizontally stacks elements.
|
||||||
///
|
///
|
||||||
|
|
|
@ -1213,6 +1213,28 @@ pub fn word_consists_of_emojis(s: &str) -> bool {
|
||||||
prev_end == s.len()
|
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 {
|
pub fn default<D: Default>() -> D {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
@ -1639,4 +1661,20 @@ Line 3"#
|
||||||
"这是什\n么 钢\n笔"
|
"这是什\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