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}