1pub mod cursor_position;
2
3use cursor_position::{LineIndicatorFormat, UserCaretPosition};
4use editor::{
5 Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, actions::Tab, scroll::Autoscroll,
6};
7use gpui::{
8 App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
9 Subscription, div, prelude::*,
10};
11use language::Buffer;
12use settings::Settings;
13use text::{Bias, Point};
14use theme::ActiveTheme;
15use ui::prelude::*;
16use util::paths::FILE_ROW_COLUMN_DELIMITER;
17use workspace::ModalView;
18
19pub fn init(cx: &mut App) {
20 LineIndicatorFormat::register(cx);
21 cx.observe_new(GoToLine::register).detach();
22}
23
24pub struct GoToLine {
25 line_editor: Entity<Editor>,
26 active_editor: Entity<Editor>,
27 current_text: SharedString,
28 prev_scroll_position: Option<gpui::Point<f32>>,
29 _subscriptions: Vec<Subscription>,
30}
31
32impl ModalView for GoToLine {}
33
34impl Focusable for GoToLine {
35 fn focus_handle(&self, cx: &App) -> FocusHandle {
36 self.line_editor.focus_handle(cx)
37 }
38}
39impl EventEmitter<DismissEvent> for GoToLine {}
40
41enum GoToLineRowHighlights {}
42
43impl GoToLine {
44 fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
45 let handle = cx.entity().downgrade();
46 editor
47 .register_action(move |_: &editor::actions::ToggleGoToLine, window, cx| {
48 let Some(editor_handle) = handle.upgrade() else {
49 return;
50 };
51 let Some(workspace) = editor_handle.read(cx).workspace() else {
52 return;
53 };
54 let editor = editor_handle.read(cx);
55 let Some((_, buffer, _)) = editor.active_excerpt(cx) else {
56 return;
57 };
58 workspace.update(cx, |workspace, cx| {
59 workspace.toggle_modal(window, cx, move |window, cx| {
60 GoToLine::new(editor_handle, buffer, window, cx)
61 });
62 })
63 })
64 .detach();
65 }
66
67 pub fn new(
68 active_editor: Entity<Editor>,
69 active_buffer: Entity<Buffer>,
70 window: &mut Window,
71 cx: &mut Context<Self>,
72 ) -> Self {
73 let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
74 let user_caret = UserCaretPosition::at_selection_end(
75 &editor.selections.last::<Point>(cx),
76 &editor.buffer().read(cx).snapshot(cx),
77 );
78
79 let snapshot = active_buffer.read(cx).snapshot();
80 let last_line = editor
81 .buffer()
82 .read(cx)
83 .excerpts_for_buffer(snapshot.remote_id(), cx)
84 .into_iter()
85 .map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row)
86 .max()
87 .unwrap_or(0);
88
89 (user_caret, last_line, editor.scroll_position(cx))
90 });
91
92 let line = user_caret.line.get();
93 let column = user_caret.character.get();
94
95 let line_editor = cx.new(|cx| {
96 let mut editor = Editor::single_line(window, cx);
97 let editor_handle = cx.entity().downgrade();
98 editor
99 .register_action::<Tab>({
100 move |_, window, cx| {
101 let Some(editor) = editor_handle.upgrade() else {
102 return;
103 };
104 editor.update(cx, |editor, cx| {
105 if let Some(placeholder_text) = editor.placeholder_text() {
106 if editor.text(cx).is_empty() {
107 let placeholder_text = placeholder_text.to_string();
108 editor.set_text(placeholder_text, window, cx);
109 }
110 }
111 });
112 }
113 })
114 .detach();
115 editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
116 editor
117 });
118 let line_editor_change = cx.subscribe_in(&line_editor, window, Self::on_line_editor_event);
119
120 let current_text = format!(
121 "Current Line: {} of {} (column {})",
122 line,
123 last_line + 1,
124 column
125 );
126
127 Self {
128 line_editor,
129 active_editor,
130 current_text: current_text.into(),
131 prev_scroll_position: Some(scroll_position),
132 _subscriptions: vec![line_editor_change, cx.on_release_in(window, Self::release)],
133 }
134 }
135
136 fn release(&mut self, window: &mut Window, cx: &mut App) {
137 let scroll_position = self.prev_scroll_position.take();
138 self.active_editor.update(cx, |editor, cx| {
139 editor.clear_row_highlights::<GoToLineRowHighlights>();
140 if let Some(scroll_position) = scroll_position {
141 editor.set_scroll_position(scroll_position, window, cx);
142 }
143 cx.notify();
144 })
145 }
146
147 fn on_line_editor_event(
148 &mut self,
149 _: &Entity<Editor>,
150 event: &editor::EditorEvent,
151 _window: &mut Window,
152 cx: &mut Context<Self>,
153 ) {
154 match event {
155 editor::EditorEvent::Blurred => {
156 self.prev_scroll_position.take();
157 cx.emit(DismissEvent)
158 }
159 editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
160 _ => {}
161 }
162 }
163
164 fn highlight_current_line(&mut self, cx: &mut Context<Self>) {
165 self.active_editor.update(cx, |editor, cx| {
166 editor.clear_row_highlights::<GoToLineRowHighlights>();
167 let snapshot = editor.buffer().read(cx).snapshot(cx);
168 let Some(start) = self.anchor_from_query(&snapshot, cx) else {
169 return;
170 };
171 let mut start_point = start.to_point(&snapshot);
172 start_point.column = 0;
173 // Force non-empty range to ensure the line is highlighted.
174 let mut end_point = snapshot.clip_point(start_point + Point::new(0, 1), Bias::Left);
175 if start_point == end_point {
176 end_point = snapshot.clip_point(start_point + Point::new(1, 0), Bias::Left);
177 }
178
179 let end = snapshot.anchor_after(end_point);
180 editor.highlight_rows::<GoToLineRowHighlights>(
181 start..end,
182 cx.theme().colors().editor_highlighted_line_background,
183 true,
184 cx,
185 );
186 editor.request_autoscroll(Autoscroll::center(), cx);
187 });
188 cx.notify();
189 }
190
191 fn anchor_from_query(
192 &self,
193 snapshot: &MultiBufferSnapshot,
194 cx: &Context<Editor>,
195 ) -> Option<Anchor> {
196 let (query_row, query_char) = self.line_and_char_from_query(cx)?;
197 let row = query_row.saturating_sub(1);
198 let character = query_char.unwrap_or(0).saturating_sub(1);
199
200 let start_offset = Point::new(row, 0).to_offset(snapshot);
201 const MAX_BYTES_IN_UTF_8: u32 = 4;
202 let max_end_offset = snapshot
203 .clip_point(
204 Point::new(row, character * MAX_BYTES_IN_UTF_8 + 1),
205 Bias::Right,
206 )
207 .to_offset(snapshot);
208
209 let mut chars_to_iterate = character;
210 let mut end_offset = start_offset;
211 'outer: for text_chunk in snapshot.text_for_range(start_offset..max_end_offset) {
212 let mut offset_increment = 0;
213 for c in text_chunk.chars() {
214 if chars_to_iterate == 0 {
215 end_offset += offset_increment;
216 break 'outer;
217 } else {
218 chars_to_iterate -= 1;
219 offset_increment += c.len_utf8();
220 }
221 }
222 end_offset += offset_increment;
223 }
224 Some(snapshot.anchor_before(snapshot.clip_offset(end_offset, Bias::Left)))
225 }
226
227 fn line_and_char_from_query(&self, cx: &App) -> Option<(u32, Option<u32>)> {
228 let input = self.line_editor.read(cx).text(cx);
229 let mut components = input
230 .splitn(2, FILE_ROW_COLUMN_DELIMITER)
231 .map(str::trim)
232 .fuse();
233 let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
234 let column = components.next().and_then(|col| col.parse::<u32>().ok());
235 Some((row, column))
236 }
237
238 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
239 cx.emit(DismissEvent);
240 }
241
242 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
243 self.active_editor.update(cx, |editor, cx| {
244 let snapshot = editor.buffer().read(cx).snapshot(cx);
245 let Some(start) = self.anchor_from_query(&snapshot, cx) else {
246 return;
247 };
248 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
249 s.select_anchor_ranges([start..start])
250 });
251 editor.focus_handle(cx).focus(window);
252 cx.notify()
253 });
254 self.prev_scroll_position.take();
255
256 cx.emit(DismissEvent);
257 }
258}
259
260impl Render for GoToLine {
261 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
262 let help_text = match self.line_and_char_from_query(cx) {
263 Some((line, Some(character))) => {
264 format!("Go to line {line}, character {character}").into()
265 }
266 Some((line, None)) => format!("Go to line {line}").into(),
267 None => self.current_text.clone(),
268 };
269
270 v_flex()
271 .w(rems(24.))
272 .elevation_2(cx)
273 .key_context("GoToLine")
274 .on_action(cx.listener(Self::cancel))
275 .on_action(cx.listener(Self::confirm))
276 .child(
277 div()
278 .border_b_1()
279 .border_color(cx.theme().colors().border_variant)
280 .px_2()
281 .py_1()
282 .child(self.line_editor.clone()),
283 )
284 .child(
285 h_flex()
286 .px_2()
287 .py_1()
288 .gap_1()
289 .child(Label::new(help_text).color(Color::Muted)),
290 )
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use cursor_position::{CursorPosition, SelectionStats, UserCaretPosition};
298 use editor::actions::{MoveRight, MoveToBeginning, SelectAll};
299 use gpui::{TestAppContext, VisualTestContext};
300 use indoc::indoc;
301 use project::{FakeFs, Project};
302 use serde_json::json;
303 use std::{num::NonZeroU32, sync::Arc, time::Duration};
304 use util::path;
305 use workspace::{AppState, Workspace};
306
307 #[gpui::test]
308 async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
309 init_test(cx);
310 let fs = FakeFs::new(cx.executor());
311 fs.insert_tree(
312 path!("/dir"),
313 json!({
314 "a.rs": indoc!{"
315 struct SingleLine; // display line 0
316 // display line 1
317 struct MultiLine { // display line 2
318 field_1: i32, // display line 3
319 field_2: i32, // display line 4
320 } // display line 5
321 // display line 6
322 struct Another { // display line 7
323 field_1: i32, // display line 8
324 field_2: i32, // display line 9
325 field_3: i32, // display line 10
326 field_4: i32, // display line 11
327 } // display line 12
328 "}
329 }),
330 )
331 .await;
332
333 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
334 let (workspace, cx) =
335 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
336 let worktree_id = workspace.update(cx, |workspace, cx| {
337 workspace.project().update(cx, |project, cx| {
338 project.worktrees(cx).next().unwrap().read(cx).id()
339 })
340 });
341 let _buffer = project
342 .update(cx, |project, cx| {
343 project.open_local_buffer(path!("/dir/a.rs"), cx)
344 })
345 .await
346 .unwrap();
347 let editor = workspace
348 .update_in(cx, |workspace, window, cx| {
349 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
350 })
351 .await
352 .unwrap()
353 .downcast::<Editor>()
354 .unwrap();
355
356 let go_to_line_view = open_go_to_line_view(&workspace, cx);
357 assert_eq!(
358 highlighted_display_rows(&editor, cx),
359 Vec::<u32>::new(),
360 "Initially opened go to line modal should not highlight any rows"
361 );
362 assert_single_caret_at_row(&editor, 0, cx);
363
364 cx.simulate_input("1");
365 assert_eq!(
366 highlighted_display_rows(&editor, cx),
367 vec![0],
368 "Go to line modal should highlight a row, corresponding to the query"
369 );
370 assert_single_caret_at_row(&editor, 0, cx);
371
372 cx.simulate_input("8");
373 assert_eq!(
374 highlighted_display_rows(&editor, cx),
375 vec![13],
376 "If the query is too large, the last row should be highlighted"
377 );
378 assert_single_caret_at_row(&editor, 0, cx);
379
380 cx.dispatch_action(menu::Cancel);
381 drop(go_to_line_view);
382 editor.update(cx, |_, _| {});
383 assert_eq!(
384 highlighted_display_rows(&editor, cx),
385 Vec::<u32>::new(),
386 "After cancelling and closing the modal, no rows should be highlighted"
387 );
388 assert_single_caret_at_row(&editor, 0, cx);
389
390 let go_to_line_view = open_go_to_line_view(&workspace, cx);
391 assert_eq!(
392 highlighted_display_rows(&editor, cx),
393 Vec::<u32>::new(),
394 "Reopened modal should not highlight any rows"
395 );
396 assert_single_caret_at_row(&editor, 0, cx);
397
398 let expected_highlighted_row = 4;
399 cx.simulate_input("5");
400 assert_eq!(
401 highlighted_display_rows(&editor, cx),
402 vec![expected_highlighted_row]
403 );
404 assert_single_caret_at_row(&editor, 0, cx);
405 cx.dispatch_action(menu::Confirm);
406 drop(go_to_line_view);
407 editor.update(cx, |_, _| {});
408 assert_eq!(
409 highlighted_display_rows(&editor, cx),
410 Vec::<u32>::new(),
411 "After confirming and closing the modal, no rows should be highlighted"
412 );
413 // On confirm, should place the caret on the highlighted row.
414 assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
415 }
416
417 #[gpui::test]
418 async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
419 init_test(cx);
420
421 let fs = FakeFs::new(cx.executor());
422 fs.insert_tree(
423 path!("/dir"),
424 json!({
425 "a.rs": "ēlo"
426 }),
427 )
428 .await;
429
430 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
431 let (workspace, cx) =
432 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
433 workspace.update_in(cx, |workspace, window, cx| {
434 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
435 workspace.status_bar().update(cx, |status_bar, cx| {
436 status_bar.add_right_item(cursor_position, window, cx);
437 });
438 });
439
440 let worktree_id = workspace.update(cx, |workspace, cx| {
441 workspace.project().update(cx, |project, cx| {
442 project.worktrees(cx).next().unwrap().read(cx).id()
443 })
444 });
445 let _buffer = project
446 .update(cx, |project, cx| {
447 project.open_local_buffer(path!("/dir/a.rs"), cx)
448 })
449 .await
450 .unwrap();
451 let editor = workspace
452 .update_in(cx, |workspace, window, cx| {
453 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
454 })
455 .await
456 .unwrap()
457 .downcast::<Editor>()
458 .unwrap();
459
460 cx.executor().advance_clock(Duration::from_millis(200));
461 workspace.update(cx, |workspace, cx| {
462 assert_eq!(
463 &SelectionStats {
464 lines: 0,
465 characters: 0,
466 selections: 1,
467 },
468 workspace
469 .status_bar()
470 .read(cx)
471 .item_of_type::<CursorPosition>()
472 .expect("missing cursor position item")
473 .read(cx)
474 .selection_stats(),
475 "No selections should be initially"
476 );
477 });
478 editor.update_in(cx, |editor, window, cx| {
479 editor.select_all(&SelectAll, window, cx)
480 });
481 cx.executor().advance_clock(Duration::from_millis(200));
482 workspace.update(cx, |workspace, cx| {
483 assert_eq!(
484 &SelectionStats {
485 lines: 1,
486 characters: 3,
487 selections: 1,
488 },
489 workspace
490 .status_bar()
491 .read(cx)
492 .item_of_type::<CursorPosition>()
493 .expect("missing cursor position item")
494 .read(cx)
495 .selection_stats(),
496 "After selecting a text with multibyte unicode characters, the character count should be correct"
497 );
498 });
499 }
500
501 #[gpui::test]
502 async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
503 init_test(cx);
504
505 let text = "ēlo你好";
506 let fs = FakeFs::new(cx.executor());
507 fs.insert_tree(
508 path!("/dir"),
509 json!({
510 "a.rs": text
511 }),
512 )
513 .await;
514
515 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
516 let (workspace, cx) =
517 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
518 workspace.update_in(cx, |workspace, window, cx| {
519 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
520 workspace.status_bar().update(cx, |status_bar, cx| {
521 status_bar.add_right_item(cursor_position, window, cx);
522 });
523 });
524
525 let worktree_id = workspace.update(cx, |workspace, cx| {
526 workspace.project().update(cx, |project, cx| {
527 project.worktrees(cx).next().unwrap().read(cx).id()
528 })
529 });
530 let _buffer = project
531 .update(cx, |project, cx| {
532 project.open_local_buffer(path!("/dir/a.rs"), cx)
533 })
534 .await
535 .unwrap();
536 let editor = workspace
537 .update_in(cx, |workspace, window, cx| {
538 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
539 })
540 .await
541 .unwrap()
542 .downcast::<Editor>()
543 .unwrap();
544
545 editor.update_in(cx, |editor, window, cx| {
546 editor.move_to_beginning(&MoveToBeginning, window, cx)
547 });
548 cx.executor().advance_clock(Duration::from_millis(200));
549 assert_eq!(
550 user_caret_position(1, 1),
551 current_position(&workspace, cx),
552 "Beginning of the line should be at first line, before any characters"
553 );
554
555 for (i, c) in text.chars().enumerate() {
556 let i = i as u32 + 1;
557 editor.update_in(cx, |editor, window, cx| {
558 editor.move_right(&MoveRight, window, cx)
559 });
560 cx.executor().advance_clock(Duration::from_millis(200));
561 assert_eq!(
562 user_caret_position(1, i + 1),
563 current_position(&workspace, cx),
564 "Wrong position for char '{c}' in string '{text}'",
565 );
566 }
567
568 editor.update_in(cx, |editor, window, cx| {
569 editor.move_right(&MoveRight, window, cx)
570 });
571 cx.executor().advance_clock(Duration::from_millis(200));
572 assert_eq!(
573 user_caret_position(1, text.chars().count() as u32 + 1),
574 current_position(&workspace, cx),
575 "After reaching the end of the text, position should not change when moving right"
576 );
577 }
578
579 #[gpui::test]
580 async fn test_go_into_unicode(cx: &mut TestAppContext) {
581 init_test(cx);
582
583 let text = "ēlo你好";
584 let fs = FakeFs::new(cx.executor());
585 fs.insert_tree(
586 path!("/dir"),
587 json!({
588 "a.rs": text
589 }),
590 )
591 .await;
592
593 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
594 let (workspace, cx) =
595 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
596 workspace.update_in(cx, |workspace, window, cx| {
597 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
598 workspace.status_bar().update(cx, |status_bar, cx| {
599 status_bar.add_right_item(cursor_position, window, cx);
600 });
601 });
602
603 let worktree_id = workspace.update(cx, |workspace, cx| {
604 workspace.project().update(cx, |project, cx| {
605 project.worktrees(cx).next().unwrap().read(cx).id()
606 })
607 });
608 let _buffer = project
609 .update(cx, |project, cx| {
610 project.open_local_buffer(path!("/dir/a.rs"), cx)
611 })
612 .await
613 .unwrap();
614 let editor = workspace
615 .update_in(cx, |workspace, window, cx| {
616 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
617 })
618 .await
619 .unwrap()
620 .downcast::<Editor>()
621 .unwrap();
622
623 editor.update_in(cx, |editor, window, cx| {
624 editor.move_to_beginning(&MoveToBeginning, window, cx)
625 });
626 cx.executor().advance_clock(Duration::from_millis(200));
627 assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
628
629 for (i, c) in text.chars().enumerate() {
630 let i = i as u32 + 1;
631 let point = user_caret_position(1, i + 1);
632 go_to_point(point, user_caret_position(1, i), &workspace, cx);
633 cx.executor().advance_clock(Duration::from_millis(200));
634 assert_eq!(
635 point,
636 current_position(&workspace, cx),
637 "When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
638 );
639 }
640
641 go_to_point(
642 user_caret_position(111, 222),
643 user_caret_position(1, text.chars().count() as u32 + 1),
644 &workspace,
645 cx,
646 );
647 cx.executor().advance_clock(Duration::from_millis(200));
648 assert_eq!(
649 user_caret_position(1, text.chars().count() as u32 + 1),
650 current_position(&workspace, cx),
651 "When going into too large point, should go to the end of the text"
652 );
653 }
654
655 fn current_position(
656 workspace: &Entity<Workspace>,
657 cx: &mut VisualTestContext,
658 ) -> UserCaretPosition {
659 workspace.update(cx, |workspace, cx| {
660 workspace
661 .status_bar()
662 .read(cx)
663 .item_of_type::<CursorPosition>()
664 .expect("missing cursor position item")
665 .read(cx)
666 .position()
667 .expect("No position found")
668 })
669 }
670
671 fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
672 UserCaretPosition {
673 line: NonZeroU32::new(line).unwrap(),
674 character: NonZeroU32::new(character).unwrap(),
675 }
676 }
677
678 fn go_to_point(
679 new_point: UserCaretPosition,
680 expected_placeholder: UserCaretPosition,
681 workspace: &Entity<Workspace>,
682 cx: &mut VisualTestContext,
683 ) {
684 let go_to_line_view = open_go_to_line_view(workspace, cx);
685 go_to_line_view.update(cx, |go_to_line_view, cx| {
686 assert_eq!(
687 go_to_line_view
688 .line_editor
689 .read(cx)
690 .placeholder_text()
691 .expect("No placeholder text"),
692 format!(
693 "{}:{}",
694 expected_placeholder.line, expected_placeholder.character
695 )
696 );
697 });
698 cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
699 cx.dispatch_action(menu::Confirm);
700 }
701
702 fn open_go_to_line_view(
703 workspace: &Entity<Workspace>,
704 cx: &mut VisualTestContext,
705 ) -> Entity<GoToLine> {
706 cx.dispatch_action(editor::actions::ToggleGoToLine);
707 workspace.update(cx, |workspace, cx| {
708 workspace.active_modal::<GoToLine>(cx).unwrap().clone()
709 })
710 }
711
712 fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
713 editor.update_in(cx, |editor, window, cx| {
714 editor
715 .highlighted_display_rows(window, cx)
716 .into_keys()
717 .map(|r| r.0)
718 .collect()
719 })
720 }
721
722 #[track_caller]
723 fn assert_single_caret_at_row(
724 editor: &Entity<Editor>,
725 buffer_row: u32,
726 cx: &mut VisualTestContext,
727 ) {
728 let selections = editor.update(cx, |editor, cx| {
729 editor
730 .selections
731 .all::<rope::Point>(cx)
732 .into_iter()
733 .map(|s| s.start..s.end)
734 .collect::<Vec<_>>()
735 });
736 assert!(
737 selections.len() == 1,
738 "Expected one caret selection but got: {selections:?}"
739 );
740 let selection = &selections[0];
741 assert!(
742 selection.start == selection.end,
743 "Expected a single caret selection, but got: {selection:?}"
744 );
745 assert_eq!(selection.start.row, buffer_row);
746 }
747
748 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
749 cx.update(|cx| {
750 let state = AppState::test(cx);
751 language::init(cx);
752 crate::init(cx);
753 editor::init(cx);
754 workspace::init_settings(cx);
755 Project::init_settings(cx);
756 state
757 })
758 }
759}