1use anyhow::Result;
2use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
3use gpui::{
4 AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
5 Window,
6};
7use language::Buffer;
8use language::language_settings::SoftWrap;
9use project::{Project, ProjectPath};
10use std::path::Path;
11use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
12
13/// Path used for unsaved buffer that contains style json. To support the json language server, this
14/// matches the name used in the generated schemas.
15const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
16
17pub(crate) struct DivInspector {
18 project: Entity<Project>,
19 inspector_id: Option<InspectorElementId>,
20 state: Option<DivInspectorState>,
21 style_buffer: Option<Entity<Buffer>>,
22 style_editor: Option<Entity<Editor>>,
23 last_error: Option<SharedString>,
24}
25
26impl DivInspector {
27 pub fn new(
28 project: Entity<Project>,
29 window: &mut Window,
30 cx: &mut Context<Self>,
31 ) -> DivInspector {
32 // Open the buffer once, so it can then be used for each editor.
33 cx.spawn_in(window, {
34 let project = project.clone();
35 async move |this, cx| Self::open_style_buffer(project, this, cx).await
36 })
37 .detach();
38
39 DivInspector {
40 project,
41 inspector_id: None,
42 state: None,
43 style_buffer: None,
44 style_editor: None,
45 last_error: None,
46 }
47 }
48
49 async fn open_style_buffer(
50 project: Entity<Project>,
51 this: WeakEntity<DivInspector>,
52 cx: &mut AsyncWindowContext,
53 ) -> Result<()> {
54 let worktree = project
55 .update(cx, |project, cx| {
56 project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
57 })?
58 .await?;
59
60 let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
61 worktree_id: worktree.id(),
62 path: Path::new("").into(),
63 })?;
64
65 let style_buffer = project
66 .update(cx, |project, cx| project.open_path(project_path, cx))?
67 .await?
68 .1;
69
70 project.update(cx, |project, cx| {
71 project.register_buffer_with_language_servers(&style_buffer, cx)
72 })?;
73
74 this.update_in(cx, |this, window, cx| {
75 this.style_buffer = Some(style_buffer);
76 if let Some(id) = this.inspector_id.clone() {
77 let state =
78 window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
79 if let Some(state) = state {
80 this.update_inspected_element(&id, state, window, cx);
81 cx.notify();
82 }
83 }
84 })?;
85
86 Ok(())
87 }
88
89 pub fn update_inspected_element(
90 &mut self,
91 id: &InspectorElementId,
92 state: DivInspectorState,
93 window: &mut Window,
94 cx: &mut Context<Self>,
95 ) {
96 let base_style_json = serde_json::to_string_pretty(&state.base_style);
97 self.state = Some(state);
98
99 if self.inspector_id.as_ref() == Some(id) {
100 return;
101 } else {
102 self.inspector_id = Some(id.clone());
103 }
104 let Some(style_buffer) = self.style_buffer.clone() else {
105 return;
106 };
107
108 let base_style_json = match base_style_json {
109 Ok(base_style_json) => base_style_json,
110 Err(err) => {
111 self.style_editor = None;
112 self.last_error =
113 Some(format!("Failed to convert base_style to JSON: {err}").into());
114 return;
115 }
116 };
117 self.last_error = None;
118
119 style_buffer.update(cx, |style_buffer, cx| {
120 style_buffer.set_text(base_style_json, cx)
121 });
122
123 let style_editor = cx.new(|cx| {
124 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
125 let mut editor = Editor::new(
126 EditorMode::full(),
127 multi_buffer,
128 Some(self.project.clone()),
129 window,
130 cx,
131 );
132 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
133 editor.set_show_line_numbers(false, cx);
134 editor.set_show_code_actions(false, cx);
135 editor.set_show_breakpoints(false, cx);
136 editor.set_show_git_diff_gutter(false, cx);
137 editor.set_show_runnables(false, cx);
138 editor.set_show_edit_predictions(Some(false), window, cx);
139 editor
140 });
141
142 cx.subscribe_in(&style_editor, window, {
143 let id = id.clone();
144 move |this, editor, event: &EditorEvent, window, cx| match event {
145 EditorEvent::BufferEdited => {
146 let base_style_json = editor.read(cx).text(cx);
147 match serde_json_lenient::from_str(&base_style_json) {
148 Ok(new_base_style) => {
149 window.with_inspector_state::<DivInspectorState, _>(
150 Some(&id),
151 cx,
152 |state, _window| {
153 if let Some(state) = state.as_mut() {
154 *state.base_style = new_base_style;
155 }
156 },
157 );
158 window.refresh();
159 this.last_error = None;
160 }
161 Err(err) => this.last_error = Some(err.to_string().into()),
162 }
163 }
164 _ => {}
165 }
166 })
167 .detach();
168
169 self.style_editor = Some(style_editor);
170 }
171}
172
173impl Render for DivInspector {
174 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175 v_flex()
176 .size_full()
177 .gap_2()
178 .when_some(self.state.as_ref(), |this, state| {
179 this.child(
180 v_flex()
181 .child(Label::new("Layout").size(LabelSize::Large))
182 .child(render_layout_state(state, cx)),
183 )
184 })
185 .when_some(self.style_editor.as_ref(), |this, style_editor| {
186 this.child(
187 v_flex()
188 .gap_2()
189 .child(Label::new("Style").size(LabelSize::Large))
190 .child(div().h_128().child(style_editor.clone()))
191 .when_some(self.last_error.as_ref(), |this, last_error| {
192 this.child(
193 div()
194 .w_full()
195 .border_1()
196 .border_color(Color::Error.color(cx))
197 .child(Label::new(last_error)),
198 )
199 }),
200 )
201 })
202 .when_none(&self.style_editor, |this| {
203 this.child(Label::new("Loading..."))
204 })
205 .into_any_element()
206 }
207}
208
209fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
210 v_flex()
211 .child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
212 .child(
213 div()
214 .id("content-size")
215 .text_ui(cx)
216 .tooltip(Tooltip::text("Size of the element's children"))
217 .child(if state.content_size != state.bounds.size {
218 format!("Content size: {}", state.content_size)
219 } else {
220 "".to_string()
221 }),
222 )
223}