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 workspace::{AppState, Workspace};
302
303 #[gpui::test]
304 async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
305 init_test(cx);
306 let fs = FakeFs::new(cx.executor());
307 fs.insert_tree(
308 "/dir",
309 json!({
310 "a.rs": indoc!{"
311 struct SingleLine; // display line 0
312 // display line 1
313 struct MultiLine { // display line 2
314 field_1: i32, // display line 3
315 field_2: i32, // display line 4
316 } // display line 5
317 // display line 6
318 struct Another { // display line 7
319 field_1: i32, // display line 8
320 field_2: i32, // display line 9
321 field_3: i32, // display line 10
322 field_4: i32, // display line 11
323 } // display line 12
324 "}
325 }),
326 )
327 .await;
328
329 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
330 let (workspace, cx) =
331 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
332 let worktree_id = workspace.update(cx, |workspace, cx| {
333 workspace.project().update(cx, |project, cx| {
334 project.worktrees(cx).next().unwrap().read(cx).id()
335 })
336 });
337 let _buffer = project
338 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
339 .await
340 .unwrap();
341 let editor = workspace
342 .update_in(cx, |workspace, window, cx| {
343 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
344 })
345 .await
346 .unwrap()
347 .downcast::<Editor>()
348 .unwrap();
349
350 let go_to_line_view = open_go_to_line_view(&workspace, cx);
351 assert_eq!(
352 highlighted_display_rows(&editor, cx),
353 Vec::<u32>::new(),
354 "Initially opened go to line modal should not highlight any rows"
355 );
356 assert_single_caret_at_row(&editor, 0, cx);
357
358 cx.simulate_input("1");
359 assert_eq!(
360 highlighted_display_rows(&editor, cx),
361 vec![0],
362 "Go to line modal should highlight a row, corresponding to the query"
363 );
364 assert_single_caret_at_row(&editor, 0, cx);
365
366 cx.simulate_input("8");
367 assert_eq!(
368 highlighted_display_rows(&editor, cx),
369 vec![13],
370 "If the query is too large, the last row should be highlighted"
371 );
372 assert_single_caret_at_row(&editor, 0, cx);
373
374 cx.dispatch_action(menu::Cancel);
375 drop(go_to_line_view);
376 editor.update(cx, |_, _| {});
377 assert_eq!(
378 highlighted_display_rows(&editor, cx),
379 Vec::<u32>::new(),
380 "After cancelling and closing the modal, no rows should be highlighted"
381 );
382 assert_single_caret_at_row(&editor, 0, cx);
383
384 let go_to_line_view = open_go_to_line_view(&workspace, cx);
385 assert_eq!(
386 highlighted_display_rows(&editor, cx),
387 Vec::<u32>::new(),
388 "Reopened modal should not highlight any rows"
389 );
390 assert_single_caret_at_row(&editor, 0, cx);
391
392 let expected_highlighted_row = 4;
393 cx.simulate_input("5");
394 assert_eq!(
395 highlighted_display_rows(&editor, cx),
396 vec![expected_highlighted_row]
397 );
398 assert_single_caret_at_row(&editor, 0, cx);
399 cx.dispatch_action(menu::Confirm);
400 drop(go_to_line_view);
401 editor.update(cx, |_, _| {});
402 assert_eq!(
403 highlighted_display_rows(&editor, cx),
404 Vec::<u32>::new(),
405 "After confirming and closing the modal, no rows should be highlighted"
406 );
407 // On confirm, should place the caret on the highlighted row.
408 assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
409 }
410
411 #[gpui::test]
412 async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
413 init_test(cx);
414
415 let fs = FakeFs::new(cx.executor());
416 fs.insert_tree(
417 "/dir",
418 json!({
419 "a.rs": "ēlo"
420 }),
421 )
422 .await;
423
424 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
425 let (workspace, cx) =
426 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
427 workspace.update_in(cx, |workspace, window, cx| {
428 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
429 workspace.status_bar().update(cx, |status_bar, cx| {
430 status_bar.add_right_item(cursor_position, window, cx);
431 });
432 });
433
434 let worktree_id = workspace.update(cx, |workspace, cx| {
435 workspace.project().update(cx, |project, cx| {
436 project.worktrees(cx).next().unwrap().read(cx).id()
437 })
438 });
439 let _buffer = project
440 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
441 .await
442 .unwrap();
443 let editor = workspace
444 .update_in(cx, |workspace, window, cx| {
445 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
446 })
447 .await
448 .unwrap()
449 .downcast::<Editor>()
450 .unwrap();
451
452 cx.executor().advance_clock(Duration::from_millis(200));
453 workspace.update(cx, |workspace, cx| {
454 assert_eq!(
455 &SelectionStats {
456 lines: 0,
457 characters: 0,
458 selections: 1,
459 },
460 workspace
461 .status_bar()
462 .read(cx)
463 .item_of_type::<CursorPosition>()
464 .expect("missing cursor position item")
465 .read(cx)
466 .selection_stats(),
467 "No selections should be initially"
468 );
469 });
470 editor.update_in(cx, |editor, window, cx| {
471 editor.select_all(&SelectAll, window, cx)
472 });
473 cx.executor().advance_clock(Duration::from_millis(200));
474 workspace.update(cx, |workspace, cx| {
475 assert_eq!(
476 &SelectionStats {
477 lines: 1,
478 characters: 3,
479 selections: 1,
480 },
481 workspace
482 .status_bar()
483 .read(cx)
484 .item_of_type::<CursorPosition>()
485 .expect("missing cursor position item")
486 .read(cx)
487 .selection_stats(),
488 "After selecting a text with multibyte unicode characters, the character count should be correct"
489 );
490 });
491 }
492
493 #[gpui::test]
494 async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
495 init_test(cx);
496
497 let text = "ēlo你好";
498 let fs = FakeFs::new(cx.executor());
499 fs.insert_tree(
500 "/dir",
501 json!({
502 "a.rs": text
503 }),
504 )
505 .await;
506
507 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
508 let (workspace, cx) =
509 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
510 workspace.update_in(cx, |workspace, window, cx| {
511 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
512 workspace.status_bar().update(cx, |status_bar, cx| {
513 status_bar.add_right_item(cursor_position, window, cx);
514 });
515 });
516
517 let worktree_id = workspace.update(cx, |workspace, cx| {
518 workspace.project().update(cx, |project, cx| {
519 project.worktrees(cx).next().unwrap().read(cx).id()
520 })
521 });
522 let _buffer = project
523 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
524 .await
525 .unwrap();
526 let editor = workspace
527 .update_in(cx, |workspace, window, cx| {
528 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
529 })
530 .await
531 .unwrap()
532 .downcast::<Editor>()
533 .unwrap();
534
535 editor.update_in(cx, |editor, window, cx| {
536 editor.move_to_beginning(&MoveToBeginning, window, cx)
537 });
538 cx.executor().advance_clock(Duration::from_millis(200));
539 assert_eq!(
540 user_caret_position(1, 1),
541 current_position(&workspace, cx),
542 "Beginning of the line should be at first line, before any characters"
543 );
544
545 for (i, c) in text.chars().enumerate() {
546 let i = i as u32 + 1;
547 editor.update_in(cx, |editor, window, cx| {
548 editor.move_right(&MoveRight, window, cx)
549 });
550 cx.executor().advance_clock(Duration::from_millis(200));
551 assert_eq!(
552 user_caret_position(1, i + 1),
553 current_position(&workspace, cx),
554 "Wrong position for char '{c}' in string '{text}'",
555 );
556 }
557
558 editor.update_in(cx, |editor, window, cx| {
559 editor.move_right(&MoveRight, window, cx)
560 });
561 cx.executor().advance_clock(Duration::from_millis(200));
562 assert_eq!(
563 user_caret_position(1, text.chars().count() as u32 + 1),
564 current_position(&workspace, cx),
565 "After reaching the end of the text, position should not change when moving right"
566 );
567 }
568
569 #[gpui::test]
570 async fn test_go_into_unicode(cx: &mut TestAppContext) {
571 init_test(cx);
572
573 let text = "ēlo你好";
574 let fs = FakeFs::new(cx.executor());
575 fs.insert_tree(
576 "/dir",
577 json!({
578 "a.rs": text
579 }),
580 )
581 .await;
582
583 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
584 let (workspace, cx) =
585 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
586 workspace.update_in(cx, |workspace, window, cx| {
587 let cursor_position = cx.new(|_| CursorPosition::new(workspace));
588 workspace.status_bar().update(cx, |status_bar, cx| {
589 status_bar.add_right_item(cursor_position, window, cx);
590 });
591 });
592
593 let worktree_id = workspace.update(cx, |workspace, cx| {
594 workspace.project().update(cx, |project, cx| {
595 project.worktrees(cx).next().unwrap().read(cx).id()
596 })
597 });
598 let _buffer = project
599 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
600 .await
601 .unwrap();
602 let editor = workspace
603 .update_in(cx, |workspace, window, cx| {
604 workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
605 })
606 .await
607 .unwrap()
608 .downcast::<Editor>()
609 .unwrap();
610
611 editor.update_in(cx, |editor, window, cx| {
612 editor.move_to_beginning(&MoveToBeginning, window, cx)
613 });
614 cx.executor().advance_clock(Duration::from_millis(200));
615 assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
616
617 for (i, c) in text.chars().enumerate() {
618 let i = i as u32 + 1;
619 let point = user_caret_position(1, i + 1);
620 go_to_point(point, user_caret_position(1, i), &workspace, cx);
621 cx.executor().advance_clock(Duration::from_millis(200));
622 assert_eq!(
623 point,
624 current_position(&workspace, cx),
625 "When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
626 );
627 }
628
629 go_to_point(
630 user_caret_position(111, 222),
631 user_caret_position(1, text.chars().count() as u32 + 1),
632 &workspace,
633 cx,
634 );
635 cx.executor().advance_clock(Duration::from_millis(200));
636 assert_eq!(
637 user_caret_position(1, text.chars().count() as u32 + 1),
638 current_position(&workspace, cx),
639 "When going into too large point, should go to the end of the text"
640 );
641 }
642
643 fn current_position(
644 workspace: &Entity<Workspace>,
645 cx: &mut VisualTestContext,
646 ) -> UserCaretPosition {
647 workspace.update(cx, |workspace, cx| {
648 workspace
649 .status_bar()
650 .read(cx)
651 .item_of_type::<CursorPosition>()
652 .expect("missing cursor position item")
653 .read(cx)
654 .position()
655 .expect("No position found")
656 })
657 }
658
659 fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
660 UserCaretPosition {
661 line: NonZeroU32::new(line).unwrap(),
662 character: NonZeroU32::new(character).unwrap(),
663 }
664 }
665
666 fn go_to_point(
667 new_point: UserCaretPosition,
668 expected_placeholder: UserCaretPosition,
669 workspace: &Entity<Workspace>,
670 cx: &mut VisualTestContext,
671 ) {
672 let go_to_line_view = open_go_to_line_view(workspace, cx);
673 go_to_line_view.update(cx, |go_to_line_view, cx| {
674 assert_eq!(
675 go_to_line_view
676 .line_editor
677 .read(cx)
678 .placeholder_text()
679 .expect("No placeholder text"),
680 format!(
681 "{}:{}",
682 expected_placeholder.line, expected_placeholder.character
683 )
684 );
685 });
686 cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
687 cx.dispatch_action(menu::Confirm);
688 }
689
690 fn open_go_to_line_view(
691 workspace: &Entity<Workspace>,
692 cx: &mut VisualTestContext,
693 ) -> Entity<GoToLine> {
694 cx.dispatch_action(editor::actions::ToggleGoToLine);
695 workspace.update(cx, |workspace, cx| {
696 workspace.active_modal::<GoToLine>(cx).unwrap().clone()
697 })
698 }
699
700 fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
701 editor.update_in(cx, |editor, window, cx| {
702 editor
703 .highlighted_display_rows(window, cx)
704 .into_keys()
705 .map(|r| r.0)
706 .collect()
707 })
708 }
709
710 #[track_caller]
711 fn assert_single_caret_at_row(
712 editor: &Entity<Editor>,
713 buffer_row: u32,
714 cx: &mut VisualTestContext,
715 ) {
716 let selections = editor.update(cx, |editor, cx| {
717 editor
718 .selections
719 .all::<rope::Point>(cx)
720 .into_iter()
721 .map(|s| s.start..s.end)
722 .collect::<Vec<_>>()
723 });
724 assert!(
725 selections.len() == 1,
726 "Expected one caret selection but got: {selections:?}"
727 );
728 let selection = &selections[0];
729 assert!(
730 selection.start == selection.end,
731 "Expected a single caret selection, but got: {selection:?}"
732 );
733 assert_eq!(selection.start.row, buffer_row);
734 }
735
736 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
737 cx.update(|cx| {
738 let state = AppState::test(cx);
739 language::init(cx);
740 crate::init(cx);
741 editor::init(cx);
742 workspace::init_settings(cx);
743 Project::init_settings(cx);
744 state
745 })
746 }
747}