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:
Michael Sloan 2025-05-26 11:43:57 -06:00 committed by GitHub
parent 6253b95f82
commit 649072d140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1778 additions and 316 deletions

View file

@ -1,26 +1,64 @@
use anyhow::Result;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
use anyhow::{Result, anyhow};
use editor::{Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MultiBuffer};
use fuzzy::StringMatch;
use gpui::{
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
Window,
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
};
use language::Buffer;
use language::language_settings::SoftWrap;
use project::{Project, ProjectPath};
use language::{
Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
};
use project::lsp_store::CompletionDocumentation;
use project::{Completion, CompletionSource, Project, ProjectPath};
use std::cell::RefCell;
use std::fmt::Write as _;
use std::ops::Range;
use std::path::Path;
use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
use std::rc::Rc;
use std::sync::LazyLock;
use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this
/// matches the name used in the generated schemas.
const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
pub(crate) struct DivInspector {
state: State,
project: Entity<Project>,
inspector_id: Option<InspectorElementId>,
state: Option<DivInspectorState>,
style_buffer: Option<Entity<Buffer>>,
style_editor: Option<Entity<Editor>>,
last_error: Option<SharedString>,
inspector_state: Option<DivInspectorState>,
/// Value of `DivInspectorState.base_style` when initially picked.
initial_style: StyleRefinement,
/// Portion of `initial_style` that can't be converted to rust code.
unconvertible_style: StyleRefinement,
/// Edits the user has made to the json buffer: `json_editor - (unconvertible_style + rust_editor)`.
json_style_overrides: StyleRefinement,
/// Error to display from parsing the json, or if serialization errors somehow occur.
json_style_error: Option<SharedString>,
/// Currently selected completion.
rust_completion: Option<String>,
/// Range that will be replaced by the completion if selected.
rust_completion_replace_range: Option<Range<Anchor>>,
}
enum State {
Loading,
BuffersLoaded {
rust_style_buffer: Entity<Buffer>,
json_style_buffer: Entity<Buffer>,
},
Ready {
rust_style_buffer: Entity<Buffer>,
rust_style_editor: Entity<Editor>,
json_style_buffer: Entity<Buffer>,
json_style_editor: Entity<Editor>,
},
LoadError {
message: SharedString,
},
}
impl DivInspector {
@ -29,32 +67,402 @@ impl DivInspector {
window: &mut Window,
cx: &mut Context<Self>,
) -> DivInspector {
// Open the buffer once, so it can then be used for each editor.
// Open the buffers once, so they can then be used for each editor.
cx.spawn_in(window, {
let languages = project.read(cx).languages().clone();
let project = project.clone();
async move |this, cx| Self::open_style_buffer(project, this, cx).await
async move |this, cx| {
// Open the JSON style buffer in the inspector-specific project, so that it runs the
// JSON language server.
let json_style_buffer =
Self::create_buffer_in_project(ZED_INSPECTOR_STYLE_JSON, &project, cx).await;
// Create Rust style buffer without adding it to the project / buffer_store, so that
// Rust Analyzer doesn't get started for it.
let rust_language_result = languages.language_for_name("Rust").await;
let rust_style_buffer = rust_language_result.and_then(|rust_language| {
cx.new(|cx| Buffer::local("", cx).with_language(rust_language, cx))
});
match json_style_buffer.and_then(|json_style_buffer| {
rust_style_buffer
.map(|rust_style_buffer| (json_style_buffer, rust_style_buffer))
}) {
Ok((json_style_buffer, rust_style_buffer)) => {
this.update_in(cx, |this, window, cx| {
this.state = State::BuffersLoaded {
json_style_buffer: json_style_buffer,
rust_style_buffer: rust_style_buffer,
};
// Initialize editors immediately instead of waiting for
// `update_inspected_element`. This avoids continuing to show
// "Loading..." until the user moves the mouse to a different element.
if let Some(id) = this.inspector_id.take() {
let inspector_state =
window.with_inspector_state(Some(&id), cx, |state, _window| {
state.clone()
});
if let Some(inspector_state) = inspector_state {
this.update_inspected_element(&id, inspector_state, window, cx);
cx.notify();
}
}
})
.ok();
}
Err(err) => {
this.update(cx, |this, _cx| {
this.state = State::LoadError {
message: format!(
"Failed to create buffers for style editing: {err}"
)
.into(),
};
})
.ok();
}
}
}
})
.detach();
DivInspector {
state: State::Loading,
project,
inspector_id: None,
state: None,
style_buffer: None,
style_editor: None,
last_error: None,
inspector_state: None,
initial_style: StyleRefinement::default(),
unconvertible_style: StyleRefinement::default(),
json_style_overrides: StyleRefinement::default(),
rust_completion: None,
rust_completion_replace_range: None,
json_style_error: None,
}
}
async fn open_style_buffer(
project: Entity<Project>,
this: WeakEntity<DivInspector>,
pub fn update_inspected_element(
&mut self,
id: &InspectorElementId,
inspector_state: DivInspectorState,
window: &mut Window,
cx: &mut Context<Self>,
) {
let style = (*inspector_state.base_style).clone();
self.inspector_state = Some(inspector_state);
if self.inspector_id.as_ref() == Some(id) {
return;
}
self.inspector_id = Some(id.clone());
self.initial_style = style.clone();
let (rust_style_buffer, json_style_buffer) = match &self.state {
State::BuffersLoaded {
rust_style_buffer,
json_style_buffer,
}
| State::Ready {
rust_style_buffer,
json_style_buffer,
..
} => (rust_style_buffer.clone(), json_style_buffer.clone()),
State::Loading | State::LoadError { .. } => return,
};
let json_style_editor = self.create_editor(json_style_buffer.clone(), window, cx);
let rust_style_editor = self.create_editor(rust_style_buffer.clone(), window, cx);
rust_style_editor.update(cx, {
let div_inspector = cx.entity();
|rust_style_editor, _cx| {
rust_style_editor.set_completion_provider(Some(Rc::new(
RustStyleCompletionProvider { div_inspector },
)));
}
});
let rust_style = match self.reset_style_editors(&rust_style_buffer, &json_style_buffer, cx)
{
Ok(rust_style) => {
self.json_style_error = None;
rust_style
}
Err(err) => {
self.json_style_error = Some(format!("{err}").into());
return;
}
};
cx.subscribe_in(&json_style_editor, window, {
let id = id.clone();
let rust_style_buffer = rust_style_buffer.clone();
move |this, editor, event: &EditorEvent, window, cx| match event {
EditorEvent::BufferEdited => {
let style_json = editor.read(cx).text(cx);
match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
Ok(new_style) => {
let (rust_style, _) = this.style_from_rust_buffer_snapshot(
&rust_style_buffer.read(cx).snapshot(),
);
let mut unconvertible_plus_rust = this.unconvertible_style.clone();
unconvertible_plus_rust.refine(&rust_style);
// The serialization of `DefiniteLength::Fraction` does not perfectly
// roundtrip because with f32, `(x / 100.0 * 100.0) == x` is not always
// true (such as for `p_1_3`). This can cause these values to
// erroneously appear in `json_style_overrides` since they are not
// perfectly equal. Roundtripping before `subtract` fixes this.
unconvertible_plus_rust =
serde_json::to_string(&unconvertible_plus_rust)
.ok()
.and_then(|json| {
serde_json_lenient::from_str_lenient(&json).ok()
})
.unwrap_or(unconvertible_plus_rust);
this.json_style_overrides =
new_style.subtract(&unconvertible_plus_rust);
window.with_inspector_state::<DivInspectorState, _>(
Some(&id),
cx,
|inspector_state, _window| {
if let Some(inspector_state) = inspector_state.as_mut() {
*inspector_state.base_style = new_style;
}
},
);
window.refresh();
this.json_style_error = None;
}
Err(err) => this.json_style_error = Some(err.to_string().into()),
}
}
_ => {}
}
})
.detach();
cx.subscribe(&rust_style_editor, {
let json_style_buffer = json_style_buffer.clone();
let rust_style_buffer = rust_style_buffer.clone();
move |this, _editor, event: &EditorEvent, cx| match event {
EditorEvent::BufferEdited => {
this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
}
_ => {}
}
})
.detach();
self.unconvertible_style = style.subtract(&rust_style);
self.json_style_overrides = StyleRefinement::default();
self.state = State::Ready {
rust_style_buffer,
rust_style_editor,
json_style_buffer,
json_style_editor,
};
}
fn reset_style(&mut self, cx: &mut App) {
match &self.state {
State::Ready {
rust_style_buffer,
json_style_buffer,
..
} => {
if let Err(err) = self.reset_style_editors(
&rust_style_buffer.clone(),
&json_style_buffer.clone(),
cx,
) {
self.json_style_error = Some(format!("{err}").into());
} else {
self.json_style_error = None;
}
}
_ => {}
}
}
fn reset_style_editors(
&self,
rust_style_buffer: &Entity<Buffer>,
json_style_buffer: &Entity<Buffer>,
cx: &mut App,
) -> Result<StyleRefinement> {
let json_text = match serde_json::to_string_pretty(&self.initial_style) {
Ok(json_text) => json_text,
Err(err) => {
return Err(anyhow!("Failed to convert style to JSON: {err}"));
}
};
let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style);
rust_style_buffer.update(cx, |rust_style_buffer, cx| {
rust_style_buffer.set_text(rust_code, cx);
let snapshot = rust_style_buffer.snapshot();
let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
Self::set_rust_buffer_diagnostics(
unrecognized_ranges,
rust_style_buffer,
&snapshot,
cx,
);
});
json_style_buffer.update(cx, |json_style_buffer, cx| {
json_style_buffer.set_text(json_text, cx);
});
Ok(rust_style)
}
fn handle_rust_completion_selection_change(
&mut self,
rust_completion: Option<String>,
cx: &mut Context<Self>,
) {
self.rust_completion = rust_completion;
if let State::Ready {
rust_style_buffer,
json_style_buffer,
..
} = &self.state
{
self.update_json_style_from_rust(
&json_style_buffer.clone(),
&rust_style_buffer.clone(),
cx,
);
}
}
fn update_json_style_from_rust(
&mut self,
json_style_buffer: &Entity<Buffer>,
rust_style_buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) {
let rust_style = rust_style_buffer.update(cx, |rust_style_buffer, cx| {
let snapshot = rust_style_buffer.snapshot();
let (rust_style, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
Self::set_rust_buffer_diagnostics(
unrecognized_ranges,
rust_style_buffer,
&snapshot,
cx,
);
rust_style
});
// Preserve parts of the json style which do not come from the unconvertible style or rust
// style. This way user edits to the json style are preserved when they are not overridden
// by the rust style.
//
// This results in a behavior where user changes to the json style that do overlap with the
// rust style will get set to the rust style when the user edits the rust style. It would be
// possible to update the rust style when the json style changes, but this is undesirable
// as the user may be working on the actual code in the rust style.
let mut new_style = self.unconvertible_style.clone();
new_style.refine(&self.json_style_overrides);
let new_style = new_style.refined(rust_style);
match serde_json::to_string_pretty(&new_style) {
Ok(json) => {
json_style_buffer.update(cx, |json_style_buffer, cx| {
json_style_buffer.set_text(json, cx);
});
}
Err(err) => {
self.json_style_error = Some(err.to_string().into());
}
}
}
fn style_from_rust_buffer_snapshot(
&self,
snapshot: &BufferSnapshot,
) -> (StyleRefinement, Vec<Range<Anchor>>) {
let method_names = if let Some((completion, completion_range)) = self
.rust_completion
.as_ref()
.zip(self.rust_completion_replace_range.as_ref())
{
let before_text = snapshot
.text_for_range(0..completion_range.start.to_offset(&snapshot))
.collect::<String>();
let after_text = snapshot
.text_for_range(
completion_range.end.to_offset(&snapshot)
..snapshot.clip_offset(usize::MAX, Bias::Left),
)
.collect::<String>();
let mut method_names = split_str_with_ranges(&before_text, is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string()))
.collect::<Vec<_>>();
method_names.push((None, completion.clone()));
method_names.extend(
split_str_with_ranges(&after_text, is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string())),
);
method_names
} else {
split_str_with_ranges(&snapshot.text(), is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string()))
.collect::<Vec<_>>()
};
let mut style = StyleRefinement::default();
let mut unrecognized_ranges = Vec::new();
for (range, name) in method_names {
if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
style = method.invoke(style);
} else if let Some(range) = range {
unrecognized_ranges
.push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
}
}
(style, unrecognized_ranges)
}
fn set_rust_buffer_diagnostics(
unrecognized_ranges: Vec<Range<Anchor>>,
rust_style_buffer: &mut Buffer,
snapshot: &BufferSnapshot,
cx: &mut Context<Buffer>,
) {
let diagnostic_entries = unrecognized_ranges
.into_iter()
.enumerate()
.map(|(ix, range)| DiagnosticEntry {
range,
diagnostic: Diagnostic {
message: "unrecognized".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
group_id: ix,
..Default::default()
},
});
let diagnostics = DiagnosticSet::from_sorted_entries(diagnostic_entries, snapshot);
rust_style_buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
}
async fn create_buffer_in_project(
path: impl AsRef<Path>,
project: &Entity<Project>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
) -> Result<Entity<Buffer>> {
let worktree = project
.update(cx, |project, cx| {
project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
})?
.update(cx, |project, cx| project.create_worktree(path, false, cx))?
.await?;
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
@ -62,66 +470,22 @@ impl DivInspector {
path: Path::new("").into(),
})?;
let style_buffer = project
let buffer = project
.update(cx, |project, cx| project.open_path(project_path, cx))?
.await?
.1;
project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&style_buffer, cx)
})?;
this.update_in(cx, |this, window, cx| {
this.style_buffer = Some(style_buffer);
if let Some(id) = this.inspector_id.clone() {
let state =
window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
if let Some(state) = state {
this.update_inspected_element(&id, state, window, cx);
cx.notify();
}
}
})?;
Ok(())
Ok(buffer)
}
pub fn update_inspected_element(
&mut self,
id: &InspectorElementId,
state: DivInspectorState,
fn create_editor(
&self,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let base_style_json = serde_json::to_string_pretty(&state.base_style);
self.state = Some(state);
if self.inspector_id.as_ref() == Some(id) {
return;
} else {
self.inspector_id = Some(id.clone());
}
let Some(style_buffer) = self.style_buffer.clone() else {
return;
};
let base_style_json = match base_style_json {
Ok(base_style_json) => base_style_json,
Err(err) => {
self.style_editor = None;
self.last_error =
Some(format!("Failed to convert base_style to JSON: {err}").into());
return;
}
};
self.last_error = None;
style_buffer.update(cx, |style_buffer, cx| {
style_buffer.set_text(base_style_json, cx)
});
let style_editor = cx.new(|cx| {
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
) -> Entity<Editor> {
cx.new(|cx| {
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
EditorMode::full(),
multi_buffer,
@ -137,36 +501,7 @@ impl DivInspector {
editor.set_show_runnables(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor
});
cx.subscribe_in(&style_editor, window, {
let id = id.clone();
move |this, editor, event: &EditorEvent, window, cx| match event {
EditorEvent::BufferEdited => {
let base_style_json = editor.read(cx).text(cx);
match serde_json_lenient::from_str(&base_style_json) {
Ok(new_base_style) => {
window.with_inspector_state::<DivInspectorState, _>(
Some(&id),
cx,
|state, _window| {
if let Some(state) = state.as_mut() {
*state.base_style = new_base_style;
}
},
);
window.refresh();
this.last_error = None;
}
Err(err) => this.last_error = Some(err.to_string().into()),
}
}
_ => {}
}
})
.detach();
self.style_editor = Some(style_editor);
}
}
@ -175,49 +510,223 @@ impl Render for DivInspector {
v_flex()
.size_full()
.gap_2()
.when_some(self.state.as_ref(), |this, state| {
.when_some(self.inspector_state.as_ref(), |this, inspector_state| {
this.child(
v_flex()
.child(Label::new("Layout").size(LabelSize::Large))
.child(render_layout_state(state, cx)),
.child(render_layout_state(inspector_state, cx)),
)
})
.when_some(self.style_editor.as_ref(), |this, style_editor| {
this.child(
v_flex()
.gap_2()
.child(Label::new("Style").size(LabelSize::Large))
.child(div().h_128().child(style_editor.clone()))
.when_some(self.last_error.as_ref(), |this, last_error| {
this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(last_error)),
.map(|this| match &self.state {
State::Loading | State::BuffersLoaded { .. } => {
this.child(Label::new("Loading..."))
}
State::LoadError { message } => this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(message)),
),
State::Ready {
rust_style_editor,
json_style_editor,
..
} => this
.child(
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Label::new("Rust Style").size(LabelSize::Large))
.child(
IconButton::new("reset-style", IconName::Eraser)
.tooltip(Tooltip::text("Reset style"))
.on_click(cx.listener(|this, _, _window, cx| {
this.reset_style(cx);
})),
),
)
}),
)
})
.when_none(&self.style_editor, |this| {
this.child(Label::new("Loading..."))
.child(div().h_64().child(rust_style_editor.clone())),
)
.child(
v_flex()
.gap_2()
.child(Label::new("JSON Style").size(LabelSize::Large))
.child(div().h_128().child(json_style_editor.clone()))
.when_some(self.json_style_error.as_ref(), |this, last_error| {
this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(last_error)),
)
}),
),
})
.into_any_element()
}
}
fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
v_flex()
.child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
.child(
div()
.text_ui(cx)
.child(format!("Bounds: {}", inspector_state.bounds)),
)
.child(
div()
.id("content-size")
.text_ui(cx)
.tooltip(Tooltip::text("Size of the element's children"))
.child(if state.content_size != state.bounds.size {
format!("Content size: {}", state.content_size)
} else {
"".to_string()
}),
.child(
if inspector_state.content_size != inspector_state.bounds.size {
format!("Content size: {}", inspector_state.content_size)
} else {
"".to_string()
},
),
)
}
static STYLE_METHODS: LazyLock<Vec<(Box<StyleRefinement>, FunctionReflection<StyleRefinement>)>> =
LazyLock::new(|| {
// Include StyledExt methods first so that those methods take precedence.
styled_ext_reflection::methods::<StyleRefinement>()
.into_iter()
.chain(styled_reflection::methods::<StyleRefinement>())
.map(|method| (Box::new(method.invoke(StyleRefinement::default())), method))
.collect()
});
fn guess_rust_code_from_style(goal_style: &StyleRefinement) -> (String, StyleRefinement) {
let mut subset_methods = Vec::new();
for (style, method) in STYLE_METHODS.iter() {
if goal_style.is_superset_of(style) {
subset_methods.push(method);
}
}
let mut code = "fn build() -> Div {\n div()".to_string();
let mut style = StyleRefinement::default();
for method in subset_methods {
let before_change = style.clone();
style = method.invoke(style);
if before_change != style {
let _ = write!(code, "\n .{}()", &method.name);
}
}
code.push_str("\n}");
(code, style)
}
fn is_not_identifier_char(c: char) -> bool {
!c.is_alphanumeric() && c != '_'
}
struct RustStyleCompletionProvider {
div_inspector: Entity<DivInspector>,
}
impl CompletionProvider for RustStyleCompletionProvider {
fn completions(
&self,
_excerpt_id: ExcerptId,
buffer: &Entity<Buffer>,
position: Anchor,
_: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<project::Completion>>>> {
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
else {
return Task::ready(Ok(Some(Vec::new())));
};
self.div_inspector.update(cx, |div_inspector, _cx| {
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
});
Task::ready(Ok(Some(
STYLE_METHODS
.iter()
.map(|(_, method)| Completion {
replace_range: replace_range.clone(),
new_text: format!(".{}()", method.name),
label: CodeLabel::plain(method.name.to_string(), None),
icon_path: None,
documentation: method.documentation.map(|documentation| {
CompletionDocumentation::MultiLineMarkdown(documentation.into())
}),
source: CompletionSource::Custom,
insert_text_mode: None,
confirm: None,
})
.collect(),
)))
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_: &str,
_: bool,
cx: &mut Context<Editor>,
) -> bool {
completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()
}
fn selection_changed(&self, mat: Option<&StringMatch>, _window: &mut Window, cx: &mut App) {
let div_inspector = self.div_inspector.clone();
let rust_completion = mat.as_ref().map(|mat| mat.string.clone());
cx.defer(move |cx| {
div_inspector.update(cx, |div_inspector, cx| {
div_inspector.handle_rust_completion_selection_change(rust_completion, cx);
});
});
}
fn sort_completions(&self) -> bool {
false
}
}
fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
let point = anchor.to_point(&snapshot);
let offset = point.to_offset(&snapshot);
let line_start = Point::new(point.row, 0).to_offset(&snapshot);
let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot);
let mut lines = snapshot.text_for_range(line_start..line_end).lines();
let line = lines.next()?;
let start_in_line = &line[..offset - line_start]
.rfind(|c| is_not_identifier_char(c) && c != '.')
.map(|ix| ix + 1)
.unwrap_or(0);
let end_in_line = &line[offset - line_start..]
.rfind(|c| is_not_identifier_char(c) && c != '(' && c != ')')
.unwrap_or(line_end - line_start);
if end_in_line > start_in_line {
let replace_start = snapshot.anchor_before(line_start + start_in_line);
let replace_end = snapshot.anchor_before(line_start + end_in_line);
Some(replace_start..replace_end)
} else {
None
}
}