use anyhow::{Result, anyhow}; use editor::{ Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MinimapVisibility, MultiBuffer, }; use fuzzy::StringMatch; use gpui::{ AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection, }; use language::language_settings::SoftWrap; use language::{ Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _, }; use project::lsp_store::CompletionDocumentation; use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath}; use std::fmt::Write as _; use std::ops::Range; use std::path::Path; 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_JSON: &str = "/zed-inspector-style.json"; pub(crate) struct DivInspector { state: State, project: Entity, inspector_id: Option, inspector_state: Option, /// 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, /// Currently selected completion. rust_completion: Option, /// Range that will be replaced by the completion if selected. rust_completion_replace_range: Option>, } enum State { Loading, BuffersLoaded { rust_style_buffer: Entity, json_style_buffer: Entity, }, Ready { rust_style_buffer: Entity, rust_style_editor: Entity, json_style_buffer: Entity, json_style_editor: Entity, }, LoadError { message: SharedString, }, } impl DivInspector { pub fn new( project: Entity, window: &mut Window, cx: &mut Context, ) -> DivInspector { // 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| { // 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, 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, 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, } } pub fn update_inspected_element( &mut self, id: &InspectorElementId, inspector_state: DivInspectorState, window: &mut Window, cx: &mut Context, ) { 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| { if event == &EditorEvent::BufferEdited { let style_json = editor.read(cx).text(cx); match serde_json_lenient::from_str_lenient::(&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::( 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| { if let EditorEvent::BufferEdited = event { 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) { if let State::Ready { rust_style_buffer, json_style_buffer, .. } = &self.state { 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, json_style_buffer: &Entity, cx: &mut App, ) -> Result { 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, cx: &mut Context, ) { 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, rust_style_buffer: &Entity, cx: &mut Context, ) { 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>) { 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::(); let after_text = snapshot .text_for_range( completion_range.end.to_offset(snapshot) ..snapshot.clip_offset(usize::MAX, Bias::Left), ) .collect::(); 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::>(); 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::>() }; 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>, rust_style_buffer: &mut Buffer, snapshot: &BufferSnapshot, cx: &mut Context, ) { 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, project: &Entity, cx: &mut AsyncWindowContext, ) -> Result> { let worktree = project .update(cx, |project, cx| project.create_worktree(path, false, cx))? .await?; let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath { worktree_id: worktree.id(), path: Path::new("").into(), })?; let buffer = project .update(cx, |project, cx| project.open_path(project_path, cx))? .await? .1; Ok(buffer) } fn create_editor( &self, buffer: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { cx.new(|cx| { let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new( EditorMode::full(), multi_buffer, Some(self.project.clone()), window, cx, ); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_line_numbers(false, cx); editor.set_show_code_actions(false, cx); editor.set_show_breakpoints(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_show_runnables(false, cx); editor.set_show_edit_predictions(Some(false), window, cx); editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); editor }) } } impl Render for DivInspector { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .gap_2() .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(inspector_state, cx)), ) }) .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); })), ), ) .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(inspector_state: &DivInspectorState, cx: &App) -> Div { v_flex() .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 inspector_state.content_size != inspector_state.bounds.size { format!("Content size: {}", inspector_state.content_size) } else { "".to_string() }, ), ) } static STYLE_METHODS: LazyLock, FunctionReflection)>> = LazyLock::new(|| { // Include StyledExt methods first so that those methods take precedence. styled_ext_reflection::methods::() .into_iter() .chain(styled_reflection::methods::()) .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, } impl CompletionProvider for RustStyleCompletionProvider { fn completions( &self, _excerpt_id: ExcerptId, buffer: &Entity, position: Anchor, _: editor::CompletionContext, _window: &mut Window, cx: &mut Context, ) -> Task>> { let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position) else { return Task::ready(Ok(Vec::new())); }; self.div_inspector.update(cx, |div_inspector, _cx| { div_inspector.rust_completion_replace_range = Some(replace_range.clone()); }); Task::ready(Ok(vec![CompletionResponse { completions: 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(), is_incomplete: false, }])) } fn is_completion_trigger( &self, buffer: &Entity, position: language::Anchor, _text: &str, _trigger_in_words: bool, _menu_is_open: bool, cx: &mut Context, ) -> 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> { 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_after(line_start + end_in_line); Some(replace_start..replace_end) } else { None } }