1use anyhow::{Result, anyhow};
2use convert_case::{Case, Casing};
3use editor::{
4 Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MinimapVisibility,
5 MultiBuffer,
6};
7use fuzzy::StringMatch;
8use gpui::Hsla;
9use gpui::{
10 AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
11 StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
12};
13use language::language_settings::SoftWrap;
14use language::{
15 Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
16 DiagnosticSeverity, LanguageServerId, ToOffset as _,
17};
18use project::lsp_store::CompletionDocumentation;
19use project::{
20 Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project,
21 ProjectPath,
22};
23use std::fmt::Write as _;
24use std::ops::Range;
25use std::path::Path;
26use std::rc::Rc;
27use std::sync::LazyLock;
28use strum::IntoEnumIterator;
29use theme::{StatusColorField, ThemeColorField};
30use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
31use util::{FieldAccessByEnum, TakeUntilExt, split_str_with_ranges};
32
33/// Path used for unsaved buffer that contains style json. To support the json language server, this
34/// matches the name used in the generated schemas.
35const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json");
36
37pub(crate) struct DivInspector {
38 state: State,
39 project: Entity<Project>,
40 inspector_id: Option<InspectorElementId>,
41 inspector_state: Option<DivInspectorState>,
42 /// Value of `DivInspectorState.base_style` when initially picked.
43 initial_style: StyleRefinement,
44 /// Portion of `initial_style` that can't be converted to rust code.
45 unconvertible_style: StyleRefinement,
46 /// Edits the user has made to the json buffer: `json_editor - (unconvertible_style + rust_editor)`.
47 json_style_overrides: StyleRefinement,
48 /// Error to display from parsing the json, or if serialization errors somehow occur.
49 json_style_error: Option<SharedString>,
50 /// Currently selected completion.
51 rust_completion: Option<String>,
52 /// Range that will be replaced by the completion if selected.
53 rust_completion_replace_range: Option<Range<Anchor>>,
54}
55
56enum State {
57 Loading,
58 BuffersLoaded {
59 rust_style_buffer: Entity<Buffer>,
60 json_style_buffer: Entity<Buffer>,
61 },
62 Ready {
63 rust_style_buffer: Entity<Buffer>,
64 rust_style_editor: Entity<Editor>,
65 json_style_buffer: Entity<Buffer>,
66 json_style_editor: Entity<Editor>,
67 },
68 LoadError {
69 message: SharedString,
70 },
71}
72
73impl DivInspector {
74 pub fn new(
75 project: Entity<Project>,
76 window: &mut Window,
77 cx: &mut Context<Self>,
78 ) -> DivInspector {
79 // Open the buffers once, so they can then be used for each editor.
80 cx.spawn_in(window, {
81 let languages = project.read(cx).languages().clone();
82 let project = project.clone();
83 async move |this, cx| {
84 // Open the JSON style buffer in the inspector-specific project, so that it runs the
85 // JSON language server.
86 let json_style_buffer =
87 Self::create_buffer_in_project(ZED_INSPECTOR_STYLE_JSON, &project, cx).await;
88
89 // Create Rust style buffer without adding it to the project / buffer_store, so that
90 // Rust Analyzer doesn't get started for it.
91 let rust_language_result = languages.language_for_name("Rust").await;
92 let rust_style_buffer = rust_language_result.and_then(|rust_language| {
93 cx.new(|cx| Buffer::local("", cx).with_language(rust_language, cx))
94 });
95
96 match json_style_buffer.and_then(|json_style_buffer| {
97 rust_style_buffer
98 .map(|rust_style_buffer| (json_style_buffer, rust_style_buffer))
99 }) {
100 Ok((json_style_buffer, rust_style_buffer)) => {
101 this.update_in(cx, |this, window, cx| {
102 this.state = State::BuffersLoaded {
103 json_style_buffer,
104 rust_style_buffer,
105 };
106
107 // Initialize editors immediately instead of waiting for
108 // `update_inspected_element`. This avoids continuing to show
109 // "Loading..." until the user moves the mouse to a different element.
110 if let Some(id) = this.inspector_id.take() {
111 let inspector_state =
112 window.with_inspector_state(Some(&id), cx, |state, _window| {
113 state.clone()
114 });
115 if let Some(inspector_state) = inspector_state {
116 this.update_inspected_element(&id, inspector_state, window, cx);
117 cx.notify();
118 }
119 }
120 })
121 .ok();
122 }
123 Err(err) => {
124 this.update(cx, |this, _cx| {
125 this.state = State::LoadError {
126 message: format!(
127 "Failed to create buffers for style editing: {err}"
128 )
129 .into(),
130 };
131 })
132 .ok();
133 }
134 }
135 }
136 })
137 .detach();
138
139 DivInspector {
140 state: State::Loading,
141 project,
142 inspector_id: None,
143 inspector_state: None,
144 initial_style: StyleRefinement::default(),
145 unconvertible_style: StyleRefinement::default(),
146 json_style_overrides: StyleRefinement::default(),
147 rust_completion: None,
148 rust_completion_replace_range: None,
149 json_style_error: None,
150 }
151 }
152
153 pub fn update_inspected_element(
154 &mut self,
155 id: &InspectorElementId,
156 inspector_state: DivInspectorState,
157 window: &mut Window,
158 cx: &mut Context<Self>,
159 ) {
160 let style = (*inspector_state.base_style).clone();
161 self.inspector_state = Some(inspector_state);
162
163 if self.inspector_id.as_ref() == Some(id) {
164 return;
165 }
166
167 self.inspector_id = Some(id.clone());
168 self.initial_style = style.clone();
169
170 let (rust_style_buffer, json_style_buffer) = match &self.state {
171 State::BuffersLoaded {
172 rust_style_buffer,
173 json_style_buffer,
174 }
175 | State::Ready {
176 rust_style_buffer,
177 json_style_buffer,
178 ..
179 } => (rust_style_buffer.clone(), json_style_buffer.clone()),
180 State::Loading | State::LoadError { .. } => return,
181 };
182
183 let json_style_editor = self.create_editor(json_style_buffer.clone(), window, cx);
184 let rust_style_editor = self.create_editor(rust_style_buffer.clone(), window, cx);
185
186 rust_style_editor.update(cx, {
187 let div_inspector = cx.entity();
188 |rust_style_editor, _cx| {
189 rust_style_editor.set_completion_provider(Some(Rc::new(
190 RustStyleCompletionProvider { div_inspector },
191 )));
192 }
193 });
194
195 let rust_style = match self.reset_style_editors(&rust_style_buffer, &json_style_buffer, cx)
196 {
197 Ok(rust_style) => {
198 self.json_style_error = None;
199 rust_style
200 }
201 Err(err) => {
202 self.json_style_error = Some(format!("{err}").into());
203 return;
204 }
205 };
206
207 cx.subscribe_in(&json_style_editor, window, {
208 let id = id.clone();
209 let rust_style_buffer = rust_style_buffer.clone();
210 move |this, editor, event: &EditorEvent, window, cx| {
211 if event == &EditorEvent::BufferEdited {
212 let style_json = editor.read(cx).text(cx);
213 match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
214 Ok(new_style) => {
215 let (rust_style, _) = this.style_from_rust_buffer_snapshot(
216 &rust_style_buffer.read(cx).snapshot(),
217 cx,
218 );
219
220 let mut unconvertible_plus_rust = this.unconvertible_style.clone();
221 unconvertible_plus_rust.refine(&rust_style);
222
223 // The serialization of `DefiniteLength::Fraction` does not perfectly
224 // roundtrip because with f32, `(x / 100.0 * 100.0) == x` is not always
225 // true (such as for `p_1_3`). This can cause these values to
226 // erroneously appear in `json_style_overrides` since they are not
227 // perfectly equal. Roundtripping before `subtract` fixes this.
228 unconvertible_plus_rust =
229 serde_json::to_string(&unconvertible_plus_rust)
230 .ok()
231 .and_then(|json| {
232 serde_json_lenient::from_str_lenient(&json).ok()
233 })
234 .unwrap_or(unconvertible_plus_rust);
235
236 this.json_style_overrides =
237 new_style.subtract(&unconvertible_plus_rust);
238
239 window.with_inspector_state::<DivInspectorState, _>(
240 Some(&id),
241 cx,
242 |inspector_state, _window| {
243 if let Some(inspector_state) = inspector_state.as_mut() {
244 *inspector_state.base_style = new_style;
245 }
246 },
247 );
248 window.refresh();
249 this.json_style_error = None;
250 }
251 Err(err) => this.json_style_error = Some(err.to_string().into()),
252 }
253 }
254 }
255 })
256 .detach();
257
258 cx.subscribe(&rust_style_editor, {
259 let json_style_buffer = json_style_buffer.clone();
260 let rust_style_buffer = rust_style_buffer.clone();
261 move |this, _editor, event: &EditorEvent, cx| {
262 if let EditorEvent::BufferEdited = event {
263 this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
264 }
265 }
266 })
267 .detach();
268
269 self.unconvertible_style = style.subtract(&rust_style);
270 self.json_style_overrides = StyleRefinement::default();
271 self.state = State::Ready {
272 rust_style_buffer,
273 rust_style_editor,
274 json_style_buffer,
275 json_style_editor,
276 };
277 }
278
279 fn reset_style(&mut self, cx: &mut App) {
280 if let State::Ready {
281 rust_style_buffer,
282 json_style_buffer,
283 ..
284 } = &self.state
285 {
286 if let Err(err) =
287 self.reset_style_editors(&rust_style_buffer.clone(), &json_style_buffer.clone(), cx)
288 {
289 self.json_style_error = Some(format!("{err}").into());
290 } else {
291 self.json_style_error = None;
292 }
293 }
294 }
295
296 fn reset_style_editors(
297 &self,
298 rust_style_buffer: &Entity<Buffer>,
299 json_style_buffer: &Entity<Buffer>,
300 cx: &mut App,
301 ) -> Result<StyleRefinement> {
302 let json_text = match serde_json::to_string_pretty(&self.initial_style) {
303 Ok(json_text) => json_text,
304 Err(err) => {
305 return Err(anyhow!("Failed to convert style to JSON: {err}"));
306 }
307 };
308
309 let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style, cx);
310 rust_style_buffer.update(cx, |rust_style_buffer, cx| {
311 rust_style_buffer.set_text(rust_code, cx);
312 let snapshot = rust_style_buffer.snapshot();
313 let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot, cx);
314 Self::set_rust_buffer_diagnostics(
315 unrecognized_ranges,
316 rust_style_buffer,
317 &snapshot,
318 cx,
319 );
320 });
321 json_style_buffer.update(cx, |json_style_buffer, cx| {
322 json_style_buffer.set_text(json_text, cx);
323 });
324
325 Ok(rust_style)
326 }
327
328 fn handle_rust_completion_selection_change(
329 &mut self,
330 rust_completion: Option<String>,
331 cx: &mut Context<Self>,
332 ) {
333 self.rust_completion = rust_completion;
334 if let State::Ready {
335 rust_style_buffer,
336 json_style_buffer,
337 ..
338 } = &self.state
339 {
340 self.update_json_style_from_rust(
341 &json_style_buffer.clone(),
342 &rust_style_buffer.clone(),
343 cx,
344 );
345 }
346 }
347
348 fn update_json_style_from_rust(
349 &mut self,
350 json_style_buffer: &Entity<Buffer>,
351 rust_style_buffer: &Entity<Buffer>,
352 cx: &mut Context<Self>,
353 ) {
354 let rust_style = rust_style_buffer.update(cx, |rust_style_buffer, cx| {
355 let snapshot = rust_style_buffer.snapshot();
356 let (rust_style, unrecognized_ranges) =
357 self.style_from_rust_buffer_snapshot(&snapshot, cx);
358 Self::set_rust_buffer_diagnostics(
359 unrecognized_ranges,
360 rust_style_buffer,
361 &snapshot,
362 cx,
363 );
364 rust_style
365 });
366
367 // Preserve parts of the json style which do not come from the unconvertible style or rust
368 // style. This way user edits to the json style are preserved when they are not overridden
369 // by the rust style.
370 //
371 // This results in a behavior where user changes to the json style that do overlap with the
372 // rust style will get set to the rust style when the user edits the rust style. It would be
373 // possible to update the rust style when the json style changes, but this is undesirable
374 // as the user may be working on the actual code in the rust style.
375 let mut new_style = self.unconvertible_style.clone();
376 new_style.refine(&self.json_style_overrides);
377 let new_style = new_style.refined(rust_style);
378
379 match serde_json::to_string_pretty(&new_style) {
380 Ok(json) => {
381 json_style_buffer.update(cx, |json_style_buffer, cx| {
382 json_style_buffer.set_text(json, cx);
383 });
384 }
385 Err(err) => {
386 self.json_style_error = Some(err.to_string().into());
387 }
388 }
389 }
390
391 fn style_from_rust_buffer_snapshot(
392 &self,
393 snapshot: &BufferSnapshot,
394 cx: &App,
395 ) -> (StyleRefinement, Vec<Range<Anchor>>) {
396 let method_names = if let Some((completion, completion_range)) = self
397 .rust_completion
398 .as_ref()
399 .zip(self.rust_completion_replace_range.as_ref())
400 {
401 let before_text = snapshot
402 .text_for_range(0..completion_range.start.to_offset(snapshot))
403 .collect::<String>();
404 let after_text = snapshot
405 .text_for_range(
406 completion_range.end.to_offset(snapshot)
407 ..snapshot.clip_offset(usize::MAX, Bias::Left),
408 )
409 .collect::<String>();
410 let mut method_names = split_str_with_ranges(&before_text, is_not_identifier_char)
411 .into_iter()
412 .map(|(range, name)| (Some(range), name.to_string()))
413 .collect::<Vec<_>>();
414 method_names.push((None, completion.clone()));
415 method_names.extend(
416 split_str_with_ranges(&after_text, is_not_identifier_char)
417 .into_iter()
418 .map(|(range, name)| (Some(range), name.to_string())),
419 );
420 method_names
421 } else {
422 split_str_with_ranges(&snapshot.text(), is_not_identifier_char)
423 .into_iter()
424 .map(|(range, name)| (Some(range), name.to_string()))
425 .collect::<Vec<_>>()
426 };
427
428 guess_style_from_rust_code(method_names, snapshot, cx)
429 }
430
431 fn set_rust_buffer_diagnostics(
432 unrecognized_ranges: Vec<Range<Anchor>>,
433 rust_style_buffer: &mut Buffer,
434 snapshot: &BufferSnapshot,
435 cx: &mut Context<Buffer>,
436 ) {
437 let diagnostic_entries = unrecognized_ranges
438 .into_iter()
439 .enumerate()
440 .map(|(ix, range)| DiagnosticEntry {
441 range,
442 diagnostic: Diagnostic {
443 message: "unrecognized".to_string(),
444 severity: DiagnosticSeverity::WARNING,
445 is_primary: true,
446 group_id: ix,
447 ..Default::default()
448 },
449 });
450 let diagnostics = DiagnosticSet::from_sorted_entries(diagnostic_entries, snapshot);
451 rust_style_buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
452 }
453
454 async fn create_buffer_in_project(
455 path: impl AsRef<Path>,
456 project: &Entity<Project>,
457 cx: &mut AsyncWindowContext,
458 ) -> Result<Entity<Buffer>> {
459 let worktree = project
460 .update(cx, |project, cx| project.create_worktree(path, false, cx))?
461 .await?;
462
463 let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
464 worktree_id: worktree.id(),
465 path: Path::new("").into(),
466 })?;
467
468 let buffer = project
469 .update(cx, |project, cx| project.open_path(project_path, cx))?
470 .await?
471 .1;
472
473 Ok(buffer)
474 }
475
476 fn create_editor(
477 &self,
478 buffer: Entity<Buffer>,
479 window: &mut Window,
480 cx: &mut Context<Self>,
481 ) -> Entity<Editor> {
482 cx.new(|cx| {
483 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
484 let mut editor = Editor::new(
485 EditorMode::full(),
486 multi_buffer,
487 Some(self.project.clone()),
488 window,
489 cx,
490 );
491 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
492 editor.set_show_line_numbers(false, cx);
493 editor.set_show_code_actions(false, cx);
494 editor.set_show_breakpoints(false, cx);
495 editor.set_show_git_diff_gutter(false, cx);
496 editor.set_show_runnables(false, cx);
497 editor.set_show_edit_predictions(Some(false), window, cx);
498 editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
499 editor
500 })
501 }
502}
503
504impl Render for DivInspector {
505 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
506 v_flex()
507 .size_full()
508 .gap_2()
509 .when_some(self.inspector_state.as_ref(), |this, inspector_state| {
510 this.child(
511 v_flex()
512 .child(Label::new("Layout").size(LabelSize::Large))
513 .child(render_layout_state(inspector_state, cx)),
514 )
515 })
516 .map(|this| match &self.state {
517 State::Loading | State::BuffersLoaded { .. } => {
518 this.child(Label::new("Loading..."))
519 }
520 State::LoadError { message } => this.child(
521 div()
522 .w_full()
523 .border_1()
524 .border_color(Color::Error.color(cx))
525 .child(Label::new(message)),
526 ),
527 State::Ready {
528 rust_style_editor,
529 json_style_editor,
530 ..
531 } => this
532 .child(
533 v_flex()
534 .gap_2()
535 .child(
536 h_flex()
537 .justify_between()
538 .child(Label::new("Rust Style").size(LabelSize::Large))
539 .child(
540 IconButton::new("reset-style", IconName::Eraser)
541 .tooltip(Tooltip::text("Reset style"))
542 .on_click(cx.listener(|this, _, _window, cx| {
543 this.reset_style(cx);
544 })),
545 ),
546 )
547 .child(div().h_64().child(rust_style_editor.clone())),
548 )
549 .child(
550 v_flex()
551 .gap_2()
552 .child(Label::new("JSON Style").size(LabelSize::Large))
553 .child(div().h_128().child(json_style_editor.clone()))
554 .when_some(self.json_style_error.as_ref(), |this, last_error| {
555 this.child(
556 div()
557 .w_full()
558 .border_1()
559 .border_color(Color::Error.color(cx))
560 .child(Label::new(last_error)),
561 )
562 }),
563 ),
564 })
565 .into_any_element()
566 }
567}
568
569fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
570 v_flex()
571 .child(
572 div()
573 .text_ui(cx)
574 .child(format!("Bounds: {}", inspector_state.bounds)),
575 )
576 .child(
577 div()
578 .id("content-size")
579 .text_ui(cx)
580 .tooltip(Tooltip::text("Size of the element's children"))
581 .child(
582 if inspector_state.content_size != inspector_state.bounds.size {
583 format!("Content size: {}", inspector_state.content_size)
584 } else {
585 "".to_string()
586 },
587 ),
588 )
589}
590
591static STYLE_METHODS: LazyLock<Vec<(Box<StyleRefinement>, FunctionReflection<StyleRefinement>)>> =
592 LazyLock::new(|| {
593 // Include StyledExt methods first so that those methods take precedence.
594 styled_ext_reflection::methods::<StyleRefinement>()
595 .into_iter()
596 .chain(styled_reflection::methods::<StyleRefinement>())
597 .map(|method| (Box::new(method.invoke(StyleRefinement::default())), method))
598 .collect()
599 });
600
601static COLOR_METHODS: &[ColorMethod] = &[
602 ColorMethod {
603 name: "border_color",
604 set: |style, color| style.border_color(color),
605 get: |style| style.border_color,
606 },
607 ColorMethod {
608 name: "text_color",
609 set: |style, color| style.text_color(color),
610 get: |style| style.text.as_ref().and_then(|text_style| text_style.color),
611 },
612 ColorMethod {
613 name: "text_decoration_color",
614 set: |style, color| style.text_decoration_color(color),
615 get: |style| {
616 style.text.as_ref().and_then(|text_style| {
617 text_style
618 .underline
619 .as_ref()
620 .and_then(|underline| underline.color)
621 })
622 },
623 },
624 ColorMethod {
625 name: "text_bg",
626 set: |style, color| style.text_bg(color),
627 get: |style| {
628 style
629 .text
630 .as_ref()
631 .and_then(|text_style| text_style.background_color)
632 },
633 },
634 ColorMethod {
635 name: "bg",
636 set: |style, color| style.bg(color),
637 get: |style| {
638 style.background.as_ref().and_then(|background| {
639 background
640 .color()
641 .and_then(|background| background.as_solid())
642 })
643 },
644 },
645];
646
647struct ColorMethod {
648 name: &'static str,
649 set: fn(StyleRefinement, Hsla) -> StyleRefinement,
650 get: fn(&StyleRefinement) -> Option<Hsla>,
651}
652
653fn guess_rust_code_from_style(goal_style: &StyleRefinement, cx: &App) -> (String, StyleRefinement) {
654 let mut subset_methods = Vec::new();
655 for (style, method) in STYLE_METHODS.iter() {
656 if goal_style.is_superset_of(style) {
657 subset_methods.push(method);
658 }
659 }
660
661 let mut code = String::new();
662 let mut style = StyleRefinement::default();
663 for method in subset_methods {
664 let before_change = style.clone();
665 style = method.invoke(style);
666 if before_change != style {
667 let _ = write!(code, ".{}()\n", &method.name);
668 }
669 }
670
671 let theme = cx.theme();
672 for color_method in COLOR_METHODS {
673 if let Some(color) = (color_method.get)(&goal_style) {
674 let mut found_match = false;
675 for theme_color_field in ThemeColorField::iter() {
676 // TODO: If proper color provenance information is added, then the
677 // `make_colors_unique` hack in the theme loading code can be removed.
678 if *theme.colors().get_field_by_enum(theme_color_field) == color {
679 found_match = true;
680 let _ = write!(
681 code,
682 ".{}(colors.{})\n",
683 color_method.name,
684 theme_color_field.as_ref().to_case(Case::Snake)
685 );
686 }
687 }
688 for status_color_field in StatusColorField::iter() {
689 if *theme.status().get_field_by_enum(status_color_field) == color {
690 found_match = true;
691 let _ = write!(
692 code,
693 ".{}(status.{})\n",
694 color_method.name,
695 status_color_field.as_ref().to_case(Case::Snake)
696 );
697 }
698 }
699 if found_match {
700 style = (color_method.set)(style, color);
701 }
702 }
703 }
704
705 (code, style)
706}
707
708fn guess_style_from_rust_code(
709 method_names: Vec<(Option<Range<usize>>, String)>,
710 snapshot: &BufferSnapshot,
711 cx: &App,
712) -> (StyleRefinement, Vec<Range<Anchor>>) {
713 let theme = cx.theme();
714 let mut style = StyleRefinement::default();
715 let mut unrecognized_ranges = Vec::new();
716 let mut preceded_by_color_method: Option<&ColorMethod> = None;
717 for (range, name) in method_names {
718 if name == "colors" || name == "status" {
719 continue;
720 }
721 if let Some(color_method) = preceded_by_color_method {
722 if let Some(field) = ThemeColorField::iter()
723 .find(|field| name == field.as_ref().to_case(Case::Snake).as_str())
724 {
725 style = (color_method.set)(style, *theme.colors().get_field_by_enum(field));
726 continue;
727 }
728 if let Some(field) = StatusColorField::iter()
729 .find(|field| name == field.as_ref().to_case(Case::Snake).as_str())
730 {
731 style = (color_method.set)(style, *theme.status().get_field_by_enum(field));
732 continue;
733 }
734 }
735 if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
736 preceded_by_color_method = None;
737 style = method.invoke(style);
738 continue;
739 }
740 if let Some(color_method) = COLOR_METHODS.iter().find(|m| m.name == name) {
741 preceded_by_color_method = Some(color_method);
742 continue;
743 }
744 if let Some(range) = range {
745 unrecognized_ranges
746 .push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
747 }
748 }
749
750 (style, unrecognized_ranges)
751}
752
753fn is_not_identifier_char(c: char) -> bool {
754 !c.is_alphanumeric() && c != '_'
755}
756
757struct RustStyleCompletionProvider {
758 div_inspector: Entity<DivInspector>,
759}
760
761impl CompletionProvider for RustStyleCompletionProvider {
762 fn completions(
763 &self,
764 _excerpt_id: ExcerptId,
765 buffer: &Entity<Buffer>,
766 position: Anchor,
767 _: editor::CompletionContext,
768 _window: &mut Window,
769 cx: &mut Context<Editor>,
770 ) -> Task<Result<Vec<CompletionResponse>>> {
771 let snapshot: &text::BufferSnapshot = &buffer.read(cx);
772 let Some(replace_range) = completion_replace_range(snapshot, &position) else {
773 return Task::ready(Ok(Vec::new()));
774 };
775
776 let preceded_by_colors;
777 let preceded_by_status;
778 {
779 let rev_chars_before = snapshot
780 .reversed_chars_at(position)
781 .take(50)
782 .collect::<String>();
783 let mut rev_words_before = rev_chars_before.split(is_not_identifier_char);
784 let rev_first_word_before = rev_words_before.next();
785 let rev_second_word_before = rev_words_before.next();
786 preceded_by_colors =
787 rev_first_word_before == Some("sroloc") || rev_second_word_before == Some("sroloc");
788 preceded_by_status =
789 rev_first_word_before == Some("sutats") || rev_second_word_before == Some("sutats");
790 }
791
792 self.div_inspector.update(cx, |div_inspector, _cx| {
793 div_inspector.rust_completion_replace_range = Some(replace_range.clone());
794 });
795
796 if preceded_by_colors {
797 Task::ready(Ok(vec![CompletionResponse {
798 completions: ThemeColorField::iter()
799 .map(|color_field| {
800 let name = color_field.as_ref().to_case(Case::Snake);
801 Completion {
802 replace_range: replace_range.clone(),
803 new_text: format!(".{}", name),
804 label: CodeLabel::plain(name, None),
805 icon_path: None,
806 documentation: None,
807 source: CompletionSource::Custom,
808 insert_text_mode: None,
809 confirm: None,
810 }
811 })
812 .collect(),
813 display_options: CompletionDisplayOptions::default(),
814 is_incomplete: false,
815 }]))
816 } else if preceded_by_status {
817 Task::ready(Ok(vec![CompletionResponse {
818 completions: StatusColorField::iter()
819 .map(|color_field| {
820 let name = color_field.as_ref().to_case(Case::Snake);
821 Completion {
822 replace_range: replace_range.clone(),
823 new_text: format!(".{}", name),
824 label: CodeLabel::plain(name, None),
825 icon_path: None,
826 documentation: None,
827 source: CompletionSource::Custom,
828 insert_text_mode: None,
829 confirm: None,
830 }
831 })
832 .collect(),
833 display_options: CompletionDisplayOptions::default(),
834 is_incomplete: false,
835 }]))
836 } else {
837 Task::ready(Ok(vec![CompletionResponse {
838 completions: STYLE_METHODS
839 .iter()
840 .map(|(_, method)| Completion {
841 replace_range: replace_range.clone(),
842 new_text: format!(".{}()", method.name),
843 label: CodeLabel::plain(method.name.to_string(), None),
844 icon_path: None,
845 documentation: method.documentation.map(|documentation| {
846 CompletionDocumentation::MultiLineMarkdown(documentation.into())
847 }),
848 source: CompletionSource::Custom,
849 insert_text_mode: None,
850 confirm: None,
851 })
852 .chain(COLOR_METHODS.iter().map(|method| Completion {
853 replace_range: replace_range.clone(),
854 new_text: format!(".{}(colors.", method.name),
855 label: CodeLabel::plain(method.name.to_string(), None),
856 icon_path: None,
857 documentation: None,
858 source: CompletionSource::Custom,
859 insert_text_mode: None,
860 confirm: None,
861 }))
862 .collect(),
863 display_options: CompletionDisplayOptions::default(),
864 is_incomplete: false,
865 }]))
866 }
867 }
868
869 fn is_completion_trigger(
870 &self,
871 buffer: &Entity<language::Buffer>,
872 position: language::Anchor,
873 _text: &str,
874 _trigger_in_words: bool,
875 _menu_is_open: bool,
876 cx: &mut Context<Editor>,
877 ) -> bool {
878 let snapshot: &text::BufferSnapshot = &buffer.read(cx);
879 completion_replace_range(snapshot, &position).is_some()
880 }
881
882 fn selection_changed(&self, mat: Option<&StringMatch>, _window: &mut Window, cx: &mut App) {
883 let div_inspector = self.div_inspector.clone();
884 let rust_completion = mat.as_ref().map(|mat| mat.string.clone());
885 cx.defer(move |cx| {
886 div_inspector.update(cx, |div_inspector, cx| {
887 div_inspector.handle_rust_completion_selection_change(rust_completion, cx);
888 });
889 });
890 }
891
892 fn sort_completions(&self) -> bool {
893 false
894 }
895}
896
897fn completion_replace_range(
898 snapshot: &text::BufferSnapshot,
899 anchor: &Anchor,
900) -> Option<Range<Anchor>> {
901 let offset = anchor.to_offset(snapshot);
902 let start: usize = snapshot
903 .reversed_chars_at(offset)
904 .take_until(|c| is_not_identifier_char(*c))
905 .map(|char| char.len_utf8())
906 .sum();
907 let end: usize = snapshot
908 .chars_at(offset)
909 .take_until(|c| is_not_identifier_char(*c))
910 .skip(1)
911 .map(|char| char.len_utf8())
912 .sum();
913 Some(snapshot.anchor_after(offset - start)..snapshot.anchor_before(offset + end))
914}