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