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