div_inspector.rs

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