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