div_inspector.rs

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