1use std::{cell::OnceCell, collections::HashMap, fmt::Write as _, rc::Rc, sync::OnceLock};
2
3use anyhow::Result;
4use gpui::{
5 AnyElement, App, AppContext, Context, DivInspectorState, Entity, Inspector, InspectorElementId,
6 InteractiveElement as _, IntoElement, KeyBinding, ParentElement as _, Refineable as _, Render,
7 SharedString, StyleRefinement, Styled, Subscription, Task, Window, actions, div,
8 inspector_reflection::FunctionReflection, prelude::FluentBuilder, px,
9};
10use lsp_types::{
11 CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit, Diagnostic,
12 DiagnosticSeverity, Position, TextEdit,
13};
14use ropey::Rope;
15
16use crate::{
17 ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
18 alert::Alert,
19 button::{Button, ButtonVariants},
20 clipboard::Clipboard,
21 description_list::DescriptionList,
22 h_flex,
23 input::{CompletionProvider, Editor, EditorState, InputEvent, RopeExt, TabSize},
24 link::Link,
25 v_flex,
26};
27
28actions!(inspector, [ToggleInspector]);
29
30pub(crate) fn init(cx: &mut App) {
32 cx.bind_keys(vec![
33 #[cfg(target_os = "macos")]
34 KeyBinding::new("cmd-alt-i", ToggleInspector, None),
35 #[cfg(not(target_os = "macos"))]
36 KeyBinding::new("ctrl-shift-i", ToggleInspector, None),
37 ]);
38
39 cx.on_action(|_: &ToggleInspector, cx| {
40 let Some(active_window) = cx.active_window() else {
41 return;
42 };
43
44 cx.defer(move |cx| {
45 _ = active_window.update(cx, |_, window, cx| {
46 window.toggle_inspector(cx);
47 });
48 });
49 });
50
51 let inspector_el = OnceCell::new();
52 cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
53 let el = inspector_el.get_or_init(|| cx.new(|cx| DivInspector::new(window, cx)));
54 el.update(cx, |this, cx| {
55 this.update_inspected_element(id, state.clone(), window, cx);
56 this.render(window, cx).into_any_element()
57 })
58 });
59
60 cx.set_inspector_renderer(Box::new(render_inspector));
61}
62
63struct InspectorEditor {
64 state: Entity<EditorState>,
66 error: Option<SharedString>,
68 editing: bool,
70}
71
72pub struct DivInspector {
73 inspector_id: Option<InspectorElementId>,
74 inspector_state: Option<DivInspectorState>,
75 rust_state: InspectorEditor,
76 json_state: InspectorEditor,
77 initial_style: StyleRefinement,
79 unconvertible_style: StyleRefinement,
81 _subscriptions: Vec<Subscription>,
82}
83
84impl DivInspector {
85 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
86 let lsp_provider = Rc::new(LspProvider {});
87
88 let json_input_state = cx.new(|cx| {
89 EditorState::new(window, cx)
90 .language("json")
91 .line_number(false)
92 });
93
94 let rust_input_state = cx.new(|cx| {
95 EditorState::new(window, cx)
96 .language("rust")
97 .line_number(false)
98 .tab_size(TabSize {
99 tab_size: 4,
100 hard_tabs: false,
101 })
102 });
103 rust_input_state.update(cx, |state, cx| {
104 state.lsp_mut().completion_provider = Some(lsp_provider.clone());
105 cx.notify();
106 });
107
108 let _subscriptions = vec![
109 cx.subscribe_in(
110 &json_input_state,
111 window,
112 |this: &mut DivInspector, state, event: &InputEvent, window, cx| match event {
113 InputEvent::Change => {
114 let new_style = state.read(cx).value();
115 this.edit_json(new_style.as_str(), window, cx);
116 }
117 _ => {}
118 },
119 ),
120 cx.subscribe_in(
121 &rust_input_state,
122 window,
123 |this: &mut DivInspector, state, event: &InputEvent, window, cx| match event {
124 InputEvent::Change => {
125 let new_style = state.read(cx).value();
126 this.edit_rust(new_style.as_str(), window, cx);
127 }
128 _ => {}
129 },
130 ),
131 ];
132
133 let rust_state = InspectorEditor {
134 state: rust_input_state,
135 error: None,
136 editing: false,
137 };
138
139 let json_state = InspectorEditor {
140 state: json_input_state,
141 error: None,
142 editing: false,
143 };
144
145 Self {
146 inspector_id: None,
147 inspector_state: None,
148 rust_state,
149 json_state,
150 initial_style: Default::default(),
151 unconvertible_style: Default::default(),
152 _subscriptions,
153 }
154 }
155
156 pub fn update_inspected_element(
157 &mut self,
158 inspector_id: InspectorElementId,
159 state: DivInspectorState,
160 window: &mut Window,
161 cx: &mut Context<Self>,
162 ) {
163 if self.inspector_id.as_ref() == Some(&inspector_id) {
165 return;
166 }
167
168 let initial_style = state.base_style.as_ref();
169 self.initial_style = initial_style.clone();
170 self.json_state.editing = false;
171 self.update_json_from_style(initial_style, window, cx);
172 self.rust_state.editing = false;
173 let rust_style = self.update_rust_from_style(initial_style, window, cx);
174 self.unconvertible_style = initial_style.subtract(&rust_style);
175 self.inspector_id = Some(inspector_id);
176 self.inspector_state = Some(state);
177 cx.notify();
178 }
179
180 fn edit_json(&mut self, code: &str, window: &mut Window, cx: &mut Context<Self>) {
181 if !self.json_state.editing {
182 self.json_state.editing = true;
183 return;
184 }
185
186 match serde_json::from_str::<StyleRefinement>(code) {
187 Ok(new_style) => {
188 self.json_state.error = None;
189 self.rust_state.error = None;
190 self.rust_state.editing = false;
191 let rust_style = self.update_rust_from_style(&new_style, window, cx);
192 self.unconvertible_style = new_style.subtract(&rust_style);
193 self.update_element_style(new_style, window, cx);
194 }
195 Err(e) => {
196 self.json_state.error = Some(e.to_string().trim_end().to_string().into());
197 window.refresh();
198 }
199 }
200 }
201
202 fn edit_rust(&mut self, code: &str, window: &mut Window, cx: &mut Context<Self>) {
203 if !self.rust_state.editing {
204 self.rust_state.editing = true;
205 return;
206 }
207
208 let (new_style, diagnostics) = rust_to_style(self.unconvertible_style.clone(), code);
209 self.rust_state.state.update(cx, |state, cx| {
210 if let Some(set) = state.diagnostics_mut() {
211 set.clear();
212 set.extend(diagnostics);
213 }
214 cx.notify();
215 });
216 self.json_state.error = None;
217 self.json_state.editing = false;
218 self.update_json_from_style(&new_style, window, cx);
219 self.update_element_style(new_style, window, cx);
220 }
221
222 fn update_element_style(
223 &self,
224 style: StyleRefinement,
225 window: &mut Window,
226 cx: &mut Context<Self>,
227 ) {
228 window.with_inspector_state::<DivInspectorState, _>(
229 self.inspector_id.as_ref(),
230 cx,
231 |state, _window| {
232 if let Some(state) = state {
233 *state.base_style = style;
234 }
235 },
236 );
237 window.refresh();
238 }
239
240 fn reset_style(&mut self, window: &mut Window, cx: &mut Context<Self>) {
241 self.rust_state.editing = false;
242 let rust_style = self.update_rust_from_style(&self.initial_style, window, cx);
243 self.unconvertible_style = self.initial_style.subtract(&rust_style);
244 self.json_state.editing = false;
245 self.update_json_from_style(&self.initial_style, window, cx);
246 if let Some(state) = self.inspector_state.as_mut() {
247 *state.base_style = self.initial_style.clone();
248 }
249 }
250
251 fn update_json_from_style(
252 &self,
253 style: &StyleRefinement,
254 window: &mut Window,
255 cx: &mut Context<Self>,
256 ) {
257 self.json_state.state.update(cx, |state, cx| {
258 state.set_value(style_to_json(style), window, cx);
259 });
260 }
261
262 fn update_rust_from_style(
263 &self,
264 style: &StyleRefinement,
265 window: &mut Window,
266 cx: &mut Context<Self>,
267 ) -> StyleRefinement {
268 self.rust_state.state.update(cx, |state, cx| {
269 let (rust_code, rust_style) = style_to_rust(style);
270 state.set_value(rust_code, window, cx);
271 rust_style
272 })
273 }
274}
275
276fn style_to_json(style: &StyleRefinement) -> String {
277 serde_json::to_string_pretty(style).unwrap_or_else(|e| format!("{{ \"error\": \"{}\" }}", e))
278}
279
280struct StyleMethods {
281 table: Vec<(Box<StyleRefinement>, FunctionReflection<StyleRefinement>)>,
282 map: HashMap<&'static str, FunctionReflection<StyleRefinement>>,
283}
284
285impl StyleMethods {
286 fn get() -> &'static Self {
287 static STYLE_METHODS: OnceLock<StyleMethods> = OnceLock::new();
288 STYLE_METHODS.get_or_init(|| {
289 let table: Vec<_> = [
290 gpui_base::styled_ext_reflection_methods::<StyleRefinement>(),
291 gpui::styled_reflection::methods::<StyleRefinement>(),
292 ]
293 .into_iter()
294 .flatten()
295 .map(|method| (Box::new(method.invoke(StyleRefinement::default())), method))
296 .collect();
297 let map = table
298 .iter()
299 .map(|(_, method)| (method.name, method.clone()))
300 .collect();
301
302 Self { table, map }
303 })
304 }
305}
306
307fn style_to_rust(input_style: &StyleRefinement) -> (String, StyleRefinement) {
308 let methods: Vec<_> = StyleMethods::get()
309 .table
310 .iter()
311 .filter_map(|(style, method)| {
312 if input_style.is_superset_of(style) {
313 Some(method)
314 } else {
315 None
316 }
317 })
318 .collect();
319 let mut code = "fn build() -> Div {\n div()\n".to_string();
320 let mut style = StyleRefinement::default();
321 for method in methods {
322 let before_invoke = style.clone();
323 style = method.invoke(style);
324 if style != before_invoke {
325 _ = write!(code, " .{}()\n", method.name);
326 }
327 }
328 code.push_str("}");
329 (code, style)
330}
331
332fn rust_to_style(mut style: StyleRefinement, source: &str) -> (StyleRefinement, Vec<Diagnostic>) {
333 let rope = Rope::from(source);
334 let Some(begin) = source.find("div()").map(|i| i + "div()".len()) else {
335 let start_pos = Position::new(0, 0);
336 let end_pos = rope.offset_to_position(rope.len());
337
338 return (
339 style,
340 vec![Diagnostic {
341 range: lsp_types::Range::new(start_pos, end_pos),
342 severity: Some(DiagnosticSeverity::ERROR),
343 message: "expected `div()`".into(),
344 ..Default::default()
345 }],
346 );
347 };
348
349 let mut methods = vec![];
350 let mut offset = 0;
351 let mut method_offset = 0;
352 let mut method = String::new();
353 for line in rope.iter_lines() {
354 if line.to_string().trim().starts_with("//") {
355 offset += line.len() + 1;
356 continue;
357 }
358
359 for c in line.chars() {
360 offset += c.len_utf8();
361 if offset < begin {
362 continue;
363 }
364
365 if c.is_ascii_alphanumeric() || c == '_' {
366 method.push(c);
367 method_offset = offset;
368 } else {
369 if !method.is_empty() {
370 methods.push((method_offset, method.clone()));
371 }
372 method.clear();
373 }
374 }
375
376 offset += 1;
378 }
379
380 let mut diagnostics = vec![];
381 let style_methods = StyleMethods::get();
382
383 for (offset, method) in methods {
384 match style_methods.map.get(method.as_str()) {
385 Some(method_reflection) => style = method_reflection.invoke(style),
386 None => {
387 let message = format!("unknown method `{}`", method);
388 let start = rope.offset_to_position(offset.saturating_sub(method.len()));
389 let end = rope.offset_to_position(offset);
390 let diagnostic = lsp_types::Diagnostic {
391 range: lsp_types::Range::new(start, end),
392 severity: Some(DiagnosticSeverity::ERROR),
393 message,
394 ..Default::default()
395 };
396
397 diagnostics.push(diagnostic);
398 }
399 }
400 }
401
402 (style, diagnostics)
403}
404
405impl Render for DivInspector {
406 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
407 v_flex().size_full().gap_y_4().text_sm().when_some(
408 self.inspector_state.as_ref(),
409 |this, state| {
410 this.child(
411 DescriptionList::new()
412 .columns(1)
413 .label_width(px(110.))
414 .bordered(false)
415 .item("Origin", format!("{}", state.bounds.origin), 1)
416 .item("Size", format!("{}", state.bounds.size), 1)
417 .item("Content Size", format!("{}", state.content_size), 1),
418 )
419 .child(
420 v_flex()
421 .flex_1()
422 .h_2_5()
423 .gap_y_3()
424 .child(
425 h_flex()
426 .justify_between()
427 .gap_x_2()
428 .child("Rust Styles")
429 .child(Button::new("rust-reset").label("Reset").small().on_click(
430 cx.listener(|this, _, window, cx| {
431 this.reset_style(window, cx);
432 }),
433 )),
434 )
435 .child(
436 v_flex()
437 .flex_1()
438 .gap_y_1()
439 .font_family(cx.theme().mono_font_family.clone())
440 .text_size(cx.theme().mono_font_size)
441 .child(Editor::new(&self.rust_state.state).h(gpui::relative(1.)))
442 .when_some(self.rust_state.error.clone(), |this, err| {
443 this.child(Alert::error("rust-error", err).text_xs())
444 }),
445 ),
446 )
447 .child(
448 v_flex()
449 .flex_1()
450 .gap_y_3()
451 .h_2_5()
452 .flex_shrink_0()
453 .child(
454 h_flex()
455 .gap_x_2()
456 .child(div().flex_1().child("JSON Styles"))
457 .child(Button::new("json-reset").label("Reset").small().on_click(
458 cx.listener(|this, _, window, cx| {
459 this.reset_style(window, cx);
460 }),
461 )),
462 )
463 .child(
464 v_flex()
465 .flex_1()
466 .gap_y_1()
467 .font_family(cx.theme().mono_font_family.clone())
468 .text_size(cx.theme().mono_font_size)
469 .child(Editor::new(&self.json_state.state).h(gpui::relative(1.)))
470 .when_some(self.json_state.error.clone(), |this, err| {
471 this.child(Alert::error("json-error", err).text_xs())
472 }),
473 ),
474 )
475 },
476 )
477 }
478}
479
480fn render_inspector(
481 inspector: &mut Inspector,
482 window: &mut Window,
483 cx: &mut Context<Inspector>,
484) -> AnyElement {
485 let inspector_element_id = inspector.active_element_id();
486 let source_location =
487 inspector_element_id.map(|id| SharedString::new(format!("{}", id.path.source_location)));
488 let element_global_id = inspector_element_id.map(|id| format!("{}", id.path.global_id));
489
490 v_flex()
491 .id("inspector")
492 .font_family(cx.theme().font_family.clone())
493 .size_full()
494 .bg(cx.theme().tokens.background)
495 .border_l_1()
496 .border_color(cx.theme().border)
497 .text_color(cx.theme().foreground)
498 .child(
499 h_flex()
500 .w_full()
501 .justify_between()
502 .gap_2()
503 .h(TITLE_BAR_HEIGHT)
504 .line_height(TITLE_BAR_HEIGHT)
505 .overflow_x_hidden()
506 .px_2()
507 .border_b_1()
508 .border_color(cx.theme().title_bar_border)
509 .bg(cx.theme().tokens.title_bar)
510 .child(
511 h_flex()
512 .gap_2()
513 .text_sm()
514 .child(
515 Button::new("inspect")
516 .icon(IconName::Inspector)
517 .selected(inspector.is_picking())
518 .toggled(inspector.is_picking())
519 .small()
520 .ghost()
521 .on_click(cx.listener(|this, _, window, _| {
522 this.start_picking();
523 window.refresh();
524 })),
525 )
526 .child("Inspector"),
527 )
528 .child(
529 Button::new("close")
530 .icon(IconName::Close)
531 .small()
532 .ghost()
533 .on_click(|_, window, cx| {
534 window.dispatch_action(Box::new(ToggleInspector), cx);
535 }),
536 ),
537 )
538 .child(
539 v_flex()
540 .flex_1()
541 .p_3()
542 .gap_y_3()
543 .text_sm()
544 .when_some(source_location, |this, source_location| {
545 this.child(
546 h_flex()
547 .gap_x_2()
548 .text_sm()
549 .child(
550 Link::new("source-location")
551 .href(format!("file://{}", source_location))
552 .child(source_location.clone())
553 .flex_1()
554 .overflow_x_hidden(),
555 )
556 .child(Clipboard::new("copy-source-location").value(source_location)),
557 )
558 })
559 .children(element_global_id)
560 .children(inspector.render_inspector_states(window, cx)),
561 )
562 .into_any_element()
563}
564
565struct LspProvider {}
566
567impl CompletionProvider for LspProvider {
568 fn completions(
569 &self,
570 rope: &ropey::Rope,
571 offset: usize,
572 _: lsp_types::CompletionContext,
573 _: &mut Window,
574 cx: &mut App,
575 ) -> Task<Result<CompletionResponse>> {
576 let mut left_offset = 0;
577 while left_offset < 100 {
578 match rope.char_at(offset.saturating_sub(left_offset)) {
579 Some('.') => {
580 break;
581 }
582 None => break,
583 _ => {}
584 }
585 left_offset += 1;
586 }
587 let start = offset.saturating_sub(left_offset);
588 let trigger_character = rope.slice(start..offset).to_string();
589 if !trigger_character.starts_with('.') {
590 return Task::ready(Ok(CompletionResponse::Array(vec![])));
591 }
592
593 let start_pos = rope.offset_to_position(start);
594 let end_pos = rope.offset_to_position(offset);
595
596 cx.background_spawn(async move {
597 let styles = StyleMethods::get()
598 .map
599 .iter()
600 .filter_map(|(name, method)| {
601 let prefix = &trigger_character[1..];
602 if name.starts_with(&prefix) {
603 Some(CompletionItem {
604 label: name.to_string(),
605 filter_text: Some(prefix.to_string()),
606 kind: Some(CompletionItemKind::METHOD),
607 detail: Some("()".to_string()),
608 documentation: method
609 .documentation
610 .as_ref()
611 .map(|doc| lsp_types::Documentation::String(doc.to_string())),
612 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
613 range: lsp_types::Range {
614 start: start_pos,
615 end: end_pos,
616 },
617 new_text: format!(".{}()", name),
618 })),
619 ..Default::default()
620 })
621 } else {
622 None
623 }
624 })
625 .collect::<Vec<_>>();
626
627 Ok(CompletionResponse::Array(styles))
628 })
629 }
630
631 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
632 true
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use gpui::{AbsoluteLength, DefiniteLength, Length, rems};
639 use indoc::indoc;
640 use lsp_types::Position;
641
642 #[test]
643 fn test_rust_to_style() {
644 let (style, diagnostics) = super::rust_to_style(
645 Default::default(),
646 indoc! {r#"
647 fn build() -> Div {
648 div()
649 .p_1()
650 // This is a comment
651 .mx_2()
652 }
653 "#},
654 );
655 assert_eq!(diagnostics, vec![]);
656 assert_eq!(
657 style.padding.left,
658 Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.25))))
659 );
660 assert_eq!(
661 style.margin.left,
662 Some(Length::Definite(DefiniteLength::Absolute(
663 AbsoluteLength::Rems(rems(0.5))
664 )))
665 );
666
667 let (_, diagnostics) = super::rust_to_style(
668 Default::default(),
669 indoc! {r#"
670 fn build() -> Div {
671 div()
672 .p_1()
673 // This is a comment
674 .unknown_method
675 .bad_method()
676 }
677 "#},
678 );
679
680 assert_eq!(diagnostics.len(), 2);
681 assert_eq!(diagnostics[0].message, "unknown method `unknown_method`");
682 assert_eq!(diagnostics[0].range.start, Position::new(4, 9));
683 assert_eq!(diagnostics[0].range.end, Position::new(4, 23));
684 assert_eq!(diagnostics[1].message, "unknown method `bad_method`");
685 assert_eq!(diagnostics[1].range.start, Position::new(5, 9));
686 assert_eq!(diagnostics[1].range.end, Position::new(5, 19));
687 }
688}