1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10
11use futures::StreamExt;
12use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
13use indoc::indoc;
14use language::{
15 language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
16 BracketPairConfig,
17 Capability::ReadWrite,
18 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
19};
20use parking_lot::Mutex;
21use project::project_settings::{LspSettings, ProjectSettings};
22use project::FakeFs;
23use serde_json::{self, json};
24use std::sync::atomic;
25use std::sync::atomic::AtomicUsize;
26use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
27use unindent::Unindent;
28use util::{
29 assert_set_eq,
30 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
31};
32use workspace::{
33 item::{FollowEvent, FollowableItem, Item, ItemHandle},
34 NavigationEntry, ViewId,
35};
36
37#[gpui::test]
38fn test_edit_events(cx: &mut TestAppContext) {
39 init_test(cx, |_| {});
40
41 let buffer = cx.new_model(|cx| {
42 let mut buffer =
43 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "123456");
44 buffer.set_group_interval(Duration::from_secs(1));
45 buffer
46 });
47
48 let events = Rc::new(RefCell::new(Vec::new()));
49 let editor1 = cx.add_window({
50 let events = events.clone();
51 |cx| {
52 let view = cx.view().clone();
53 cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
54 if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
55 events.borrow_mut().push(("editor1", event.clone()));
56 }
57 })
58 .detach();
59 Editor::for_buffer(buffer.clone(), None, cx)
60 }
61 });
62
63 let editor2 = cx.add_window({
64 let events = events.clone();
65 |cx| {
66 cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
67 if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
68 events.borrow_mut().push(("editor2", event.clone()));
69 }
70 })
71 .detach();
72 Editor::for_buffer(buffer.clone(), None, cx)
73 }
74 });
75
76 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
77
78 // Mutating editor 1 will emit an `Edited` event only for that editor.
79 _ = editor1.update(cx, |editor, cx| editor.insert("X", cx));
80 assert_eq!(
81 mem::take(&mut *events.borrow_mut()),
82 [
83 ("editor1", EditorEvent::Edited),
84 ("editor1", EditorEvent::BufferEdited),
85 ("editor2", EditorEvent::BufferEdited),
86 ]
87 );
88
89 // Mutating editor 2 will emit an `Edited` event only for that editor.
90 _ = editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
91 assert_eq!(
92 mem::take(&mut *events.borrow_mut()),
93 [
94 ("editor2", EditorEvent::Edited),
95 ("editor1", EditorEvent::BufferEdited),
96 ("editor2", EditorEvent::BufferEdited),
97 ]
98 );
99
100 // Undoing on editor 1 will emit an `Edited` event only for that editor.
101 _ = editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
102 assert_eq!(
103 mem::take(&mut *events.borrow_mut()),
104 [
105 ("editor1", EditorEvent::Edited),
106 ("editor1", EditorEvent::BufferEdited),
107 ("editor2", EditorEvent::BufferEdited),
108 ]
109 );
110
111 // Redoing on editor 1 will emit an `Edited` event only for that editor.
112 _ = editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", EditorEvent::Edited),
117 ("editor1", EditorEvent::BufferEdited),
118 ("editor2", EditorEvent::BufferEdited),
119 ]
120 );
121
122 // Undoing on editor 2 will emit an `Edited` event only for that editor.
123 _ = editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", EditorEvent::Edited),
128 ("editor1", EditorEvent::BufferEdited),
129 ("editor2", EditorEvent::BufferEdited),
130 ]
131 );
132
133 // Redoing on editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", EditorEvent::Edited),
139 ("editor1", EditorEvent::BufferEdited),
140 ("editor2", EditorEvent::BufferEdited),
141 ]
142 );
143
144 // No event is emitted when the mutation is a no-op.
145 _ = editor2.update(cx, |editor, cx| {
146 editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
147
148 editor.backspace(&Backspace, cx);
149 });
150 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
151}
152
153#[gpui::test]
154fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
155 init_test(cx, |_| {});
156
157 let mut now = Instant::now();
158 let buffer = cx.new_model(|cx| {
159 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "123456")
160 });
161 let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
162 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
163 let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));
164
165 _ = editor.update(cx, |editor, cx| {
166 editor.start_transaction_at(now, cx);
167 editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
168
169 editor.insert("cd", cx);
170 editor.end_transaction_at(now, cx);
171 assert_eq!(editor.text(cx), "12cd56");
172 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
173
174 editor.start_transaction_at(now, cx);
175 editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
176 editor.insert("e", cx);
177 editor.end_transaction_at(now, cx);
178 assert_eq!(editor.text(cx), "12cde6");
179 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
180
181 now += group_interval + Duration::from_millis(1);
182 editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
183
184 // Simulate an edit in another editor
185 _ = buffer.update(cx, |buffer, cx| {
186 buffer.start_transaction_at(now, cx);
187 buffer.edit([(0..1, "a")], None, cx);
188 buffer.edit([(1..1, "b")], None, cx);
189 buffer.end_transaction_at(now, cx);
190 });
191
192 assert_eq!(editor.text(cx), "ab2cde6");
193 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
194
195 // Last transaction happened past the group interval in a different editor.
196 // Undo it individually and don't restore selections.
197 editor.undo(&Undo, cx);
198 assert_eq!(editor.text(cx), "12cde6");
199 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
200
201 // First two transactions happened within the group interval in this editor.
202 // Undo them together and restore selections.
203 editor.undo(&Undo, cx);
204 editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
205 assert_eq!(editor.text(cx), "123456");
206 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
207
208 // Redo the first two transactions together.
209 editor.redo(&Redo, cx);
210 assert_eq!(editor.text(cx), "12cde6");
211 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
212
213 // Redo the last transaction on its own.
214 editor.redo(&Redo, cx);
215 assert_eq!(editor.text(cx), "ab2cde6");
216 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
217
218 // Test empty transactions.
219 editor.start_transaction_at(now, cx);
220 editor.end_transaction_at(now, cx);
221 editor.undo(&Undo, cx);
222 assert_eq!(editor.text(cx), "12cde6");
223 });
224}
225
226#[gpui::test]
227fn test_ime_composition(cx: &mut TestAppContext) {
228 init_test(cx, |_| {});
229
230 let buffer = cx.new_model(|cx| {
231 let mut buffer =
232 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcde");
233 // Ensure automatic grouping doesn't occur.
234 buffer.set_group_interval(Duration::ZERO);
235 buffer
236 });
237
238 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
239 cx.add_window(|cx| {
240 let mut editor = build_editor(buffer.clone(), cx);
241
242 // Start a new IME composition.
243 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
244 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx);
245 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx);
246 assert_eq!(editor.text(cx), "äbcde");
247 assert_eq!(
248 editor.marked_text_ranges(cx),
249 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
250 );
251
252 // Finalize IME composition.
253 editor.replace_text_in_range(None, "ā", cx);
254 assert_eq!(editor.text(cx), "ābcde");
255 assert_eq!(editor.marked_text_ranges(cx), None);
256
257 // IME composition edits are grouped and are undone/redone at once.
258 editor.undo(&Default::default(), cx);
259 assert_eq!(editor.text(cx), "abcde");
260 assert_eq!(editor.marked_text_ranges(cx), None);
261 editor.redo(&Default::default(), cx);
262 assert_eq!(editor.text(cx), "ābcde");
263 assert_eq!(editor.marked_text_ranges(cx), None);
264
265 // Start a new IME composition.
266 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
267 assert_eq!(
268 editor.marked_text_ranges(cx),
269 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
270 );
271
272 // Undoing during an IME composition cancels it.
273 editor.undo(&Default::default(), cx);
274 assert_eq!(editor.text(cx), "ābcde");
275 assert_eq!(editor.marked_text_ranges(cx), None);
276
277 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
278 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx);
279 assert_eq!(editor.text(cx), "ābcdè");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
283 );
284
285 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
286 editor.replace_text_in_range(Some(4..999), "ę", cx);
287 assert_eq!(editor.text(cx), "ābcdę");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // Start a new IME composition with multiple cursors.
291 editor.change_selections(None, cx, |s| {
292 s.select_ranges([
293 OffsetUtf16(1)..OffsetUtf16(1),
294 OffsetUtf16(3)..OffsetUtf16(3),
295 OffsetUtf16(5)..OffsetUtf16(5),
296 ])
297 });
298 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
299 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![
303 OffsetUtf16(0)..OffsetUtf16(3),
304 OffsetUtf16(4)..OffsetUtf16(7),
305 OffsetUtf16(8)..OffsetUtf16(11)
306 ])
307 );
308
309 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
310 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
311 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
312 assert_eq!(
313 editor.marked_text_ranges(cx),
314 Some(vec![
315 OffsetUtf16(1)..OffsetUtf16(2),
316 OffsetUtf16(5)..OffsetUtf16(6),
317 OffsetUtf16(9)..OffsetUtf16(10)
318 ])
319 );
320
321 // Finalize IME composition with multiple cursors.
322 editor.replace_text_in_range(Some(9..10), "2", cx);
323 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
324 assert_eq!(editor.marked_text_ranges(cx), None);
325
326 editor
327 });
328}
329
330#[gpui::test]
331fn test_selection_with_mouse(cx: &mut TestAppContext) {
332 init_test(cx, |_| {});
333
334 let editor = cx.add_window(|cx| {
335 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
336 build_editor(buffer, cx)
337 });
338
339 _ = editor.update(cx, |view, cx| {
340 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
341 });
342 assert_eq!(
343 editor
344 .update(cx, |view, cx| view.selections.display_ranges(cx))
345 .unwrap(),
346 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
347 );
348
349 _ = editor.update(cx, |view, cx| {
350 view.update_selection(
351 DisplayPoint::new(3, 3),
352 0,
353 gpui::Point::<f32>::default(),
354 cx,
355 );
356 });
357
358 assert_eq!(
359 editor
360 .update(cx, |view, cx| view.selections.display_ranges(cx))
361 .unwrap(),
362 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
363 );
364
365 _ = editor.update(cx, |view, cx| {
366 view.update_selection(
367 DisplayPoint::new(1, 1),
368 0,
369 gpui::Point::<f32>::default(),
370 cx,
371 );
372 });
373
374 assert_eq!(
375 editor
376 .update(cx, |view, cx| view.selections.display_ranges(cx))
377 .unwrap(),
378 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
379 );
380
381 _ = editor.update(cx, |view, cx| {
382 view.end_selection(cx);
383 view.update_selection(
384 DisplayPoint::new(3, 3),
385 0,
386 gpui::Point::<f32>::default(),
387 cx,
388 );
389 });
390
391 assert_eq!(
392 editor
393 .update(cx, |view, cx| view.selections.display_ranges(cx))
394 .unwrap(),
395 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
396 );
397
398 _ = editor.update(cx, |view, cx| {
399 view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
400 view.update_selection(
401 DisplayPoint::new(0, 0),
402 0,
403 gpui::Point::<f32>::default(),
404 cx,
405 );
406 });
407
408 assert_eq!(
409 editor
410 .update(cx, |view, cx| view.selections.display_ranges(cx))
411 .unwrap(),
412 [
413 DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
414 DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
415 ]
416 );
417
418 _ = editor.update(cx, |view, cx| {
419 view.end_selection(cx);
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |view, cx| view.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
427 );
428}
429
430#[gpui::test]
431fn test_canceling_pending_selection(cx: &mut TestAppContext) {
432 init_test(cx, |_| {});
433
434 let view = cx.add_window(|cx| {
435 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
436 build_editor(buffer, cx)
437 });
438
439 _ = view.update(cx, |view, cx| {
440 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
441 assert_eq!(
442 view.selections.display_ranges(cx),
443 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
444 );
445 });
446
447 _ = view.update(cx, |view, cx| {
448 view.update_selection(
449 DisplayPoint::new(3, 3),
450 0,
451 gpui::Point::<f32>::default(),
452 cx,
453 );
454 assert_eq!(
455 view.selections.display_ranges(cx),
456 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
457 );
458 });
459
460 _ = view.update(cx, |view, cx| {
461 view.cancel(&Cancel, cx);
462 view.update_selection(
463 DisplayPoint::new(1, 1),
464 0,
465 gpui::Point::<f32>::default(),
466 cx,
467 );
468 assert_eq!(
469 view.selections.display_ranges(cx),
470 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
471 );
472 });
473}
474
475#[gpui::test]
476fn test_clone(cx: &mut TestAppContext) {
477 init_test(cx, |_| {});
478
479 let (text, selection_ranges) = marked_text_ranges(
480 indoc! {"
481 one
482 two
483 threeˇ
484 four
485 fiveˇ
486 "},
487 true,
488 );
489
490 let editor = cx.add_window(|cx| {
491 let buffer = MultiBuffer::build_simple(&text, cx);
492 build_editor(buffer, cx)
493 });
494
495 _ = editor.update(cx, |editor, cx| {
496 editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
497 editor.fold_ranges(
498 [
499 Point::new(1, 0)..Point::new(2, 0),
500 Point::new(3, 0)..Point::new(4, 0),
501 ],
502 true,
503 cx,
504 );
505 });
506
507 let cloned_editor = editor
508 .update(cx, |editor, cx| {
509 cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx)))
510 })
511 .unwrap();
512
513 let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
514 let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
515
516 assert_eq!(
517 cloned_editor
518 .update(cx, |e, cx| e.display_text(cx))
519 .unwrap(),
520 editor.update(cx, |e, cx| e.display_text(cx)).unwrap()
521 );
522 assert_eq!(
523 cloned_snapshot
524 .folds_in_range(0..text.len())
525 .collect::<Vec<_>>(),
526 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
527 );
528 assert_set_eq!(
529 cloned_editor
530 .update(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
531 .unwrap(),
532 editor
533 .update(cx, |editor, cx| editor.selections.ranges(cx))
534 .unwrap()
535 );
536 assert_set_eq!(
537 cloned_editor
538 .update(cx, |e, cx| e.selections.display_ranges(cx))
539 .unwrap(),
540 editor
541 .update(cx, |e, cx| e.selections.display_ranges(cx))
542 .unwrap()
543 );
544}
545
546#[gpui::test]
547async fn test_navigation_history(cx: &mut TestAppContext) {
548 init_test(cx, |_| {});
549
550 use workspace::item::Item;
551
552 let fs = FakeFs::new(cx.executor());
553 let project = Project::test(fs, [], cx).await;
554 let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
555 let pane = workspace
556 .update(cx, |workspace, _| workspace.active_pane().clone())
557 .unwrap();
558
559 _ = workspace.update(cx, |_v, cx| {
560 cx.new_view(|cx| {
561 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
562 let mut editor = build_editor(buffer.clone(), cx);
563 let handle = cx.view();
564 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
565
566 fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
567 editor.nav_history.as_mut().unwrap().pop_backward(cx)
568 }
569
570 // Move the cursor a small distance.
571 // Nothing is added to the navigation history.
572 editor.change_selections(None, cx, |s| {
573 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
574 });
575 editor.change_selections(None, cx, |s| {
576 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
577 });
578 assert!(pop_history(&mut editor, cx).is_none());
579
580 // Move the cursor a large distance.
581 // The history can jump back to the previous position.
582 editor.change_selections(None, cx, |s| {
583 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
584 });
585 let nav_entry = pop_history(&mut editor, cx).unwrap();
586 editor.navigate(nav_entry.data.unwrap(), cx);
587 assert_eq!(nav_entry.item.id(), cx.entity_id());
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
591 );
592 assert!(pop_history(&mut editor, cx).is_none());
593
594 // Move the cursor a small distance via the mouse.
595 // Nothing is added to the navigation history.
596 editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
597 editor.end_selection(cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
601 );
602 assert!(pop_history(&mut editor, cx).is_none());
603
604 // Move the cursor a large distance via the mouse.
605 // The history can jump back to the previous position.
606 editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
607 editor.end_selection(cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
611 );
612 let nav_entry = pop_history(&mut editor, cx).unwrap();
613 editor.navigate(nav_entry.data.unwrap(), cx);
614 assert_eq!(nav_entry.item.id(), cx.entity_id());
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
618 );
619 assert!(pop_history(&mut editor, cx).is_none());
620
621 // Set scroll position to check later
622 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
623 let original_scroll_position = editor.scroll_manager.anchor();
624
625 // Jump to the end of the document and adjust scroll
626 editor.move_to_end(&MoveToEnd, cx);
627 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
628 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
629
630 let nav_entry = pop_history(&mut editor, cx).unwrap();
631 editor.navigate(nav_entry.data.unwrap(), cx);
632 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
633
634 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
635 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
636 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
637 let invalid_point = Point::new(9999, 0);
638 editor.navigate(
639 Box::new(NavigationData {
640 cursor_anchor: invalid_anchor,
641 cursor_position: invalid_point,
642 scroll_anchor: ScrollAnchor {
643 anchor: invalid_anchor,
644 offset: Default::default(),
645 },
646 scroll_top_row: invalid_point.row,
647 }),
648 cx,
649 );
650 assert_eq!(
651 editor.selections.display_ranges(cx),
652 &[editor.max_point(cx)..editor.max_point(cx)]
653 );
654 assert_eq!(
655 editor.scroll_position(cx),
656 gpui::Point::new(0., editor.max_point(cx).row() as f32)
657 );
658
659 editor
660 })
661 });
662}
663
664#[gpui::test]
665fn test_cancel(cx: &mut TestAppContext) {
666 init_test(cx, |_| {});
667
668 let view = cx.add_window(|cx| {
669 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
670 build_editor(buffer, cx)
671 });
672
673 _ = view.update(cx, |view, cx| {
674 view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
675 view.update_selection(
676 DisplayPoint::new(1, 1),
677 0,
678 gpui::Point::<f32>::default(),
679 cx,
680 );
681 view.end_selection(cx);
682
683 view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
684 view.update_selection(
685 DisplayPoint::new(0, 3),
686 0,
687 gpui::Point::<f32>::default(),
688 cx,
689 );
690 view.end_selection(cx);
691 assert_eq!(
692 view.selections.display_ranges(cx),
693 [
694 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
695 DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
696 ]
697 );
698 });
699
700 _ = view.update(cx, |view, cx| {
701 view.cancel(&Cancel, cx);
702 assert_eq!(
703 view.selections.display_ranges(cx),
704 [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
705 );
706 });
707
708 _ = view.update(cx, |view, cx| {
709 view.cancel(&Cancel, cx);
710 assert_eq!(
711 view.selections.display_ranges(cx),
712 [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
713 );
714 });
715}
716
717#[gpui::test]
718fn test_fold_action(cx: &mut TestAppContext) {
719 init_test(cx, |_| {});
720
721 let view = cx.add_window(|cx| {
722 let buffer = MultiBuffer::build_simple(
723 &"
724 impl Foo {
725 // Hello!
726
727 fn a() {
728 1
729 }
730
731 fn b() {
732 2
733 }
734
735 fn c() {
736 3
737 }
738 }
739 "
740 .unindent(),
741 cx,
742 );
743 build_editor(buffer.clone(), cx)
744 });
745
746 _ = view.update(cx, |view, cx| {
747 view.change_selections(None, cx, |s| {
748 s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
749 });
750 view.fold(&Fold, cx);
751 assert_eq!(
752 view.display_text(cx),
753 "
754 impl Foo {
755 // Hello!
756
757 fn a() {
758 1
759 }
760
761 fn b() {⋯
762 }
763
764 fn c() {⋯
765 }
766 }
767 "
768 .unindent(),
769 );
770
771 view.fold(&Fold, cx);
772 assert_eq!(
773 view.display_text(cx),
774 "
775 impl Foo {⋯
776 }
777 "
778 .unindent(),
779 );
780
781 view.unfold_lines(&UnfoldLines, cx);
782 assert_eq!(
783 view.display_text(cx),
784 "
785 impl Foo {
786 // Hello!
787
788 fn a() {
789 1
790 }
791
792 fn b() {⋯
793 }
794
795 fn c() {⋯
796 }
797 }
798 "
799 .unindent(),
800 );
801
802 view.unfold_lines(&UnfoldLines, cx);
803 assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
804 });
805}
806
807#[gpui::test]
808fn test_move_cursor(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
812 let view = cx.add_window(|cx| build_editor(buffer.clone(), cx));
813
814 _ = buffer.update(cx, |buffer, cx| {
815 buffer.edit(
816 vec![
817 (Point::new(1, 0)..Point::new(1, 0), "\t"),
818 (Point::new(1, 1)..Point::new(1, 1), "\t"),
819 ],
820 None,
821 cx,
822 );
823 });
824 _ = view.update(cx, |view, cx| {
825 assert_eq!(
826 view.selections.display_ranges(cx),
827 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
828 );
829
830 view.move_down(&MoveDown, cx);
831 assert_eq!(
832 view.selections.display_ranges(cx),
833 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
834 );
835
836 view.move_right(&MoveRight, cx);
837 assert_eq!(
838 view.selections.display_ranges(cx),
839 &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
840 );
841
842 view.move_left(&MoveLeft, cx);
843 assert_eq!(
844 view.selections.display_ranges(cx),
845 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
846 );
847
848 view.move_up(&MoveUp, cx);
849 assert_eq!(
850 view.selections.display_ranges(cx),
851 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
852 );
853
854 view.move_to_end(&MoveToEnd, cx);
855 assert_eq!(
856 view.selections.display_ranges(cx),
857 &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
858 );
859
860 view.move_to_beginning(&MoveToBeginning, cx);
861 assert_eq!(
862 view.selections.display_ranges(cx),
863 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
864 );
865
866 view.change_selections(None, cx, |s| {
867 s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
868 });
869 view.select_to_beginning(&SelectToBeginning, cx);
870 assert_eq!(
871 view.selections.display_ranges(cx),
872 &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
873 );
874
875 view.select_to_end(&SelectToEnd, cx);
876 assert_eq!(
877 view.selections.display_ranges(cx),
878 &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
879 );
880 });
881}
882
883#[gpui::test]
884fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
885 init_test(cx, |_| {});
886
887 let view = cx.add_window(|cx| {
888 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
889 build_editor(buffer.clone(), cx)
890 });
891
892 assert_eq!('ⓐ'.len_utf8(), 3);
893 assert_eq!('α'.len_utf8(), 2);
894
895 _ = view.update(cx, |view, cx| {
896 view.fold_ranges(
897 vec![
898 Point::new(0, 6)..Point::new(0, 12),
899 Point::new(1, 2)..Point::new(1, 4),
900 Point::new(2, 4)..Point::new(2, 8),
901 ],
902 true,
903 cx,
904 );
905 assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
906
907 view.move_right(&MoveRight, cx);
908 assert_eq!(
909 view.selections.display_ranges(cx),
910 &[empty_range(0, "ⓐ".len())]
911 );
912 view.move_right(&MoveRight, cx);
913 assert_eq!(
914 view.selections.display_ranges(cx),
915 &[empty_range(0, "ⓐⓑ".len())]
916 );
917 view.move_right(&MoveRight, cx);
918 assert_eq!(
919 view.selections.display_ranges(cx),
920 &[empty_range(0, "ⓐⓑ⋯".len())]
921 );
922
923 view.move_down(&MoveDown, cx);
924 assert_eq!(
925 view.selections.display_ranges(cx),
926 &[empty_range(1, "ab⋯e".len())]
927 );
928 view.move_left(&MoveLeft, cx);
929 assert_eq!(
930 view.selections.display_ranges(cx),
931 &[empty_range(1, "ab⋯".len())]
932 );
933 view.move_left(&MoveLeft, cx);
934 assert_eq!(
935 view.selections.display_ranges(cx),
936 &[empty_range(1, "ab".len())]
937 );
938 view.move_left(&MoveLeft, cx);
939 assert_eq!(
940 view.selections.display_ranges(cx),
941 &[empty_range(1, "a".len())]
942 );
943
944 view.move_down(&MoveDown, cx);
945 assert_eq!(
946 view.selections.display_ranges(cx),
947 &[empty_range(2, "α".len())]
948 );
949 view.move_right(&MoveRight, cx);
950 assert_eq!(
951 view.selections.display_ranges(cx),
952 &[empty_range(2, "αβ".len())]
953 );
954 view.move_right(&MoveRight, cx);
955 assert_eq!(
956 view.selections.display_ranges(cx),
957 &[empty_range(2, "αβ⋯".len())]
958 );
959 view.move_right(&MoveRight, cx);
960 assert_eq!(
961 view.selections.display_ranges(cx),
962 &[empty_range(2, "αβ⋯ε".len())]
963 );
964
965 view.move_up(&MoveUp, cx);
966 assert_eq!(
967 view.selections.display_ranges(cx),
968 &[empty_range(1, "ab⋯e".len())]
969 );
970 view.move_down(&MoveDown, cx);
971 assert_eq!(
972 view.selections.display_ranges(cx),
973 &[empty_range(2, "αβ⋯ε".len())]
974 );
975 view.move_up(&MoveUp, cx);
976 assert_eq!(
977 view.selections.display_ranges(cx),
978 &[empty_range(1, "ab⋯e".len())]
979 );
980
981 view.move_up(&MoveUp, cx);
982 assert_eq!(
983 view.selections.display_ranges(cx),
984 &[empty_range(0, "ⓐⓑ".len())]
985 );
986 view.move_left(&MoveLeft, cx);
987 assert_eq!(
988 view.selections.display_ranges(cx),
989 &[empty_range(0, "ⓐ".len())]
990 );
991 view.move_left(&MoveLeft, cx);
992 assert_eq!(
993 view.selections.display_ranges(cx),
994 &[empty_range(0, "".len())]
995 );
996 });
997}
998
999#[gpui::test]
1000fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1001 init_test(cx, |_| {});
1002
1003 let view = cx.add_window(|cx| {
1004 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1005 build_editor(buffer.clone(), cx)
1006 });
1007 _ = view.update(cx, |view, cx| {
1008 view.change_selections(None, cx, |s| {
1009 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1010 });
1011 view.move_down(&MoveDown, cx);
1012 assert_eq!(
1013 view.selections.display_ranges(cx),
1014 &[empty_range(1, "abcd".len())]
1015 );
1016
1017 view.move_down(&MoveDown, cx);
1018 assert_eq!(
1019 view.selections.display_ranges(cx),
1020 &[empty_range(2, "αβγ".len())]
1021 );
1022
1023 view.move_down(&MoveDown, cx);
1024 assert_eq!(
1025 view.selections.display_ranges(cx),
1026 &[empty_range(3, "abcd".len())]
1027 );
1028
1029 view.move_down(&MoveDown, cx);
1030 assert_eq!(
1031 view.selections.display_ranges(cx),
1032 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1033 );
1034
1035 view.move_up(&MoveUp, cx);
1036 assert_eq!(
1037 view.selections.display_ranges(cx),
1038 &[empty_range(3, "abcd".len())]
1039 );
1040
1041 view.move_up(&MoveUp, cx);
1042 assert_eq!(
1043 view.selections.display_ranges(cx),
1044 &[empty_range(2, "αβγ".len())]
1045 );
1046 });
1047}
1048
1049#[gpui::test]
1050fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1051 init_test(cx, |_| {});
1052
1053 let view = cx.add_window(|cx| {
1054 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1055 build_editor(buffer, cx)
1056 });
1057 _ = view.update(cx, |view, cx| {
1058 view.change_selections(None, cx, |s| {
1059 s.select_display_ranges([
1060 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
1061 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1062 ]);
1063 });
1064 });
1065
1066 _ = view.update(cx, |view, cx| {
1067 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1068 assert_eq!(
1069 view.selections.display_ranges(cx),
1070 &[
1071 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1072 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1073 ]
1074 );
1075 });
1076
1077 _ = view.update(cx, |view, cx| {
1078 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1079 assert_eq!(
1080 view.selections.display_ranges(cx),
1081 &[
1082 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1083 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1084 ]
1085 );
1086 });
1087
1088 _ = view.update(cx, |view, cx| {
1089 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1090 assert_eq!(
1091 view.selections.display_ranges(cx),
1092 &[
1093 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1094 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1095 ]
1096 );
1097 });
1098
1099 _ = view.update(cx, |view, cx| {
1100 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1101 assert_eq!(
1102 view.selections.display_ranges(cx),
1103 &[
1104 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1105 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1106 ]
1107 );
1108 });
1109
1110 // Moving to the end of line again is a no-op.
1111 _ = view.update(cx, |view, cx| {
1112 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1113 assert_eq!(
1114 view.selections.display_ranges(cx),
1115 &[
1116 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1117 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1118 ]
1119 );
1120 });
1121
1122 _ = view.update(cx, |view, cx| {
1123 view.move_left(&MoveLeft, cx);
1124 view.select_to_beginning_of_line(
1125 &SelectToBeginningOfLine {
1126 stop_at_soft_wraps: true,
1127 },
1128 cx,
1129 );
1130 assert_eq!(
1131 view.selections.display_ranges(cx),
1132 &[
1133 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1134 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1135 ]
1136 );
1137 });
1138
1139 _ = view.update(cx, |view, cx| {
1140 view.select_to_beginning_of_line(
1141 &SelectToBeginningOfLine {
1142 stop_at_soft_wraps: true,
1143 },
1144 cx,
1145 );
1146 assert_eq!(
1147 view.selections.display_ranges(cx),
1148 &[
1149 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1150 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
1151 ]
1152 );
1153 });
1154
1155 _ = view.update(cx, |view, cx| {
1156 view.select_to_beginning_of_line(
1157 &SelectToBeginningOfLine {
1158 stop_at_soft_wraps: true,
1159 },
1160 cx,
1161 );
1162 assert_eq!(
1163 view.selections.display_ranges(cx),
1164 &[
1165 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1166 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1167 ]
1168 );
1169 });
1170
1171 _ = view.update(cx, |view, cx| {
1172 view.select_to_end_of_line(
1173 &SelectToEndOfLine {
1174 stop_at_soft_wraps: true,
1175 },
1176 cx,
1177 );
1178 assert_eq!(
1179 view.selections.display_ranges(cx),
1180 &[
1181 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
1182 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
1183 ]
1184 );
1185 });
1186
1187 _ = view.update(cx, |view, cx| {
1188 view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
1189 assert_eq!(view.display_text(cx), "ab\n de");
1190 assert_eq!(
1191 view.selections.display_ranges(cx),
1192 &[
1193 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1194 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1195 ]
1196 );
1197 });
1198
1199 _ = view.update(cx, |view, cx| {
1200 view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1201 assert_eq!(view.display_text(cx), "\n");
1202 assert_eq!(
1203 view.selections.display_ranges(cx),
1204 &[
1205 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1206 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1207 ]
1208 );
1209 });
1210}
1211
1212#[gpui::test]
1213fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1214 init_test(cx, |_| {});
1215
1216 let view = cx.add_window(|cx| {
1217 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1218 build_editor(buffer, cx)
1219 });
1220 _ = view.update(cx, |view, cx| {
1221 view.change_selections(None, cx, |s| {
1222 s.select_display_ranges([
1223 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
1224 DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
1225 ])
1226 });
1227
1228 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1229 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1230
1231 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1232 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx);
1233
1234 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1235 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx);
1236
1237 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1238 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1239
1240 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1241 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx);
1242
1243 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1244 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx);
1245
1246 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1247 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1248
1249 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1250 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1251
1252 view.move_right(&MoveRight, cx);
1253 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1254 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1255
1256 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1257 assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx);
1258
1259 view.select_to_next_word_end(&SelectToNextWordEnd, cx);
1260 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1261 });
1262}
1263
1264#[gpui::test]
1265fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1266 init_test(cx, |_| {});
1267
1268 let view = cx.add_window(|cx| {
1269 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1270 build_editor(buffer, cx)
1271 });
1272
1273 _ = view.update(cx, |view, cx| {
1274 view.set_wrap_width(Some(140.0.into()), cx);
1275 assert_eq!(
1276 view.display_text(cx),
1277 "use one::{\n two::three::\n four::five\n};"
1278 );
1279
1280 view.change_selections(None, cx, |s| {
1281 s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
1282 });
1283
1284 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1285 assert_eq!(
1286 view.selections.display_ranges(cx),
1287 &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
1288 );
1289
1290 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1291 assert_eq!(
1292 view.selections.display_ranges(cx),
1293 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1294 );
1295
1296 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1297 assert_eq!(
1298 view.selections.display_ranges(cx),
1299 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1300 );
1301
1302 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1303 assert_eq!(
1304 view.selections.display_ranges(cx),
1305 &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
1306 );
1307
1308 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1309 assert_eq!(
1310 view.selections.display_ranges(cx),
1311 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1312 );
1313
1314 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1315 assert_eq!(
1316 view.selections.display_ranges(cx),
1317 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1318 );
1319 });
1320}
1321
1322#[gpui::test]
1323async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1324 init_test(cx, |_| {});
1325 let mut cx = EditorTestContext::new(cx).await;
1326
1327 let line_height = cx.editor(|editor, cx| {
1328 editor
1329 .style()
1330 .unwrap()
1331 .text
1332 .line_height_in_pixels(cx.rem_size())
1333 });
1334 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1335
1336 cx.set_state(
1337 &r#"ˇone
1338 two
1339
1340 three
1341 fourˇ
1342 five
1343
1344 six"#
1345 .unindent(),
1346 );
1347
1348 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1349 cx.assert_editor_state(
1350 &r#"one
1351 two
1352 ˇ
1353 three
1354 four
1355 five
1356 ˇ
1357 six"#
1358 .unindent(),
1359 );
1360
1361 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1362 cx.assert_editor_state(
1363 &r#"one
1364 two
1365
1366 three
1367 four
1368 five
1369 ˇ
1370 sixˇ"#
1371 .unindent(),
1372 );
1373
1374 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1375 cx.assert_editor_state(
1376 &r#"one
1377 two
1378
1379 three
1380 four
1381 five
1382
1383 sixˇ"#
1384 .unindent(),
1385 );
1386
1387 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1388 cx.assert_editor_state(
1389 &r#"one
1390 two
1391
1392 three
1393 four
1394 five
1395 ˇ
1396 six"#
1397 .unindent(),
1398 );
1399
1400 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1401 cx.assert_editor_state(
1402 &r#"one
1403 two
1404 ˇ
1405 three
1406 four
1407 five
1408
1409 six"#
1410 .unindent(),
1411 );
1412
1413 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1414 cx.assert_editor_state(
1415 &r#"ˇone
1416 two
1417
1418 three
1419 four
1420 five
1421
1422 six"#
1423 .unindent(),
1424 );
1425}
1426
1427#[gpui::test]
1428async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
1429 init_test(cx, |_| {});
1430 let mut cx = EditorTestContext::new(cx).await;
1431 let line_height = cx.editor(|editor, cx| {
1432 editor
1433 .style()
1434 .unwrap()
1435 .text
1436 .line_height_in_pixels(cx.rem_size())
1437 });
1438 let window = cx.window;
1439 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
1440
1441 cx.set_state(
1442 &r#"ˇone
1443 two
1444 three
1445 four
1446 five
1447 six
1448 seven
1449 eight
1450 nine
1451 ten
1452 "#,
1453 );
1454
1455 cx.update_editor(|editor, cx| {
1456 assert_eq!(
1457 editor.snapshot(cx).scroll_position(),
1458 gpui::Point::new(0., 0.)
1459 );
1460 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1461 assert_eq!(
1462 editor.snapshot(cx).scroll_position(),
1463 gpui::Point::new(0., 3.)
1464 );
1465 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1466 assert_eq!(
1467 editor.snapshot(cx).scroll_position(),
1468 gpui::Point::new(0., 6.)
1469 );
1470 editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
1471 assert_eq!(
1472 editor.snapshot(cx).scroll_position(),
1473 gpui::Point::new(0., 3.)
1474 );
1475
1476 editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
1477 assert_eq!(
1478 editor.snapshot(cx).scroll_position(),
1479 gpui::Point::new(0., 1.)
1480 );
1481 editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
1482 assert_eq!(
1483 editor.snapshot(cx).scroll_position(),
1484 gpui::Point::new(0., 3.)
1485 );
1486 });
1487}
1488
1489#[gpui::test]
1490async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
1491 init_test(cx, |_| {});
1492 let mut cx = EditorTestContext::new(cx).await;
1493
1494 let line_height = cx.update_editor(|editor, cx| {
1495 editor.set_vertical_scroll_margin(2, cx);
1496 editor
1497 .style()
1498 .unwrap()
1499 .text
1500 .line_height_in_pixels(cx.rem_size())
1501 });
1502 let window = cx.window;
1503 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
1504
1505 cx.set_state(
1506 &r#"ˇone
1507 two
1508 three
1509 four
1510 five
1511 six
1512 seven
1513 eight
1514 nine
1515 ten
1516 "#,
1517 );
1518 cx.update_editor(|editor, cx| {
1519 assert_eq!(
1520 editor.snapshot(cx).scroll_position(),
1521 gpui::Point::new(0., 0.0)
1522 );
1523 });
1524
1525 // Add a cursor below the visible area. Since both cursors cannot fit
1526 // on screen, the editor autoscrolls to reveal the newest cursor, and
1527 // allows the vertical scroll margin below that cursor.
1528 cx.update_editor(|editor, cx| {
1529 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1530 selections.select_ranges([
1531 Point::new(0, 0)..Point::new(0, 0),
1532 Point::new(6, 0)..Point::new(6, 0),
1533 ]);
1534 })
1535 });
1536 cx.update_editor(|editor, cx| {
1537 assert_eq!(
1538 editor.snapshot(cx).scroll_position(),
1539 gpui::Point::new(0., 3.0)
1540 );
1541 });
1542
1543 // Move down. The editor cursor scrolls down to track the newest cursor.
1544 cx.update_editor(|editor, cx| {
1545 editor.move_down(&Default::default(), cx);
1546 });
1547 cx.update_editor(|editor, cx| {
1548 assert_eq!(
1549 editor.snapshot(cx).scroll_position(),
1550 gpui::Point::new(0., 4.0)
1551 );
1552 });
1553
1554 // Add a cursor above the visible area. Since both cursors fit on screen,
1555 // the editor scrolls to show both.
1556 cx.update_editor(|editor, cx| {
1557 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1558 selections.select_ranges([
1559 Point::new(1, 0)..Point::new(1, 0),
1560 Point::new(6, 0)..Point::new(6, 0),
1561 ]);
1562 })
1563 });
1564 cx.update_editor(|editor, cx| {
1565 assert_eq!(
1566 editor.snapshot(cx).scroll_position(),
1567 gpui::Point::new(0., 1.0)
1568 );
1569 });
1570}
1571
1572#[gpui::test]
1573async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
1574 init_test(cx, |_| {});
1575 let mut cx = EditorTestContext::new(cx).await;
1576
1577 let line_height = cx.editor(|editor, cx| {
1578 editor
1579 .style()
1580 .unwrap()
1581 .text
1582 .line_height_in_pixels(cx.rem_size())
1583 });
1584 let window = cx.window;
1585 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
1586 cx.set_state(
1587 &r#"
1588 ˇone
1589 two
1590 threeˇ
1591 four
1592 five
1593 six
1594 seven
1595 eight
1596 nine
1597 ten
1598 "#
1599 .unindent(),
1600 );
1601
1602 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1603 cx.assert_editor_state(
1604 &r#"
1605 one
1606 two
1607 three
1608 ˇfour
1609 five
1610 sixˇ
1611 seven
1612 eight
1613 nine
1614 ten
1615 "#
1616 .unindent(),
1617 );
1618
1619 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1620 cx.assert_editor_state(
1621 &r#"
1622 one
1623 two
1624 three
1625 four
1626 five
1627 six
1628 ˇseven
1629 eight
1630 nineˇ
1631 ten
1632 "#
1633 .unindent(),
1634 );
1635
1636 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1637 cx.assert_editor_state(
1638 &r#"
1639 one
1640 two
1641 three
1642 ˇfour
1643 five
1644 sixˇ
1645 seven
1646 eight
1647 nine
1648 ten
1649 "#
1650 .unindent(),
1651 );
1652
1653 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1654 cx.assert_editor_state(
1655 &r#"
1656 ˇone
1657 two
1658 threeˇ
1659 four
1660 five
1661 six
1662 seven
1663 eight
1664 nine
1665 ten
1666 "#
1667 .unindent(),
1668 );
1669
1670 // Test select collapsing
1671 cx.update_editor(|editor, cx| {
1672 editor.move_page_down(&MovePageDown::default(), cx);
1673 editor.move_page_down(&MovePageDown::default(), cx);
1674 editor.move_page_down(&MovePageDown::default(), cx);
1675 });
1676 cx.assert_editor_state(
1677 &r#"
1678 one
1679 two
1680 three
1681 four
1682 five
1683 six
1684 seven
1685 eight
1686 nine
1687 ˇten
1688 ˇ"#
1689 .unindent(),
1690 );
1691}
1692
1693#[gpui::test]
1694async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
1695 init_test(cx, |_| {});
1696 let mut cx = EditorTestContext::new(cx).await;
1697 cx.set_state("one «two threeˇ» four");
1698 cx.update_editor(|editor, cx| {
1699 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1700 assert_eq!(editor.text(cx), " four");
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707
1708 let view = cx.add_window(|cx| {
1709 let buffer = MultiBuffer::build_simple("one two three four", cx);
1710 build_editor(buffer.clone(), cx)
1711 });
1712
1713 _ = view.update(cx, |view, cx| {
1714 view.change_selections(None, cx, |s| {
1715 s.select_display_ranges([
1716 // an empty selection - the preceding word fragment is deleted
1717 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1718 // characters selected - they are deleted
1719 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
1720 ])
1721 });
1722 view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
1723 assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
1724 });
1725
1726 _ = view.update(cx, |view, cx| {
1727 view.change_selections(None, cx, |s| {
1728 s.select_display_ranges([
1729 // an empty selection - the following word fragment is deleted
1730 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1731 // characters selected - they are deleted
1732 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
1733 ])
1734 });
1735 view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
1736 assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
1737 });
1738}
1739
1740#[gpui::test]
1741fn test_newline(cx: &mut TestAppContext) {
1742 init_test(cx, |_| {});
1743
1744 let view = cx.add_window(|cx| {
1745 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
1746 build_editor(buffer.clone(), cx)
1747 });
1748
1749 _ = view.update(cx, |view, cx| {
1750 view.change_selections(None, cx, |s| {
1751 s.select_display_ranges([
1752 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1753 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1754 DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
1755 ])
1756 });
1757
1758 view.newline(&Newline, cx);
1759 assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
1760 });
1761}
1762
1763#[gpui::test]
1764fn test_newline_with_old_selections(cx: &mut TestAppContext) {
1765 init_test(cx, |_| {});
1766
1767 let editor = cx.add_window(|cx| {
1768 let buffer = MultiBuffer::build_simple(
1769 "
1770 a
1771 b(
1772 X
1773 )
1774 c(
1775 X
1776 )
1777 "
1778 .unindent()
1779 .as_str(),
1780 cx,
1781 );
1782 let mut editor = build_editor(buffer.clone(), cx);
1783 editor.change_selections(None, cx, |s| {
1784 s.select_ranges([
1785 Point::new(2, 4)..Point::new(2, 5),
1786 Point::new(5, 4)..Point::new(5, 5),
1787 ])
1788 });
1789 editor
1790 });
1791
1792 _ = editor.update(cx, |editor, cx| {
1793 // Edit the buffer directly, deleting ranges surrounding the editor's selections
1794 editor.buffer.update(cx, |buffer, cx| {
1795 buffer.edit(
1796 [
1797 (Point::new(1, 2)..Point::new(3, 0), ""),
1798 (Point::new(4, 2)..Point::new(6, 0), ""),
1799 ],
1800 None,
1801 cx,
1802 );
1803 assert_eq!(
1804 buffer.read(cx).text(),
1805 "
1806 a
1807 b()
1808 c()
1809 "
1810 .unindent()
1811 );
1812 });
1813 assert_eq!(
1814 editor.selections.ranges(cx),
1815 &[
1816 Point::new(1, 2)..Point::new(1, 2),
1817 Point::new(2, 2)..Point::new(2, 2),
1818 ],
1819 );
1820
1821 editor.newline(&Newline, cx);
1822 assert_eq!(
1823 editor.text(cx),
1824 "
1825 a
1826 b(
1827 )
1828 c(
1829 )
1830 "
1831 .unindent()
1832 );
1833
1834 // The selections are moved after the inserted newlines
1835 assert_eq!(
1836 editor.selections.ranges(cx),
1837 &[
1838 Point::new(2, 0)..Point::new(2, 0),
1839 Point::new(4, 0)..Point::new(4, 0),
1840 ],
1841 );
1842 });
1843}
1844
1845#[gpui::test]
1846async fn test_newline_above(cx: &mut gpui::TestAppContext) {
1847 init_test(cx, |settings| {
1848 settings.defaults.tab_size = NonZeroU32::new(4)
1849 });
1850
1851 let language = Arc::new(
1852 Language::new(
1853 LanguageConfig::default(),
1854 Some(tree_sitter_rust::language()),
1855 )
1856 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1857 .unwrap(),
1858 );
1859
1860 let mut cx = EditorTestContext::new(cx).await;
1861 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1862 cx.set_state(indoc! {"
1863 const a: ˇA = (
1864 (ˇ
1865 «const_functionˇ»(ˇ),
1866 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1867 )ˇ
1868 ˇ);ˇ
1869 "});
1870
1871 cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
1872 cx.assert_editor_state(indoc! {"
1873 ˇ
1874 const a: A = (
1875 ˇ
1876 (
1877 ˇ
1878 ˇ
1879 const_function(),
1880 ˇ
1881 ˇ
1882 ˇ
1883 ˇ
1884 something_else,
1885 ˇ
1886 )
1887 ˇ
1888 ˇ
1889 );
1890 "});
1891}
1892
1893#[gpui::test]
1894async fn test_newline_below(cx: &mut gpui::TestAppContext) {
1895 init_test(cx, |settings| {
1896 settings.defaults.tab_size = NonZeroU32::new(4)
1897 });
1898
1899 let language = Arc::new(
1900 Language::new(
1901 LanguageConfig::default(),
1902 Some(tree_sitter_rust::language()),
1903 )
1904 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1905 .unwrap(),
1906 );
1907
1908 let mut cx = EditorTestContext::new(cx).await;
1909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1910 cx.set_state(indoc! {"
1911 const a: ˇA = (
1912 (ˇ
1913 «const_functionˇ»(ˇ),
1914 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1915 )ˇ
1916 ˇ);ˇ
1917 "});
1918
1919 cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
1920 cx.assert_editor_state(indoc! {"
1921 const a: A = (
1922 ˇ
1923 (
1924 ˇ
1925 const_function(),
1926 ˇ
1927 ˇ
1928 something_else,
1929 ˇ
1930 ˇ
1931 ˇ
1932 ˇ
1933 )
1934 ˇ
1935 );
1936 ˇ
1937 ˇ
1938 "});
1939}
1940
1941#[gpui::test]
1942async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
1943 init_test(cx, |settings| {
1944 settings.defaults.tab_size = NonZeroU32::new(4)
1945 });
1946
1947 let language = Arc::new(Language::new(
1948 LanguageConfig {
1949 line_comments: vec!["//".into()],
1950 ..LanguageConfig::default()
1951 },
1952 None,
1953 ));
1954 {
1955 let mut cx = EditorTestContext::new(cx).await;
1956 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1957 cx.set_state(indoc! {"
1958 // Fooˇ
1959 "});
1960
1961 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1962 cx.assert_editor_state(indoc! {"
1963 // Foo
1964 //ˇ
1965 "});
1966 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
1967 cx.set_state(indoc! {"
1968 ˇ// Foo
1969 "});
1970 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1971 cx.assert_editor_state(indoc! {"
1972
1973 ˇ// Foo
1974 "});
1975 }
1976 // Ensure that comment continuations can be disabled.
1977 update_test_language_settings(cx, |settings| {
1978 settings.defaults.extend_comment_on_newline = Some(false);
1979 });
1980 let mut cx = EditorTestContext::new(cx).await;
1981 cx.set_state(indoc! {"
1982 // Fooˇ
1983 "});
1984 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1985 cx.assert_editor_state(indoc! {"
1986 // Foo
1987 ˇ
1988 "});
1989}
1990
1991#[gpui::test]
1992fn test_insert_with_old_selections(cx: &mut TestAppContext) {
1993 init_test(cx, |_| {});
1994
1995 let editor = cx.add_window(|cx| {
1996 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
1997 let mut editor = build_editor(buffer.clone(), cx);
1998 editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
1999 editor
2000 });
2001
2002 _ = editor.update(cx, |editor, cx| {
2003 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2004 editor.buffer.update(cx, |buffer, cx| {
2005 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2006 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2007 });
2008 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2009
2010 editor.insert("Z", cx);
2011 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2012
2013 // The selections are moved after the inserted characters
2014 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_tab(cx: &mut gpui::TestAppContext) {
2020 init_test(cx, |settings| {
2021 settings.defaults.tab_size = NonZeroU32::new(3)
2022 });
2023
2024 let mut cx = EditorTestContext::new(cx).await;
2025 cx.set_state(indoc! {"
2026 ˇabˇc
2027 ˇ🏀ˇ🏀ˇefg
2028 dˇ
2029 "});
2030 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2031 cx.assert_editor_state(indoc! {"
2032 ˇab ˇc
2033 ˇ🏀 ˇ🏀 ˇefg
2034 d ˇ
2035 "});
2036
2037 cx.set_state(indoc! {"
2038 a
2039 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2040 "});
2041 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2042 cx.assert_editor_state(indoc! {"
2043 a
2044 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2045 "});
2046}
2047
2048#[gpui::test]
2049async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2050 init_test(cx, |_| {});
2051
2052 let mut cx = EditorTestContext::new(cx).await;
2053 let language = Arc::new(
2054 Language::new(
2055 LanguageConfig::default(),
2056 Some(tree_sitter_rust::language()),
2057 )
2058 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2059 .unwrap(),
2060 );
2061 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2062
2063 // cursors that are already at the suggested indent level insert
2064 // a soft tab. cursors that are to the left of the suggested indent
2065 // auto-indent their line.
2066 cx.set_state(indoc! {"
2067 ˇ
2068 const a: B = (
2069 c(
2070 d(
2071 ˇ
2072 )
2073 ˇ
2074 ˇ )
2075 );
2076 "});
2077 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2078 cx.assert_editor_state(indoc! {"
2079 ˇ
2080 const a: B = (
2081 c(
2082 d(
2083 ˇ
2084 )
2085 ˇ
2086 ˇ)
2087 );
2088 "});
2089
2090 // handle auto-indent when there are multiple cursors on the same line
2091 cx.set_state(indoc! {"
2092 const a: B = (
2093 c(
2094 ˇ ˇ
2095 ˇ )
2096 );
2097 "});
2098 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2099 cx.assert_editor_state(indoc! {"
2100 const a: B = (
2101 c(
2102 ˇ
2103 ˇ)
2104 );
2105 "});
2106}
2107
2108#[gpui::test]
2109async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2110 init_test(cx, |settings| {
2111 settings.defaults.tab_size = NonZeroU32::new(4)
2112 });
2113
2114 let language = Arc::new(
2115 Language::new(
2116 LanguageConfig::default(),
2117 Some(tree_sitter_rust::language()),
2118 )
2119 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2120 .unwrap(),
2121 );
2122
2123 let mut cx = EditorTestContext::new(cx).await;
2124 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2125 cx.set_state(indoc! {"
2126 fn a() {
2127 if b {
2128 \t ˇc
2129 }
2130 }
2131 "});
2132
2133 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2134 cx.assert_editor_state(indoc! {"
2135 fn a() {
2136 if b {
2137 ˇc
2138 }
2139 }
2140 "});
2141}
2142
2143#[gpui::test]
2144async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2145 init_test(cx, |settings| {
2146 settings.defaults.tab_size = NonZeroU32::new(4);
2147 });
2148
2149 let mut cx = EditorTestContext::new(cx).await;
2150
2151 cx.set_state(indoc! {"
2152 «oneˇ» «twoˇ»
2153 three
2154 four
2155 "});
2156 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2157 cx.assert_editor_state(indoc! {"
2158 «oneˇ» «twoˇ»
2159 three
2160 four
2161 "});
2162
2163 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2164 cx.assert_editor_state(indoc! {"
2165 «oneˇ» «twoˇ»
2166 three
2167 four
2168 "});
2169
2170 // select across line ending
2171 cx.set_state(indoc! {"
2172 one two
2173 t«hree
2174 ˇ» four
2175 "});
2176 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2177 cx.assert_editor_state(indoc! {"
2178 one two
2179 t«hree
2180 ˇ» four
2181 "});
2182
2183 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2184 cx.assert_editor_state(indoc! {"
2185 one two
2186 t«hree
2187 ˇ» four
2188 "});
2189
2190 // Ensure that indenting/outdenting works when the cursor is at column 0.
2191 cx.set_state(indoc! {"
2192 one two
2193 ˇthree
2194 four
2195 "});
2196 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2197 cx.assert_editor_state(indoc! {"
2198 one two
2199 ˇthree
2200 four
2201 "});
2202
2203 cx.set_state(indoc! {"
2204 one two
2205 ˇ three
2206 four
2207 "});
2208 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2209 cx.assert_editor_state(indoc! {"
2210 one two
2211 ˇthree
2212 four
2213 "});
2214}
2215
2216#[gpui::test]
2217async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2218 init_test(cx, |settings| {
2219 settings.defaults.hard_tabs = Some(true);
2220 });
2221
2222 let mut cx = EditorTestContext::new(cx).await;
2223
2224 // select two ranges on one line
2225 cx.set_state(indoc! {"
2226 «oneˇ» «twoˇ»
2227 three
2228 four
2229 "});
2230 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2231 cx.assert_editor_state(indoc! {"
2232 \t«oneˇ» «twoˇ»
2233 three
2234 four
2235 "});
2236 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2237 cx.assert_editor_state(indoc! {"
2238 \t\t«oneˇ» «twoˇ»
2239 three
2240 four
2241 "});
2242 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2243 cx.assert_editor_state(indoc! {"
2244 \t«oneˇ» «twoˇ»
2245 three
2246 four
2247 "});
2248 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2249 cx.assert_editor_state(indoc! {"
2250 «oneˇ» «twoˇ»
2251 three
2252 four
2253 "});
2254
2255 // select across a line ending
2256 cx.set_state(indoc! {"
2257 one two
2258 t«hree
2259 ˇ»four
2260 "});
2261 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2262 cx.assert_editor_state(indoc! {"
2263 one two
2264 \tt«hree
2265 ˇ»four
2266 "});
2267 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2268 cx.assert_editor_state(indoc! {"
2269 one two
2270 \t\tt«hree
2271 ˇ»four
2272 "});
2273 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2274 cx.assert_editor_state(indoc! {"
2275 one two
2276 \tt«hree
2277 ˇ»four
2278 "});
2279 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2280 cx.assert_editor_state(indoc! {"
2281 one two
2282 t«hree
2283 ˇ»four
2284 "});
2285
2286 // Ensure that indenting/outdenting works when the cursor is at column 0.
2287 cx.set_state(indoc! {"
2288 one two
2289 ˇthree
2290 four
2291 "});
2292 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2293 cx.assert_editor_state(indoc! {"
2294 one two
2295 ˇthree
2296 four
2297 "});
2298 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2299 cx.assert_editor_state(indoc! {"
2300 one two
2301 \tˇthree
2302 four
2303 "});
2304 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2305 cx.assert_editor_state(indoc! {"
2306 one two
2307 ˇthree
2308 four
2309 "});
2310}
2311
2312#[gpui::test]
2313fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
2314 init_test(cx, |settings| {
2315 settings.languages.extend([
2316 (
2317 "TOML".into(),
2318 LanguageSettingsContent {
2319 tab_size: NonZeroU32::new(2),
2320 ..Default::default()
2321 },
2322 ),
2323 (
2324 "Rust".into(),
2325 LanguageSettingsContent {
2326 tab_size: NonZeroU32::new(4),
2327 ..Default::default()
2328 },
2329 ),
2330 ]);
2331 });
2332
2333 let toml_language = Arc::new(Language::new(
2334 LanguageConfig {
2335 name: "TOML".into(),
2336 ..Default::default()
2337 },
2338 None,
2339 ));
2340 let rust_language = Arc::new(Language::new(
2341 LanguageConfig {
2342 name: "Rust".into(),
2343 ..Default::default()
2344 },
2345 None,
2346 ));
2347
2348 let toml_buffer = cx.new_model(|cx| {
2349 Buffer::new(
2350 0,
2351 BufferId::new(cx.entity_id().as_u64()).unwrap(),
2352 "a = 1\nb = 2\n",
2353 )
2354 .with_language(toml_language, cx)
2355 });
2356 let rust_buffer = cx.new_model(|cx| {
2357 Buffer::new(
2358 0,
2359 BufferId::new(cx.entity_id().as_u64()).unwrap(),
2360 "const c: usize = 3;\n",
2361 )
2362 .with_language(rust_language, cx)
2363 });
2364 let multibuffer = cx.new_model(|cx| {
2365 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
2366 multibuffer.push_excerpts(
2367 toml_buffer.clone(),
2368 [ExcerptRange {
2369 context: Point::new(0, 0)..Point::new(2, 0),
2370 primary: None,
2371 }],
2372 cx,
2373 );
2374 multibuffer.push_excerpts(
2375 rust_buffer.clone(),
2376 [ExcerptRange {
2377 context: Point::new(0, 0)..Point::new(1, 0),
2378 primary: None,
2379 }],
2380 cx,
2381 );
2382 multibuffer
2383 });
2384
2385 cx.add_window(|cx| {
2386 let mut editor = build_editor(multibuffer, cx);
2387
2388 assert_eq!(
2389 editor.text(cx),
2390 indoc! {"
2391 a = 1
2392 b = 2
2393
2394 const c: usize = 3;
2395 "}
2396 );
2397
2398 select_ranges(
2399 &mut editor,
2400 indoc! {"
2401 «aˇ» = 1
2402 b = 2
2403
2404 «const c:ˇ» usize = 3;
2405 "},
2406 cx,
2407 );
2408
2409 editor.tab(&Tab, cx);
2410 assert_text_with_selections(
2411 &mut editor,
2412 indoc! {"
2413 «aˇ» = 1
2414 b = 2
2415
2416 «const c:ˇ» usize = 3;
2417 "},
2418 cx,
2419 );
2420 editor.tab_prev(&TabPrev, cx);
2421 assert_text_with_selections(
2422 &mut editor,
2423 indoc! {"
2424 «aˇ» = 1
2425 b = 2
2426
2427 «const c:ˇ» usize = 3;
2428 "},
2429 cx,
2430 );
2431
2432 editor
2433 });
2434}
2435
2436#[gpui::test]
2437async fn test_backspace(cx: &mut gpui::TestAppContext) {
2438 init_test(cx, |_| {});
2439
2440 let mut cx = EditorTestContext::new(cx).await;
2441
2442 // Basic backspace
2443 cx.set_state(indoc! {"
2444 onˇe two three
2445 fou«rˇ» five six
2446 seven «ˇeight nine
2447 »ten
2448 "});
2449 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2450 cx.assert_editor_state(indoc! {"
2451 oˇe two three
2452 fouˇ five six
2453 seven ˇten
2454 "});
2455
2456 // Test backspace inside and around indents
2457 cx.set_state(indoc! {"
2458 zero
2459 ˇone
2460 ˇtwo
2461 ˇ ˇ ˇ three
2462 ˇ ˇ four
2463 "});
2464 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2465 cx.assert_editor_state(indoc! {"
2466 zero
2467 ˇone
2468 ˇtwo
2469 ˇ threeˇ four
2470 "});
2471
2472 // Test backspace with line_mode set to true
2473 cx.update_editor(|e, _| e.selections.line_mode = true);
2474 cx.set_state(indoc! {"
2475 The ˇquick ˇbrown
2476 fox jumps over
2477 the lazy dog
2478 ˇThe qu«ick bˇ»rown"});
2479 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2480 cx.assert_editor_state(indoc! {"
2481 ˇfox jumps over
2482 the lazy dogˇ"});
2483}
2484
2485#[gpui::test]
2486async fn test_delete(cx: &mut gpui::TestAppContext) {
2487 init_test(cx, |_| {});
2488
2489 let mut cx = EditorTestContext::new(cx).await;
2490 cx.set_state(indoc! {"
2491 onˇe two three
2492 fou«rˇ» five six
2493 seven «ˇeight nine
2494 »ten
2495 "});
2496 cx.update_editor(|e, cx| e.delete(&Delete, cx));
2497 cx.assert_editor_state(indoc! {"
2498 onˇ two three
2499 fouˇ five six
2500 seven ˇten
2501 "});
2502
2503 // Test backspace with line_mode set to true
2504 cx.update_editor(|e, _| e.selections.line_mode = true);
2505 cx.set_state(indoc! {"
2506 The ˇquick ˇbrown
2507 fox «ˇjum»ps over
2508 the lazy dog
2509 ˇThe qu«ick bˇ»rown"});
2510 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2511 cx.assert_editor_state("ˇthe lazy dogˇ");
2512}
2513
2514#[gpui::test]
2515fn test_delete_line(cx: &mut TestAppContext) {
2516 init_test(cx, |_| {});
2517
2518 let view = cx.add_window(|cx| {
2519 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2520 build_editor(buffer, cx)
2521 });
2522 _ = view.update(cx, |view, cx| {
2523 view.change_selections(None, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2526 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2527 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2528 ])
2529 });
2530 view.delete_line(&DeleteLine, cx);
2531 assert_eq!(view.display_text(cx), "ghi");
2532 assert_eq!(
2533 view.selections.display_ranges(cx),
2534 vec![
2535 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
2536 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
2537 ]
2538 );
2539 });
2540
2541 let view = cx.add_window(|cx| {
2542 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2543 build_editor(buffer, cx)
2544 });
2545 _ = view.update(cx, |view, cx| {
2546 view.change_selections(None, cx, |s| {
2547 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
2548 });
2549 view.delete_line(&DeleteLine, cx);
2550 assert_eq!(view.display_text(cx), "ghi\n");
2551 assert_eq!(
2552 view.selections.display_ranges(cx),
2553 vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
2554 );
2555 });
2556}
2557
2558#[gpui::test]
2559fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
2560 init_test(cx, |_| {});
2561
2562 cx.add_window(|cx| {
2563 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2564 let mut editor = build_editor(buffer.clone(), cx);
2565 let buffer = buffer.read(cx).as_singleton().unwrap();
2566
2567 assert_eq!(
2568 editor.selections.ranges::<Point>(cx),
2569 &[Point::new(0, 0)..Point::new(0, 0)]
2570 );
2571
2572 // When on single line, replace newline at end by space
2573 editor.join_lines(&JoinLines, cx);
2574 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2575 assert_eq!(
2576 editor.selections.ranges::<Point>(cx),
2577 &[Point::new(0, 3)..Point::new(0, 3)]
2578 );
2579
2580 // When multiple lines are selected, remove newlines that are spanned by the selection
2581 editor.change_selections(None, cx, |s| {
2582 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
2583 });
2584 editor.join_lines(&JoinLines, cx);
2585 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
2586 assert_eq!(
2587 editor.selections.ranges::<Point>(cx),
2588 &[Point::new(0, 11)..Point::new(0, 11)]
2589 );
2590
2591 // Undo should be transactional
2592 editor.undo(&Undo, cx);
2593 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2594 assert_eq!(
2595 editor.selections.ranges::<Point>(cx),
2596 &[Point::new(0, 5)..Point::new(2, 2)]
2597 );
2598
2599 // When joining an empty line don't insert a space
2600 editor.change_selections(None, cx, |s| {
2601 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
2602 });
2603 editor.join_lines(&JoinLines, cx);
2604 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
2605 assert_eq!(
2606 editor.selections.ranges::<Point>(cx),
2607 [Point::new(2, 3)..Point::new(2, 3)]
2608 );
2609
2610 // We can remove trailing newlines
2611 editor.join_lines(&JoinLines, cx);
2612 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2613 assert_eq!(
2614 editor.selections.ranges::<Point>(cx),
2615 [Point::new(2, 3)..Point::new(2, 3)]
2616 );
2617
2618 // We don't blow up on the last line
2619 editor.join_lines(&JoinLines, cx);
2620 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2621 assert_eq!(
2622 editor.selections.ranges::<Point>(cx),
2623 [Point::new(2, 3)..Point::new(2, 3)]
2624 );
2625
2626 // reset to test indentation
2627 editor.buffer.update(cx, |buffer, cx| {
2628 buffer.edit(
2629 [
2630 (Point::new(1, 0)..Point::new(1, 2), " "),
2631 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
2632 ],
2633 None,
2634 cx,
2635 )
2636 });
2637
2638 // We remove any leading spaces
2639 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
2640 editor.change_selections(None, cx, |s| {
2641 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
2642 });
2643 editor.join_lines(&JoinLines, cx);
2644 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
2645
2646 // We don't insert a space for a line containing only spaces
2647 editor.join_lines(&JoinLines, cx);
2648 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
2649
2650 // We ignore any leading tabs
2651 editor.join_lines(&JoinLines, cx);
2652 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
2653
2654 editor
2655 });
2656}
2657
2658#[gpui::test]
2659fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
2660 init_test(cx, |_| {});
2661
2662 cx.add_window(|cx| {
2663 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2664 let mut editor = build_editor(buffer.clone(), cx);
2665 let buffer = buffer.read(cx).as_singleton().unwrap();
2666
2667 editor.change_selections(None, cx, |s| {
2668 s.select_ranges([
2669 Point::new(0, 2)..Point::new(1, 1),
2670 Point::new(1, 2)..Point::new(1, 2),
2671 Point::new(3, 1)..Point::new(3, 2),
2672 ])
2673 });
2674
2675 editor.join_lines(&JoinLines, cx);
2676 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
2677
2678 assert_eq!(
2679 editor.selections.ranges::<Point>(cx),
2680 [
2681 Point::new(0, 7)..Point::new(0, 7),
2682 Point::new(1, 3)..Point::new(1, 3)
2683 ]
2684 );
2685 editor
2686 });
2687}
2688
2689#[gpui::test]
2690async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
2691 init_test(cx, |_| {});
2692
2693 let mut cx = EditorTestContext::new(cx).await;
2694
2695 // Test sort_lines_case_insensitive()
2696 cx.set_state(indoc! {"
2697 «z
2698 y
2699 x
2700 Z
2701 Y
2702 Xˇ»
2703 "});
2704 cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
2705 cx.assert_editor_state(indoc! {"
2706 «x
2707 X
2708 y
2709 Y
2710 z
2711 Zˇ»
2712 "});
2713
2714 // Test reverse_lines()
2715 cx.set_state(indoc! {"
2716 «5
2717 4
2718 3
2719 2
2720 1ˇ»
2721 "});
2722 cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
2723 cx.assert_editor_state(indoc! {"
2724 «1
2725 2
2726 3
2727 4
2728 5ˇ»
2729 "});
2730
2731 // Skip testing shuffle_line()
2732
2733 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
2734 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
2735
2736 // Don't manipulate when cursor is on single line, but expand the selection
2737 cx.set_state(indoc! {"
2738 ddˇdd
2739 ccc
2740 bb
2741 a
2742 "});
2743 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2744 cx.assert_editor_state(indoc! {"
2745 «ddddˇ»
2746 ccc
2747 bb
2748 a
2749 "});
2750
2751 // Basic manipulate case
2752 // Start selection moves to column 0
2753 // End of selection shrinks to fit shorter line
2754 cx.set_state(indoc! {"
2755 dd«d
2756 ccc
2757 bb
2758 aaaaaˇ»
2759 "});
2760 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2761 cx.assert_editor_state(indoc! {"
2762 «aaaaa
2763 bb
2764 ccc
2765 dddˇ»
2766 "});
2767
2768 // Manipulate case with newlines
2769 cx.set_state(indoc! {"
2770 dd«d
2771 ccc
2772
2773 bb
2774 aaaaa
2775
2776 ˇ»
2777 "});
2778 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2779 cx.assert_editor_state(indoc! {"
2780 «
2781
2782 aaaaa
2783 bb
2784 ccc
2785 dddˇ»
2786
2787 "});
2788
2789 // Adding new line
2790 cx.set_state(indoc! {"
2791 aa«a
2792 bbˇ»b
2793 "});
2794 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added_line")));
2795 cx.assert_editor_state(indoc! {"
2796 «aaa
2797 bbb
2798 added_lineˇ»
2799 "});
2800
2801 // Removing line
2802 cx.set_state(indoc! {"
2803 aa«a
2804 bbbˇ»
2805 "});
2806 cx.update_editor(|e, cx| {
2807 e.manipulate_lines(cx, |lines| {
2808 lines.pop();
2809 })
2810 });
2811 cx.assert_editor_state(indoc! {"
2812 «aaaˇ»
2813 "});
2814
2815 // Removing all lines
2816 cx.set_state(indoc! {"
2817 aa«a
2818 bbbˇ»
2819 "});
2820 cx.update_editor(|e, cx| {
2821 e.manipulate_lines(cx, |lines| {
2822 lines.drain(..);
2823 })
2824 });
2825 cx.assert_editor_state(indoc! {"
2826 ˇ
2827 "});
2828}
2829
2830#[gpui::test]
2831async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
2832 init_test(cx, |_| {});
2833
2834 let mut cx = EditorTestContext::new(cx).await;
2835
2836 // Consider continuous selection as single selection
2837 cx.set_state(indoc! {"
2838 Aaa«aa
2839 cˇ»c«c
2840 bb
2841 aaaˇ»aa
2842 "});
2843 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2844 cx.assert_editor_state(indoc! {"
2845 «Aaaaa
2846 ccc
2847 bb
2848 aaaaaˇ»
2849 "});
2850
2851 cx.set_state(indoc! {"
2852 Aaa«aa
2853 cˇ»c«c
2854 bb
2855 aaaˇ»aa
2856 "});
2857 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2858 cx.assert_editor_state(indoc! {"
2859 «Aaaaa
2860 ccc
2861 bbˇ»
2862 "});
2863
2864 // Consider non continuous selection as distinct dedup operations
2865 cx.set_state(indoc! {"
2866 «aaaaa
2867 bb
2868 aaaaa
2869 aaaaaˇ»
2870
2871 aaa«aaˇ»
2872 "});
2873 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2874 cx.assert_editor_state(indoc! {"
2875 «aaaaa
2876 bbˇ»
2877
2878 «aaaaaˇ»
2879 "});
2880}
2881
2882#[gpui::test]
2883async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
2884 init_test(cx, |_| {});
2885
2886 let mut cx = EditorTestContext::new(cx).await;
2887
2888 cx.set_state(indoc! {"
2889 «Aaa
2890 aAa
2891 Aaaˇ»
2892 "});
2893 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2894 cx.assert_editor_state(indoc! {"
2895 «Aaa
2896 aAaˇ»
2897 "});
2898
2899 cx.set_state(indoc! {"
2900 «Aaa
2901 aAa
2902 aaAˇ»
2903 "});
2904 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2905 cx.assert_editor_state(indoc! {"
2906 «Aaaˇ»
2907 "});
2908}
2909
2910#[gpui::test]
2911async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
2912 init_test(cx, |_| {});
2913
2914 let mut cx = EditorTestContext::new(cx).await;
2915
2916 // Manipulate with multiple selections on a single line
2917 cx.set_state(indoc! {"
2918 dd«dd
2919 cˇ»c«c
2920 bb
2921 aaaˇ»aa
2922 "});
2923 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2924 cx.assert_editor_state(indoc! {"
2925 «aaaaa
2926 bb
2927 ccc
2928 ddddˇ»
2929 "});
2930
2931 // Manipulate with multiple disjoin selections
2932 cx.set_state(indoc! {"
2933 5«
2934 4
2935 3
2936 2
2937 1ˇ»
2938
2939 dd«dd
2940 ccc
2941 bb
2942 aaaˇ»aa
2943 "});
2944 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2945 cx.assert_editor_state(indoc! {"
2946 «1
2947 2
2948 3
2949 4
2950 5ˇ»
2951
2952 «aaaaa
2953 bb
2954 ccc
2955 ddddˇ»
2956 "});
2957
2958 // Adding lines on each selection
2959 cx.set_state(indoc! {"
2960 2«
2961 1ˇ»
2962
2963 bb«bb
2964 aaaˇ»aa
2965 "});
2966 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added line")));
2967 cx.assert_editor_state(indoc! {"
2968 «2
2969 1
2970 added lineˇ»
2971
2972 «bbbb
2973 aaaaa
2974 added lineˇ»
2975 "});
2976
2977 // Removing lines on each selection
2978 cx.set_state(indoc! {"
2979 2«
2980 1ˇ»
2981
2982 bb«bb
2983 aaaˇ»aa
2984 "});
2985 cx.update_editor(|e, cx| {
2986 e.manipulate_lines(cx, |lines| {
2987 lines.pop();
2988 })
2989 });
2990 cx.assert_editor_state(indoc! {"
2991 «2ˇ»
2992
2993 «bbbbˇ»
2994 "});
2995}
2996
2997#[gpui::test]
2998async fn test_manipulate_text(cx: &mut TestAppContext) {
2999 init_test(cx, |_| {});
3000
3001 let mut cx = EditorTestContext::new(cx).await;
3002
3003 // Test convert_to_upper_case()
3004 cx.set_state(indoc! {"
3005 «hello worldˇ»
3006 "});
3007 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3008 cx.assert_editor_state(indoc! {"
3009 «HELLO WORLDˇ»
3010 "});
3011
3012 // Test convert_to_lower_case()
3013 cx.set_state(indoc! {"
3014 «HELLO WORLDˇ»
3015 "});
3016 cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
3017 cx.assert_editor_state(indoc! {"
3018 «hello worldˇ»
3019 "});
3020
3021 // Test multiple line, single selection case
3022 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3023 cx.set_state(indoc! {"
3024 «The quick brown
3025 fox jumps over
3026 the lazy dogˇ»
3027 "});
3028 cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
3029 cx.assert_editor_state(indoc! {"
3030 «The Quick Brown
3031 Fox Jumps Over
3032 The Lazy Dogˇ»
3033 "});
3034
3035 // Test multiple line, single selection case
3036 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3037 cx.set_state(indoc! {"
3038 «The quick brown
3039 fox jumps over
3040 the lazy dogˇ»
3041 "});
3042 cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
3043 cx.assert_editor_state(indoc! {"
3044 «TheQuickBrown
3045 FoxJumpsOver
3046 TheLazyDogˇ»
3047 "});
3048
3049 // From here on out, test more complex cases of manipulate_text()
3050
3051 // Test no selection case - should affect words cursors are in
3052 // Cursor at beginning, middle, and end of word
3053 cx.set_state(indoc! {"
3054 ˇhello big beauˇtiful worldˇ
3055 "});
3056 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3057 cx.assert_editor_state(indoc! {"
3058 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3059 "});
3060
3061 // Test multiple selections on a single line and across multiple lines
3062 cx.set_state(indoc! {"
3063 «Theˇ» quick «brown
3064 foxˇ» jumps «overˇ»
3065 the «lazyˇ» dog
3066 "});
3067 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3068 cx.assert_editor_state(indoc! {"
3069 «THEˇ» quick «BROWN
3070 FOXˇ» jumps «OVERˇ»
3071 the «LAZYˇ» dog
3072 "});
3073
3074 // Test case where text length grows
3075 cx.set_state(indoc! {"
3076 «tschüߡ»
3077 "});
3078 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3079 cx.assert_editor_state(indoc! {"
3080 «TSCHÜSSˇ»
3081 "});
3082
3083 // Test to make sure we don't crash when text shrinks
3084 cx.set_state(indoc! {"
3085 aaa_bbbˇ
3086 "});
3087 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3088 cx.assert_editor_state(indoc! {"
3089 «aaaBbbˇ»
3090 "});
3091
3092 // Test to make sure we all aware of the fact that each word can grow and shrink
3093 // Final selections should be aware of this fact
3094 cx.set_state(indoc! {"
3095 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3096 "});
3097 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3098 cx.assert_editor_state(indoc! {"
3099 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3100 "});
3101}
3102
3103#[gpui::test]
3104fn test_duplicate_line(cx: &mut TestAppContext) {
3105 init_test(cx, |_| {});
3106
3107 let view = cx.add_window(|cx| {
3108 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3109 build_editor(buffer, cx)
3110 });
3111 _ = view.update(cx, |view, cx| {
3112 view.change_selections(None, cx, |s| {
3113 s.select_display_ranges([
3114 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3115 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3116 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3117 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3118 ])
3119 });
3120 view.duplicate_line(&DuplicateLine::default(), cx);
3121 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3122 assert_eq!(
3123 view.selections.display_ranges(cx),
3124 vec![
3125 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
3126 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
3127 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3128 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3129 ]
3130 );
3131 });
3132
3133 let view = cx.add_window(|cx| {
3134 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3135 build_editor(buffer, cx)
3136 });
3137 _ = view.update(cx, |view, cx| {
3138 view.change_selections(None, cx, |s| {
3139 s.select_display_ranges([
3140 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3141 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3142 ])
3143 });
3144 view.duplicate_line(&DuplicateLine::default(), cx);
3145 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3146 assert_eq!(
3147 view.selections.display_ranges(cx),
3148 vec![
3149 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
3150 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
3151 ]
3152 );
3153 });
3154
3155 // With `move_upwards` the selections stay in place, except for
3156 // the lines inserted above them
3157 let view = cx.add_window(|cx| {
3158 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3159 build_editor(buffer, cx)
3160 });
3161 _ = view.update(cx, |view, cx| {
3162 view.change_selections(None, cx, |s| {
3163 s.select_display_ranges([
3164 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3165 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3166 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3167 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3168 ])
3169 });
3170 view.duplicate_line(&DuplicateLine { move_upwards: true }, cx);
3171 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3172 assert_eq!(
3173 view.selections.display_ranges(cx),
3174 vec![
3175 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3176 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3177 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3178 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3179 ]
3180 );
3181 });
3182
3183 let view = cx.add_window(|cx| {
3184 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3185 build_editor(buffer, cx)
3186 });
3187 _ = view.update(cx, |view, cx| {
3188 view.change_selections(None, cx, |s| {
3189 s.select_display_ranges([
3190 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3191 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3192 ])
3193 });
3194 view.duplicate_line(&DuplicateLine { move_upwards: true }, cx);
3195 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3196 assert_eq!(
3197 view.selections.display_ranges(cx),
3198 vec![
3199 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3200 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3201 ]
3202 );
3203 });
3204}
3205
3206#[gpui::test]
3207fn test_move_line_up_down(cx: &mut TestAppContext) {
3208 init_test(cx, |_| {});
3209
3210 let view = cx.add_window(|cx| {
3211 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3212 build_editor(buffer, cx)
3213 });
3214 _ = view.update(cx, |view, cx| {
3215 view.fold_ranges(
3216 vec![
3217 Point::new(0, 2)..Point::new(1, 2),
3218 Point::new(2, 3)..Point::new(4, 1),
3219 Point::new(7, 0)..Point::new(8, 4),
3220 ],
3221 true,
3222 cx,
3223 );
3224 view.change_selections(None, cx, |s| {
3225 s.select_display_ranges([
3226 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3227 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3228 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3229 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
3230 ])
3231 });
3232 assert_eq!(
3233 view.display_text(cx),
3234 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
3235 );
3236
3237 view.move_line_up(&MoveLineUp, cx);
3238 assert_eq!(
3239 view.display_text(cx),
3240 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
3241 );
3242 assert_eq!(
3243 view.selections.display_ranges(cx),
3244 vec![
3245 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3246 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3247 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3248 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3249 ]
3250 );
3251 });
3252
3253 _ = view.update(cx, |view, cx| {
3254 view.move_line_down(&MoveLineDown, cx);
3255 assert_eq!(
3256 view.display_text(cx),
3257 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
3258 );
3259 assert_eq!(
3260 view.selections.display_ranges(cx),
3261 vec![
3262 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3263 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3264 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3265 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3266 ]
3267 );
3268 });
3269
3270 _ = view.update(cx, |view, cx| {
3271 view.move_line_down(&MoveLineDown, cx);
3272 assert_eq!(
3273 view.display_text(cx),
3274 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
3275 );
3276 assert_eq!(
3277 view.selections.display_ranges(cx),
3278 vec![
3279 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3280 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3281 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3282 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3283 ]
3284 );
3285 });
3286
3287 _ = view.update(cx, |view, cx| {
3288 view.move_line_up(&MoveLineUp, cx);
3289 assert_eq!(
3290 view.display_text(cx),
3291 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
3292 );
3293 assert_eq!(
3294 view.selections.display_ranges(cx),
3295 vec![
3296 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3297 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3298 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3299 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3300 ]
3301 );
3302 });
3303}
3304
3305#[gpui::test]
3306fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
3307 init_test(cx, |_| {});
3308
3309 let editor = cx.add_window(|cx| {
3310 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3311 build_editor(buffer, cx)
3312 });
3313 _ = editor.update(cx, |editor, cx| {
3314 let snapshot = editor.buffer.read(cx).snapshot(cx);
3315 editor.insert_blocks(
3316 [BlockProperties {
3317 style: BlockStyle::Fixed,
3318 position: snapshot.anchor_after(Point::new(2, 0)),
3319 disposition: BlockDisposition::Below,
3320 height: 1,
3321 render: Arc::new(|_| div().into_any()),
3322 }],
3323 Some(Autoscroll::fit()),
3324 cx,
3325 );
3326 editor.change_selections(None, cx, |s| {
3327 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
3328 });
3329 editor.move_line_down(&MoveLineDown, cx);
3330 });
3331}
3332
3333#[gpui::test]
3334fn test_transpose(cx: &mut TestAppContext) {
3335 init_test(cx, |_| {});
3336
3337 _ = cx.add_window(|cx| {
3338 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
3339 editor.set_style(EditorStyle::default(), cx);
3340 editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
3341 editor.transpose(&Default::default(), cx);
3342 assert_eq!(editor.text(cx), "bac");
3343 assert_eq!(editor.selections.ranges(cx), [2..2]);
3344
3345 editor.transpose(&Default::default(), cx);
3346 assert_eq!(editor.text(cx), "bca");
3347 assert_eq!(editor.selections.ranges(cx), [3..3]);
3348
3349 editor.transpose(&Default::default(), cx);
3350 assert_eq!(editor.text(cx), "bac");
3351 assert_eq!(editor.selections.ranges(cx), [3..3]);
3352
3353 editor
3354 });
3355
3356 _ = cx.add_window(|cx| {
3357 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3358 editor.set_style(EditorStyle::default(), cx);
3359 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3360 editor.transpose(&Default::default(), cx);
3361 assert_eq!(editor.text(cx), "acb\nde");
3362 assert_eq!(editor.selections.ranges(cx), [3..3]);
3363
3364 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3365 editor.transpose(&Default::default(), cx);
3366 assert_eq!(editor.text(cx), "acbd\ne");
3367 assert_eq!(editor.selections.ranges(cx), [5..5]);
3368
3369 editor.transpose(&Default::default(), cx);
3370 assert_eq!(editor.text(cx), "acbde\n");
3371 assert_eq!(editor.selections.ranges(cx), [6..6]);
3372
3373 editor.transpose(&Default::default(), cx);
3374 assert_eq!(editor.text(cx), "acbd\ne");
3375 assert_eq!(editor.selections.ranges(cx), [6..6]);
3376
3377 editor
3378 });
3379
3380 _ = cx.add_window(|cx| {
3381 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3382 editor.set_style(EditorStyle::default(), cx);
3383 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
3384 editor.transpose(&Default::default(), cx);
3385 assert_eq!(editor.text(cx), "bacd\ne");
3386 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
3387
3388 editor.transpose(&Default::default(), cx);
3389 assert_eq!(editor.text(cx), "bcade\n");
3390 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
3391
3392 editor.transpose(&Default::default(), cx);
3393 assert_eq!(editor.text(cx), "bcda\ne");
3394 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3395
3396 editor.transpose(&Default::default(), cx);
3397 assert_eq!(editor.text(cx), "bcade\n");
3398 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3399
3400 editor.transpose(&Default::default(), cx);
3401 assert_eq!(editor.text(cx), "bcaed\n");
3402 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
3403
3404 editor
3405 });
3406
3407 _ = cx.add_window(|cx| {
3408 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
3409 editor.set_style(EditorStyle::default(), cx);
3410 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3411 editor.transpose(&Default::default(), cx);
3412 assert_eq!(editor.text(cx), "🏀🍐✋");
3413 assert_eq!(editor.selections.ranges(cx), [8..8]);
3414
3415 editor.transpose(&Default::default(), cx);
3416 assert_eq!(editor.text(cx), "🏀✋🍐");
3417 assert_eq!(editor.selections.ranges(cx), [11..11]);
3418
3419 editor.transpose(&Default::default(), cx);
3420 assert_eq!(editor.text(cx), "🏀🍐✋");
3421 assert_eq!(editor.selections.ranges(cx), [11..11]);
3422
3423 editor
3424 });
3425}
3426
3427#[gpui::test]
3428async fn test_clipboard(cx: &mut gpui::TestAppContext) {
3429 init_test(cx, |_| {});
3430
3431 let mut cx = EditorTestContext::new(cx).await;
3432
3433 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
3434 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3435 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
3436
3437 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
3438 cx.set_state("two ˇfour ˇsix ˇ");
3439 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3440 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
3441
3442 // Paste again but with only two cursors. Since the number of cursors doesn't
3443 // match the number of slices in the clipboard, the entire clipboard text
3444 // is pasted at each cursor.
3445 cx.set_state("ˇtwo one✅ four three six five ˇ");
3446 cx.update_editor(|e, cx| {
3447 e.handle_input("( ", cx);
3448 e.paste(&Paste, cx);
3449 e.handle_input(") ", cx);
3450 });
3451 cx.assert_editor_state(
3452 &([
3453 "( one✅ ",
3454 "three ",
3455 "five ) ˇtwo one✅ four three six five ( one✅ ",
3456 "three ",
3457 "five ) ˇ",
3458 ]
3459 .join("\n")),
3460 );
3461
3462 // Cut with three selections, one of which is full-line.
3463 cx.set_state(indoc! {"
3464 1«2ˇ»3
3465 4ˇ567
3466 «8ˇ»9"});
3467 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3468 cx.assert_editor_state(indoc! {"
3469 1ˇ3
3470 ˇ9"});
3471
3472 // Paste with three selections, noticing how the copied selection that was full-line
3473 // gets inserted before the second cursor.
3474 cx.set_state(indoc! {"
3475 1ˇ3
3476 9ˇ
3477 «oˇ»ne"});
3478 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3479 cx.assert_editor_state(indoc! {"
3480 12ˇ3
3481 4567
3482 9ˇ
3483 8ˇne"});
3484
3485 // Copy with a single cursor only, which writes the whole line into the clipboard.
3486 cx.set_state(indoc! {"
3487 The quick brown
3488 fox juˇmps over
3489 the lazy dog"});
3490 cx.update_editor(|e, cx| e.copy(&Copy, cx));
3491 assert_eq!(
3492 cx.read_from_clipboard().map(|item| item.text().to_owned()),
3493 Some("fox jumps over\n".to_owned())
3494 );
3495
3496 // Paste with three selections, noticing how the copied full-line selection is inserted
3497 // before the empty selections but replaces the selection that is non-empty.
3498 cx.set_state(indoc! {"
3499 Tˇhe quick brown
3500 «foˇ»x jumps over
3501 tˇhe lazy dog"});
3502 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3503 cx.assert_editor_state(indoc! {"
3504 fox jumps over
3505 Tˇhe quick brown
3506 fox jumps over
3507 ˇx jumps over
3508 fox jumps over
3509 tˇhe lazy dog"});
3510}
3511
3512#[gpui::test]
3513async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
3514 init_test(cx, |_| {});
3515
3516 let mut cx = EditorTestContext::new(cx).await;
3517 let language = Arc::new(Language::new(
3518 LanguageConfig::default(),
3519 Some(tree_sitter_rust::language()),
3520 ));
3521 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3522
3523 // Cut an indented block, without the leading whitespace.
3524 cx.set_state(indoc! {"
3525 const a: B = (
3526 c(),
3527 «d(
3528 e,
3529 f
3530 )ˇ»
3531 );
3532 "});
3533 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3534 cx.assert_editor_state(indoc! {"
3535 const a: B = (
3536 c(),
3537 ˇ
3538 );
3539 "});
3540
3541 // Paste it at the same position.
3542 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3543 cx.assert_editor_state(indoc! {"
3544 const a: B = (
3545 c(),
3546 d(
3547 e,
3548 f
3549 )ˇ
3550 );
3551 "});
3552
3553 // Paste it at a line with a lower indent level.
3554 cx.set_state(indoc! {"
3555 ˇ
3556 const a: B = (
3557 c(),
3558 );
3559 "});
3560 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3561 cx.assert_editor_state(indoc! {"
3562 d(
3563 e,
3564 f
3565 )ˇ
3566 const a: B = (
3567 c(),
3568 );
3569 "});
3570
3571 // Cut an indented block, with the leading whitespace.
3572 cx.set_state(indoc! {"
3573 const a: B = (
3574 c(),
3575 « d(
3576 e,
3577 f
3578 )
3579 ˇ»);
3580 "});
3581 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3582 cx.assert_editor_state(indoc! {"
3583 const a: B = (
3584 c(),
3585 ˇ);
3586 "});
3587
3588 // Paste it at the same position.
3589 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3590 cx.assert_editor_state(indoc! {"
3591 const a: B = (
3592 c(),
3593 d(
3594 e,
3595 f
3596 )
3597 ˇ);
3598 "});
3599
3600 // Paste it at a line with a higher indent level.
3601 cx.set_state(indoc! {"
3602 const a: B = (
3603 c(),
3604 d(
3605 e,
3606 fˇ
3607 )
3608 );
3609 "});
3610 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3611 cx.assert_editor_state(indoc! {"
3612 const a: B = (
3613 c(),
3614 d(
3615 e,
3616 f d(
3617 e,
3618 f
3619 )
3620 ˇ
3621 )
3622 );
3623 "});
3624}
3625
3626#[gpui::test]
3627fn test_select_all(cx: &mut TestAppContext) {
3628 init_test(cx, |_| {});
3629
3630 let view = cx.add_window(|cx| {
3631 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
3632 build_editor(buffer, cx)
3633 });
3634 _ = view.update(cx, |view, cx| {
3635 view.select_all(&SelectAll, cx);
3636 assert_eq!(
3637 view.selections.display_ranges(cx),
3638 &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
3639 );
3640 });
3641}
3642
3643#[gpui::test]
3644fn test_select_line(cx: &mut TestAppContext) {
3645 init_test(cx, |_| {});
3646
3647 let view = cx.add_window(|cx| {
3648 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
3649 build_editor(buffer, cx)
3650 });
3651 _ = view.update(cx, |view, cx| {
3652 view.change_selections(None, cx, |s| {
3653 s.select_display_ranges([
3654 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3655 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3656 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3657 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
3658 ])
3659 });
3660 view.select_line(&SelectLine, cx);
3661 assert_eq!(
3662 view.selections.display_ranges(cx),
3663 vec![
3664 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
3665 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
3666 ]
3667 );
3668 });
3669
3670 _ = view.update(cx, |view, cx| {
3671 view.select_line(&SelectLine, cx);
3672 assert_eq!(
3673 view.selections.display_ranges(cx),
3674 vec![
3675 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
3676 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
3677 ]
3678 );
3679 });
3680
3681 _ = view.update(cx, |view, cx| {
3682 view.select_line(&SelectLine, cx);
3683 assert_eq!(
3684 view.selections.display_ranges(cx),
3685 vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
3686 );
3687 });
3688}
3689
3690#[gpui::test]
3691fn test_split_selection_into_lines(cx: &mut TestAppContext) {
3692 init_test(cx, |_| {});
3693
3694 let view = cx.add_window(|cx| {
3695 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
3696 build_editor(buffer, cx)
3697 });
3698 _ = view.update(cx, |view, cx| {
3699 view.fold_ranges(
3700 vec![
3701 Point::new(0, 2)..Point::new(1, 2),
3702 Point::new(2, 3)..Point::new(4, 1),
3703 Point::new(7, 0)..Point::new(8, 4),
3704 ],
3705 true,
3706 cx,
3707 );
3708 view.change_selections(None, cx, |s| {
3709 s.select_display_ranges([
3710 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3711 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3712 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3713 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
3714 ])
3715 });
3716 assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
3717 });
3718
3719 _ = view.update(cx, |view, cx| {
3720 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3721 assert_eq!(
3722 view.display_text(cx),
3723 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
3724 );
3725 assert_eq!(
3726 view.selections.display_ranges(cx),
3727 [
3728 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3729 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3730 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3731 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
3732 ]
3733 );
3734 });
3735
3736 _ = view.update(cx, |view, cx| {
3737 view.change_selections(None, cx, |s| {
3738 s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
3739 });
3740 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3741 assert_eq!(
3742 view.display_text(cx),
3743 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
3744 );
3745 assert_eq!(
3746 view.selections.display_ranges(cx),
3747 [
3748 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
3749 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
3750 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
3751 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
3752 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
3753 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
3754 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
3755 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
3756 ]
3757 );
3758 });
3759}
3760
3761#[gpui::test]
3762async fn test_add_selection_above_below(cx: &mut TestAppContext) {
3763 init_test(cx, |_| {});
3764
3765 let mut cx = EditorTestContext::new(cx).await;
3766
3767 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
3768 cx.set_state(indoc!(
3769 r#"abc
3770 defˇghi
3771
3772 jk
3773 nlmo
3774 "#
3775 ));
3776
3777 cx.update_editor(|editor, cx| {
3778 editor.add_selection_above(&Default::default(), cx);
3779 });
3780
3781 cx.assert_editor_state(indoc!(
3782 r#"abcˇ
3783 defˇghi
3784
3785 jk
3786 nlmo
3787 "#
3788 ));
3789
3790 cx.update_editor(|editor, cx| {
3791 editor.add_selection_above(&Default::default(), cx);
3792 });
3793
3794 cx.assert_editor_state(indoc!(
3795 r#"abcˇ
3796 defˇghi
3797
3798 jk
3799 nlmo
3800 "#
3801 ));
3802
3803 cx.update_editor(|view, cx| {
3804 view.add_selection_below(&Default::default(), cx);
3805 });
3806
3807 cx.assert_editor_state(indoc!(
3808 r#"abc
3809 defˇghi
3810
3811 jk
3812 nlmo
3813 "#
3814 ));
3815
3816 cx.update_editor(|view, cx| {
3817 view.undo_selection(&Default::default(), cx);
3818 });
3819
3820 cx.assert_editor_state(indoc!(
3821 r#"abcˇ
3822 defˇghi
3823
3824 jk
3825 nlmo
3826 "#
3827 ));
3828
3829 cx.update_editor(|view, cx| {
3830 view.redo_selection(&Default::default(), cx);
3831 });
3832
3833 cx.assert_editor_state(indoc!(
3834 r#"abc
3835 defˇghi
3836
3837 jk
3838 nlmo
3839 "#
3840 ));
3841
3842 cx.update_editor(|view, cx| {
3843 view.add_selection_below(&Default::default(), cx);
3844 });
3845
3846 cx.assert_editor_state(indoc!(
3847 r#"abc
3848 defˇghi
3849
3850 jk
3851 nlmˇo
3852 "#
3853 ));
3854
3855 cx.update_editor(|view, cx| {
3856 view.add_selection_below(&Default::default(), cx);
3857 });
3858
3859 cx.assert_editor_state(indoc!(
3860 r#"abc
3861 defˇghi
3862
3863 jk
3864 nlmˇo
3865 "#
3866 ));
3867
3868 // change selections
3869 cx.set_state(indoc!(
3870 r#"abc
3871 def«ˇg»hi
3872
3873 jk
3874 nlmo
3875 "#
3876 ));
3877
3878 cx.update_editor(|view, cx| {
3879 view.add_selection_below(&Default::default(), cx);
3880 });
3881
3882 cx.assert_editor_state(indoc!(
3883 r#"abc
3884 def«ˇg»hi
3885
3886 jk
3887 nlm«ˇo»
3888 "#
3889 ));
3890
3891 cx.update_editor(|view, cx| {
3892 view.add_selection_below(&Default::default(), cx);
3893 });
3894
3895 cx.assert_editor_state(indoc!(
3896 r#"abc
3897 def«ˇg»hi
3898
3899 jk
3900 nlm«ˇo»
3901 "#
3902 ));
3903
3904 cx.update_editor(|view, cx| {
3905 view.add_selection_above(&Default::default(), cx);
3906 });
3907
3908 cx.assert_editor_state(indoc!(
3909 r#"abc
3910 def«ˇg»hi
3911
3912 jk
3913 nlmo
3914 "#
3915 ));
3916
3917 cx.update_editor(|view, cx| {
3918 view.add_selection_above(&Default::default(), cx);
3919 });
3920
3921 cx.assert_editor_state(indoc!(
3922 r#"abc
3923 def«ˇg»hi
3924
3925 jk
3926 nlmo
3927 "#
3928 ));
3929
3930 // Change selections again
3931 cx.set_state(indoc!(
3932 r#"a«bc
3933 defgˇ»hi
3934
3935 jk
3936 nlmo
3937 "#
3938 ));
3939
3940 cx.update_editor(|view, cx| {
3941 view.add_selection_below(&Default::default(), cx);
3942 });
3943
3944 cx.assert_editor_state(indoc!(
3945 r#"a«bcˇ»
3946 d«efgˇ»hi
3947
3948 j«kˇ»
3949 nlmo
3950 "#
3951 ));
3952
3953 cx.update_editor(|view, cx| {
3954 view.add_selection_below(&Default::default(), cx);
3955 });
3956 cx.assert_editor_state(indoc!(
3957 r#"a«bcˇ»
3958 d«efgˇ»hi
3959
3960 j«kˇ»
3961 n«lmoˇ»
3962 "#
3963 ));
3964 cx.update_editor(|view, cx| {
3965 view.add_selection_above(&Default::default(), cx);
3966 });
3967
3968 cx.assert_editor_state(indoc!(
3969 r#"a«bcˇ»
3970 d«efgˇ»hi
3971
3972 j«kˇ»
3973 nlmo
3974 "#
3975 ));
3976
3977 // Change selections again
3978 cx.set_state(indoc!(
3979 r#"abc
3980 d«ˇefghi
3981
3982 jk
3983 nlm»o
3984 "#
3985 ));
3986
3987 cx.update_editor(|view, cx| {
3988 view.add_selection_above(&Default::default(), cx);
3989 });
3990
3991 cx.assert_editor_state(indoc!(
3992 r#"a«ˇbc»
3993 d«ˇef»ghi
3994
3995 j«ˇk»
3996 n«ˇlm»o
3997 "#
3998 ));
3999
4000 cx.update_editor(|view, cx| {
4001 view.add_selection_below(&Default::default(), cx);
4002 });
4003
4004 cx.assert_editor_state(indoc!(
4005 r#"abc
4006 d«ˇef»ghi
4007
4008 j«ˇk»
4009 n«ˇlm»o
4010 "#
4011 ));
4012}
4013
4014#[gpui::test]
4015async fn test_select_next(cx: &mut gpui::TestAppContext) {
4016 init_test(cx, |_| {});
4017
4018 let mut cx = EditorTestContext::new(cx).await;
4019 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4020
4021 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4022 .unwrap();
4023 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4024
4025 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4026 .unwrap();
4027 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4028
4029 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4030 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4031
4032 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4033 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4034
4035 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4036 .unwrap();
4037 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4038
4039 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4040 .unwrap();
4041 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4042}
4043
4044#[gpui::test]
4045async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
4046 init_test(cx, |_| {});
4047
4048 let mut cx = EditorTestContext::new(cx).await;
4049 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4050
4051 cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches, cx))
4052 .unwrap();
4053 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4054}
4055
4056#[gpui::test]
4057async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4058 init_test(cx, |_| {});
4059
4060 let mut cx = EditorTestContext::new(cx).await;
4061 cx.set_state(
4062 r#"let foo = 2;
4063lˇet foo = 2;
4064let fooˇ = 2;
4065let foo = 2;
4066let foo = ˇ2;"#,
4067 );
4068
4069 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4070 .unwrap();
4071 cx.assert_editor_state(
4072 r#"let foo = 2;
4073«letˇ» foo = 2;
4074let «fooˇ» = 2;
4075let foo = 2;
4076let foo = «2ˇ»;"#,
4077 );
4078
4079 // noop for multiple selections with different contents
4080 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4081 .unwrap();
4082 cx.assert_editor_state(
4083 r#"let foo = 2;
4084«letˇ» foo = 2;
4085let «fooˇ» = 2;
4086let foo = 2;
4087let foo = «2ˇ»;"#,
4088 );
4089}
4090
4091#[gpui::test]
4092async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
4093 init_test(cx, |_| {});
4094
4095 let mut cx = EditorTestContext::new(cx).await;
4096 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4097
4098 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4099 .unwrap();
4100 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4101
4102 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4103 .unwrap();
4104 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4105
4106 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4107 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4108
4109 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4110 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4111
4112 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4113 .unwrap();
4114 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
4115
4116 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4117 .unwrap();
4118 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
4119
4120 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4121 .unwrap();
4122 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4123}
4124
4125#[gpui::test]
4126async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4127 init_test(cx, |_| {});
4128
4129 let mut cx = EditorTestContext::new(cx).await;
4130 cx.set_state(
4131 r#"let foo = 2;
4132lˇet foo = 2;
4133let fooˇ = 2;
4134let foo = 2;
4135let foo = ˇ2;"#,
4136 );
4137
4138 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4139 .unwrap();
4140 cx.assert_editor_state(
4141 r#"let foo = 2;
4142«letˇ» foo = 2;
4143let «fooˇ» = 2;
4144let foo = 2;
4145let foo = «2ˇ»;"#,
4146 );
4147
4148 // noop for multiple selections with different contents
4149 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4150 .unwrap();
4151 cx.assert_editor_state(
4152 r#"let foo = 2;
4153«letˇ» foo = 2;
4154let «fooˇ» = 2;
4155let foo = 2;
4156let foo = «2ˇ»;"#,
4157 );
4158}
4159
4160#[gpui::test]
4161async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
4162 init_test(cx, |_| {});
4163
4164 let mut cx = EditorTestContext::new(cx).await;
4165 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
4166
4167 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4168 .unwrap();
4169 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4170
4171 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4172 .unwrap();
4173 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4174
4175 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4176 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4177
4178 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4179 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4180
4181 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4182 .unwrap();
4183 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
4184
4185 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4186 .unwrap();
4187 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4188}
4189
4190#[gpui::test]
4191async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
4192 init_test(cx, |_| {});
4193
4194 let language = Arc::new(Language::new(
4195 LanguageConfig::default(),
4196 Some(tree_sitter_rust::language()),
4197 ));
4198
4199 let text = r#"
4200 use mod1::mod2::{mod3, mod4};
4201
4202 fn fn_1(param1: bool, param2: &str) {
4203 let var1 = "text";
4204 }
4205 "#
4206 .unindent();
4207
4208 let buffer = cx.new_model(|cx| {
4209 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4210 .with_language(language, cx)
4211 });
4212 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4213 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4214
4215 view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4216 .await;
4217
4218 _ = view.update(cx, |view, cx| {
4219 view.change_selections(None, cx, |s| {
4220 s.select_display_ranges([
4221 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4222 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4223 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4224 ]);
4225 });
4226 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4227 });
4228 assert_eq!(
4229 view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
4230 &[
4231 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4232 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4233 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4234 ]
4235 );
4236
4237 _ = view.update(cx, |view, cx| {
4238 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4239 });
4240 assert_eq!(
4241 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4242 &[
4243 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4244 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4245 ]
4246 );
4247
4248 _ = view.update(cx, |view, cx| {
4249 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4250 });
4251 assert_eq!(
4252 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4253 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4254 );
4255
4256 // Trying to expand the selected syntax node one more time has no effect.
4257 _ = view.update(cx, |view, cx| {
4258 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4259 });
4260 assert_eq!(
4261 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4262 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4263 );
4264
4265 _ = view.update(cx, |view, cx| {
4266 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4267 });
4268 assert_eq!(
4269 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4270 &[
4271 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4272 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4273 ]
4274 );
4275
4276 _ = view.update(cx, |view, cx| {
4277 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4278 });
4279 assert_eq!(
4280 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4281 &[
4282 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4283 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4284 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4285 ]
4286 );
4287
4288 _ = view.update(cx, |view, cx| {
4289 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4290 });
4291 assert_eq!(
4292 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4293 &[
4294 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4295 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4296 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4297 ]
4298 );
4299
4300 // Trying to shrink the selected syntax node one more time has no effect.
4301 _ = view.update(cx, |view, cx| {
4302 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4303 });
4304 assert_eq!(
4305 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4306 &[
4307 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4308 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4309 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4310 ]
4311 );
4312
4313 // Ensure that we keep expanding the selection if the larger selection starts or ends within
4314 // a fold.
4315 _ = view.update(cx, |view, cx| {
4316 view.fold_ranges(
4317 vec![
4318 Point::new(0, 21)..Point::new(0, 24),
4319 Point::new(3, 20)..Point::new(3, 22),
4320 ],
4321 true,
4322 cx,
4323 );
4324 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4325 });
4326 assert_eq!(
4327 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4328 &[
4329 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4330 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4331 DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
4332 ]
4333 );
4334}
4335
4336#[gpui::test]
4337async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
4338 init_test(cx, |_| {});
4339
4340 let language = Arc::new(
4341 Language::new(
4342 LanguageConfig {
4343 brackets: BracketPairConfig {
4344 pairs: vec![
4345 BracketPair {
4346 start: "{".to_string(),
4347 end: "}".to_string(),
4348 close: false,
4349 newline: true,
4350 },
4351 BracketPair {
4352 start: "(".to_string(),
4353 end: ")".to_string(),
4354 close: false,
4355 newline: true,
4356 },
4357 ],
4358 ..Default::default()
4359 },
4360 ..Default::default()
4361 },
4362 Some(tree_sitter_rust::language()),
4363 )
4364 .with_indents_query(
4365 r#"
4366 (_ "(" ")" @end) @indent
4367 (_ "{" "}" @end) @indent
4368 "#,
4369 )
4370 .unwrap(),
4371 );
4372
4373 let text = "fn a() {}";
4374
4375 let buffer = cx.new_model(|cx| {
4376 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4377 .with_language(language, cx)
4378 });
4379 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4380 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4381 editor
4382 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
4383 .await;
4384
4385 _ = editor.update(cx, |editor, cx| {
4386 editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
4387 editor.newline(&Newline, cx);
4388 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
4389 assert_eq!(
4390 editor.selections.ranges(cx),
4391 &[
4392 Point::new(1, 4)..Point::new(1, 4),
4393 Point::new(3, 4)..Point::new(3, 4),
4394 Point::new(5, 0)..Point::new(5, 0)
4395 ]
4396 );
4397 });
4398}
4399
4400#[gpui::test]
4401async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let mut cx = EditorTestContext::new(cx).await;
4405
4406 let language = Arc::new(Language::new(
4407 LanguageConfig {
4408 brackets: BracketPairConfig {
4409 pairs: vec![
4410 BracketPair {
4411 start: "{".to_string(),
4412 end: "}".to_string(),
4413 close: true,
4414 newline: true,
4415 },
4416 BracketPair {
4417 start: "(".to_string(),
4418 end: ")".to_string(),
4419 close: true,
4420 newline: true,
4421 },
4422 BracketPair {
4423 start: "/*".to_string(),
4424 end: " */".to_string(),
4425 close: true,
4426 newline: true,
4427 },
4428 BracketPair {
4429 start: "[".to_string(),
4430 end: "]".to_string(),
4431 close: false,
4432 newline: true,
4433 },
4434 BracketPair {
4435 start: "\"".to_string(),
4436 end: "\"".to_string(),
4437 close: true,
4438 newline: false,
4439 },
4440 ],
4441 ..Default::default()
4442 },
4443 autoclose_before: "})]".to_string(),
4444 ..Default::default()
4445 },
4446 Some(tree_sitter_rust::language()),
4447 ));
4448
4449 cx.language_registry().add(language.clone());
4450 cx.update_buffer(|buffer, cx| {
4451 buffer.set_language(Some(language), cx);
4452 });
4453
4454 cx.set_state(
4455 &r#"
4456 🏀ˇ
4457 εˇ
4458 ❤️ˇ
4459 "#
4460 .unindent(),
4461 );
4462
4463 // autoclose multiple nested brackets at multiple cursors
4464 cx.update_editor(|view, cx| {
4465 view.handle_input("{", cx);
4466 view.handle_input("{", cx);
4467 view.handle_input("{", cx);
4468 });
4469 cx.assert_editor_state(
4470 &"
4471 🏀{{{ˇ}}}
4472 ε{{{ˇ}}}
4473 ❤️{{{ˇ}}}
4474 "
4475 .unindent(),
4476 );
4477
4478 // insert a different closing bracket
4479 cx.update_editor(|view, cx| {
4480 view.handle_input(")", cx);
4481 });
4482 cx.assert_editor_state(
4483 &"
4484 🏀{{{)ˇ}}}
4485 ε{{{)ˇ}}}
4486 ❤️{{{)ˇ}}}
4487 "
4488 .unindent(),
4489 );
4490
4491 // skip over the auto-closed brackets when typing a closing bracket
4492 cx.update_editor(|view, cx| {
4493 view.move_right(&MoveRight, cx);
4494 view.handle_input("}", cx);
4495 view.handle_input("}", cx);
4496 view.handle_input("}", cx);
4497 });
4498 cx.assert_editor_state(
4499 &"
4500 🏀{{{)}}}}ˇ
4501 ε{{{)}}}}ˇ
4502 ❤️{{{)}}}}ˇ
4503 "
4504 .unindent(),
4505 );
4506
4507 // autoclose multi-character pairs
4508 cx.set_state(
4509 &"
4510 ˇ
4511 ˇ
4512 "
4513 .unindent(),
4514 );
4515 cx.update_editor(|view, cx| {
4516 view.handle_input("/", cx);
4517 view.handle_input("*", cx);
4518 });
4519 cx.assert_editor_state(
4520 &"
4521 /*ˇ */
4522 /*ˇ */
4523 "
4524 .unindent(),
4525 );
4526
4527 // one cursor autocloses a multi-character pair, one cursor
4528 // does not autoclose.
4529 cx.set_state(
4530 &"
4531 /ˇ
4532 ˇ
4533 "
4534 .unindent(),
4535 );
4536 cx.update_editor(|view, cx| view.handle_input("*", cx));
4537 cx.assert_editor_state(
4538 &"
4539 /*ˇ */
4540 *ˇ
4541 "
4542 .unindent(),
4543 );
4544
4545 // Don't autoclose if the next character isn't whitespace and isn't
4546 // listed in the language's "autoclose_before" section.
4547 cx.set_state("ˇa b");
4548 cx.update_editor(|view, cx| view.handle_input("{", cx));
4549 cx.assert_editor_state("{ˇa b");
4550
4551 // Don't autoclose if `close` is false for the bracket pair
4552 cx.set_state("ˇ");
4553 cx.update_editor(|view, cx| view.handle_input("[", cx));
4554 cx.assert_editor_state("[ˇ");
4555
4556 // Surround with brackets if text is selected
4557 cx.set_state("«aˇ» b");
4558 cx.update_editor(|view, cx| view.handle_input("{", cx));
4559 cx.assert_editor_state("{«aˇ»} b");
4560
4561 // Autclose pair where the start and end characters are the same
4562 cx.set_state("aˇ");
4563 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4564 cx.assert_editor_state("a\"ˇ\"");
4565 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4566 cx.assert_editor_state("a\"\"ˇ");
4567}
4568
4569#[gpui::test]
4570async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 let language = Arc::new(Language::new(
4578 LanguageConfig {
4579 brackets: BracketPairConfig {
4580 pairs: vec![
4581 BracketPair {
4582 start: "{".to_string(),
4583 end: "}".to_string(),
4584 close: true,
4585 newline: true,
4586 },
4587 BracketPair {
4588 start: "(".to_string(),
4589 end: ")".to_string(),
4590 close: true,
4591 newline: true,
4592 },
4593 BracketPair {
4594 start: "[".to_string(),
4595 end: "]".to_string(),
4596 close: false,
4597 newline: true,
4598 },
4599 ],
4600 ..Default::default()
4601 },
4602 autoclose_before: "})]".to_string(),
4603 ..Default::default()
4604 },
4605 Some(tree_sitter_rust::language()),
4606 ));
4607
4608 cx.language_registry().add(language.clone());
4609 cx.update_buffer(|buffer, cx| {
4610 buffer.set_language(Some(language), cx);
4611 });
4612
4613 cx.set_state(
4614 &"
4615 ˇ
4616 ˇ
4617 ˇ
4618 "
4619 .unindent(),
4620 );
4621
4622 // ensure only matching closing brackets are skipped over
4623 cx.update_editor(|view, cx| {
4624 view.handle_input("}", cx);
4625 view.move_left(&MoveLeft, cx);
4626 view.handle_input(")", cx);
4627 view.move_left(&MoveLeft, cx);
4628 });
4629 cx.assert_editor_state(
4630 &"
4631 ˇ)}
4632 ˇ)}
4633 ˇ)}
4634 "
4635 .unindent(),
4636 );
4637
4638 // skip-over closing brackets at multiple cursors
4639 cx.update_editor(|view, cx| {
4640 view.handle_input(")", cx);
4641 view.handle_input("}", cx);
4642 });
4643 cx.assert_editor_state(
4644 &"
4645 )}ˇ
4646 )}ˇ
4647 )}ˇ
4648 "
4649 .unindent(),
4650 );
4651
4652 // ignore non-close brackets
4653 cx.update_editor(|view, cx| {
4654 view.handle_input("]", cx);
4655 view.move_left(&MoveLeft, cx);
4656 view.handle_input("]", cx);
4657 });
4658 cx.assert_editor_state(
4659 &"
4660 )}]ˇ]
4661 )}]ˇ]
4662 )}]ˇ]
4663 "
4664 .unindent(),
4665 );
4666}
4667
4668#[gpui::test]
4669async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
4670 init_test(cx, |_| {});
4671
4672 let mut cx = EditorTestContext::new(cx).await;
4673
4674 let html_language = Arc::new(
4675 Language::new(
4676 LanguageConfig {
4677 name: "HTML".into(),
4678 brackets: BracketPairConfig {
4679 pairs: vec![
4680 BracketPair {
4681 start: "<".into(),
4682 end: ">".into(),
4683 close: true,
4684 ..Default::default()
4685 },
4686 BracketPair {
4687 start: "{".into(),
4688 end: "}".into(),
4689 close: true,
4690 ..Default::default()
4691 },
4692 BracketPair {
4693 start: "(".into(),
4694 end: ")".into(),
4695 close: true,
4696 ..Default::default()
4697 },
4698 ],
4699 ..Default::default()
4700 },
4701 autoclose_before: "})]>".into(),
4702 ..Default::default()
4703 },
4704 Some(tree_sitter_html::language()),
4705 )
4706 .with_injection_query(
4707 r#"
4708 (script_element
4709 (raw_text) @content
4710 (#set! "language" "javascript"))
4711 "#,
4712 )
4713 .unwrap(),
4714 );
4715
4716 let javascript_language = Arc::new(Language::new(
4717 LanguageConfig {
4718 name: "JavaScript".into(),
4719 brackets: BracketPairConfig {
4720 pairs: vec![
4721 BracketPair {
4722 start: "/*".into(),
4723 end: " */".into(),
4724 close: true,
4725 ..Default::default()
4726 },
4727 BracketPair {
4728 start: "{".into(),
4729 end: "}".into(),
4730 close: true,
4731 ..Default::default()
4732 },
4733 BracketPair {
4734 start: "(".into(),
4735 end: ")".into(),
4736 close: true,
4737 ..Default::default()
4738 },
4739 ],
4740 ..Default::default()
4741 },
4742 autoclose_before: "})]>".into(),
4743 ..Default::default()
4744 },
4745 Some(tree_sitter_typescript::language_tsx()),
4746 ));
4747
4748 cx.language_registry().add(html_language.clone());
4749 cx.language_registry().add(javascript_language.clone());
4750
4751 cx.update_buffer(|buffer, cx| {
4752 buffer.set_language(Some(html_language), cx);
4753 });
4754
4755 cx.set_state(
4756 &r#"
4757 <body>ˇ
4758 <script>
4759 var x = 1;ˇ
4760 </script>
4761 </body>ˇ
4762 "#
4763 .unindent(),
4764 );
4765
4766 // Precondition: different languages are active at different locations.
4767 cx.update_editor(|editor, cx| {
4768 let snapshot = editor.snapshot(cx);
4769 let cursors = editor.selections.ranges::<usize>(cx);
4770 let languages = cursors
4771 .iter()
4772 .map(|c| snapshot.language_at(c.start).unwrap().name())
4773 .collect::<Vec<_>>();
4774 assert_eq!(
4775 languages,
4776 &["HTML".into(), "JavaScript".into(), "HTML".into()]
4777 );
4778 });
4779
4780 // Angle brackets autoclose in HTML, but not JavaScript.
4781 cx.update_editor(|editor, cx| {
4782 editor.handle_input("<", cx);
4783 editor.handle_input("a", cx);
4784 });
4785 cx.assert_editor_state(
4786 &r#"
4787 <body><aˇ>
4788 <script>
4789 var x = 1;<aˇ
4790 </script>
4791 </body><aˇ>
4792 "#
4793 .unindent(),
4794 );
4795
4796 // Curly braces and parens autoclose in both HTML and JavaScript.
4797 cx.update_editor(|editor, cx| {
4798 editor.handle_input(" b=", cx);
4799 editor.handle_input("{", cx);
4800 editor.handle_input("c", cx);
4801 editor.handle_input("(", cx);
4802 });
4803 cx.assert_editor_state(
4804 &r#"
4805 <body><a b={c(ˇ)}>
4806 <script>
4807 var x = 1;<a b={c(ˇ)}
4808 </script>
4809 </body><a b={c(ˇ)}>
4810 "#
4811 .unindent(),
4812 );
4813
4814 // Brackets that were already autoclosed are skipped.
4815 cx.update_editor(|editor, cx| {
4816 editor.handle_input(")", cx);
4817 editor.handle_input("d", cx);
4818 editor.handle_input("}", cx);
4819 });
4820 cx.assert_editor_state(
4821 &r#"
4822 <body><a b={c()d}ˇ>
4823 <script>
4824 var x = 1;<a b={c()d}ˇ
4825 </script>
4826 </body><a b={c()d}ˇ>
4827 "#
4828 .unindent(),
4829 );
4830 cx.update_editor(|editor, cx| {
4831 editor.handle_input(">", cx);
4832 });
4833 cx.assert_editor_state(
4834 &r#"
4835 <body><a b={c()d}>ˇ
4836 <script>
4837 var x = 1;<a b={c()d}>ˇ
4838 </script>
4839 </body><a b={c()d}>ˇ
4840 "#
4841 .unindent(),
4842 );
4843
4844 // Reset
4845 cx.set_state(
4846 &r#"
4847 <body>ˇ
4848 <script>
4849 var x = 1;ˇ
4850 </script>
4851 </body>ˇ
4852 "#
4853 .unindent(),
4854 );
4855
4856 cx.update_editor(|editor, cx| {
4857 editor.handle_input("<", cx);
4858 });
4859 cx.assert_editor_state(
4860 &r#"
4861 <body><ˇ>
4862 <script>
4863 var x = 1;<ˇ
4864 </script>
4865 </body><ˇ>
4866 "#
4867 .unindent(),
4868 );
4869
4870 // When backspacing, the closing angle brackets are removed.
4871 cx.update_editor(|editor, cx| {
4872 editor.backspace(&Backspace, cx);
4873 });
4874 cx.assert_editor_state(
4875 &r#"
4876 <body>ˇ
4877 <script>
4878 var x = 1;ˇ
4879 </script>
4880 </body>ˇ
4881 "#
4882 .unindent(),
4883 );
4884
4885 // Block comments autoclose in JavaScript, but not HTML.
4886 cx.update_editor(|editor, cx| {
4887 editor.handle_input("/", cx);
4888 editor.handle_input("*", cx);
4889 });
4890 cx.assert_editor_state(
4891 &r#"
4892 <body>/*ˇ
4893 <script>
4894 var x = 1;/*ˇ */
4895 </script>
4896 </body>/*ˇ
4897 "#
4898 .unindent(),
4899 );
4900}
4901
4902#[gpui::test]
4903async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
4904 init_test(cx, |_| {});
4905
4906 let mut cx = EditorTestContext::new(cx).await;
4907
4908 let rust_language = Arc::new(
4909 Language::new(
4910 LanguageConfig {
4911 name: "Rust".into(),
4912 brackets: serde_json::from_value(json!([
4913 { "start": "{", "end": "}", "close": true, "newline": true },
4914 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
4915 ]))
4916 .unwrap(),
4917 autoclose_before: "})]>".into(),
4918 ..Default::default()
4919 },
4920 Some(tree_sitter_rust::language()),
4921 )
4922 .with_override_query("(string_literal) @string")
4923 .unwrap(),
4924 );
4925
4926 cx.language_registry().add(rust_language.clone());
4927 cx.update_buffer(|buffer, cx| {
4928 buffer.set_language(Some(rust_language), cx);
4929 });
4930
4931 cx.set_state(
4932 &r#"
4933 let x = ˇ
4934 "#
4935 .unindent(),
4936 );
4937
4938 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
4939 cx.update_editor(|editor, cx| {
4940 editor.handle_input("\"", cx);
4941 });
4942 cx.assert_editor_state(
4943 &r#"
4944 let x = "ˇ"
4945 "#
4946 .unindent(),
4947 );
4948
4949 // Inserting another quotation mark. The cursor moves across the existing
4950 // automatically-inserted quotation mark.
4951 cx.update_editor(|editor, cx| {
4952 editor.handle_input("\"", cx);
4953 });
4954 cx.assert_editor_state(
4955 &r#"
4956 let x = ""ˇ
4957 "#
4958 .unindent(),
4959 );
4960
4961 // Reset
4962 cx.set_state(
4963 &r#"
4964 let x = ˇ
4965 "#
4966 .unindent(),
4967 );
4968
4969 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
4970 cx.update_editor(|editor, cx| {
4971 editor.handle_input("\"", cx);
4972 editor.handle_input(" ", cx);
4973 editor.move_left(&Default::default(), cx);
4974 editor.handle_input("\\", cx);
4975 editor.handle_input("\"", cx);
4976 });
4977 cx.assert_editor_state(
4978 &r#"
4979 let x = "\"ˇ "
4980 "#
4981 .unindent(),
4982 );
4983
4984 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
4985 // mark. Nothing is inserted.
4986 cx.update_editor(|editor, cx| {
4987 editor.move_right(&Default::default(), cx);
4988 editor.handle_input("\"", cx);
4989 });
4990 cx.assert_editor_state(
4991 &r#"
4992 let x = "\" "ˇ
4993 "#
4994 .unindent(),
4995 );
4996}
4997
4998#[gpui::test]
4999async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
5000 init_test(cx, |_| {});
5001
5002 let language = Arc::new(Language::new(
5003 LanguageConfig {
5004 brackets: BracketPairConfig {
5005 pairs: vec![
5006 BracketPair {
5007 start: "{".to_string(),
5008 end: "}".to_string(),
5009 close: true,
5010 newline: true,
5011 },
5012 BracketPair {
5013 start: "/* ".to_string(),
5014 end: "*/".to_string(),
5015 close: true,
5016 ..Default::default()
5017 },
5018 ],
5019 ..Default::default()
5020 },
5021 ..Default::default()
5022 },
5023 Some(tree_sitter_rust::language()),
5024 ));
5025
5026 let text = r#"
5027 a
5028 b
5029 c
5030 "#
5031 .unindent();
5032
5033 let buffer = cx.new_model(|cx| {
5034 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
5035 .with_language(language, cx)
5036 });
5037 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5038 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5039 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5040 .await;
5041
5042 _ = view.update(cx, |view, cx| {
5043 view.change_selections(None, cx, |s| {
5044 s.select_display_ranges([
5045 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5046 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5047 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
5048 ])
5049 });
5050
5051 view.handle_input("{", cx);
5052 view.handle_input("{", cx);
5053 view.handle_input("{", cx);
5054 assert_eq!(
5055 view.text(cx),
5056 "
5057 {{{a}}}
5058 {{{b}}}
5059 {{{c}}}
5060 "
5061 .unindent()
5062 );
5063 assert_eq!(
5064 view.selections.display_ranges(cx),
5065 [
5066 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
5067 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
5068 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
5069 ]
5070 );
5071
5072 view.undo(&Undo, cx);
5073 view.undo(&Undo, cx);
5074 view.undo(&Undo, cx);
5075 assert_eq!(
5076 view.text(cx),
5077 "
5078 a
5079 b
5080 c
5081 "
5082 .unindent()
5083 );
5084 assert_eq!(
5085 view.selections.display_ranges(cx),
5086 [
5087 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5088 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5089 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5090 ]
5091 );
5092
5093 // Ensure inserting the first character of a multi-byte bracket pair
5094 // doesn't surround the selections with the bracket.
5095 view.handle_input("/", cx);
5096 assert_eq!(
5097 view.text(cx),
5098 "
5099 /
5100 /
5101 /
5102 "
5103 .unindent()
5104 );
5105 assert_eq!(
5106 view.selections.display_ranges(cx),
5107 [
5108 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5109 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5110 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5111 ]
5112 );
5113
5114 view.undo(&Undo, cx);
5115 assert_eq!(
5116 view.text(cx),
5117 "
5118 a
5119 b
5120 c
5121 "
5122 .unindent()
5123 );
5124 assert_eq!(
5125 view.selections.display_ranges(cx),
5126 [
5127 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5128 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5129 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5130 ]
5131 );
5132
5133 // Ensure inserting the last character of a multi-byte bracket pair
5134 // doesn't surround the selections with the bracket.
5135 view.handle_input("*", cx);
5136 assert_eq!(
5137 view.text(cx),
5138 "
5139 *
5140 *
5141 *
5142 "
5143 .unindent()
5144 );
5145 assert_eq!(
5146 view.selections.display_ranges(cx),
5147 [
5148 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5149 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5150 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5151 ]
5152 );
5153 });
5154}
5155
5156#[gpui::test]
5157async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
5158 init_test(cx, |_| {});
5159
5160 let language = Arc::new(Language::new(
5161 LanguageConfig {
5162 brackets: BracketPairConfig {
5163 pairs: vec![BracketPair {
5164 start: "{".to_string(),
5165 end: "}".to_string(),
5166 close: true,
5167 newline: true,
5168 }],
5169 ..Default::default()
5170 },
5171 autoclose_before: "}".to_string(),
5172 ..Default::default()
5173 },
5174 Some(tree_sitter_rust::language()),
5175 ));
5176
5177 let text = r#"
5178 a
5179 b
5180 c
5181 "#
5182 .unindent();
5183
5184 let buffer = cx.new_model(|cx| {
5185 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
5186 .with_language(language, cx)
5187 });
5188 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5189 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5190 editor
5191 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5192 .await;
5193
5194 _ = editor.update(cx, |editor, cx| {
5195 editor.change_selections(None, cx, |s| {
5196 s.select_ranges([
5197 Point::new(0, 1)..Point::new(0, 1),
5198 Point::new(1, 1)..Point::new(1, 1),
5199 Point::new(2, 1)..Point::new(2, 1),
5200 ])
5201 });
5202
5203 editor.handle_input("{", cx);
5204 editor.handle_input("{", cx);
5205 editor.handle_input("_", cx);
5206 assert_eq!(
5207 editor.text(cx),
5208 "
5209 a{{_}}
5210 b{{_}}
5211 c{{_}}
5212 "
5213 .unindent()
5214 );
5215 assert_eq!(
5216 editor.selections.ranges::<Point>(cx),
5217 [
5218 Point::new(0, 4)..Point::new(0, 4),
5219 Point::new(1, 4)..Point::new(1, 4),
5220 Point::new(2, 4)..Point::new(2, 4)
5221 ]
5222 );
5223
5224 editor.backspace(&Default::default(), cx);
5225 editor.backspace(&Default::default(), cx);
5226 assert_eq!(
5227 editor.text(cx),
5228 "
5229 a{}
5230 b{}
5231 c{}
5232 "
5233 .unindent()
5234 );
5235 assert_eq!(
5236 editor.selections.ranges::<Point>(cx),
5237 [
5238 Point::new(0, 2)..Point::new(0, 2),
5239 Point::new(1, 2)..Point::new(1, 2),
5240 Point::new(2, 2)..Point::new(2, 2)
5241 ]
5242 );
5243
5244 editor.delete_to_previous_word_start(&Default::default(), cx);
5245 assert_eq!(
5246 editor.text(cx),
5247 "
5248 a
5249 b
5250 c
5251 "
5252 .unindent()
5253 );
5254 assert_eq!(
5255 editor.selections.ranges::<Point>(cx),
5256 [
5257 Point::new(0, 1)..Point::new(0, 1),
5258 Point::new(1, 1)..Point::new(1, 1),
5259 Point::new(2, 1)..Point::new(2, 1)
5260 ]
5261 );
5262 });
5263}
5264
5265#[gpui::test]
5266async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
5267 init_test(cx, |settings| {
5268 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
5269 });
5270
5271 let mut cx = EditorTestContext::new(cx).await;
5272
5273 let language = Arc::new(Language::new(
5274 LanguageConfig {
5275 brackets: BracketPairConfig {
5276 pairs: vec![
5277 BracketPair {
5278 start: "{".to_string(),
5279 end: "}".to_string(),
5280 close: true,
5281 newline: true,
5282 },
5283 BracketPair {
5284 start: "(".to_string(),
5285 end: ")".to_string(),
5286 close: true,
5287 newline: true,
5288 },
5289 BracketPair {
5290 start: "[".to_string(),
5291 end: "]".to_string(),
5292 close: false,
5293 newline: true,
5294 },
5295 ],
5296 ..Default::default()
5297 },
5298 autoclose_before: "})]".to_string(),
5299 ..Default::default()
5300 },
5301 Some(tree_sitter_rust::language()),
5302 ));
5303
5304 cx.language_registry().add(language.clone());
5305 cx.update_buffer(|buffer, cx| {
5306 buffer.set_language(Some(language), cx);
5307 });
5308
5309 cx.set_state(
5310 &"
5311 {(ˇ)}
5312 [[ˇ]]
5313 {(ˇ)}
5314 "
5315 .unindent(),
5316 );
5317
5318 cx.update_editor(|view, cx| {
5319 view.backspace(&Default::default(), cx);
5320 view.backspace(&Default::default(), cx);
5321 });
5322
5323 cx.assert_editor_state(
5324 &"
5325 ˇ
5326 ˇ]]
5327 ˇ
5328 "
5329 .unindent(),
5330 );
5331
5332 cx.update_editor(|view, cx| {
5333 view.handle_input("{", cx);
5334 view.handle_input("{", cx);
5335 view.move_right(&MoveRight, cx);
5336 view.move_right(&MoveRight, cx);
5337 view.move_left(&MoveLeft, cx);
5338 view.move_left(&MoveLeft, cx);
5339 view.backspace(&Default::default(), cx);
5340 });
5341
5342 cx.assert_editor_state(
5343 &"
5344 {ˇ}
5345 {ˇ}]]
5346 {ˇ}
5347 "
5348 .unindent(),
5349 );
5350
5351 cx.update_editor(|view, cx| {
5352 view.backspace(&Default::default(), cx);
5353 });
5354
5355 cx.assert_editor_state(
5356 &"
5357 ˇ
5358 ˇ]]
5359 ˇ
5360 "
5361 .unindent(),
5362 );
5363}
5364
5365#[gpui::test]
5366async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let language = Arc::new(Language::new(
5370 LanguageConfig::default(),
5371 Some(tree_sitter_rust::language()),
5372 ));
5373
5374 let buffer = cx.new_model(|cx| {
5375 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
5376 .with_language(language, cx)
5377 });
5378 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5379 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5380 editor
5381 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5382 .await;
5383
5384 _ = editor.update(cx, |editor, cx| {
5385 editor.set_auto_replace_emoji_shortcode(true);
5386
5387 editor.handle_input("Hello ", cx);
5388 editor.handle_input(":wave", cx);
5389 assert_eq!(editor.text(cx), "Hello :wave".unindent());
5390
5391 editor.handle_input(":", cx);
5392 assert_eq!(editor.text(cx), "Hello 👋".unindent());
5393
5394 editor.handle_input(" :smile", cx);
5395 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
5396
5397 editor.handle_input(":", cx);
5398 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
5399
5400 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
5401 editor.handle_input(":wave", cx);
5402 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
5403
5404 editor.handle_input(":", cx);
5405 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
5406
5407 editor.handle_input(":1", cx);
5408 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
5409
5410 editor.handle_input(":", cx);
5411 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
5412
5413 // Ensure shortcode does not get replaced when it is part of a word
5414 editor.handle_input(" Test:wave", cx);
5415 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
5416
5417 editor.handle_input(":", cx);
5418 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
5419
5420 editor.set_auto_replace_emoji_shortcode(false);
5421
5422 // Ensure shortcode does not get replaced when auto replace is off
5423 editor.handle_input(" :wave", cx);
5424 assert_eq!(
5425 editor.text(cx),
5426 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
5427 );
5428
5429 editor.handle_input(":", cx);
5430 assert_eq!(
5431 editor.text(cx),
5432 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
5433 );
5434 });
5435}
5436
5437#[gpui::test]
5438async fn test_snippets(cx: &mut gpui::TestAppContext) {
5439 init_test(cx, |_| {});
5440
5441 let (text, insertion_ranges) = marked_text_ranges(
5442 indoc! {"
5443 a.ˇ b
5444 a.ˇ b
5445 a.ˇ b
5446 "},
5447 false,
5448 );
5449
5450 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
5451 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5452
5453 _ = editor.update(cx, |editor, cx| {
5454 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
5455
5456 editor
5457 .insert_snippet(&insertion_ranges, snippet, cx)
5458 .unwrap();
5459
5460 fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
5461 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
5462 assert_eq!(editor.text(cx), expected_text);
5463 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
5464 }
5465
5466 assert(
5467 editor,
5468 cx,
5469 indoc! {"
5470 a.f(«one», two, «three») b
5471 a.f(«one», two, «three») b
5472 a.f(«one», two, «three») b
5473 "},
5474 );
5475
5476 // Can't move earlier than the first tab stop
5477 assert!(!editor.move_to_prev_snippet_tabstop(cx));
5478 assert(
5479 editor,
5480 cx,
5481 indoc! {"
5482 a.f(«one», two, «three») b
5483 a.f(«one», two, «three») b
5484 a.f(«one», two, «three») b
5485 "},
5486 );
5487
5488 assert!(editor.move_to_next_snippet_tabstop(cx));
5489 assert(
5490 editor,
5491 cx,
5492 indoc! {"
5493 a.f(one, «two», three) b
5494 a.f(one, «two», three) b
5495 a.f(one, «two», three) b
5496 "},
5497 );
5498
5499 editor.move_to_prev_snippet_tabstop(cx);
5500 assert(
5501 editor,
5502 cx,
5503 indoc! {"
5504 a.f(«one», two, «three») b
5505 a.f(«one», two, «three») b
5506 a.f(«one», two, «three») b
5507 "},
5508 );
5509
5510 assert!(editor.move_to_next_snippet_tabstop(cx));
5511 assert(
5512 editor,
5513 cx,
5514 indoc! {"
5515 a.f(one, «two», three) b
5516 a.f(one, «two», three) b
5517 a.f(one, «two», three) b
5518 "},
5519 );
5520 assert!(editor.move_to_next_snippet_tabstop(cx));
5521 assert(
5522 editor,
5523 cx,
5524 indoc! {"
5525 a.f(one, two, three)ˇ b
5526 a.f(one, two, three)ˇ b
5527 a.f(one, two, three)ˇ b
5528 "},
5529 );
5530
5531 // As soon as the last tab stop is reached, snippet state is gone
5532 editor.move_to_prev_snippet_tabstop(cx);
5533 assert(
5534 editor,
5535 cx,
5536 indoc! {"
5537 a.f(one, two, three)ˇ b
5538 a.f(one, two, three)ˇ b
5539 a.f(one, two, three)ˇ b
5540 "},
5541 );
5542 });
5543}
5544
5545#[gpui::test]
5546async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
5547 init_test(cx, |_| {});
5548
5549 let fs = FakeFs::new(cx.executor());
5550 fs.insert_file("/file.rs", Default::default()).await;
5551
5552 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5553
5554 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5555 language_registry.add(rust_lang());
5556 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5557 "Rust",
5558 FakeLspAdapter {
5559 capabilities: lsp::ServerCapabilities {
5560 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5561 ..Default::default()
5562 },
5563 ..Default::default()
5564 },
5565 );
5566
5567 let buffer = project
5568 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5569 .await
5570 .unwrap();
5571
5572 cx.executor().start_waiting();
5573 let fake_server = fake_servers.next().await.unwrap();
5574
5575 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5576 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5577 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5578 assert!(cx.read(|cx| editor.is_dirty(cx)));
5579
5580 let save = editor
5581 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5582 .unwrap();
5583 fake_server
5584 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5585 assert_eq!(
5586 params.text_document.uri,
5587 lsp::Url::from_file_path("/file.rs").unwrap()
5588 );
5589 assert_eq!(params.options.tab_size, 4);
5590 Ok(Some(vec![lsp::TextEdit::new(
5591 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5592 ", ".to_string(),
5593 )]))
5594 })
5595 .next()
5596 .await;
5597 cx.executor().start_waiting();
5598 let _x = save.await;
5599
5600 assert_eq!(
5601 editor.update(cx, |editor, cx| editor.text(cx)),
5602 "one, two\nthree\n"
5603 );
5604 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5605
5606 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5607 assert!(cx.read(|cx| editor.is_dirty(cx)));
5608
5609 // Ensure we can still save even if formatting hangs.
5610 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5611 assert_eq!(
5612 params.text_document.uri,
5613 lsp::Url::from_file_path("/file.rs").unwrap()
5614 );
5615 futures::future::pending::<()>().await;
5616 unreachable!()
5617 });
5618 let save = editor
5619 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5620 .unwrap();
5621 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5622 cx.executor().start_waiting();
5623 save.await;
5624 assert_eq!(
5625 editor.update(cx, |editor, cx| editor.text(cx)),
5626 "one\ntwo\nthree\n"
5627 );
5628 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5629
5630 // Set rust language override and assert overridden tabsize is sent to language server
5631 update_test_language_settings(cx, |settings| {
5632 settings.languages.insert(
5633 "Rust".into(),
5634 LanguageSettingsContent {
5635 tab_size: NonZeroU32::new(8),
5636 ..Default::default()
5637 },
5638 );
5639 });
5640
5641 let save = editor
5642 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5643 .unwrap();
5644 fake_server
5645 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5646 assert_eq!(
5647 params.text_document.uri,
5648 lsp::Url::from_file_path("/file.rs").unwrap()
5649 );
5650 assert_eq!(params.options.tab_size, 8);
5651 Ok(Some(vec![]))
5652 })
5653 .next()
5654 .await;
5655 cx.executor().start_waiting();
5656 save.await;
5657}
5658
5659#[gpui::test]
5660async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
5661 init_test(cx, |_| {});
5662
5663 let fs = FakeFs::new(cx.executor());
5664 fs.insert_file("/file.rs", Default::default()).await;
5665
5666 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5667
5668 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5669 language_registry.add(rust_lang());
5670 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5671 "Rust",
5672 FakeLspAdapter {
5673 capabilities: lsp::ServerCapabilities {
5674 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
5675 ..Default::default()
5676 },
5677 ..Default::default()
5678 },
5679 );
5680
5681 let buffer = project
5682 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5683 .await
5684 .unwrap();
5685
5686 cx.executor().start_waiting();
5687 let fake_server = fake_servers.next().await.unwrap();
5688
5689 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5690 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5691 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5692 assert!(cx.read(|cx| editor.is_dirty(cx)));
5693
5694 let save = editor
5695 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5696 .unwrap();
5697 fake_server
5698 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5699 assert_eq!(
5700 params.text_document.uri,
5701 lsp::Url::from_file_path("/file.rs").unwrap()
5702 );
5703 assert_eq!(params.options.tab_size, 4);
5704 Ok(Some(vec![lsp::TextEdit::new(
5705 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5706 ", ".to_string(),
5707 )]))
5708 })
5709 .next()
5710 .await;
5711 cx.executor().start_waiting();
5712 save.await;
5713 assert_eq!(
5714 editor.update(cx, |editor, cx| editor.text(cx)),
5715 "one, two\nthree\n"
5716 );
5717 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5718
5719 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5720 assert!(cx.read(|cx| editor.is_dirty(cx)));
5721
5722 // Ensure we can still save even if formatting hangs.
5723 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
5724 move |params, _| async move {
5725 assert_eq!(
5726 params.text_document.uri,
5727 lsp::Url::from_file_path("/file.rs").unwrap()
5728 );
5729 futures::future::pending::<()>().await;
5730 unreachable!()
5731 },
5732 );
5733 let save = editor
5734 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5735 .unwrap();
5736 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5737 cx.executor().start_waiting();
5738 save.await;
5739 assert_eq!(
5740 editor.update(cx, |editor, cx| editor.text(cx)),
5741 "one\ntwo\nthree\n"
5742 );
5743 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5744
5745 // Set rust language override and assert overridden tabsize is sent to language server
5746 update_test_language_settings(cx, |settings| {
5747 settings.languages.insert(
5748 "Rust".into(),
5749 LanguageSettingsContent {
5750 tab_size: NonZeroU32::new(8),
5751 ..Default::default()
5752 },
5753 );
5754 });
5755
5756 let save = editor
5757 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5758 .unwrap();
5759 fake_server
5760 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5761 assert_eq!(
5762 params.text_document.uri,
5763 lsp::Url::from_file_path("/file.rs").unwrap()
5764 );
5765 assert_eq!(params.options.tab_size, 8);
5766 Ok(Some(vec![]))
5767 })
5768 .next()
5769 .await;
5770 cx.executor().start_waiting();
5771 save.await;
5772}
5773
5774#[gpui::test]
5775async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
5776 init_test(cx, |settings| {
5777 settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
5778 });
5779
5780 let fs = FakeFs::new(cx.executor());
5781 fs.insert_file("/file.rs", Default::default()).await;
5782
5783 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5784
5785 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5786 language_registry.add(Arc::new(Language::new(
5787 LanguageConfig {
5788 name: "Rust".into(),
5789 matcher: LanguageMatcher {
5790 path_suffixes: vec!["rs".to_string()],
5791 ..Default::default()
5792 },
5793 // Enable Prettier formatting for the same buffer, and ensure
5794 // LSP is called instead of Prettier.
5795 prettier_parser_name: Some("test_parser".to_string()),
5796 ..Default::default()
5797 },
5798 Some(tree_sitter_rust::language()),
5799 )));
5800 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5801 "Rust",
5802 FakeLspAdapter {
5803 capabilities: lsp::ServerCapabilities {
5804 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5805 ..Default::default()
5806 },
5807 ..Default::default()
5808 },
5809 );
5810
5811 let buffer = project
5812 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5813 .await
5814 .unwrap();
5815
5816 cx.executor().start_waiting();
5817 let fake_server = fake_servers.next().await.unwrap();
5818
5819 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5820 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5821 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5822
5823 let format = editor
5824 .update(cx, |editor, cx| {
5825 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
5826 })
5827 .unwrap();
5828 fake_server
5829 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5830 assert_eq!(
5831 params.text_document.uri,
5832 lsp::Url::from_file_path("/file.rs").unwrap()
5833 );
5834 assert_eq!(params.options.tab_size, 4);
5835 Ok(Some(vec![lsp::TextEdit::new(
5836 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5837 ", ".to_string(),
5838 )]))
5839 })
5840 .next()
5841 .await;
5842 cx.executor().start_waiting();
5843 format.await;
5844 assert_eq!(
5845 editor.update(cx, |editor, cx| editor.text(cx)),
5846 "one, two\nthree\n"
5847 );
5848
5849 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5850 // Ensure we don't lock if formatting hangs.
5851 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5852 assert_eq!(
5853 params.text_document.uri,
5854 lsp::Url::from_file_path("/file.rs").unwrap()
5855 );
5856 futures::future::pending::<()>().await;
5857 unreachable!()
5858 });
5859 let format = editor
5860 .update(cx, |editor, cx| {
5861 editor.perform_format(project, FormatTrigger::Manual, cx)
5862 })
5863 .unwrap();
5864 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5865 cx.executor().start_waiting();
5866 format.await;
5867 assert_eq!(
5868 editor.update(cx, |editor, cx| editor.text(cx)),
5869 "one\ntwo\nthree\n"
5870 );
5871}
5872
5873#[gpui::test]
5874async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
5875 init_test(cx, |_| {});
5876
5877 let mut cx = EditorLspTestContext::new_rust(
5878 lsp::ServerCapabilities {
5879 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5880 ..Default::default()
5881 },
5882 cx,
5883 )
5884 .await;
5885
5886 cx.set_state(indoc! {"
5887 one.twoˇ
5888 "});
5889
5890 // The format request takes a long time. When it completes, it inserts
5891 // a newline and an indent before the `.`
5892 cx.lsp
5893 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
5894 let executor = cx.background_executor().clone();
5895 async move {
5896 executor.timer(Duration::from_millis(100)).await;
5897 Ok(Some(vec![lsp::TextEdit {
5898 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
5899 new_text: "\n ".into(),
5900 }]))
5901 }
5902 });
5903
5904 // Submit a format request.
5905 let format_1 = cx
5906 .update_editor(|editor, cx| editor.format(&Format, cx))
5907 .unwrap();
5908 cx.executor().run_until_parked();
5909
5910 // Submit a second format request.
5911 let format_2 = cx
5912 .update_editor(|editor, cx| editor.format(&Format, cx))
5913 .unwrap();
5914 cx.executor().run_until_parked();
5915
5916 // Wait for both format requests to complete
5917 cx.executor().advance_clock(Duration::from_millis(200));
5918 cx.executor().start_waiting();
5919 format_1.await.unwrap();
5920 cx.executor().start_waiting();
5921 format_2.await.unwrap();
5922
5923 // The formatting edits only happens once.
5924 cx.assert_editor_state(indoc! {"
5925 one
5926 .twoˇ
5927 "});
5928}
5929
5930#[gpui::test]
5931async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
5932 init_test(cx, |settings| {
5933 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
5934 });
5935
5936 let mut cx = EditorLspTestContext::new_rust(
5937 lsp::ServerCapabilities {
5938 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5939 ..Default::default()
5940 },
5941 cx,
5942 )
5943 .await;
5944
5945 // Set up a buffer white some trailing whitespace and no trailing newline.
5946 cx.set_state(
5947 &[
5948 "one ", //
5949 "twoˇ", //
5950 "three ", //
5951 "four", //
5952 ]
5953 .join("\n"),
5954 );
5955
5956 // Submit a format request.
5957 let format = cx
5958 .update_editor(|editor, cx| editor.format(&Format, cx))
5959 .unwrap();
5960
5961 // Record which buffer changes have been sent to the language server
5962 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
5963 cx.lsp
5964 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
5965 let buffer_changes = buffer_changes.clone();
5966 move |params, _| {
5967 buffer_changes.lock().extend(
5968 params
5969 .content_changes
5970 .into_iter()
5971 .map(|e| (e.range.unwrap(), e.text)),
5972 );
5973 }
5974 });
5975
5976 // Handle formatting requests to the language server.
5977 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
5978 let buffer_changes = buffer_changes.clone();
5979 move |_, _| {
5980 // When formatting is requested, trailing whitespace has already been stripped,
5981 // and the trailing newline has already been added.
5982 assert_eq!(
5983 &buffer_changes.lock()[1..],
5984 &[
5985 (
5986 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
5987 "".into()
5988 ),
5989 (
5990 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
5991 "".into()
5992 ),
5993 (
5994 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
5995 "\n".into()
5996 ),
5997 ]
5998 );
5999
6000 // Insert blank lines between each line of the buffer.
6001 async move {
6002 Ok(Some(vec![
6003 lsp::TextEdit {
6004 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
6005 new_text: "\n".into(),
6006 },
6007 lsp::TextEdit {
6008 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
6009 new_text: "\n".into(),
6010 },
6011 ]))
6012 }
6013 }
6014 });
6015
6016 // After formatting the buffer, the trailing whitespace is stripped,
6017 // a newline is appended, and the edits provided by the language server
6018 // have been applied.
6019 format.await.unwrap();
6020 cx.assert_editor_state(
6021 &[
6022 "one", //
6023 "", //
6024 "twoˇ", //
6025 "", //
6026 "three", //
6027 "four", //
6028 "", //
6029 ]
6030 .join("\n"),
6031 );
6032
6033 // Undoing the formatting undoes the trailing whitespace removal, the
6034 // trailing newline, and the LSP edits.
6035 cx.update_buffer(|buffer, cx| buffer.undo(cx));
6036 cx.assert_editor_state(
6037 &[
6038 "one ", //
6039 "twoˇ", //
6040 "three ", //
6041 "four", //
6042 ]
6043 .join("\n"),
6044 );
6045}
6046
6047#[gpui::test]
6048async fn test_completion(cx: &mut gpui::TestAppContext) {
6049 init_test(cx, |_| {});
6050
6051 let mut cx = EditorLspTestContext::new_rust(
6052 lsp::ServerCapabilities {
6053 completion_provider: Some(lsp::CompletionOptions {
6054 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
6055 resolve_provider: Some(true),
6056 ..Default::default()
6057 }),
6058 ..Default::default()
6059 },
6060 cx,
6061 )
6062 .await;
6063
6064 cx.set_state(indoc! {"
6065 oneˇ
6066 two
6067 three
6068 "});
6069 cx.simulate_keystroke(".");
6070 handle_completion_request(
6071 &mut cx,
6072 indoc! {"
6073 one.|<>
6074 two
6075 three
6076 "},
6077 vec!["first_completion", "second_completion"],
6078 )
6079 .await;
6080 cx.condition(|editor, _| editor.context_menu_visible())
6081 .await;
6082 let apply_additional_edits = cx.update_editor(|editor, cx| {
6083 editor.context_menu_next(&Default::default(), cx);
6084 editor
6085 .confirm_completion(&ConfirmCompletion::default(), cx)
6086 .unwrap()
6087 });
6088 cx.assert_editor_state(indoc! {"
6089 one.second_completionˇ
6090 two
6091 three
6092 "});
6093
6094 handle_resolve_completion_request(
6095 &mut cx,
6096 Some(vec![
6097 (
6098 //This overlaps with the primary completion edit which is
6099 //misbehavior from the LSP spec, test that we filter it out
6100 indoc! {"
6101 one.second_ˇcompletion
6102 two
6103 threeˇ
6104 "},
6105 "overlapping additional edit",
6106 ),
6107 (
6108 indoc! {"
6109 one.second_completion
6110 two
6111 threeˇ
6112 "},
6113 "\nadditional edit",
6114 ),
6115 ]),
6116 )
6117 .await;
6118 apply_additional_edits.await.unwrap();
6119 cx.assert_editor_state(indoc! {"
6120 one.second_completionˇ
6121 two
6122 three
6123 additional edit
6124 "});
6125
6126 cx.set_state(indoc! {"
6127 one.second_completion
6128 twoˇ
6129 threeˇ
6130 additional edit
6131 "});
6132 cx.simulate_keystroke(" ");
6133 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6134 cx.simulate_keystroke("s");
6135 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6136
6137 cx.assert_editor_state(indoc! {"
6138 one.second_completion
6139 two sˇ
6140 three sˇ
6141 additional edit
6142 "});
6143 handle_completion_request(
6144 &mut cx,
6145 indoc! {"
6146 one.second_completion
6147 two s
6148 three <s|>
6149 additional edit
6150 "},
6151 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6152 )
6153 .await;
6154 cx.condition(|editor, _| editor.context_menu_visible())
6155 .await;
6156
6157 cx.simulate_keystroke("i");
6158
6159 handle_completion_request(
6160 &mut cx,
6161 indoc! {"
6162 one.second_completion
6163 two si
6164 three <si|>
6165 additional edit
6166 "},
6167 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6168 )
6169 .await;
6170 cx.condition(|editor, _| editor.context_menu_visible())
6171 .await;
6172
6173 let apply_additional_edits = cx.update_editor(|editor, cx| {
6174 editor
6175 .confirm_completion(&ConfirmCompletion::default(), cx)
6176 .unwrap()
6177 });
6178 cx.assert_editor_state(indoc! {"
6179 one.second_completion
6180 two sixth_completionˇ
6181 three sixth_completionˇ
6182 additional edit
6183 "});
6184
6185 handle_resolve_completion_request(&mut cx, None).await;
6186 apply_additional_edits.await.unwrap();
6187
6188 _ = cx.update(|cx| {
6189 cx.update_global::<SettingsStore, _>(|settings, cx| {
6190 settings.update_user_settings::<EditorSettings>(cx, |settings| {
6191 settings.show_completions_on_input = Some(false);
6192 });
6193 })
6194 });
6195 cx.set_state("editorˇ");
6196 cx.simulate_keystroke(".");
6197 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6198 cx.simulate_keystroke("c");
6199 cx.simulate_keystroke("l");
6200 cx.simulate_keystroke("o");
6201 cx.assert_editor_state("editor.cloˇ");
6202 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6203 cx.update_editor(|editor, cx| {
6204 editor.show_completions(&ShowCompletions, cx);
6205 });
6206 handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
6207 cx.condition(|editor, _| editor.context_menu_visible())
6208 .await;
6209 let apply_additional_edits = cx.update_editor(|editor, cx| {
6210 editor
6211 .confirm_completion(&ConfirmCompletion::default(), cx)
6212 .unwrap()
6213 });
6214 cx.assert_editor_state("editor.closeˇ");
6215 handle_resolve_completion_request(&mut cx, None).await;
6216 apply_additional_edits.await.unwrap();
6217}
6218
6219#[gpui::test]
6220async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
6221 init_test(cx, |_| {});
6222 let mut cx = EditorTestContext::new(cx).await;
6223 let language = Arc::new(Language::new(
6224 LanguageConfig {
6225 line_comments: vec!["// ".into()],
6226 ..Default::default()
6227 },
6228 Some(tree_sitter_rust::language()),
6229 ));
6230 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6231
6232 // If multiple selections intersect a line, the line is only toggled once.
6233 cx.set_state(indoc! {"
6234 fn a() {
6235 «//b();
6236 ˇ»// «c();
6237 //ˇ» d();
6238 }
6239 "});
6240
6241 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6242
6243 cx.assert_editor_state(indoc! {"
6244 fn a() {
6245 «b();
6246 c();
6247 ˇ» d();
6248 }
6249 "});
6250
6251 // The comment prefix is inserted at the same column for every line in a
6252 // selection.
6253 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6254
6255 cx.assert_editor_state(indoc! {"
6256 fn a() {
6257 // «b();
6258 // c();
6259 ˇ»// d();
6260 }
6261 "});
6262
6263 // If a selection ends at the beginning of a line, that line is not toggled.
6264 cx.set_selections_state(indoc! {"
6265 fn a() {
6266 // b();
6267 «// c();
6268 ˇ» // d();
6269 }
6270 "});
6271
6272 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6273
6274 cx.assert_editor_state(indoc! {"
6275 fn a() {
6276 // b();
6277 «c();
6278 ˇ» // d();
6279 }
6280 "});
6281
6282 // If a selection span a single line and is empty, the line is toggled.
6283 cx.set_state(indoc! {"
6284 fn a() {
6285 a();
6286 b();
6287 ˇ
6288 }
6289 "});
6290
6291 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6292
6293 cx.assert_editor_state(indoc! {"
6294 fn a() {
6295 a();
6296 b();
6297 //•ˇ
6298 }
6299 "});
6300
6301 // If a selection span multiple lines, empty lines are not toggled.
6302 cx.set_state(indoc! {"
6303 fn a() {
6304 «a();
6305
6306 c();ˇ»
6307 }
6308 "});
6309
6310 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6311
6312 cx.assert_editor_state(indoc! {"
6313 fn a() {
6314 // «a();
6315
6316 // c();ˇ»
6317 }
6318 "});
6319}
6320
6321#[gpui::test]
6322async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
6323 init_test(cx, |_| {});
6324
6325 let language = Arc::new(Language::new(
6326 LanguageConfig {
6327 line_comments: vec!["// ".into()],
6328 ..Default::default()
6329 },
6330 Some(tree_sitter_rust::language()),
6331 ));
6332
6333 let mut cx = EditorTestContext::new(cx).await;
6334
6335 cx.language_registry().add(language.clone());
6336 cx.update_buffer(|buffer, cx| {
6337 buffer.set_language(Some(language), cx);
6338 });
6339
6340 let toggle_comments = &ToggleComments {
6341 advance_downwards: true,
6342 };
6343
6344 // Single cursor on one line -> advance
6345 // Cursor moves horizontally 3 characters as well on non-blank line
6346 cx.set_state(indoc!(
6347 "fn a() {
6348 ˇdog();
6349 cat();
6350 }"
6351 ));
6352 cx.update_editor(|editor, cx| {
6353 editor.toggle_comments(toggle_comments, cx);
6354 });
6355 cx.assert_editor_state(indoc!(
6356 "fn a() {
6357 // dog();
6358 catˇ();
6359 }"
6360 ));
6361
6362 // Single selection on one line -> don't advance
6363 cx.set_state(indoc!(
6364 "fn a() {
6365 «dog()ˇ»;
6366 cat();
6367 }"
6368 ));
6369 cx.update_editor(|editor, cx| {
6370 editor.toggle_comments(toggle_comments, cx);
6371 });
6372 cx.assert_editor_state(indoc!(
6373 "fn a() {
6374 // «dog()ˇ»;
6375 cat();
6376 }"
6377 ));
6378
6379 // Multiple cursors on one line -> advance
6380 cx.set_state(indoc!(
6381 "fn a() {
6382 ˇdˇog();
6383 cat();
6384 }"
6385 ));
6386 cx.update_editor(|editor, cx| {
6387 editor.toggle_comments(toggle_comments, cx);
6388 });
6389 cx.assert_editor_state(indoc!(
6390 "fn a() {
6391 // dog();
6392 catˇ(ˇ);
6393 }"
6394 ));
6395
6396 // Multiple cursors on one line, with selection -> don't advance
6397 cx.set_state(indoc!(
6398 "fn a() {
6399 ˇdˇog«()ˇ»;
6400 cat();
6401 }"
6402 ));
6403 cx.update_editor(|editor, cx| {
6404 editor.toggle_comments(toggle_comments, cx);
6405 });
6406 cx.assert_editor_state(indoc!(
6407 "fn a() {
6408 // ˇdˇog«()ˇ»;
6409 cat();
6410 }"
6411 ));
6412
6413 // Single cursor on one line -> advance
6414 // Cursor moves to column 0 on blank line
6415 cx.set_state(indoc!(
6416 "fn a() {
6417 ˇdog();
6418
6419 cat();
6420 }"
6421 ));
6422 cx.update_editor(|editor, cx| {
6423 editor.toggle_comments(toggle_comments, cx);
6424 });
6425 cx.assert_editor_state(indoc!(
6426 "fn a() {
6427 // dog();
6428 ˇ
6429 cat();
6430 }"
6431 ));
6432
6433 // Single cursor on one line -> advance
6434 // Cursor starts and ends at column 0
6435 cx.set_state(indoc!(
6436 "fn a() {
6437 ˇ dog();
6438 cat();
6439 }"
6440 ));
6441 cx.update_editor(|editor, cx| {
6442 editor.toggle_comments(toggle_comments, cx);
6443 });
6444 cx.assert_editor_state(indoc!(
6445 "fn a() {
6446 // dog();
6447 ˇ cat();
6448 }"
6449 ));
6450}
6451
6452#[gpui::test]
6453async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
6454 init_test(cx, |_| {});
6455
6456 let mut cx = EditorTestContext::new(cx).await;
6457
6458 let html_language = Arc::new(
6459 Language::new(
6460 LanguageConfig {
6461 name: "HTML".into(),
6462 block_comment: Some(("<!-- ".into(), " -->".into())),
6463 ..Default::default()
6464 },
6465 Some(tree_sitter_html::language()),
6466 )
6467 .with_injection_query(
6468 r#"
6469 (script_element
6470 (raw_text) @content
6471 (#set! "language" "javascript"))
6472 "#,
6473 )
6474 .unwrap(),
6475 );
6476
6477 let javascript_language = Arc::new(Language::new(
6478 LanguageConfig {
6479 name: "JavaScript".into(),
6480 line_comments: vec!["// ".into()],
6481 ..Default::default()
6482 },
6483 Some(tree_sitter_typescript::language_tsx()),
6484 ));
6485
6486 cx.language_registry().add(html_language.clone());
6487 cx.language_registry().add(javascript_language.clone());
6488 cx.update_buffer(|buffer, cx| {
6489 buffer.set_language(Some(html_language), cx);
6490 });
6491
6492 // Toggle comments for empty selections
6493 cx.set_state(
6494 &r#"
6495 <p>A</p>ˇ
6496 <p>B</p>ˇ
6497 <p>C</p>ˇ
6498 "#
6499 .unindent(),
6500 );
6501 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6502 cx.assert_editor_state(
6503 &r#"
6504 <!-- <p>A</p>ˇ -->
6505 <!-- <p>B</p>ˇ -->
6506 <!-- <p>C</p>ˇ -->
6507 "#
6508 .unindent(),
6509 );
6510 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6511 cx.assert_editor_state(
6512 &r#"
6513 <p>A</p>ˇ
6514 <p>B</p>ˇ
6515 <p>C</p>ˇ
6516 "#
6517 .unindent(),
6518 );
6519
6520 // Toggle comments for mixture of empty and non-empty selections, where
6521 // multiple selections occupy a given line.
6522 cx.set_state(
6523 &r#"
6524 <p>A«</p>
6525 <p>ˇ»B</p>ˇ
6526 <p>C«</p>
6527 <p>ˇ»D</p>ˇ
6528 "#
6529 .unindent(),
6530 );
6531
6532 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6533 cx.assert_editor_state(
6534 &r#"
6535 <!-- <p>A«</p>
6536 <p>ˇ»B</p>ˇ -->
6537 <!-- <p>C«</p>
6538 <p>ˇ»D</p>ˇ -->
6539 "#
6540 .unindent(),
6541 );
6542 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6543 cx.assert_editor_state(
6544 &r#"
6545 <p>A«</p>
6546 <p>ˇ»B</p>ˇ
6547 <p>C«</p>
6548 <p>ˇ»D</p>ˇ
6549 "#
6550 .unindent(),
6551 );
6552
6553 // Toggle comments when different languages are active for different
6554 // selections.
6555 cx.set_state(
6556 &r#"
6557 ˇ<script>
6558 ˇvar x = new Y();
6559 ˇ</script>
6560 "#
6561 .unindent(),
6562 );
6563 cx.executor().run_until_parked();
6564 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6565 cx.assert_editor_state(
6566 &r#"
6567 <!-- ˇ<script> -->
6568 // ˇvar x = new Y();
6569 <!-- ˇ</script> -->
6570 "#
6571 .unindent(),
6572 );
6573}
6574
6575#[gpui::test]
6576fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
6577 init_test(cx, |_| {});
6578
6579 let buffer = cx.new_model(|cx| {
6580 Buffer::new(
6581 0,
6582 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6583 sample_text(3, 4, 'a'),
6584 )
6585 });
6586 let multibuffer = cx.new_model(|cx| {
6587 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6588 multibuffer.push_excerpts(
6589 buffer.clone(),
6590 [
6591 ExcerptRange {
6592 context: Point::new(0, 0)..Point::new(0, 4),
6593 primary: None,
6594 },
6595 ExcerptRange {
6596 context: Point::new(1, 0)..Point::new(1, 4),
6597 primary: None,
6598 },
6599 ],
6600 cx,
6601 );
6602 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
6603 multibuffer
6604 });
6605
6606 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6607 _ = view.update(cx, |view, cx| {
6608 assert_eq!(view.text(cx), "aaaa\nbbbb");
6609 view.change_selections(None, cx, |s| {
6610 s.select_ranges([
6611 Point::new(0, 0)..Point::new(0, 0),
6612 Point::new(1, 0)..Point::new(1, 0),
6613 ])
6614 });
6615
6616 view.handle_input("X", cx);
6617 assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
6618 assert_eq!(
6619 view.selections.ranges(cx),
6620 [
6621 Point::new(0, 1)..Point::new(0, 1),
6622 Point::new(1, 1)..Point::new(1, 1),
6623 ]
6624 );
6625
6626 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
6627 view.change_selections(None, cx, |s| {
6628 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
6629 });
6630 view.backspace(&Default::default(), cx);
6631 assert_eq!(view.text(cx), "Xa\nbbb");
6632 assert_eq!(
6633 view.selections.ranges(cx),
6634 [Point::new(1, 0)..Point::new(1, 0)]
6635 );
6636
6637 view.change_selections(None, cx, |s| {
6638 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
6639 });
6640 view.backspace(&Default::default(), cx);
6641 assert_eq!(view.text(cx), "X\nbb");
6642 assert_eq!(
6643 view.selections.ranges(cx),
6644 [Point::new(0, 1)..Point::new(0, 1)]
6645 );
6646 });
6647}
6648
6649#[gpui::test]
6650fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
6651 init_test(cx, |_| {});
6652
6653 let markers = vec![('[', ']').into(), ('(', ')').into()];
6654 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
6655 indoc! {"
6656 [aaaa
6657 (bbbb]
6658 cccc)",
6659 },
6660 markers.clone(),
6661 );
6662 let excerpt_ranges = markers.into_iter().map(|marker| {
6663 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
6664 ExcerptRange {
6665 context,
6666 primary: None,
6667 }
6668 });
6669 let buffer = cx.new_model(|cx| {
6670 Buffer::new(
6671 0,
6672 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6673 initial_text,
6674 )
6675 });
6676 let multibuffer = cx.new_model(|cx| {
6677 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6678 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
6679 multibuffer
6680 });
6681
6682 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6683 _ = view.update(cx, |view, cx| {
6684 let (expected_text, selection_ranges) = marked_text_ranges(
6685 indoc! {"
6686 aaaa
6687 bˇbbb
6688 bˇbbˇb
6689 cccc"
6690 },
6691 true,
6692 );
6693 assert_eq!(view.text(cx), expected_text);
6694 view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
6695
6696 view.handle_input("X", cx);
6697
6698 let (expected_text, expected_selections) = marked_text_ranges(
6699 indoc! {"
6700 aaaa
6701 bXˇbbXb
6702 bXˇbbXˇb
6703 cccc"
6704 },
6705 false,
6706 );
6707 assert_eq!(view.text(cx), expected_text);
6708 assert_eq!(view.selections.ranges(cx), expected_selections);
6709
6710 view.newline(&Newline, cx);
6711 let (expected_text, expected_selections) = marked_text_ranges(
6712 indoc! {"
6713 aaaa
6714 bX
6715 ˇbbX
6716 b
6717 bX
6718 ˇbbX
6719 ˇb
6720 cccc"
6721 },
6722 false,
6723 );
6724 assert_eq!(view.text(cx), expected_text);
6725 assert_eq!(view.selections.ranges(cx), expected_selections);
6726 });
6727}
6728
6729#[gpui::test]
6730fn test_refresh_selections(cx: &mut TestAppContext) {
6731 init_test(cx, |_| {});
6732
6733 let buffer = cx.new_model(|cx| {
6734 Buffer::new(
6735 0,
6736 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6737 sample_text(3, 4, 'a'),
6738 )
6739 });
6740 let mut excerpt1_id = None;
6741 let multibuffer = cx.new_model(|cx| {
6742 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6743 excerpt1_id = multibuffer
6744 .push_excerpts(
6745 buffer.clone(),
6746 [
6747 ExcerptRange {
6748 context: Point::new(0, 0)..Point::new(1, 4),
6749 primary: None,
6750 },
6751 ExcerptRange {
6752 context: Point::new(1, 0)..Point::new(2, 4),
6753 primary: None,
6754 },
6755 ],
6756 cx,
6757 )
6758 .into_iter()
6759 .next();
6760 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6761 multibuffer
6762 });
6763
6764 let editor = cx.add_window(|cx| {
6765 let mut editor = build_editor(multibuffer.clone(), cx);
6766 let snapshot = editor.snapshot(cx);
6767 editor.change_selections(None, cx, |s| {
6768 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
6769 });
6770 editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
6771 assert_eq!(
6772 editor.selections.ranges(cx),
6773 [
6774 Point::new(1, 3)..Point::new(1, 3),
6775 Point::new(2, 1)..Point::new(2, 1),
6776 ]
6777 );
6778 editor
6779 });
6780
6781 // Refreshing selections is a no-op when excerpts haven't changed.
6782 _ = editor.update(cx, |editor, cx| {
6783 editor.change_selections(None, cx, |s| s.refresh());
6784 assert_eq!(
6785 editor.selections.ranges(cx),
6786 [
6787 Point::new(1, 3)..Point::new(1, 3),
6788 Point::new(2, 1)..Point::new(2, 1),
6789 ]
6790 );
6791 });
6792
6793 _ = multibuffer.update(cx, |multibuffer, cx| {
6794 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6795 });
6796 _ = editor.update(cx, |editor, cx| {
6797 // Removing an excerpt causes the first selection to become degenerate.
6798 assert_eq!(
6799 editor.selections.ranges(cx),
6800 [
6801 Point::new(0, 0)..Point::new(0, 0),
6802 Point::new(0, 1)..Point::new(0, 1)
6803 ]
6804 );
6805
6806 // Refreshing selections will relocate the first selection to the original buffer
6807 // location.
6808 editor.change_selections(None, cx, |s| s.refresh());
6809 assert_eq!(
6810 editor.selections.ranges(cx),
6811 [
6812 Point::new(0, 1)..Point::new(0, 1),
6813 Point::new(0, 3)..Point::new(0, 3)
6814 ]
6815 );
6816 assert!(editor.selections.pending_anchor().is_some());
6817 });
6818}
6819
6820#[gpui::test]
6821fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
6822 init_test(cx, |_| {});
6823
6824 let buffer = cx.new_model(|cx| {
6825 Buffer::new(
6826 0,
6827 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6828 sample_text(3, 4, 'a'),
6829 )
6830 });
6831 let mut excerpt1_id = None;
6832 let multibuffer = cx.new_model(|cx| {
6833 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6834 excerpt1_id = multibuffer
6835 .push_excerpts(
6836 buffer.clone(),
6837 [
6838 ExcerptRange {
6839 context: Point::new(0, 0)..Point::new(1, 4),
6840 primary: None,
6841 },
6842 ExcerptRange {
6843 context: Point::new(1, 0)..Point::new(2, 4),
6844 primary: None,
6845 },
6846 ],
6847 cx,
6848 )
6849 .into_iter()
6850 .next();
6851 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6852 multibuffer
6853 });
6854
6855 let editor = cx.add_window(|cx| {
6856 let mut editor = build_editor(multibuffer.clone(), cx);
6857 let snapshot = editor.snapshot(cx);
6858 editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
6859 assert_eq!(
6860 editor.selections.ranges(cx),
6861 [Point::new(1, 3)..Point::new(1, 3)]
6862 );
6863 editor
6864 });
6865
6866 _ = multibuffer.update(cx, |multibuffer, cx| {
6867 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6868 });
6869 _ = editor.update(cx, |editor, cx| {
6870 assert_eq!(
6871 editor.selections.ranges(cx),
6872 [Point::new(0, 0)..Point::new(0, 0)]
6873 );
6874
6875 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
6876 editor.change_selections(None, cx, |s| s.refresh());
6877 assert_eq!(
6878 editor.selections.ranges(cx),
6879 [Point::new(0, 3)..Point::new(0, 3)]
6880 );
6881 assert!(editor.selections.pending_anchor().is_some());
6882 });
6883}
6884
6885#[gpui::test]
6886async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
6887 init_test(cx, |_| {});
6888
6889 let language = Arc::new(
6890 Language::new(
6891 LanguageConfig {
6892 brackets: BracketPairConfig {
6893 pairs: vec![
6894 BracketPair {
6895 start: "{".to_string(),
6896 end: "}".to_string(),
6897 close: true,
6898 newline: true,
6899 },
6900 BracketPair {
6901 start: "/* ".to_string(),
6902 end: " */".to_string(),
6903 close: true,
6904 newline: true,
6905 },
6906 ],
6907 ..Default::default()
6908 },
6909 ..Default::default()
6910 },
6911 Some(tree_sitter_rust::language()),
6912 )
6913 .with_indents_query("")
6914 .unwrap(),
6915 );
6916
6917 let text = concat!(
6918 "{ }\n", //
6919 " x\n", //
6920 " /* */\n", //
6921 "x\n", //
6922 "{{} }\n", //
6923 );
6924
6925 let buffer = cx.new_model(|cx| {
6926 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
6927 .with_language(language, cx)
6928 });
6929 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
6930 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
6931 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
6932 .await;
6933
6934 _ = view.update(cx, |view, cx| {
6935 view.change_selections(None, cx, |s| {
6936 s.select_display_ranges([
6937 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
6938 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
6939 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
6940 ])
6941 });
6942 view.newline(&Newline, cx);
6943
6944 assert_eq!(
6945 view.buffer().read(cx).read(cx).text(),
6946 concat!(
6947 "{ \n", // Suppress rustfmt
6948 "\n", //
6949 "}\n", //
6950 " x\n", //
6951 " /* \n", //
6952 " \n", //
6953 " */\n", //
6954 "x\n", //
6955 "{{} \n", //
6956 "}\n", //
6957 )
6958 );
6959 });
6960}
6961
6962#[gpui::test]
6963fn test_highlighted_ranges(cx: &mut TestAppContext) {
6964 init_test(cx, |_| {});
6965
6966 let editor = cx.add_window(|cx| {
6967 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
6968 build_editor(buffer.clone(), cx)
6969 });
6970
6971 _ = editor.update(cx, |editor, cx| {
6972 struct Type1;
6973 struct Type2;
6974
6975 let buffer = editor.buffer.read(cx).snapshot(cx);
6976
6977 let anchor_range =
6978 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
6979
6980 editor.highlight_background::<Type1>(
6981 vec![
6982 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
6983 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
6984 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
6985 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
6986 ],
6987 |_| Hsla::red(),
6988 cx,
6989 );
6990 editor.highlight_background::<Type2>(
6991 vec![
6992 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
6993 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
6994 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
6995 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
6996 ],
6997 |_| Hsla::green(),
6998 cx,
6999 );
7000
7001 let snapshot = editor.snapshot(cx);
7002 let mut highlighted_ranges = editor.background_highlights_in_range(
7003 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
7004 &snapshot,
7005 cx.theme().colors(),
7006 );
7007 // Enforce a consistent ordering based on color without relying on the ordering of the
7008 // highlight's `TypeId` which is non-executor.
7009 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
7010 assert_eq!(
7011 highlighted_ranges,
7012 &[
7013 (
7014 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
7015 Hsla::red(),
7016 ),
7017 (
7018 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7019 Hsla::red(),
7020 ),
7021 (
7022 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
7023 Hsla::green(),
7024 ),
7025 (
7026 DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
7027 Hsla::green(),
7028 ),
7029 ]
7030 );
7031 assert_eq!(
7032 editor.background_highlights_in_range(
7033 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
7034 &snapshot,
7035 cx.theme().colors(),
7036 ),
7037 &[(
7038 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7039 Hsla::red(),
7040 )]
7041 );
7042 });
7043}
7044
7045#[gpui::test]
7046async fn test_following(cx: &mut gpui::TestAppContext) {
7047 init_test(cx, |_| {});
7048
7049 let fs = FakeFs::new(cx.executor());
7050 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7051
7052 let buffer = project.update(cx, |project, cx| {
7053 let buffer = project
7054 .create_buffer(&sample_text(16, 8, 'a'), None, cx)
7055 .unwrap();
7056 cx.new_model(|cx| MultiBuffer::singleton(buffer, cx))
7057 });
7058 let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
7059 let follower = cx.update(|cx| {
7060 cx.open_window(
7061 WindowOptions {
7062 bounds: Some(Bounds::from_corners(
7063 gpui::Point::new(0_f64.into(), 0_f64.into()),
7064 gpui::Point::new(10_f64.into(), 80_f64.into()),
7065 )),
7066 ..Default::default()
7067 },
7068 |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
7069 )
7070 });
7071
7072 let is_still_following = Rc::new(RefCell::new(true));
7073 let follower_edit_event_count = Rc::new(RefCell::new(0));
7074 let pending_update = Rc::new(RefCell::new(None));
7075 _ = follower.update(cx, {
7076 let update = pending_update.clone();
7077 let is_still_following = is_still_following.clone();
7078 let follower_edit_event_count = follower_edit_event_count.clone();
7079 |_, cx| {
7080 cx.subscribe(
7081 &leader.root_view(cx).unwrap(),
7082 move |_, leader, event, cx| {
7083 leader
7084 .read(cx)
7085 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7086 },
7087 )
7088 .detach();
7089
7090 cx.subscribe(
7091 &follower.root_view(cx).unwrap(),
7092 move |_, _, event: &EditorEvent, _cx| {
7093 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
7094 *is_still_following.borrow_mut() = false;
7095 }
7096
7097 if let EditorEvent::BufferEdited = event {
7098 *follower_edit_event_count.borrow_mut() += 1;
7099 }
7100 },
7101 )
7102 .detach();
7103 }
7104 });
7105
7106 // Update the selections only
7107 _ = leader.update(cx, |leader, cx| {
7108 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7109 });
7110 follower
7111 .update(cx, |follower, cx| {
7112 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7113 })
7114 .unwrap()
7115 .await
7116 .unwrap();
7117 _ = follower.update(cx, |follower, cx| {
7118 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
7119 });
7120 assert_eq!(*is_still_following.borrow(), true);
7121 assert_eq!(*follower_edit_event_count.borrow(), 0);
7122
7123 // Update the scroll position only
7124 _ = leader.update(cx, |leader, cx| {
7125 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7126 });
7127 follower
7128 .update(cx, |follower, cx| {
7129 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7130 })
7131 .unwrap()
7132 .await
7133 .unwrap();
7134 assert_eq!(
7135 follower
7136 .update(cx, |follower, cx| follower.scroll_position(cx))
7137 .unwrap(),
7138 gpui::Point::new(1.5, 3.5)
7139 );
7140 assert_eq!(*is_still_following.borrow(), true);
7141 assert_eq!(*follower_edit_event_count.borrow(), 0);
7142
7143 // Update the selections and scroll position. The follower's scroll position is updated
7144 // via autoscroll, not via the leader's exact scroll position.
7145 _ = leader.update(cx, |leader, cx| {
7146 leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
7147 leader.request_autoscroll(Autoscroll::newest(), cx);
7148 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7149 });
7150 follower
7151 .update(cx, |follower, cx| {
7152 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7153 })
7154 .unwrap()
7155 .await
7156 .unwrap();
7157 _ = follower.update(cx, |follower, cx| {
7158 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
7159 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
7160 });
7161 assert_eq!(*is_still_following.borrow(), true);
7162
7163 // Creating a pending selection that precedes another selection
7164 _ = leader.update(cx, |leader, cx| {
7165 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7166 leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
7167 });
7168 follower
7169 .update(cx, |follower, cx| {
7170 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7171 })
7172 .unwrap()
7173 .await
7174 .unwrap();
7175 _ = follower.update(cx, |follower, cx| {
7176 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
7177 });
7178 assert_eq!(*is_still_following.borrow(), true);
7179
7180 // Extend the pending selection so that it surrounds another selection
7181 _ = leader.update(cx, |leader, cx| {
7182 leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
7183 });
7184 follower
7185 .update(cx, |follower, cx| {
7186 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7187 })
7188 .unwrap()
7189 .await
7190 .unwrap();
7191 _ = follower.update(cx, |follower, cx| {
7192 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
7193 });
7194
7195 // Scrolling locally breaks the follow
7196 _ = follower.update(cx, |follower, cx| {
7197 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
7198 follower.set_scroll_anchor(
7199 ScrollAnchor {
7200 anchor: top_anchor,
7201 offset: gpui::Point::new(0.0, 0.5),
7202 },
7203 cx,
7204 );
7205 });
7206 assert_eq!(*is_still_following.borrow(), false);
7207}
7208
7209#[gpui::test]
7210async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let fs = FakeFs::new(cx.executor());
7214 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7215 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7216 let pane = workspace
7217 .update(cx, |workspace, _| workspace.active_pane().clone())
7218 .unwrap();
7219
7220 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7221
7222 let leader = pane.update(cx, |_, cx| {
7223 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
7224 cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
7225 });
7226
7227 // Start following the editor when it has no excerpts.
7228 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7229 let follower_1 = cx
7230 .update_window(*workspace.deref(), |_, cx| {
7231 Editor::from_state_proto(
7232 pane.clone(),
7233 workspace.root_view(cx).unwrap(),
7234 ViewId {
7235 creator: Default::default(),
7236 id: 0,
7237 },
7238 &mut state_message,
7239 cx,
7240 )
7241 })
7242 .unwrap()
7243 .unwrap()
7244 .await
7245 .unwrap();
7246
7247 let update_message = Rc::new(RefCell::new(None));
7248 follower_1.update(cx, {
7249 let update = update_message.clone();
7250 |_, cx| {
7251 cx.subscribe(&leader, move |_, leader, event, cx| {
7252 leader
7253 .read(cx)
7254 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7255 })
7256 .detach();
7257 }
7258 });
7259
7260 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
7261 (
7262 project
7263 .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
7264 .unwrap(),
7265 project
7266 .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
7267 .unwrap(),
7268 )
7269 });
7270
7271 // Insert some excerpts.
7272 _ = leader.update(cx, |leader, cx| {
7273 leader.buffer.update(cx, |multibuffer, cx| {
7274 let excerpt_ids = multibuffer.push_excerpts(
7275 buffer_1.clone(),
7276 [
7277 ExcerptRange {
7278 context: 1..6,
7279 primary: None,
7280 },
7281 ExcerptRange {
7282 context: 12..15,
7283 primary: None,
7284 },
7285 ExcerptRange {
7286 context: 0..3,
7287 primary: None,
7288 },
7289 ],
7290 cx,
7291 );
7292 multibuffer.insert_excerpts_after(
7293 excerpt_ids[0],
7294 buffer_2.clone(),
7295 [
7296 ExcerptRange {
7297 context: 8..12,
7298 primary: None,
7299 },
7300 ExcerptRange {
7301 context: 0..6,
7302 primary: None,
7303 },
7304 ],
7305 cx,
7306 );
7307 });
7308 });
7309
7310 // Apply the update of adding the excerpts.
7311 follower_1
7312 .update(cx, |follower, cx| {
7313 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7314 })
7315 .await
7316 .unwrap();
7317 assert_eq!(
7318 follower_1.update(cx, |editor, cx| editor.text(cx)),
7319 leader.update(cx, |editor, cx| editor.text(cx))
7320 );
7321 update_message.borrow_mut().take();
7322
7323 // Start following separately after it already has excerpts.
7324 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7325 let follower_2 = cx
7326 .update_window(*workspace.deref(), |_, cx| {
7327 Editor::from_state_proto(
7328 pane.clone(),
7329 workspace.root_view(cx).unwrap().clone(),
7330 ViewId {
7331 creator: Default::default(),
7332 id: 0,
7333 },
7334 &mut state_message,
7335 cx,
7336 )
7337 })
7338 .unwrap()
7339 .unwrap()
7340 .await
7341 .unwrap();
7342 assert_eq!(
7343 follower_2.update(cx, |editor, cx| editor.text(cx)),
7344 leader.update(cx, |editor, cx| editor.text(cx))
7345 );
7346
7347 // Remove some excerpts.
7348 _ = leader.update(cx, |leader, cx| {
7349 leader.buffer.update(cx, |multibuffer, cx| {
7350 let excerpt_ids = multibuffer.excerpt_ids();
7351 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
7352 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
7353 });
7354 });
7355
7356 // Apply the update of removing the excerpts.
7357 follower_1
7358 .update(cx, |follower, cx| {
7359 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7360 })
7361 .await
7362 .unwrap();
7363 follower_2
7364 .update(cx, |follower, cx| {
7365 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7366 })
7367 .await
7368 .unwrap();
7369 update_message.borrow_mut().take();
7370 assert_eq!(
7371 follower_1.update(cx, |editor, cx| editor.text(cx)),
7372 leader.update(cx, |editor, cx| editor.text(cx))
7373 );
7374}
7375
7376#[gpui::test]
7377async fn go_to_prev_overlapping_diagnostic(
7378 executor: BackgroundExecutor,
7379 cx: &mut gpui::TestAppContext,
7380) {
7381 init_test(cx, |_| {});
7382
7383 let mut cx = EditorTestContext::new(cx).await;
7384 let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
7385
7386 cx.set_state(indoc! {"
7387 ˇfn func(abc def: i32) -> u32 {
7388 }
7389 "});
7390
7391 _ = cx.update(|cx| {
7392 _ = project.update(cx, |project, cx| {
7393 project
7394 .update_diagnostics(
7395 LanguageServerId(0),
7396 lsp::PublishDiagnosticsParams {
7397 uri: lsp::Url::from_file_path("/root/file").unwrap(),
7398 version: None,
7399 diagnostics: vec![
7400 lsp::Diagnostic {
7401 range: lsp::Range::new(
7402 lsp::Position::new(0, 11),
7403 lsp::Position::new(0, 12),
7404 ),
7405 severity: Some(lsp::DiagnosticSeverity::ERROR),
7406 ..Default::default()
7407 },
7408 lsp::Diagnostic {
7409 range: lsp::Range::new(
7410 lsp::Position::new(0, 12),
7411 lsp::Position::new(0, 15),
7412 ),
7413 severity: Some(lsp::DiagnosticSeverity::ERROR),
7414 ..Default::default()
7415 },
7416 lsp::Diagnostic {
7417 range: lsp::Range::new(
7418 lsp::Position::new(0, 25),
7419 lsp::Position::new(0, 28),
7420 ),
7421 severity: Some(lsp::DiagnosticSeverity::ERROR),
7422 ..Default::default()
7423 },
7424 ],
7425 },
7426 &[],
7427 cx,
7428 )
7429 .unwrap()
7430 });
7431 });
7432
7433 executor.run_until_parked();
7434
7435 cx.update_editor(|editor, cx| {
7436 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7437 });
7438
7439 cx.assert_editor_state(indoc! {"
7440 fn func(abc def: i32) -> ˇu32 {
7441 }
7442 "});
7443
7444 cx.update_editor(|editor, cx| {
7445 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7446 });
7447
7448 cx.assert_editor_state(indoc! {"
7449 fn func(abc ˇdef: i32) -> u32 {
7450 }
7451 "});
7452
7453 cx.update_editor(|editor, cx| {
7454 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7455 });
7456
7457 cx.assert_editor_state(indoc! {"
7458 fn func(abcˇ def: i32) -> u32 {
7459 }
7460 "});
7461
7462 cx.update_editor(|editor, cx| {
7463 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7464 });
7465
7466 cx.assert_editor_state(indoc! {"
7467 fn func(abc def: i32) -> ˇu32 {
7468 }
7469 "});
7470}
7471
7472#[gpui::test]
7473async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7474 init_test(cx, |_| {});
7475
7476 let mut cx = EditorTestContext::new(cx).await;
7477
7478 let diff_base = r#"
7479 use some::mod;
7480
7481 const A: u32 = 42;
7482
7483 fn main() {
7484 println!("hello");
7485
7486 println!("world");
7487 }
7488 "#
7489 .unindent();
7490
7491 // Edits are modified, removed, modified, added
7492 cx.set_state(
7493 &r#"
7494 use some::modified;
7495
7496 ˇ
7497 fn main() {
7498 println!("hello there");
7499
7500 println!("around the");
7501 println!("world");
7502 }
7503 "#
7504 .unindent(),
7505 );
7506
7507 cx.set_diff_base(Some(&diff_base));
7508 executor.run_until_parked();
7509
7510 cx.update_editor(|editor, cx| {
7511 //Wrap around the bottom of the buffer
7512 for _ in 0..3 {
7513 editor.go_to_hunk(&GoToHunk, cx);
7514 }
7515 });
7516
7517 cx.assert_editor_state(
7518 &r#"
7519 ˇuse some::modified;
7520
7521
7522 fn main() {
7523 println!("hello there");
7524
7525 println!("around the");
7526 println!("world");
7527 }
7528 "#
7529 .unindent(),
7530 );
7531
7532 cx.update_editor(|editor, cx| {
7533 //Wrap around the top of the buffer
7534 for _ in 0..2 {
7535 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7536 }
7537 });
7538
7539 cx.assert_editor_state(
7540 &r#"
7541 use some::modified;
7542
7543
7544 fn main() {
7545 ˇ println!("hello there");
7546
7547 println!("around the");
7548 println!("world");
7549 }
7550 "#
7551 .unindent(),
7552 );
7553
7554 cx.update_editor(|editor, cx| {
7555 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7556 });
7557
7558 cx.assert_editor_state(
7559 &r#"
7560 use some::modified;
7561
7562 ˇ
7563 fn main() {
7564 println!("hello there");
7565
7566 println!("around the");
7567 println!("world");
7568 }
7569 "#
7570 .unindent(),
7571 );
7572
7573 cx.update_editor(|editor, cx| {
7574 for _ in 0..3 {
7575 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7576 }
7577 });
7578
7579 cx.assert_editor_state(
7580 &r#"
7581 use some::modified;
7582
7583
7584 fn main() {
7585 ˇ println!("hello there");
7586
7587 println!("around the");
7588 println!("world");
7589 }
7590 "#
7591 .unindent(),
7592 );
7593
7594 cx.update_editor(|editor, cx| {
7595 editor.fold(&Fold, cx);
7596
7597 //Make sure that the fold only gets one hunk
7598 for _ in 0..4 {
7599 editor.go_to_hunk(&GoToHunk, cx);
7600 }
7601 });
7602
7603 cx.assert_editor_state(
7604 &r#"
7605 ˇuse some::modified;
7606
7607
7608 fn main() {
7609 println!("hello there");
7610
7611 println!("around the");
7612 println!("world");
7613 }
7614 "#
7615 .unindent(),
7616 );
7617}
7618
7619#[test]
7620fn test_split_words() {
7621 fn split(text: &str) -> Vec<&str> {
7622 split_words(text).collect()
7623 }
7624
7625 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
7626 assert_eq!(split("hello_world"), &["hello_", "world"]);
7627 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
7628 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
7629 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
7630 assert_eq!(split("helloworld"), &["helloworld"]);
7631
7632 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
7633}
7634
7635#[gpui::test]
7636async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
7637 init_test(cx, |_| {});
7638
7639 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
7640 let mut assert = |before, after| {
7641 let _state_context = cx.set_state(before);
7642 cx.update_editor(|editor, cx| {
7643 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
7644 });
7645 cx.assert_editor_state(after);
7646 };
7647
7648 // Outside bracket jumps to outside of matching bracket
7649 assert("console.logˇ(var);", "console.log(var)ˇ;");
7650 assert("console.log(var)ˇ;", "console.logˇ(var);");
7651
7652 // Inside bracket jumps to inside of matching bracket
7653 assert("console.log(ˇvar);", "console.log(varˇ);");
7654 assert("console.log(varˇ);", "console.log(ˇvar);");
7655
7656 // When outside a bracket and inside, favor jumping to the inside bracket
7657 assert(
7658 "console.log('foo', [1, 2, 3]ˇ);",
7659 "console.log(ˇ'foo', [1, 2, 3]);",
7660 );
7661 assert(
7662 "console.log(ˇ'foo', [1, 2, 3]);",
7663 "console.log('foo', [1, 2, 3]ˇ);",
7664 );
7665
7666 // Bias forward if two options are equally likely
7667 assert(
7668 "let result = curried_fun()ˇ();",
7669 "let result = curried_fun()()ˇ;",
7670 );
7671
7672 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
7673 assert(
7674 indoc! {"
7675 function test() {
7676 console.log('test')ˇ
7677 }"},
7678 indoc! {"
7679 function test() {
7680 console.logˇ('test')
7681 }"},
7682 );
7683}
7684
7685#[gpui::test(iterations = 10)]
7686async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7687 // flaky
7688 init_test(cx, |_| {});
7689
7690 let (copilot, copilot_lsp) = Copilot::fake(cx);
7691 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7692 let mut cx = EditorLspTestContext::new_rust(
7693 lsp::ServerCapabilities {
7694 completion_provider: Some(lsp::CompletionOptions {
7695 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7696 ..Default::default()
7697 }),
7698 ..Default::default()
7699 },
7700 cx,
7701 )
7702 .await;
7703
7704 // When inserting, ensure autocompletion is favored over Copilot suggestions.
7705 cx.set_state(indoc! {"
7706 oneˇ
7707 two
7708 three
7709 "});
7710 cx.simulate_keystroke(".");
7711 let _ = handle_completion_request(
7712 &mut cx,
7713 indoc! {"
7714 one.|<>
7715 two
7716 three
7717 "},
7718 vec!["completion_a", "completion_b"],
7719 );
7720 handle_copilot_completion_request(
7721 &copilot_lsp,
7722 vec![copilot::request::Completion {
7723 text: "one.copilot1".into(),
7724 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7725 ..Default::default()
7726 }],
7727 vec![],
7728 );
7729 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7730 cx.update_editor(|editor, cx| {
7731 assert!(editor.context_menu_visible());
7732 assert!(!editor.has_active_copilot_suggestion(cx));
7733
7734 // Confirming a completion inserts it and hides the context menu, without showing
7735 // the copilot suggestion afterwards.
7736 editor
7737 .confirm_completion(&Default::default(), cx)
7738 .unwrap()
7739 .detach();
7740 assert!(!editor.context_menu_visible());
7741 assert!(!editor.has_active_copilot_suggestion(cx));
7742 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
7743 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
7744 });
7745
7746 // Ensure Copilot suggestions are shown right away if no autocompletion is available.
7747 cx.set_state(indoc! {"
7748 oneˇ
7749 two
7750 three
7751 "});
7752 cx.simulate_keystroke(".");
7753 let _ = handle_completion_request(
7754 &mut cx,
7755 indoc! {"
7756 one.|<>
7757 two
7758 three
7759 "},
7760 vec![],
7761 );
7762 handle_copilot_completion_request(
7763 &copilot_lsp,
7764 vec![copilot::request::Completion {
7765 text: "one.copilot1".into(),
7766 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7767 ..Default::default()
7768 }],
7769 vec![],
7770 );
7771 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7772 cx.update_editor(|editor, cx| {
7773 assert!(!editor.context_menu_visible());
7774 assert!(editor.has_active_copilot_suggestion(cx));
7775 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7776 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7777 });
7778
7779 // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
7780 cx.set_state(indoc! {"
7781 oneˇ
7782 two
7783 three
7784 "});
7785 cx.simulate_keystroke(".");
7786 let _ = handle_completion_request(
7787 &mut cx,
7788 indoc! {"
7789 one.|<>
7790 two
7791 three
7792 "},
7793 vec!["completion_a", "completion_b"],
7794 );
7795 handle_copilot_completion_request(
7796 &copilot_lsp,
7797 vec![copilot::request::Completion {
7798 text: "one.copilot1".into(),
7799 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7800 ..Default::default()
7801 }],
7802 vec![],
7803 );
7804 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7805 cx.update_editor(|editor, cx| {
7806 assert!(editor.context_menu_visible());
7807 assert!(!editor.has_active_copilot_suggestion(cx));
7808
7809 // When hiding the context menu, the Copilot suggestion becomes visible.
7810 editor.hide_context_menu(cx);
7811 assert!(!editor.context_menu_visible());
7812 assert!(editor.has_active_copilot_suggestion(cx));
7813 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7814 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7815 });
7816
7817 // Ensure existing completion is interpolated when inserting again.
7818 cx.simulate_keystroke("c");
7819 executor.run_until_parked();
7820 cx.update_editor(|editor, cx| {
7821 assert!(!editor.context_menu_visible());
7822 assert!(editor.has_active_copilot_suggestion(cx));
7823 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7824 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7825 });
7826
7827 // After debouncing, new Copilot completions should be requested.
7828 handle_copilot_completion_request(
7829 &copilot_lsp,
7830 vec![copilot::request::Completion {
7831 text: "one.copilot2".into(),
7832 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
7833 ..Default::default()
7834 }],
7835 vec![],
7836 );
7837 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7838 cx.update_editor(|editor, cx| {
7839 assert!(!editor.context_menu_visible());
7840 assert!(editor.has_active_copilot_suggestion(cx));
7841 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7842 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7843
7844 // Canceling should remove the active Copilot suggestion.
7845 editor.cancel(&Default::default(), cx);
7846 assert!(!editor.has_active_copilot_suggestion(cx));
7847 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
7848 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7849
7850 // After canceling, tabbing shouldn't insert the previously shown suggestion.
7851 editor.tab(&Default::default(), cx);
7852 assert!(!editor.has_active_copilot_suggestion(cx));
7853 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
7854 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
7855
7856 // When undoing the previously active suggestion is shown again.
7857 editor.undo(&Default::default(), cx);
7858 assert!(editor.has_active_copilot_suggestion(cx));
7859 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7860 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7861 });
7862
7863 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
7864 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
7865 cx.update_editor(|editor, cx| {
7866 assert!(editor.has_active_copilot_suggestion(cx));
7867 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7868 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7869
7870 // Tabbing when there is an active suggestion inserts it.
7871 editor.tab(&Default::default(), cx);
7872 assert!(!editor.has_active_copilot_suggestion(cx));
7873 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7874 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
7875
7876 // When undoing the previously active suggestion is shown again.
7877 editor.undo(&Default::default(), cx);
7878 assert!(editor.has_active_copilot_suggestion(cx));
7879 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7880 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7881
7882 // Hide suggestion.
7883 editor.cancel(&Default::default(), cx);
7884 assert!(!editor.has_active_copilot_suggestion(cx));
7885 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
7886 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7887 });
7888
7889 // If an edit occurs outside of this editor but no suggestion is being shown,
7890 // we won't make it visible.
7891 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
7892 cx.update_editor(|editor, cx| {
7893 assert!(!editor.has_active_copilot_suggestion(cx));
7894 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
7895 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
7896 });
7897
7898 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
7899 cx.update_editor(|editor, cx| {
7900 editor.set_text("fn foo() {\n \n}", cx);
7901 editor.change_selections(None, cx, |s| {
7902 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
7903 });
7904 });
7905 handle_copilot_completion_request(
7906 &copilot_lsp,
7907 vec![copilot::request::Completion {
7908 text: " let x = 4;".into(),
7909 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
7910 ..Default::default()
7911 }],
7912 vec![],
7913 );
7914
7915 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
7916 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7917 cx.update_editor(|editor, cx| {
7918 assert!(editor.has_active_copilot_suggestion(cx));
7919 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7920 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7921
7922 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
7923 editor.tab(&Default::default(), cx);
7924 assert!(editor.has_active_copilot_suggestion(cx));
7925 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7926 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7927
7928 // Tabbing again accepts the suggestion.
7929 editor.tab(&Default::default(), cx);
7930 assert!(!editor.has_active_copilot_suggestion(cx));
7931 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
7932 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7933 });
7934}
7935
7936#[gpui::test(iterations = 10)]
7937async fn test_accept_partial_copilot_suggestion(
7938 executor: BackgroundExecutor,
7939 cx: &mut gpui::TestAppContext,
7940) {
7941 // flaky
7942 init_test(cx, |_| {});
7943
7944 let (copilot, copilot_lsp) = Copilot::fake(cx);
7945 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7946 let mut cx = EditorLspTestContext::new_rust(
7947 lsp::ServerCapabilities {
7948 completion_provider: Some(lsp::CompletionOptions {
7949 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7950 ..Default::default()
7951 }),
7952 ..Default::default()
7953 },
7954 cx,
7955 )
7956 .await;
7957
7958 // Setup the editor with a completion request.
7959 cx.set_state(indoc! {"
7960 oneˇ
7961 two
7962 three
7963 "});
7964 cx.simulate_keystroke(".");
7965 let _ = handle_completion_request(
7966 &mut cx,
7967 indoc! {"
7968 one.|<>
7969 two
7970 three
7971 "},
7972 vec![],
7973 );
7974 handle_copilot_completion_request(
7975 &copilot_lsp,
7976 vec![copilot::request::Completion {
7977 text: "one.copilot1".into(),
7978 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7979 ..Default::default()
7980 }],
7981 vec![],
7982 );
7983 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7984 cx.update_editor(|editor, cx| {
7985 assert!(editor.has_active_copilot_suggestion(cx));
7986
7987 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
7988 editor.accept_partial_copilot_suggestion(&Default::default(), cx);
7989 assert!(editor.has_active_copilot_suggestion(cx));
7990 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
7991 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7992
7993 // Accepting next word should accept the non-word and copilot suggestion should be gone
7994 editor.accept_partial_copilot_suggestion(&Default::default(), cx);
7995 assert!(!editor.has_active_copilot_suggestion(cx));
7996 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
7997 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7998 });
7999
8000 // Reset the editor and check non-word and whitespace completion
8001 cx.set_state(indoc! {"
8002 oneˇ
8003 two
8004 three
8005 "});
8006 cx.simulate_keystroke(".");
8007 let _ = handle_completion_request(
8008 &mut cx,
8009 indoc! {"
8010 one.|<>
8011 two
8012 three
8013 "},
8014 vec![],
8015 );
8016 handle_copilot_completion_request(
8017 &copilot_lsp,
8018 vec![copilot::request::Completion {
8019 text: "one.123. copilot\n 456".into(),
8020 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
8021 ..Default::default()
8022 }],
8023 vec![],
8024 );
8025 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8026 cx.update_editor(|editor, cx| {
8027 assert!(editor.has_active_copilot_suggestion(cx));
8028
8029 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
8030 editor.accept_partial_copilot_suggestion(&Default::default(), cx);
8031 assert!(editor.has_active_copilot_suggestion(cx));
8032 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
8033 assert_eq!(
8034 editor.display_text(cx),
8035 "one.123. copilot\n 456\ntwo\nthree\n"
8036 );
8037
8038 // Accepting next word should accept the next word and copilot suggestion should still exist
8039 editor.accept_partial_copilot_suggestion(&Default::default(), cx);
8040 assert!(editor.has_active_copilot_suggestion(cx));
8041 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
8042 assert_eq!(
8043 editor.display_text(cx),
8044 "one.123. copilot\n 456\ntwo\nthree\n"
8045 );
8046
8047 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
8048 editor.accept_partial_copilot_suggestion(&Default::default(), cx);
8049 assert!(!editor.has_active_copilot_suggestion(cx));
8050 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
8051 assert_eq!(
8052 editor.display_text(cx),
8053 "one.123. copilot\n 456\ntwo\nthree\n"
8054 );
8055 });
8056}
8057
8058#[gpui::test]
8059async fn test_copilot_completion_invalidation(
8060 executor: BackgroundExecutor,
8061 cx: &mut gpui::TestAppContext,
8062) {
8063 init_test(cx, |_| {});
8064
8065 let (copilot, copilot_lsp) = Copilot::fake(cx);
8066 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
8067 let mut cx = EditorLspTestContext::new_rust(
8068 lsp::ServerCapabilities {
8069 completion_provider: Some(lsp::CompletionOptions {
8070 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8071 ..Default::default()
8072 }),
8073 ..Default::default()
8074 },
8075 cx,
8076 )
8077 .await;
8078
8079 cx.set_state(indoc! {"
8080 one
8081 twˇ
8082 three
8083 "});
8084
8085 handle_copilot_completion_request(
8086 &copilot_lsp,
8087 vec![copilot::request::Completion {
8088 text: "two.foo()".into(),
8089 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
8090 ..Default::default()
8091 }],
8092 vec![],
8093 );
8094 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
8095 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8096 cx.update_editor(|editor, cx| {
8097 assert!(editor.has_active_copilot_suggestion(cx));
8098 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
8099 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
8100
8101 editor.backspace(&Default::default(), cx);
8102 assert!(editor.has_active_copilot_suggestion(cx));
8103 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
8104 assert_eq!(editor.text(cx), "one\nt\nthree\n");
8105
8106 editor.backspace(&Default::default(), cx);
8107 assert!(editor.has_active_copilot_suggestion(cx));
8108 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
8109 assert_eq!(editor.text(cx), "one\n\nthree\n");
8110
8111 // Deleting across the original suggestion range invalidates it.
8112 editor.backspace(&Default::default(), cx);
8113 assert!(!editor.has_active_copilot_suggestion(cx));
8114 assert_eq!(editor.display_text(cx), "one\nthree\n");
8115 assert_eq!(editor.text(cx), "one\nthree\n");
8116
8117 // Undoing the deletion restores the suggestion.
8118 editor.undo(&Default::default(), cx);
8119 assert!(editor.has_active_copilot_suggestion(cx));
8120 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
8121 assert_eq!(editor.text(cx), "one\n\nthree\n");
8122 });
8123}
8124
8125#[gpui::test]
8126async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
8127 init_test(cx, |_| {});
8128
8129 let (copilot, copilot_lsp) = Copilot::fake(cx);
8130 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
8131
8132 let buffer_1 = cx.new_model(|cx| {
8133 Buffer::new(
8134 0,
8135 BufferId::new(cx.entity_id().as_u64()).unwrap(),
8136 "a = 1\nb = 2\n",
8137 )
8138 });
8139 let buffer_2 = cx.new_model(|cx| {
8140 Buffer::new(
8141 0,
8142 BufferId::new(cx.entity_id().as_u64()).unwrap(),
8143 "c = 3\nd = 4\n",
8144 )
8145 });
8146 let multibuffer = cx.new_model(|cx| {
8147 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
8148 multibuffer.push_excerpts(
8149 buffer_1.clone(),
8150 [ExcerptRange {
8151 context: Point::new(0, 0)..Point::new(2, 0),
8152 primary: None,
8153 }],
8154 cx,
8155 );
8156 multibuffer.push_excerpts(
8157 buffer_2.clone(),
8158 [ExcerptRange {
8159 context: Point::new(0, 0)..Point::new(2, 0),
8160 primary: None,
8161 }],
8162 cx,
8163 );
8164 multibuffer
8165 });
8166 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
8167
8168 handle_copilot_completion_request(
8169 &copilot_lsp,
8170 vec![copilot::request::Completion {
8171 text: "b = 2 + a".into(),
8172 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
8173 ..Default::default()
8174 }],
8175 vec![],
8176 );
8177 _ = editor.update(cx, |editor, cx| {
8178 // Ensure copilot suggestions are shown for the first excerpt.
8179 editor.change_selections(None, cx, |s| {
8180 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
8181 });
8182 editor.next_copilot_suggestion(&Default::default(), cx);
8183 });
8184 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8185 _ = editor.update(cx, |editor, cx| {
8186 assert!(editor.has_active_copilot_suggestion(cx));
8187 assert_eq!(
8188 editor.display_text(cx),
8189 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
8190 );
8191 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
8192 });
8193
8194 handle_copilot_completion_request(
8195 &copilot_lsp,
8196 vec![copilot::request::Completion {
8197 text: "d = 4 + c".into(),
8198 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
8199 ..Default::default()
8200 }],
8201 vec![],
8202 );
8203 _ = editor.update(cx, |editor, cx| {
8204 // Move to another excerpt, ensuring the suggestion gets cleared.
8205 editor.change_selections(None, cx, |s| {
8206 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
8207 });
8208 assert!(!editor.has_active_copilot_suggestion(cx));
8209 assert_eq!(
8210 editor.display_text(cx),
8211 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
8212 );
8213 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
8214
8215 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
8216 editor.handle_input(" ", cx);
8217 assert!(!editor.has_active_copilot_suggestion(cx));
8218 assert_eq!(
8219 editor.display_text(cx),
8220 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
8221 );
8222 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
8223 });
8224
8225 // Ensure the new suggestion is displayed when the debounce timeout expires.
8226 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8227 _ = editor.update(cx, |editor, cx| {
8228 assert!(editor.has_active_copilot_suggestion(cx));
8229 assert_eq!(
8230 editor.display_text(cx),
8231 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
8232 );
8233 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
8234 });
8235}
8236
8237#[gpui::test]
8238async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
8239 init_test(cx, |settings| {
8240 settings
8241 .copilot
8242 .get_or_insert(Default::default())
8243 .disabled_globs = Some(vec![".env*".to_string()]);
8244 });
8245
8246 let (copilot, copilot_lsp) = Copilot::fake(cx);
8247 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
8248
8249 let fs = FakeFs::new(cx.executor());
8250 fs.insert_tree(
8251 "/test",
8252 json!({
8253 ".env": "SECRET=something\n",
8254 "README.md": "hello\n"
8255 }),
8256 )
8257 .await;
8258 let project = Project::test(fs, ["/test".as_ref()], cx).await;
8259
8260 let private_buffer = project
8261 .update(cx, |project, cx| {
8262 project.open_local_buffer("/test/.env", cx)
8263 })
8264 .await
8265 .unwrap();
8266 let public_buffer = project
8267 .update(cx, |project, cx| {
8268 project.open_local_buffer("/test/README.md", cx)
8269 })
8270 .await
8271 .unwrap();
8272
8273 let multibuffer = cx.new_model(|cx| {
8274 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
8275 multibuffer.push_excerpts(
8276 private_buffer.clone(),
8277 [ExcerptRange {
8278 context: Point::new(0, 0)..Point::new(1, 0),
8279 primary: None,
8280 }],
8281 cx,
8282 );
8283 multibuffer.push_excerpts(
8284 public_buffer.clone(),
8285 [ExcerptRange {
8286 context: Point::new(0, 0)..Point::new(1, 0),
8287 primary: None,
8288 }],
8289 cx,
8290 );
8291 multibuffer
8292 });
8293 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
8294
8295 let mut copilot_requests = copilot_lsp
8296 .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
8297 Ok(copilot::request::GetCompletionsResult {
8298 completions: vec![copilot::request::Completion {
8299 text: "next line".into(),
8300 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
8301 ..Default::default()
8302 }],
8303 })
8304 });
8305
8306 _ = editor.update(cx, |editor, cx| {
8307 editor.change_selections(None, cx, |selections| {
8308 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
8309 });
8310 editor.next_copilot_suggestion(&Default::default(), cx);
8311 });
8312
8313 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8314 assert!(copilot_requests.try_next().is_err());
8315
8316 _ = editor.update(cx, |editor, cx| {
8317 editor.change_selections(None, cx, |s| {
8318 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
8319 });
8320 editor.next_copilot_suggestion(&Default::default(), cx);
8321 });
8322
8323 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
8324 assert!(copilot_requests.try_next().is_ok());
8325}
8326
8327#[gpui::test]
8328async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
8329 init_test(cx, |_| {});
8330
8331 let fs = FakeFs::new(cx.executor());
8332 fs.insert_tree(
8333 "/a",
8334 json!({
8335 "main.rs": "fn main() { let a = 5; }",
8336 "other.rs": "// Test file",
8337 }),
8338 )
8339 .await;
8340 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8341
8342 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8343 language_registry.add(Arc::new(Language::new(
8344 LanguageConfig {
8345 name: "Rust".into(),
8346 matcher: LanguageMatcher {
8347 path_suffixes: vec!["rs".to_string()],
8348 ..Default::default()
8349 },
8350 brackets: BracketPairConfig {
8351 pairs: vec![BracketPair {
8352 start: "{".to_string(),
8353 end: "}".to_string(),
8354 close: true,
8355 newline: true,
8356 }],
8357 disabled_scopes_by_bracket_ix: Vec::new(),
8358 },
8359 ..Default::default()
8360 },
8361 Some(tree_sitter_rust::language()),
8362 )));
8363 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8364 "Rust",
8365 FakeLspAdapter {
8366 capabilities: lsp::ServerCapabilities {
8367 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
8368 first_trigger_character: "{".to_string(),
8369 more_trigger_character: None,
8370 }),
8371 ..Default::default()
8372 },
8373 ..Default::default()
8374 },
8375 );
8376
8377 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8378
8379 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8380
8381 let worktree_id = workspace
8382 .update(cx, |workspace, cx| {
8383 workspace.project().update(cx, |project, cx| {
8384 project.worktrees().next().unwrap().read(cx).id()
8385 })
8386 })
8387 .unwrap();
8388
8389 let buffer = project
8390 .update(cx, |project, cx| {
8391 project.open_local_buffer("/a/main.rs", cx)
8392 })
8393 .await
8394 .unwrap();
8395 cx.executor().run_until_parked();
8396 cx.executor().start_waiting();
8397 let fake_server = fake_servers.next().await.unwrap();
8398 let editor_handle = workspace
8399 .update(cx, |workspace, cx| {
8400 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
8401 })
8402 .unwrap()
8403 .await
8404 .unwrap()
8405 .downcast::<Editor>()
8406 .unwrap();
8407
8408 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
8409 assert_eq!(
8410 params.text_document_position.text_document.uri,
8411 lsp::Url::from_file_path("/a/main.rs").unwrap(),
8412 );
8413 assert_eq!(
8414 params.text_document_position.position,
8415 lsp::Position::new(0, 21),
8416 );
8417
8418 Ok(Some(vec![lsp::TextEdit {
8419 new_text: "]".to_string(),
8420 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
8421 }]))
8422 });
8423
8424 editor_handle.update(cx, |editor, cx| {
8425 editor.focus(cx);
8426 editor.change_selections(None, cx, |s| {
8427 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
8428 });
8429 editor.handle_input("{", cx);
8430 });
8431
8432 cx.executor().run_until_parked();
8433
8434 _ = buffer.update(cx, |buffer, _| {
8435 assert_eq!(
8436 buffer.text(),
8437 "fn main() { let a = {5}; }",
8438 "No extra braces from on type formatting should appear in the buffer"
8439 )
8440 });
8441}
8442
8443#[gpui::test]
8444async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
8445 init_test(cx, |_| {});
8446
8447 let fs = FakeFs::new(cx.executor());
8448 fs.insert_tree(
8449 "/a",
8450 json!({
8451 "main.rs": "fn main() { let a = 5; }",
8452 "other.rs": "// Test file",
8453 }),
8454 )
8455 .await;
8456
8457 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8458
8459 let server_restarts = Arc::new(AtomicUsize::new(0));
8460 let closure_restarts = Arc::clone(&server_restarts);
8461 let language_server_name = "test language server";
8462 let language_name: Arc<str> = "Rust".into();
8463
8464 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8465 language_registry.add(Arc::new(Language::new(
8466 LanguageConfig {
8467 name: Arc::clone(&language_name),
8468 matcher: LanguageMatcher {
8469 path_suffixes: vec!["rs".to_string()],
8470 ..Default::default()
8471 },
8472 ..Default::default()
8473 },
8474 Some(tree_sitter_rust::language()),
8475 )));
8476 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8477 "Rust",
8478 FakeLspAdapter {
8479 name: language_server_name,
8480 initialization_options: Some(json!({
8481 "testOptionValue": true
8482 })),
8483 initializer: Some(Box::new(move |fake_server| {
8484 let task_restarts = Arc::clone(&closure_restarts);
8485 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
8486 task_restarts.fetch_add(1, atomic::Ordering::Release);
8487 futures::future::ready(Ok(()))
8488 });
8489 })),
8490 ..Default::default()
8491 },
8492 );
8493
8494 let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8495 let _buffer = project
8496 .update(cx, |project, cx| {
8497 project.open_local_buffer("/a/main.rs", cx)
8498 })
8499 .await
8500 .unwrap();
8501 let _fake_server = fake_servers.next().await.unwrap();
8502 update_test_language_settings(cx, |language_settings| {
8503 language_settings.languages.insert(
8504 Arc::clone(&language_name),
8505 LanguageSettingsContent {
8506 tab_size: NonZeroU32::new(8),
8507 ..Default::default()
8508 },
8509 );
8510 });
8511 cx.executor().run_until_parked();
8512 assert_eq!(
8513 server_restarts.load(atomic::Ordering::Acquire),
8514 0,
8515 "Should not restart LSP server on an unrelated change"
8516 );
8517
8518 update_test_project_settings(cx, |project_settings| {
8519 project_settings.lsp.insert(
8520 "Some other server name".into(),
8521 LspSettings {
8522 binary: None,
8523 settings: None,
8524 initialization_options: Some(json!({
8525 "some other init value": false
8526 })),
8527 },
8528 );
8529 });
8530 cx.executor().run_until_parked();
8531 assert_eq!(
8532 server_restarts.load(atomic::Ordering::Acquire),
8533 0,
8534 "Should not restart LSP server on an unrelated LSP settings change"
8535 );
8536
8537 update_test_project_settings(cx, |project_settings| {
8538 project_settings.lsp.insert(
8539 language_server_name.into(),
8540 LspSettings {
8541 binary: None,
8542 settings: None,
8543 initialization_options: Some(json!({
8544 "anotherInitValue": false
8545 })),
8546 },
8547 );
8548 });
8549 cx.executor().run_until_parked();
8550 assert_eq!(
8551 server_restarts.load(atomic::Ordering::Acquire),
8552 1,
8553 "Should restart LSP server on a related LSP settings change"
8554 );
8555
8556 update_test_project_settings(cx, |project_settings| {
8557 project_settings.lsp.insert(
8558 language_server_name.into(),
8559 LspSettings {
8560 binary: None,
8561 settings: None,
8562 initialization_options: Some(json!({
8563 "anotherInitValue": false
8564 })),
8565 },
8566 );
8567 });
8568 cx.executor().run_until_parked();
8569 assert_eq!(
8570 server_restarts.load(atomic::Ordering::Acquire),
8571 1,
8572 "Should not restart LSP server on a related LSP settings change that is the same"
8573 );
8574
8575 update_test_project_settings(cx, |project_settings| {
8576 project_settings.lsp.insert(
8577 language_server_name.into(),
8578 LspSettings {
8579 binary: None,
8580 settings: None,
8581 initialization_options: None,
8582 },
8583 );
8584 });
8585 cx.executor().run_until_parked();
8586 assert_eq!(
8587 server_restarts.load(atomic::Ordering::Acquire),
8588 2,
8589 "Should restart LSP server on another related LSP settings change"
8590 );
8591}
8592
8593#[gpui::test]
8594async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
8595 init_test(cx, |_| {});
8596
8597 let mut cx = EditorLspTestContext::new_rust(
8598 lsp::ServerCapabilities {
8599 completion_provider: Some(lsp::CompletionOptions {
8600 trigger_characters: Some(vec![".".to_string()]),
8601 resolve_provider: Some(true),
8602 ..Default::default()
8603 }),
8604 ..Default::default()
8605 },
8606 cx,
8607 )
8608 .await;
8609
8610 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
8611 cx.simulate_keystroke(".");
8612 let completion_item = lsp::CompletionItem {
8613 label: "some".into(),
8614 kind: Some(lsp::CompletionItemKind::SNIPPET),
8615 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
8616 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
8617 kind: lsp::MarkupKind::Markdown,
8618 value: "```rust\nSome(2)\n```".to_string(),
8619 })),
8620 deprecated: Some(false),
8621 sort_text: Some("fffffff2".to_string()),
8622 filter_text: Some("some".to_string()),
8623 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
8624 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8625 range: lsp::Range {
8626 start: lsp::Position {
8627 line: 0,
8628 character: 22,
8629 },
8630 end: lsp::Position {
8631 line: 0,
8632 character: 22,
8633 },
8634 },
8635 new_text: "Some(2)".to_string(),
8636 })),
8637 additional_text_edits: Some(vec![lsp::TextEdit {
8638 range: lsp::Range {
8639 start: lsp::Position {
8640 line: 0,
8641 character: 20,
8642 },
8643 end: lsp::Position {
8644 line: 0,
8645 character: 22,
8646 },
8647 },
8648 new_text: "".to_string(),
8649 }]),
8650 ..Default::default()
8651 };
8652
8653 let closure_completion_item = completion_item.clone();
8654 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
8655 let task_completion_item = closure_completion_item.clone();
8656 async move {
8657 Ok(Some(lsp::CompletionResponse::Array(vec![
8658 task_completion_item,
8659 ])))
8660 }
8661 });
8662
8663 request.next().await;
8664
8665 cx.condition(|editor, _| editor.context_menu_visible())
8666 .await;
8667 let apply_additional_edits = cx.update_editor(|editor, cx| {
8668 editor
8669 .confirm_completion(&ConfirmCompletion::default(), cx)
8670 .unwrap()
8671 });
8672 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
8673
8674 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8675 let task_completion_item = completion_item.clone();
8676 async move { Ok(task_completion_item) }
8677 })
8678 .next()
8679 .await
8680 .unwrap();
8681 apply_additional_edits.await.unwrap();
8682 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
8683}
8684
8685#[gpui::test]
8686async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
8687 init_test(cx, |_| {});
8688
8689 let mut cx = EditorLspTestContext::new(
8690 Language::new(
8691 LanguageConfig {
8692 matcher: LanguageMatcher {
8693 path_suffixes: vec!["jsx".into()],
8694 ..Default::default()
8695 },
8696 overrides: [(
8697 "element".into(),
8698 LanguageConfigOverride {
8699 word_characters: Override::Set(['-'].into_iter().collect()),
8700 ..Default::default()
8701 },
8702 )]
8703 .into_iter()
8704 .collect(),
8705 ..Default::default()
8706 },
8707 Some(tree_sitter_typescript::language_tsx()),
8708 )
8709 .with_override_query("(jsx_self_closing_element) @element")
8710 .unwrap(),
8711 lsp::ServerCapabilities {
8712 completion_provider: Some(lsp::CompletionOptions {
8713 trigger_characters: Some(vec![":".to_string()]),
8714 ..Default::default()
8715 }),
8716 ..Default::default()
8717 },
8718 cx,
8719 )
8720 .await;
8721
8722 cx.lsp
8723 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8724 Ok(Some(lsp::CompletionResponse::Array(vec![
8725 lsp::CompletionItem {
8726 label: "bg-blue".into(),
8727 ..Default::default()
8728 },
8729 lsp::CompletionItem {
8730 label: "bg-red".into(),
8731 ..Default::default()
8732 },
8733 lsp::CompletionItem {
8734 label: "bg-yellow".into(),
8735 ..Default::default()
8736 },
8737 ])))
8738 });
8739
8740 cx.set_state(r#"<p class="bgˇ" />"#);
8741
8742 // Trigger completion when typing a dash, because the dash is an extra
8743 // word character in the 'element' scope, which contains the cursor.
8744 cx.simulate_keystroke("-");
8745 cx.executor().run_until_parked();
8746 cx.update_editor(|editor, _| {
8747 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8748 assert_eq!(
8749 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8750 &["bg-red", "bg-blue", "bg-yellow"]
8751 );
8752 } else {
8753 panic!("expected completion menu to be open");
8754 }
8755 });
8756
8757 cx.simulate_keystroke("l");
8758 cx.executor().run_until_parked();
8759 cx.update_editor(|editor, _| {
8760 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8761 assert_eq!(
8762 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8763 &["bg-blue", "bg-yellow"]
8764 );
8765 } else {
8766 panic!("expected completion menu to be open");
8767 }
8768 });
8769
8770 // When filtering completions, consider the character after the '-' to
8771 // be the start of a subword.
8772 cx.set_state(r#"<p class="yelˇ" />"#);
8773 cx.simulate_keystroke("l");
8774 cx.executor().run_until_parked();
8775 cx.update_editor(|editor, _| {
8776 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8777 assert_eq!(
8778 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8779 &["bg-yellow"]
8780 );
8781 } else {
8782 panic!("expected completion menu to be open");
8783 }
8784 });
8785}
8786
8787#[gpui::test]
8788async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
8789 init_test(cx, |settings| {
8790 settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
8791 });
8792
8793 let fs = FakeFs::new(cx.executor());
8794 fs.insert_file("/file.rs", Default::default()).await;
8795
8796 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
8797 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8798
8799 language_registry.add(Arc::new(Language::new(
8800 LanguageConfig {
8801 name: "Rust".into(),
8802 matcher: LanguageMatcher {
8803 path_suffixes: vec!["rs".to_string()],
8804 ..Default::default()
8805 },
8806 prettier_parser_name: Some("test_parser".to_string()),
8807 ..Default::default()
8808 },
8809 Some(tree_sitter_rust::language()),
8810 )));
8811
8812 let test_plugin = "test_plugin";
8813 let _ = language_registry.register_fake_lsp_adapter(
8814 "Rust",
8815 FakeLspAdapter {
8816 prettier_plugins: vec![test_plugin],
8817 ..Default::default()
8818 },
8819 );
8820
8821 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
8822 let buffer = project
8823 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
8824 .await
8825 .unwrap();
8826
8827 let buffer_text = "one\ntwo\nthree\n";
8828 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
8829 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
8830 _ = editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
8831
8832 editor
8833 .update(cx, |editor, cx| {
8834 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8835 })
8836 .unwrap()
8837 .await;
8838 assert_eq!(
8839 editor.update(cx, |editor, cx| editor.text(cx)),
8840 buffer_text.to_string() + prettier_format_suffix,
8841 "Test prettier formatting was not applied to the original buffer text",
8842 );
8843
8844 update_test_language_settings(cx, |settings| {
8845 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
8846 });
8847 let format = editor.update(cx, |editor, cx| {
8848 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8849 });
8850 format.await.unwrap();
8851 assert_eq!(
8852 editor.update(cx, |editor, cx| editor.text(cx)),
8853 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
8854 "Autoformatting (via test prettier) was not applied to the original buffer text",
8855 );
8856}
8857
8858#[gpui::test]
8859async fn test_find_all_references(cx: &mut gpui::TestAppContext) {
8860 init_test(cx, |_| {});
8861
8862 let mut cx = EditorLspTestContext::new_rust(
8863 lsp::ServerCapabilities {
8864 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8865 ..Default::default()
8866 },
8867 cx,
8868 )
8869 .await;
8870
8871 cx.set_state(indoc! {"
8872 fn foo(«paramˇ»: i64) {
8873 println!(param);
8874 }
8875 "});
8876
8877 cx.lsp
8878 .handle_request::<lsp::request::References, _, _>(move |_, _| async move {
8879 Ok(Some(vec![
8880 lsp::Location {
8881 uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
8882 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 12)),
8883 },
8884 lsp::Location {
8885 uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
8886 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 18)),
8887 },
8888 ]))
8889 });
8890
8891 let references = cx
8892 .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
8893 .unwrap();
8894
8895 cx.executor().run_until_parked();
8896
8897 cx.executor().start_waiting();
8898 references.await.unwrap();
8899
8900 cx.assert_editor_state(indoc! {"
8901 fn foo(param: i64) {
8902 println!(«paramˇ»);
8903 }
8904 "});
8905
8906 let references = cx
8907 .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
8908 .unwrap();
8909
8910 cx.executor().run_until_parked();
8911
8912 cx.executor().start_waiting();
8913 references.await.unwrap();
8914
8915 cx.assert_editor_state(indoc! {"
8916 fn foo(«paramˇ»: i64) {
8917 println!(param);
8918 }
8919 "});
8920
8921 cx.set_state(indoc! {"
8922 fn foo(param: i64) {
8923 let a = param;
8924 let aˇ = param;
8925 let a = param;
8926 println!(param);
8927 }
8928 "});
8929
8930 cx.lsp
8931 .handle_request::<lsp::request::References, _, _>(move |_, _| async move {
8932 Ok(Some(vec![lsp::Location {
8933 uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
8934 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9)),
8935 }]))
8936 });
8937
8938 let references = cx
8939 .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
8940 .unwrap();
8941
8942 cx.executor().run_until_parked();
8943
8944 cx.executor().start_waiting();
8945 references.await.unwrap();
8946
8947 cx.assert_editor_state(indoc! {"
8948 fn foo(param: i64) {
8949 let a = param;
8950 let «aˇ» = param;
8951 let a = param;
8952 println!(param);
8953 }
8954 "});
8955}
8956
8957#[gpui::test]
8958async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
8959 init_test(cx, |_| {});
8960 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8961 let base_text = indoc! {r#"struct Row;
8962struct Row1;
8963struct Row2;
8964
8965struct Row4;
8966struct Row5;
8967struct Row6;
8968
8969struct Row8;
8970struct Row9;
8971struct Row10;"#};
8972
8973 // When addition hunks are not adjacent to carets, no hunk revert is performed
8974 assert_hunk_revert(
8975 indoc! {r#"struct Row;
8976 struct Row1;
8977 struct Row1.1;
8978 struct Row1.2;
8979 struct Row2;ˇ
8980
8981 struct Row4;
8982 struct Row5;
8983 struct Row6;
8984
8985 struct Row8;
8986 ˇstruct Row9;
8987 struct Row9.1;
8988 struct Row9.2;
8989 struct Row9.3;
8990 struct Row10;"#},
8991 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
8992 indoc! {r#"struct Row;
8993 struct Row1;
8994 struct Row1.1;
8995 struct Row1.2;
8996 struct Row2;ˇ
8997
8998 struct Row4;
8999 struct Row5;
9000 struct Row6;
9001
9002 struct Row8;
9003 ˇstruct Row9;
9004 struct Row9.1;
9005 struct Row9.2;
9006 struct Row9.3;
9007 struct Row10;"#},
9008 base_text,
9009 &mut cx,
9010 );
9011 // Same for selections
9012 assert_hunk_revert(
9013 indoc! {r#"struct Row;
9014 struct Row1;
9015 struct Row2;
9016 struct Row2.1;
9017 struct Row2.2;
9018 «ˇ
9019 struct Row4;
9020 struct» Row5;
9021 «struct Row6;
9022 ˇ»
9023 struct Row9.1;
9024 struct Row9.2;
9025 struct Row9.3;
9026 struct Row8;
9027 struct Row9;
9028 struct Row10;"#},
9029 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
9030 indoc! {r#"struct Row;
9031 struct Row1;
9032 struct Row2;
9033 struct Row2.1;
9034 struct Row2.2;
9035 «ˇ
9036 struct Row4;
9037 struct» Row5;
9038 «struct Row6;
9039 ˇ»
9040 struct Row9.1;
9041 struct Row9.2;
9042 struct Row9.3;
9043 struct Row8;
9044 struct Row9;
9045 struct Row10;"#},
9046 base_text,
9047 &mut cx,
9048 );
9049
9050 // When carets and selections intersect the addition hunks, those are reverted.
9051 // Adjacent carets got merged.
9052 assert_hunk_revert(
9053 indoc! {r#"struct Row;
9054 ˇ// something on the top
9055 struct Row1;
9056 struct Row2;
9057 struct Roˇw3.1;
9058 struct Row2.2;
9059 struct Row2.3;ˇ
9060
9061 struct Row4;
9062 struct ˇRow5.1;
9063 struct Row5.2;
9064 struct «Rowˇ»5.3;
9065 struct Row5;
9066 struct Row6;
9067 ˇ
9068 struct Row9.1;
9069 struct «Rowˇ»9.2;
9070 struct «ˇRow»9.3;
9071 struct Row8;
9072 struct Row9;
9073 «ˇ// something on bottom»
9074 struct Row10;"#},
9075 vec![
9076 DiffHunkStatus::Added,
9077 DiffHunkStatus::Added,
9078 DiffHunkStatus::Added,
9079 DiffHunkStatus::Added,
9080 DiffHunkStatus::Added,
9081 ],
9082 indoc! {r#"struct Row;
9083 ˇstruct Row1;
9084 struct Row2;
9085 ˇ
9086 struct Row4;
9087 ˇstruct Row5;
9088 struct Row6;
9089 ˇ
9090 ˇstruct Row8;
9091 struct Row9;
9092 ˇstruct Row10;"#},
9093 base_text,
9094 &mut cx,
9095 );
9096}
9097
9098#[gpui::test]
9099async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
9100 init_test(cx, |_| {});
9101 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9102 let base_text = indoc! {r#"struct Row;
9103struct Row1;
9104struct Row2;
9105
9106struct Row4;
9107struct Row5;
9108struct Row6;
9109
9110struct Row8;
9111struct Row9;
9112struct Row10;"#};
9113
9114 // Modification hunks behave the same as the addition ones.
9115 assert_hunk_revert(
9116 indoc! {r#"struct Row;
9117 struct Row1;
9118 struct Row33;
9119 ˇ
9120 struct Row4;
9121 struct Row5;
9122 struct Row6;
9123 ˇ
9124 struct Row99;
9125 struct Row9;
9126 struct Row10;"#},
9127 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
9128 indoc! {r#"struct Row;
9129 struct Row1;
9130 struct Row33;
9131 ˇ
9132 struct Row4;
9133 struct Row5;
9134 struct Row6;
9135 ˇ
9136 struct Row99;
9137 struct Row9;
9138 struct Row10;"#},
9139 base_text,
9140 &mut cx,
9141 );
9142 assert_hunk_revert(
9143 indoc! {r#"struct Row;
9144 struct Row1;
9145 struct Row33;
9146 «ˇ
9147 struct Row4;
9148 struct» Row5;
9149 «struct Row6;
9150 ˇ»
9151 struct Row99;
9152 struct Row9;
9153 struct Row10;"#},
9154 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
9155 indoc! {r#"struct Row;
9156 struct Row1;
9157 struct Row33;
9158 «ˇ
9159 struct Row4;
9160 struct» Row5;
9161 «struct Row6;
9162 ˇ»
9163 struct Row99;
9164 struct Row9;
9165 struct Row10;"#},
9166 base_text,
9167 &mut cx,
9168 );
9169
9170 assert_hunk_revert(
9171 indoc! {r#"ˇstruct Row1.1;
9172 struct Row1;
9173 «ˇstr»uct Row22;
9174
9175 struct ˇRow44;
9176 struct Row5;
9177 struct «Rˇ»ow66;ˇ
9178
9179 «struˇ»ct Row88;
9180 struct Row9;
9181 struct Row1011;ˇ"#},
9182 vec![
9183 DiffHunkStatus::Modified,
9184 DiffHunkStatus::Modified,
9185 DiffHunkStatus::Modified,
9186 DiffHunkStatus::Modified,
9187 DiffHunkStatus::Modified,
9188 DiffHunkStatus::Modified,
9189 ],
9190 indoc! {r#"struct Row;
9191 ˇstruct Row1;
9192 struct Row2;
9193 ˇ
9194 struct Row4;
9195 ˇstruct Row5;
9196 struct Row6;
9197 ˇ
9198 struct Row8;
9199 ˇstruct Row9;
9200 struct Row10;ˇ"#},
9201 base_text,
9202 &mut cx,
9203 );
9204}
9205
9206#[gpui::test]
9207async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
9208 init_test(cx, |_| {});
9209 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9210 let base_text = indoc! {r#"struct Row;
9211struct Row1;
9212struct Row2;
9213
9214struct Row4;
9215struct Row5;
9216struct Row6;
9217
9218struct Row8;
9219struct Row9;
9220struct Row10;"#};
9221
9222 // Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
9223 assert_hunk_revert(
9224 indoc! {r#"struct Row;
9225 struct Row2;
9226
9227 ˇstruct Row4;
9228 struct Row5;
9229 struct Row6;
9230 ˇ
9231 struct Row8;
9232 struct Row10;"#},
9233 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
9234 indoc! {r#"struct Row;
9235 struct Row2;
9236
9237 ˇstruct Row4;
9238 struct Row5;
9239 struct Row6;
9240 ˇ
9241 struct Row8;
9242 struct Row10;"#},
9243 base_text,
9244 &mut cx,
9245 );
9246 assert_hunk_revert(
9247 indoc! {r#"struct Row;
9248 struct Row2;
9249
9250 «ˇstruct Row4;
9251 struct» Row5;
9252 «struct Row6;
9253 ˇ»
9254 struct Row8;
9255 struct Row10;"#},
9256 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
9257 indoc! {r#"struct Row;
9258 struct Row2;
9259
9260 «ˇstruct Row4;
9261 struct» Row5;
9262 «struct Row6;
9263 ˇ»
9264 struct Row8;
9265 struct Row10;"#},
9266 base_text,
9267 &mut cx,
9268 );
9269
9270 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
9271 assert_hunk_revert(
9272 indoc! {r#"struct Row;
9273 ˇstruct Row2;
9274
9275 struct Row4;
9276 struct Row5;
9277 struct Row6;
9278
9279 struct Row8;ˇ
9280 struct Row10;"#},
9281 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
9282 indoc! {r#"struct Row;
9283 struct Row1;
9284 ˇstruct Row2;
9285
9286 struct Row4;
9287 struct Row5;
9288 struct Row6;
9289
9290 struct Row8;ˇ
9291 struct Row9;
9292 struct Row10;"#},
9293 base_text,
9294 &mut cx,
9295 );
9296 assert_hunk_revert(
9297 indoc! {r#"struct Row;
9298 struct Row2«ˇ;
9299 struct Row4;
9300 struct» Row5;
9301 «struct Row6;
9302
9303 struct Row8;ˇ»
9304 struct Row10;"#},
9305 vec![
9306 DiffHunkStatus::Removed,
9307 DiffHunkStatus::Removed,
9308 DiffHunkStatus::Removed,
9309 ],
9310 indoc! {r#"struct Row;
9311 struct Row1;
9312 struct Row2«ˇ;
9313
9314 struct Row4;
9315 struct» Row5;
9316 «struct Row6;
9317
9318 struct Row8;ˇ»
9319 struct Row9;
9320 struct Row10;"#},
9321 base_text,
9322 &mut cx,
9323 );
9324}
9325
9326#[gpui::test]
9327async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
9328 init_test(cx, |_| {});
9329
9330 let cols = 4;
9331 let rows = 10;
9332 let sample_text_1 = sample_text(rows, cols, 'a');
9333 assert_eq!(
9334 sample_text_1,
9335 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9336 );
9337 let sample_text_2 = sample_text(rows, cols, 'l');
9338 assert_eq!(
9339 sample_text_2,
9340 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9341 );
9342 let sample_text_3 = sample_text(rows, cols, 'v');
9343 assert_eq!(
9344 sample_text_3,
9345 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9346 );
9347
9348 fn diff_every_buffer_row(
9349 buffer: &Model<Buffer>,
9350 sample_text: String,
9351 cols: usize,
9352 cx: &mut gpui::TestAppContext,
9353 ) {
9354 // revert first character in each row, creating one large diff hunk per buffer
9355 let is_first_char = |offset: usize| offset % cols == 0;
9356 buffer.update(cx, |buffer, cx| {
9357 buffer.set_text(
9358 sample_text
9359 .chars()
9360 .enumerate()
9361 .map(|(offset, c)| if is_first_char(offset) { 'X' } else { c })
9362 .collect::<String>(),
9363 cx,
9364 );
9365 buffer.set_diff_base(Some(sample_text), cx);
9366 });
9367 cx.executor().run_until_parked();
9368 }
9369
9370 let buffer_1 = cx.new_model(|cx| {
9371 Buffer::new(
9372 0,
9373 BufferId::new(cx.entity_id().as_u64()).unwrap(),
9374 sample_text_1.clone(),
9375 )
9376 });
9377 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
9378
9379 let buffer_2 = cx.new_model(|cx| {
9380 Buffer::new(
9381 1,
9382 BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
9383 sample_text_2.clone(),
9384 )
9385 });
9386 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
9387
9388 let buffer_3 = cx.new_model(|cx| {
9389 Buffer::new(
9390 2,
9391 BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
9392 sample_text_3.clone(),
9393 )
9394 });
9395 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
9396
9397 let multibuffer = cx.new_model(|cx| {
9398 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9399 multibuffer.push_excerpts(
9400 buffer_1.clone(),
9401 [
9402 ExcerptRange {
9403 context: Point::new(0, 0)..Point::new(3, 0),
9404 primary: None,
9405 },
9406 ExcerptRange {
9407 context: Point::new(5, 0)..Point::new(7, 0),
9408 primary: None,
9409 },
9410 ExcerptRange {
9411 context: Point::new(9, 0)..Point::new(10, 4),
9412 primary: None,
9413 },
9414 ],
9415 cx,
9416 );
9417 multibuffer.push_excerpts(
9418 buffer_2.clone(),
9419 [
9420 ExcerptRange {
9421 context: Point::new(0, 0)..Point::new(3, 0),
9422 primary: None,
9423 },
9424 ExcerptRange {
9425 context: Point::new(5, 0)..Point::new(7, 0),
9426 primary: None,
9427 },
9428 ExcerptRange {
9429 context: Point::new(9, 0)..Point::new(10, 4),
9430 primary: None,
9431 },
9432 ],
9433 cx,
9434 );
9435 multibuffer.push_excerpts(
9436 buffer_3.clone(),
9437 [
9438 ExcerptRange {
9439 context: Point::new(0, 0)..Point::new(3, 0),
9440 primary: None,
9441 },
9442 ExcerptRange {
9443 context: Point::new(5, 0)..Point::new(7, 0),
9444 primary: None,
9445 },
9446 ExcerptRange {
9447 context: Point::new(9, 0)..Point::new(10, 4),
9448 primary: None,
9449 },
9450 ],
9451 cx,
9452 );
9453 multibuffer
9454 });
9455
9456 let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
9457 editor.update(cx, |editor, cx| {
9458 assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n");
9459 editor.select_all(&SelectAll, cx);
9460 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9461 });
9462 cx.executor().run_until_parked();
9463 // When all ranges are selected, all buffer hunks are reverted.
9464 editor.update(cx, |editor, cx| {
9465 assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
9466 });
9467 buffer_1.update(cx, |buffer, _| {
9468 assert_eq!(buffer.text(), sample_text_1);
9469 });
9470 buffer_2.update(cx, |buffer, _| {
9471 assert_eq!(buffer.text(), sample_text_2);
9472 });
9473 buffer_3.update(cx, |buffer, _| {
9474 assert_eq!(buffer.text(), sample_text_3);
9475 });
9476
9477 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
9478 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
9479 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
9480 editor.update(cx, |editor, cx| {
9481 editor.change_selections(None, cx, |s| {
9482 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
9483 });
9484 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9485 });
9486 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
9487 // but not affect buffer_2 and its related excerpts.
9488 editor.update(cx, |editor, cx| {
9489 assert_eq!(
9490 editor.text(cx),
9491 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n"
9492 );
9493 });
9494 buffer_1.update(cx, |buffer, _| {
9495 assert_eq!(buffer.text(), sample_text_1);
9496 });
9497 buffer_2.update(cx, |buffer, _| {
9498 assert_eq!(
9499 buffer.text(),
9500 "XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX"
9501 );
9502 });
9503 buffer_3.update(cx, |buffer, _| {
9504 assert_eq!(
9505 buffer.text(),
9506 "XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X"
9507 );
9508 });
9509}
9510
9511#[gpui::test]
9512async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
9513 init_test(cx, |_| {});
9514
9515 let cols = 4;
9516 let rows = 10;
9517 let sample_text_1 = sample_text(rows, cols, 'a');
9518 assert_eq!(
9519 sample_text_1,
9520 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9521 );
9522 let sample_text_2 = sample_text(rows, cols, 'l');
9523 assert_eq!(
9524 sample_text_2,
9525 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9526 );
9527 let sample_text_3 = sample_text(rows, cols, 'v');
9528 assert_eq!(
9529 sample_text_3,
9530 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9531 );
9532
9533 let buffer_1 = cx.new_model(|cx| {
9534 Buffer::new(
9535 0,
9536 BufferId::new(cx.entity_id().as_u64()).unwrap(),
9537 sample_text_1.clone(),
9538 )
9539 });
9540
9541 let buffer_2 = cx.new_model(|cx| {
9542 Buffer::new(
9543 1,
9544 BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
9545 sample_text_2.clone(),
9546 )
9547 });
9548
9549 let buffer_3 = cx.new_model(|cx| {
9550 Buffer::new(
9551 2,
9552 BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
9553 sample_text_3.clone(),
9554 )
9555 });
9556
9557 let multi_buffer = cx.new_model(|cx| {
9558 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9559 multibuffer.push_excerpts(
9560 buffer_1.clone(),
9561 [
9562 ExcerptRange {
9563 context: Point::new(0, 0)..Point::new(3, 0),
9564 primary: None,
9565 },
9566 ExcerptRange {
9567 context: Point::new(5, 0)..Point::new(7, 0),
9568 primary: None,
9569 },
9570 ExcerptRange {
9571 context: Point::new(9, 0)..Point::new(10, 4),
9572 primary: None,
9573 },
9574 ],
9575 cx,
9576 );
9577 multibuffer.push_excerpts(
9578 buffer_2.clone(),
9579 [
9580 ExcerptRange {
9581 context: Point::new(0, 0)..Point::new(3, 0),
9582 primary: None,
9583 },
9584 ExcerptRange {
9585 context: Point::new(5, 0)..Point::new(7, 0),
9586 primary: None,
9587 },
9588 ExcerptRange {
9589 context: Point::new(9, 0)..Point::new(10, 4),
9590 primary: None,
9591 },
9592 ],
9593 cx,
9594 );
9595 multibuffer.push_excerpts(
9596 buffer_3.clone(),
9597 [
9598 ExcerptRange {
9599 context: Point::new(0, 0)..Point::new(3, 0),
9600 primary: None,
9601 },
9602 ExcerptRange {
9603 context: Point::new(5, 0)..Point::new(7, 0),
9604 primary: None,
9605 },
9606 ExcerptRange {
9607 context: Point::new(9, 0)..Point::new(10, 4),
9608 primary: None,
9609 },
9610 ],
9611 cx,
9612 );
9613 multibuffer
9614 });
9615
9616 let fs = FakeFs::new(cx.executor());
9617 fs.insert_tree(
9618 "/a",
9619 json!({
9620 "main.rs": sample_text_1,
9621 "other.rs": sample_text_2,
9622 "lib.rs": sample_text_3,
9623 }),
9624 )
9625 .await;
9626 let project = Project::test(fs, ["/a".as_ref()], cx).await;
9627 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
9628 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9629 let multi_buffer_editor =
9630 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
9631 let multibuffer_item_id = workspace
9632 .update(cx, |workspace, cx| {
9633 assert!(
9634 workspace.active_item(cx).is_none(),
9635 "active item should be None before the first item is added"
9636 );
9637 workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), cx);
9638 let active_item = workspace
9639 .active_item(cx)
9640 .expect("should have an active item after adding the multi buffer");
9641 assert!(
9642 !active_item.is_singleton(cx),
9643 "A multi buffer was expected to active after adding"
9644 );
9645 active_item.item_id()
9646 })
9647 .unwrap();
9648 cx.executor().run_until_parked();
9649
9650 multi_buffer_editor.update(cx, |editor, cx| {
9651 editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2)));
9652 editor.open_excerpts(&OpenExcerpts, cx);
9653 });
9654 cx.executor().run_until_parked();
9655 let first_item_id = workspace
9656 .update(cx, |workspace, cx| {
9657 let active_item = workspace
9658 .active_item(cx)
9659 .expect("should have an active item after navigating into the 1st buffer");
9660 let first_item_id = active_item.item_id();
9661 assert_ne!(
9662 first_item_id, multibuffer_item_id,
9663 "Should navigate into the 1st buffer and activate it"
9664 );
9665 assert!(
9666 active_item.is_singleton(cx),
9667 "New active item should be a singleton buffer"
9668 );
9669 assert_eq!(
9670 active_item
9671 .act_as::<Editor>(cx)
9672 .expect("should have navigated into an editor for the 1st buffer")
9673 .read(cx)
9674 .text(cx),
9675 sample_text_1
9676 );
9677
9678 workspace
9679 .go_back(workspace.active_pane().downgrade(), cx)
9680 .detach_and_log_err(cx);
9681
9682 first_item_id
9683 })
9684 .unwrap();
9685 cx.executor().run_until_parked();
9686 workspace
9687 .update(cx, |workspace, cx| {
9688 let active_item = workspace
9689 .active_item(cx)
9690 .expect("should have an active item after navigating back");
9691 assert_eq!(
9692 active_item.item_id(),
9693 multibuffer_item_id,
9694 "Should navigate back to the multi buffer"
9695 );
9696 assert!(!active_item.is_singleton(cx));
9697 })
9698 .unwrap();
9699
9700 multi_buffer_editor.update(cx, |editor, cx| {
9701 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9702 s.select_ranges(Some(39..40))
9703 });
9704 editor.open_excerpts(&OpenExcerpts, cx);
9705 });
9706 cx.executor().run_until_parked();
9707 let second_item_id = workspace
9708 .update(cx, |workspace, cx| {
9709 let active_item = workspace
9710 .active_item(cx)
9711 .expect("should have an active item after navigating into the 2nd buffer");
9712 let second_item_id = active_item.item_id();
9713 assert_ne!(
9714 second_item_id, multibuffer_item_id,
9715 "Should navigate away from the multibuffer"
9716 );
9717 assert_ne!(
9718 second_item_id, first_item_id,
9719 "Should navigate into the 2nd buffer and activate it"
9720 );
9721 assert!(
9722 active_item.is_singleton(cx),
9723 "New active item should be a singleton buffer"
9724 );
9725 assert_eq!(
9726 active_item
9727 .act_as::<Editor>(cx)
9728 .expect("should have navigated into an editor")
9729 .read(cx)
9730 .text(cx),
9731 sample_text_2
9732 );
9733
9734 workspace
9735 .go_back(workspace.active_pane().downgrade(), cx)
9736 .detach_and_log_err(cx);
9737
9738 second_item_id
9739 })
9740 .unwrap();
9741 cx.executor().run_until_parked();
9742 workspace
9743 .update(cx, |workspace, cx| {
9744 let active_item = workspace
9745 .active_item(cx)
9746 .expect("should have an active item after navigating back from the 2nd buffer");
9747 assert_eq!(
9748 active_item.item_id(),
9749 multibuffer_item_id,
9750 "Should navigate back from the 2nd buffer to the multi buffer"
9751 );
9752 assert!(!active_item.is_singleton(cx));
9753 })
9754 .unwrap();
9755
9756 multi_buffer_editor.update(cx, |editor, cx| {
9757 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9758 s.select_ranges(Some(60..70))
9759 });
9760 editor.open_excerpts(&OpenExcerpts, cx);
9761 });
9762 cx.executor().run_until_parked();
9763 workspace
9764 .update(cx, |workspace, cx| {
9765 let active_item = workspace
9766 .active_item(cx)
9767 .expect("should have an active item after navigating into the 3rd buffer");
9768 let third_item_id = active_item.item_id();
9769 assert_ne!(
9770 third_item_id, multibuffer_item_id,
9771 "Should navigate into the 3rd buffer and activate it"
9772 );
9773 assert_ne!(third_item_id, first_item_id);
9774 assert_ne!(third_item_id, second_item_id);
9775 assert!(
9776 active_item.is_singleton(cx),
9777 "New active item should be a singleton buffer"
9778 );
9779 assert_eq!(
9780 active_item
9781 .act_as::<Editor>(cx)
9782 .expect("should have navigated into an editor")
9783 .read(cx)
9784 .text(cx),
9785 sample_text_3
9786 );
9787
9788 workspace
9789 .go_back(workspace.active_pane().downgrade(), cx)
9790 .detach_and_log_err(cx);
9791 })
9792 .unwrap();
9793 cx.executor().run_until_parked();
9794 workspace
9795 .update(cx, |workspace, cx| {
9796 let active_item = workspace
9797 .active_item(cx)
9798 .expect("should have an active item after navigating back from the 3rd buffer");
9799 assert_eq!(
9800 active_item.item_id(),
9801 multibuffer_item_id,
9802 "Should navigate back from the 3rd buffer to the multi buffer"
9803 );
9804 assert!(!active_item.is_singleton(cx));
9805 })
9806 .unwrap();
9807}
9808
9809fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
9810 let point = DisplayPoint::new(row as u32, column as u32);
9811 point..point
9812}
9813
9814fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
9815 let (text, ranges) = marked_text_ranges(marked_text, true);
9816 assert_eq!(view.text(cx), text);
9817 assert_eq!(
9818 view.selections.ranges(cx),
9819 ranges,
9820 "Assert selections are {}",
9821 marked_text
9822 );
9823}
9824
9825/// Handle completion request passing a marked string specifying where the completion
9826/// should be triggered from using '|' character, what range should be replaced, and what completions
9827/// should be returned using '<' and '>' to delimit the range
9828pub fn handle_completion_request(
9829 cx: &mut EditorLspTestContext,
9830 marked_string: &str,
9831 completions: Vec<&'static str>,
9832) -> impl Future<Output = ()> {
9833 let complete_from_marker: TextRangeMarker = '|'.into();
9834 let replace_range_marker: TextRangeMarker = ('<', '>').into();
9835 let (_, mut marked_ranges) = marked_text_ranges_by(
9836 marked_string,
9837 vec![complete_from_marker.clone(), replace_range_marker.clone()],
9838 );
9839
9840 let complete_from_position =
9841 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
9842 let replace_range =
9843 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
9844
9845 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
9846 let completions = completions.clone();
9847 async move {
9848 assert_eq!(params.text_document_position.text_document.uri, url.clone());
9849 assert_eq!(
9850 params.text_document_position.position,
9851 complete_from_position
9852 );
9853 Ok(Some(lsp::CompletionResponse::Array(
9854 completions
9855 .iter()
9856 .map(|completion_text| lsp::CompletionItem {
9857 label: completion_text.to_string(),
9858 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9859 range: replace_range,
9860 new_text: completion_text.to_string(),
9861 })),
9862 ..Default::default()
9863 })
9864 .collect(),
9865 )))
9866 }
9867 });
9868
9869 async move {
9870 request.next().await;
9871 }
9872}
9873
9874fn handle_resolve_completion_request(
9875 cx: &mut EditorLspTestContext,
9876 edits: Option<Vec<(&'static str, &'static str)>>,
9877) -> impl Future<Output = ()> {
9878 let edits = edits.map(|edits| {
9879 edits
9880 .iter()
9881 .map(|(marked_string, new_text)| {
9882 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
9883 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
9884 lsp::TextEdit::new(replace_range, new_text.to_string())
9885 })
9886 .collect::<Vec<_>>()
9887 });
9888
9889 let mut request =
9890 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
9891 let edits = edits.clone();
9892 async move {
9893 Ok(lsp::CompletionItem {
9894 additional_text_edits: edits,
9895 ..Default::default()
9896 })
9897 }
9898 });
9899
9900 async move {
9901 request.next().await;
9902 }
9903}
9904
9905fn handle_copilot_completion_request(
9906 lsp: &lsp::FakeLanguageServer,
9907 completions: Vec<copilot::request::Completion>,
9908 completions_cycling: Vec<copilot::request::Completion>,
9909) {
9910 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
9911 let completions = completions.clone();
9912 async move {
9913 Ok(copilot::request::GetCompletionsResult {
9914 completions: completions.clone(),
9915 })
9916 }
9917 });
9918 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
9919 let completions_cycling = completions_cycling.clone();
9920 async move {
9921 Ok(copilot::request::GetCompletionsResult {
9922 completions: completions_cycling.clone(),
9923 })
9924 }
9925 });
9926}
9927
9928pub(crate) fn update_test_language_settings(
9929 cx: &mut TestAppContext,
9930 f: impl Fn(&mut AllLanguageSettingsContent),
9931) {
9932 _ = cx.update(|cx| {
9933 cx.update_global(|store: &mut SettingsStore, cx| {
9934 store.update_user_settings::<AllLanguageSettings>(cx, f);
9935 });
9936 });
9937}
9938
9939pub(crate) fn update_test_project_settings(
9940 cx: &mut TestAppContext,
9941 f: impl Fn(&mut ProjectSettings),
9942) {
9943 _ = cx.update(|cx| {
9944 cx.update_global(|store: &mut SettingsStore, cx| {
9945 store.update_user_settings::<ProjectSettings>(cx, f);
9946 });
9947 });
9948}
9949
9950pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
9951 _ = cx.update(|cx| {
9952 let store = SettingsStore::test(cx);
9953 cx.set_global(store);
9954 theme::init(theme::LoadThemes::JustBase, cx);
9955 release_channel::init("0.0.0", cx);
9956 client::init_settings(cx);
9957 language::init(cx);
9958 Project::init_settings(cx);
9959 workspace::init_settings(cx);
9960 crate::init(cx);
9961 });
9962
9963 update_test_language_settings(cx, f);
9964}
9965
9966pub(crate) fn rust_lang() -> Arc<Language> {
9967 Arc::new(Language::new(
9968 LanguageConfig {
9969 name: "Rust".into(),
9970 matcher: LanguageMatcher {
9971 path_suffixes: vec!["rs".to_string()],
9972 ..Default::default()
9973 },
9974 ..Default::default()
9975 },
9976 Some(tree_sitter_rust::language()),
9977 ))
9978}
9979
9980#[track_caller]
9981fn assert_hunk_revert(
9982 not_reverted_text_with_selections: &str,
9983 expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
9984 expected_reverted_text_with_selections: &str,
9985 base_text: &str,
9986 cx: &mut EditorLspTestContext,
9987) {
9988 cx.set_state(not_reverted_text_with_selections);
9989 cx.update_editor(|editor, cx| {
9990 editor
9991 .buffer()
9992 .read(cx)
9993 .as_singleton()
9994 .unwrap()
9995 .update(cx, |buffer, cx| {
9996 buffer.set_diff_base(Some(base_text.to_string()), cx);
9997 });
9998 });
9999 cx.executor().run_until_parked();
10000
10001 let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
10002 let snapshot = editor
10003 .buffer()
10004 .read(cx)
10005 .as_singleton()
10006 .unwrap()
10007 .read(cx)
10008 .snapshot();
10009 let reverted_hunk_statuses = snapshot
10010 .git_diff_hunks_in_row_range(0..u32::MAX)
10011 .map(|hunk| hunk.status())
10012 .collect::<Vec<_>>();
10013
10014 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
10015 reverted_hunk_statuses
10016 });
10017 cx.executor().run_until_parked();
10018 cx.assert_editor_state(expected_reverted_text_with_selections);
10019 assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
10020}