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