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}