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