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