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