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