1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkStatus};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, 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, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::IndentGuide;
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::project_settings::{LspSettings, ProjectSettings};
31use project::FakeFs;
32use serde_json::{self, json};
33use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
34use std::{
35 iter,
36 sync::atomic::{self, AtomicUsize},
37};
38use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
39use unindent::Unindent;
40use util::{
41 assert_set_eq, path,
42 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
43 uri,
44};
45use workspace::{
46 item::{FollowEvent, FollowableItem, Item, ItemHandle},
47 NavigationEntry, ViewId,
48};
49
50#[gpui::test]
51fn test_edit_events(cx: &mut TestAppContext) {
52 init_test(cx, |_| {});
53
54 let buffer = cx.new(|cx| {
55 let mut buffer = language::Buffer::local("123456", cx);
56 buffer.set_group_interval(Duration::from_secs(1));
57 buffer
58 });
59
60 let events = Rc::new(RefCell::new(Vec::new()));
61 let editor1 = cx.add_window({
62 let events = events.clone();
63 |window, cx| {
64 let entity = cx.entity().clone();
65 cx.subscribe_in(
66 &entity,
67 window,
68 move |_, _, event: &EditorEvent, _, _| match event {
69 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
70 EditorEvent::BufferEdited => {
71 events.borrow_mut().push(("editor1", "buffer edited"))
72 }
73 _ => {}
74 },
75 )
76 .detach();
77 Editor::for_buffer(buffer.clone(), None, window, cx)
78 }
79 });
80
81 let editor2 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 cx.subscribe_in(
85 &cx.entity().clone(),
86 window,
87 move |_, _, event: &EditorEvent, _, _| match event {
88 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
89 EditorEvent::BufferEdited => {
90 events.borrow_mut().push(("editor2", "buffer edited"))
91 }
92 _ => {}
93 },
94 )
95 .detach();
96 Editor::for_buffer(buffer.clone(), None, window, cx)
97 }
98 });
99
100 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
101
102 // Mutating editor 1 will emit an `Edited` event only for that editor.
103 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
104 assert_eq!(
105 mem::take(&mut *events.borrow_mut()),
106 [
107 ("editor1", "edited"),
108 ("editor1", "buffer edited"),
109 ("editor2", "buffer edited"),
110 ]
111 );
112
113 // Mutating editor 2 will emit an `Edited` event only for that editor.
114 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
115 assert_eq!(
116 mem::take(&mut *events.borrow_mut()),
117 [
118 ("editor2", "edited"),
119 ("editor1", "buffer edited"),
120 ("editor2", "buffer edited"),
121 ]
122 );
123
124 // Undoing on editor 1 will emit an `Edited` event only for that editor.
125 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
126 assert_eq!(
127 mem::take(&mut *events.borrow_mut()),
128 [
129 ("editor1", "edited"),
130 ("editor1", "buffer edited"),
131 ("editor2", "buffer edited"),
132 ]
133 );
134
135 // Redoing on editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Undoing on editor 2 will emit an `Edited` event only for that editor.
147 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor2", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Redoing on editor 2 will emit an `Edited` event only for that editor.
158 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor2", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // No event is emitted when the mutation is a no-op.
169 _ = editor2.update(cx, |editor, window, cx| {
170 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
171
172 editor.backspace(&Backspace, window, cx);
173 });
174 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
175}
176
177#[gpui::test]
178fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
179 init_test(cx, |_| {});
180
181 let mut now = Instant::now();
182 let group_interval = Duration::from_millis(1);
183 let buffer = cx.new(|cx| {
184 let mut buf = language::Buffer::local("123456", cx);
185 buf.set_group_interval(group_interval);
186 buf
187 });
188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
189 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
190
191 _ = editor.update(cx, |editor, window, cx| {
192 editor.start_transaction_at(now, window, cx);
193 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
194
195 editor.insert("cd", window, cx);
196 editor.end_transaction_at(now, cx);
197 assert_eq!(editor.text(cx), "12cd56");
198 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
199
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
202 editor.insert("e", window, cx);
203 editor.end_transaction_at(now, cx);
204 assert_eq!(editor.text(cx), "12cde6");
205 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
206
207 now += group_interval + Duration::from_millis(1);
208 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
209
210 // Simulate an edit in another editor
211 buffer.update(cx, |buffer, cx| {
212 buffer.start_transaction_at(now, cx);
213 buffer.edit([(0..1, "a")], None, cx);
214 buffer.edit([(1..1, "b")], None, cx);
215 buffer.end_transaction_at(now, cx);
216 });
217
218 assert_eq!(editor.text(cx), "ab2cde6");
219 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
220
221 // Last transaction happened past the group interval in a different editor.
222 // Undo it individually and don't restore selections.
223 editor.undo(&Undo, window, cx);
224 assert_eq!(editor.text(cx), "12cde6");
225 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
226
227 // First two transactions happened within the group interval in this editor.
228 // Undo them together and restore selections.
229 editor.undo(&Undo, window, cx);
230 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
231 assert_eq!(editor.text(cx), "123456");
232 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
233
234 // Redo the first two transactions together.
235 editor.redo(&Redo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
238
239 // Redo the last transaction on its own.
240 editor.redo(&Redo, window, cx);
241 assert_eq!(editor.text(cx), "ab2cde6");
242 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
243
244 // Test empty transactions.
245 editor.start_transaction_at(now, window, cx);
246 editor.end_transaction_at(now, cx);
247 editor.undo(&Undo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 });
250}
251
252#[gpui::test]
253fn test_ime_composition(cx: &mut TestAppContext) {
254 init_test(cx, |_| {});
255
256 let buffer = cx.new(|cx| {
257 let mut buffer = language::Buffer::local("abcde", cx);
258 // Ensure automatic grouping doesn't occur.
259 buffer.set_group_interval(Duration::ZERO);
260 buffer
261 });
262
263 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
264 cx.add_window(|window, cx| {
265 let mut editor = build_editor(buffer.clone(), window, cx);
266
267 // Start a new IME composition.
268 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
269 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
270 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
271 assert_eq!(editor.text(cx), "äbcde");
272 assert_eq!(
273 editor.marked_text_ranges(cx),
274 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
275 );
276
277 // Finalize IME composition.
278 editor.replace_text_in_range(None, "ā", window, cx);
279 assert_eq!(editor.text(cx), "ābcde");
280 assert_eq!(editor.marked_text_ranges(cx), None);
281
282 // IME composition edits are grouped and are undone/redone at once.
283 editor.undo(&Default::default(), window, cx);
284 assert_eq!(editor.text(cx), "abcde");
285 assert_eq!(editor.marked_text_ranges(cx), None);
286 editor.redo(&Default::default(), window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // Start a new IME composition.
291 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
292 assert_eq!(
293 editor.marked_text_ranges(cx),
294 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
295 );
296
297 // Undoing during an IME composition cancels it.
298 editor.undo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
303 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
304 assert_eq!(editor.text(cx), "ābcdè");
305 assert_eq!(
306 editor.marked_text_ranges(cx),
307 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
308 );
309
310 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
311 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
312 assert_eq!(editor.text(cx), "ābcdę");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314
315 // Start a new IME composition with multiple cursors.
316 editor.change_selections(None, window, cx, |s| {
317 s.select_ranges([
318 OffsetUtf16(1)..OffsetUtf16(1),
319 OffsetUtf16(3)..OffsetUtf16(3),
320 OffsetUtf16(5)..OffsetUtf16(5),
321 ])
322 });
323 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
324 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
325 assert_eq!(
326 editor.marked_text_ranges(cx),
327 Some(vec![
328 OffsetUtf16(0)..OffsetUtf16(3),
329 OffsetUtf16(4)..OffsetUtf16(7),
330 OffsetUtf16(8)..OffsetUtf16(11)
331 ])
332 );
333
334 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
335 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
336 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(1)..OffsetUtf16(2),
341 OffsetUtf16(5)..OffsetUtf16(6),
342 OffsetUtf16(9)..OffsetUtf16(10)
343 ])
344 );
345
346 // Finalize IME composition with multiple cursors.
347 editor.replace_text_in_range(Some(9..10), "2", window, cx);
348 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
349 assert_eq!(editor.marked_text_ranges(cx), None);
350
351 editor
352 });
353}
354
355#[gpui::test]
356fn test_selection_with_mouse(cx: &mut TestAppContext) {
357 init_test(cx, |_| {});
358
359 let editor = cx.add_window(|window, cx| {
360 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
361 build_editor(buffer, window, cx)
362 });
363
364 _ = editor.update(cx, |editor, window, cx| {
365 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
366 });
367 assert_eq!(
368 editor
369 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
370 .unwrap(),
371 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
372 );
373
374 _ = editor.update(cx, |editor, window, cx| {
375 editor.update_selection(
376 DisplayPoint::new(DisplayRow(3), 3),
377 0,
378 gpui::Point::<f32>::default(),
379 window,
380 cx,
381 );
382 });
383
384 assert_eq!(
385 editor
386 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
387 .unwrap(),
388 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
389 );
390
391 _ = editor.update(cx, |editor, window, cx| {
392 editor.update_selection(
393 DisplayPoint::new(DisplayRow(1), 1),
394 0,
395 gpui::Point::<f32>::default(),
396 window,
397 cx,
398 );
399 });
400
401 assert_eq!(
402 editor
403 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
404 .unwrap(),
405 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
406 );
407
408 _ = editor.update(cx, |editor, window, cx| {
409 editor.end_selection(window, cx);
410 editor.update_selection(
411 DisplayPoint::new(DisplayRow(3), 3),
412 0,
413 gpui::Point::<f32>::default(),
414 window,
415 cx,
416 );
417 });
418
419 assert_eq!(
420 editor
421 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
422 .unwrap(),
423 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
424 );
425
426 _ = editor.update(cx, |editor, window, cx| {
427 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
428 editor.update_selection(
429 DisplayPoint::new(DisplayRow(0), 0),
430 0,
431 gpui::Point::<f32>::default(),
432 window,
433 cx,
434 );
435 });
436
437 assert_eq!(
438 editor
439 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
440 .unwrap(),
441 [
442 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
443 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
444 ]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.end_selection(window, cx);
449 });
450
451 assert_eq!(
452 editor
453 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
454 .unwrap(),
455 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
456 );
457}
458
459#[gpui::test]
460fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
461 init_test(cx, |_| {});
462
463 let editor = cx.add_window(|window, cx| {
464 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
465 build_editor(buffer, window, cx)
466 });
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
470 });
471
472 _ = editor.update(cx, |editor, window, cx| {
473 editor.end_selection(window, cx);
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 assert_eq!(
485 editor
486 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
487 .unwrap(),
488 [
489 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
490 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
491 ]
492 );
493
494 _ = editor.update(cx, |editor, window, cx| {
495 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
496 });
497
498 _ = editor.update(cx, |editor, window, cx| {
499 editor.end_selection(window, cx);
500 });
501
502 assert_eq!(
503 editor
504 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
505 .unwrap(),
506 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
507 );
508}
509
510#[gpui::test]
511fn test_canceling_pending_selection(cx: &mut TestAppContext) {
512 init_test(cx, |_| {});
513
514 let editor = cx.add_window(|window, cx| {
515 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
516 build_editor(buffer, window, cx)
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
521 assert_eq!(
522 editor.selections.display_ranges(cx),
523 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
524 );
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.update_selection(
529 DisplayPoint::new(DisplayRow(3), 3),
530 0,
531 gpui::Point::<f32>::default(),
532 window,
533 cx,
534 );
535 assert_eq!(
536 editor.selections.display_ranges(cx),
537 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
538 );
539 });
540
541 _ = editor.update(cx, |editor, window, cx| {
542 editor.cancel(&Cancel, window, cx);
543 editor.update_selection(
544 DisplayPoint::new(DisplayRow(1), 1),
545 0,
546 gpui::Point::<f32>::default(),
547 window,
548 cx,
549 );
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
553 );
554 });
555}
556
557#[gpui::test]
558fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
559 init_test(cx, |_| {});
560
561 let editor = cx.add_window(|window, cx| {
562 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
563 build_editor(buffer, window, cx)
564 });
565
566 _ = editor.update(cx, |editor, window, cx| {
567 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
568 assert_eq!(
569 editor.selections.display_ranges(cx),
570 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
571 );
572
573 editor.move_down(&Default::default(), window, cx);
574 assert_eq!(
575 editor.selections.display_ranges(cx),
576 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
577 );
578
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_up(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
589 );
590 });
591}
592
593#[gpui::test]
594fn test_clone(cx: &mut TestAppContext) {
595 init_test(cx, |_| {});
596
597 let (text, selection_ranges) = marked_text_ranges(
598 indoc! {"
599 one
600 two
601 threeˇ
602 four
603 fiveˇ
604 "},
605 true,
606 );
607
608 let editor = cx.add_window(|window, cx| {
609 let buffer = MultiBuffer::build_simple(&text, cx);
610 build_editor(buffer, window, cx)
611 });
612
613 _ = editor.update(cx, |editor, window, cx| {
614 editor.change_selections(None, window, cx, |s| {
615 s.select_ranges(selection_ranges.clone())
616 });
617 editor.fold_creases(
618 vec![
619 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
620 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
621 ],
622 true,
623 window,
624 cx,
625 );
626 });
627
628 let cloned_editor = editor
629 .update(cx, |editor, _, cx| {
630 cx.open_window(Default::default(), |window, cx| {
631 cx.new(|cx| editor.clone(window, cx))
632 })
633 })
634 .unwrap()
635 .unwrap();
636
637 let snapshot = editor
638 .update(cx, |e, window, cx| e.snapshot(window, cx))
639 .unwrap();
640 let cloned_snapshot = cloned_editor
641 .update(cx, |e, window, cx| e.snapshot(window, cx))
642 .unwrap();
643
644 assert_eq!(
645 cloned_editor
646 .update(cx, |e, _, cx| e.display_text(cx))
647 .unwrap(),
648 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
649 );
650 assert_eq!(
651 cloned_snapshot
652 .folds_in_range(0..text.len())
653 .collect::<Vec<_>>(),
654 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
655 );
656 assert_set_eq!(
657 cloned_editor
658 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
659 .unwrap(),
660 editor
661 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
662 .unwrap()
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
667 .unwrap(),
668 editor
669 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
670 .unwrap()
671 );
672}
673
674#[gpui::test]
675async fn test_navigation_history(cx: &mut TestAppContext) {
676 init_test(cx, |_| {});
677
678 use workspace::item::Item;
679
680 let fs = FakeFs::new(cx.executor());
681 let project = Project::test(fs, [], cx).await;
682 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
683 let pane = workspace
684 .update(cx, |workspace, _, _| workspace.active_pane().clone())
685 .unwrap();
686
687 _ = workspace.update(cx, |_v, window, cx| {
688 cx.new(|cx| {
689 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
690 let mut editor = build_editor(buffer.clone(), window, cx);
691 let handle = cx.entity();
692 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
693
694 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
695 editor.nav_history.as_mut().unwrap().pop_backward(cx)
696 }
697
698 // Move the cursor a small distance.
699 // Nothing is added to the navigation history.
700 editor.change_selections(None, window, cx, |s| {
701 s.select_display_ranges([
702 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
703 ])
704 });
705 editor.change_selections(None, window, cx, |s| {
706 s.select_display_ranges([
707 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
708 ])
709 });
710 assert!(pop_history(&mut editor, cx).is_none());
711
712 // Move the cursor a large distance.
713 // The history can jump back to the previous position.
714 editor.change_selections(None, window, cx, |s| {
715 s.select_display_ranges([
716 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
717 ])
718 });
719 let nav_entry = pop_history(&mut editor, cx).unwrap();
720 editor.navigate(nav_entry.data.unwrap(), window, cx);
721 assert_eq!(nav_entry.item.id(), cx.entity_id());
722 assert_eq!(
723 editor.selections.display_ranges(cx),
724 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
725 );
726 assert!(pop_history(&mut editor, cx).is_none());
727
728 // Move the cursor a small distance via the mouse.
729 // Nothing is added to the navigation history.
730 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
731 editor.end_selection(window, cx);
732 assert_eq!(
733 editor.selections.display_ranges(cx),
734 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
735 );
736 assert!(pop_history(&mut editor, cx).is_none());
737
738 // Move the cursor a large distance via the mouse.
739 // The history can jump back to the previous position.
740 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
741 editor.end_selection(window, cx);
742 assert_eq!(
743 editor.selections.display_ranges(cx),
744 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
745 );
746 let nav_entry = pop_history(&mut editor, cx).unwrap();
747 editor.navigate(nav_entry.data.unwrap(), window, cx);
748 assert_eq!(nav_entry.item.id(), cx.entity_id());
749 assert_eq!(
750 editor.selections.display_ranges(cx),
751 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
752 );
753 assert!(pop_history(&mut editor, cx).is_none());
754
755 // Set scroll position to check later
756 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
757 let original_scroll_position = editor.scroll_manager.anchor();
758
759 // Jump to the end of the document and adjust scroll
760 editor.move_to_end(&MoveToEnd, window, cx);
761 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
762 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
763
764 let nav_entry = pop_history(&mut editor, cx).unwrap();
765 editor.navigate(nav_entry.data.unwrap(), window, cx);
766 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
767
768 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
769 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
770 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
771 let invalid_point = Point::new(9999, 0);
772 editor.navigate(
773 Box::new(NavigationData {
774 cursor_anchor: invalid_anchor,
775 cursor_position: invalid_point,
776 scroll_anchor: ScrollAnchor {
777 anchor: invalid_anchor,
778 offset: Default::default(),
779 },
780 scroll_top_row: invalid_point.row,
781 }),
782 window,
783 cx,
784 );
785 assert_eq!(
786 editor.selections.display_ranges(cx),
787 &[editor.max_point(cx)..editor.max_point(cx)]
788 );
789 assert_eq!(
790 editor.scroll_position(cx),
791 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
792 );
793
794 editor
795 })
796 });
797}
798
799#[gpui::test]
800fn test_cancel(cx: &mut TestAppContext) {
801 init_test(cx, |_| {});
802
803 let editor = cx.add_window(|window, cx| {
804 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
805 build_editor(buffer, window, cx)
806 });
807
808 _ = editor.update(cx, |editor, window, cx| {
809 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
810 editor.update_selection(
811 DisplayPoint::new(DisplayRow(1), 1),
812 0,
813 gpui::Point::<f32>::default(),
814 window,
815 cx,
816 );
817 editor.end_selection(window, cx);
818
819 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
820 editor.update_selection(
821 DisplayPoint::new(DisplayRow(0), 3),
822 0,
823 gpui::Point::<f32>::default(),
824 window,
825 cx,
826 );
827 editor.end_selection(window, cx);
828 assert_eq!(
829 editor.selections.display_ranges(cx),
830 [
831 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
832 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
833 ]
834 );
835 });
836
837 _ = editor.update(cx, |editor, window, cx| {
838 editor.cancel(&Cancel, window, cx);
839 assert_eq!(
840 editor.selections.display_ranges(cx),
841 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852}
853
854#[gpui::test]
855fn test_fold_action(cx: &mut TestAppContext) {
856 init_test(cx, |_| {});
857
858 let editor = cx.add_window(|window, cx| {
859 let buffer = MultiBuffer::build_simple(
860 &"
861 impl Foo {
862 // Hello!
863
864 fn a() {
865 1
866 }
867
868 fn b() {
869 2
870 }
871
872 fn c() {
873 3
874 }
875 }
876 "
877 .unindent(),
878 cx,
879 );
880 build_editor(buffer.clone(), window, cx)
881 });
882
883 _ = editor.update(cx, |editor, window, cx| {
884 editor.change_selections(None, window, cx, |s| {
885 s.select_display_ranges([
886 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
887 ]);
888 });
889 editor.fold(&Fold, window, cx);
890 assert_eq!(
891 editor.display_text(cx),
892 "
893 impl Foo {
894 // Hello!
895
896 fn a() {
897 1
898 }
899
900 fn b() {⋯
901 }
902
903 fn c() {⋯
904 }
905 }
906 "
907 .unindent(),
908 );
909
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {⋯
915 }
916 "
917 .unindent(),
918 );
919
920 editor.unfold_lines(&UnfoldLines, window, cx);
921 assert_eq!(
922 editor.display_text(cx),
923 "
924 impl Foo {
925 // Hello!
926
927 fn a() {
928 1
929 }
930
931 fn b() {⋯
932 }
933
934 fn c() {⋯
935 }
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 editor.buffer.read(cx).read(cx).text()
945 );
946 });
947}
948
949#[gpui::test]
950fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
951 init_test(cx, |_| {});
952
953 let editor = cx.add_window(|window, cx| {
954 let buffer = MultiBuffer::build_simple(
955 &"
956 class Foo:
957 # Hello!
958
959 def a():
960 print(1)
961
962 def b():
963 print(2)
964
965 def c():
966 print(3)
967 "
968 .unindent(),
969 cx,
970 );
971 build_editor(buffer.clone(), window, cx)
972 });
973
974 _ = editor.update(cx, |editor, window, cx| {
975 editor.change_selections(None, window, cx, |s| {
976 s.select_display_ranges([
977 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
978 ]);
979 });
980 editor.fold(&Fold, window, cx);
981 assert_eq!(
982 editor.display_text(cx),
983 "
984 class Foo:
985 # Hello!
986
987 def a():
988 print(1)
989
990 def b():⋯
991
992 def c():⋯
993 "
994 .unindent(),
995 );
996
997 editor.fold(&Fold, window, cx);
998 assert_eq!(
999 editor.display_text(cx),
1000 "
1001 class Foo:⋯
1002 "
1003 .unindent(),
1004 );
1005
1006 editor.unfold_lines(&UnfoldLines, window, cx);
1007 assert_eq!(
1008 editor.display_text(cx),
1009 "
1010 class Foo:
1011 # Hello!
1012
1013 def a():
1014 print(1)
1015
1016 def b():⋯
1017
1018 def c():⋯
1019 "
1020 .unindent(),
1021 );
1022
1023 editor.unfold_lines(&UnfoldLines, window, cx);
1024 assert_eq!(
1025 editor.display_text(cx),
1026 editor.buffer.read(cx).read(cx).text()
1027 );
1028 });
1029}
1030
1031#[gpui::test]
1032fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1033 init_test(cx, |_| {});
1034
1035 let editor = cx.add_window(|window, cx| {
1036 let buffer = MultiBuffer::build_simple(
1037 &"
1038 class Foo:
1039 # Hello!
1040
1041 def a():
1042 print(1)
1043
1044 def b():
1045 print(2)
1046
1047
1048 def c():
1049 print(3)
1050
1051
1052 "
1053 .unindent(),
1054 cx,
1055 );
1056 build_editor(buffer.clone(), window, cx)
1057 });
1058
1059 _ = editor.update(cx, |editor, window, cx| {
1060 editor.change_selections(None, window, cx, |s| {
1061 s.select_display_ranges([
1062 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1063 ]);
1064 });
1065 editor.fold(&Fold, window, cx);
1066 assert_eq!(
1067 editor.display_text(cx),
1068 "
1069 class Foo:
1070 # Hello!
1071
1072 def a():
1073 print(1)
1074
1075 def b():⋯
1076
1077
1078 def c():⋯
1079
1080
1081 "
1082 .unindent(),
1083 );
1084
1085 editor.fold(&Fold, window, cx);
1086 assert_eq!(
1087 editor.display_text(cx),
1088 "
1089 class Foo:⋯
1090
1091
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.unfold_lines(&UnfoldLines, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:
1101 # Hello!
1102
1103 def a():
1104 print(1)
1105
1106 def b():⋯
1107
1108
1109 def c():⋯
1110
1111
1112 "
1113 .unindent(),
1114 );
1115
1116 editor.unfold_lines(&UnfoldLines, window, cx);
1117 assert_eq!(
1118 editor.display_text(cx),
1119 editor.buffer.read(cx).read(cx).text()
1120 );
1121 });
1122}
1123
1124#[gpui::test]
1125fn test_fold_at_level(cx: &mut TestAppContext) {
1126 init_test(cx, |_| {});
1127
1128 let editor = cx.add_window(|window, cx| {
1129 let buffer = MultiBuffer::build_simple(
1130 &"
1131 class Foo:
1132 # Hello!
1133
1134 def a():
1135 print(1)
1136
1137 def b():
1138 print(2)
1139
1140
1141 class Bar:
1142 # World!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150
1151 "
1152 .unindent(),
1153 cx,
1154 );
1155 build_editor(buffer.clone(), window, cx)
1156 });
1157
1158 _ = editor.update(cx, |editor, window, cx| {
1159 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1160 assert_eq!(
1161 editor.display_text(cx),
1162 "
1163 class Foo:
1164 # Hello!
1165
1166 def a():⋯
1167
1168 def b():⋯
1169
1170
1171 class Bar:
1172 # World!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 "
1180 .unindent(),
1181 );
1182
1183 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 class Foo:⋯
1188
1189
1190 class Bar:⋯
1191
1192
1193 "
1194 .unindent(),
1195 );
1196
1197 editor.unfold_all(&UnfoldAll, window, cx);
1198 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1199 assert_eq!(
1200 editor.display_text(cx),
1201 "
1202 class Foo:
1203 # Hello!
1204
1205 def a():
1206 print(1)
1207
1208 def b():
1209 print(2)
1210
1211
1212 class Bar:
1213 # World!
1214
1215 def a():
1216 print(1)
1217
1218 def b():
1219 print(2)
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 assert_eq!(
1227 editor.display_text(cx),
1228 editor.buffer.read(cx).read(cx).text()
1229 );
1230 });
1231}
1232
1233#[gpui::test]
1234fn test_move_cursor(cx: &mut TestAppContext) {
1235 init_test(cx, |_| {});
1236
1237 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1238 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1239
1240 buffer.update(cx, |buffer, cx| {
1241 buffer.edit(
1242 vec![
1243 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1244 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1245 ],
1246 None,
1247 cx,
1248 );
1249 });
1250 _ = editor.update(cx, |editor, window, cx| {
1251 assert_eq!(
1252 editor.selections.display_ranges(cx),
1253 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1254 );
1255
1256 editor.move_down(&MoveDown, window, cx);
1257 assert_eq!(
1258 editor.selections.display_ranges(cx),
1259 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1260 );
1261
1262 editor.move_right(&MoveRight, window, cx);
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1266 );
1267
1268 editor.move_left(&MoveLeft, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_up(&MoveUp, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1278 );
1279
1280 editor.move_to_end(&MoveToEnd, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1284 );
1285
1286 editor.move_to_beginning(&MoveToBeginning, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.change_selections(None, window, cx, |s| {
1293 s.select_display_ranges([
1294 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1295 ]);
1296 });
1297 editor.select_to_beginning(&SelectToBeginning, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.select_to_end(&SelectToEnd, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1307 );
1308 });
1309}
1310
1311// TODO: Re-enable this test
1312#[cfg(target_os = "macos")]
1313#[gpui::test]
1314fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1315 init_test(cx, |_| {});
1316
1317 let editor = cx.add_window(|window, cx| {
1318 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1319 build_editor(buffer.clone(), window, cx)
1320 });
1321
1322 assert_eq!('🟥'.len_utf8(), 4);
1323 assert_eq!('α'.len_utf8(), 2);
1324
1325 _ = editor.update(cx, |editor, window, cx| {
1326 editor.fold_creases(
1327 vec![
1328 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1329 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1330 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1331 ],
1332 true,
1333 window,
1334 cx,
1335 );
1336 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1337
1338 editor.move_right(&MoveRight, window, cx);
1339 assert_eq!(
1340 editor.selections.display_ranges(cx),
1341 &[empty_range(0, "🟥".len())]
1342 );
1343 editor.move_right(&MoveRight, window, cx);
1344 assert_eq!(
1345 editor.selections.display_ranges(cx),
1346 &[empty_range(0, "🟥🟧".len())]
1347 );
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥🟧⋯".len())]
1352 );
1353
1354 editor.move_down(&MoveDown, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[empty_range(1, "ab⋯e".len())]
1358 );
1359 editor.move_left(&MoveLeft, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(1, "ab⋯".len())]
1363 );
1364 editor.move_left(&MoveLeft, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "a".len())]
1373 );
1374
1375 editor.move_down(&MoveDown, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(2, "α".len())]
1379 );
1380 editor.move_right(&MoveRight, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(2, "αβ".len())]
1384 );
1385 editor.move_right(&MoveRight, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "αβ⋯".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ⋯ε".len())]
1394 );
1395
1396 editor.move_up(&MoveUp, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(1, "ab⋯e".len())]
1400 );
1401 editor.move_down(&MoveDown, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ⋯ε".len())]
1405 );
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411
1412 editor.move_up(&MoveUp, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(0, "🟥🟧".len())]
1416 );
1417 editor.move_left(&MoveLeft, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(0, "🟥".len())]
1421 );
1422 editor.move_left(&MoveLeft, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "".len())]
1426 );
1427 });
1428}
1429
1430#[gpui::test]
1431fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1432 init_test(cx, |_| {});
1433
1434 let editor = cx.add_window(|window, cx| {
1435 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1436 build_editor(buffer.clone(), window, cx)
1437 });
1438 _ = editor.update(cx, |editor, window, cx| {
1439 editor.change_selections(None, window, cx, |s| {
1440 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1441 });
1442
1443 // moving above start of document should move selection to start of document,
1444 // but the next move down should still be at the original goal_x
1445 editor.move_up(&MoveUp, window, cx);
1446 assert_eq!(
1447 editor.selections.display_ranges(cx),
1448 &[empty_range(0, "".len())]
1449 );
1450
1451 editor.move_down(&MoveDown, window, cx);
1452 assert_eq!(
1453 editor.selections.display_ranges(cx),
1454 &[empty_range(1, "abcd".len())]
1455 );
1456
1457 editor.move_down(&MoveDown, window, cx);
1458 assert_eq!(
1459 editor.selections.display_ranges(cx),
1460 &[empty_range(2, "αβγ".len())]
1461 );
1462
1463 editor.move_down(&MoveDown, window, cx);
1464 assert_eq!(
1465 editor.selections.display_ranges(cx),
1466 &[empty_range(3, "abcd".len())]
1467 );
1468
1469 editor.move_down(&MoveDown, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1473 );
1474
1475 // moving past end of document should not change goal_x
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(5, "".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(5, "".len())]
1486 );
1487
1488 editor.move_up(&MoveUp, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 editor.move_up(&MoveUp, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[empty_range(3, "abcd".len())]
1498 );
1499
1500 editor.move_up(&MoveUp, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[empty_range(2, "αβγ".len())]
1504 );
1505 });
1506}
1507
1508#[gpui::test]
1509fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1510 init_test(cx, |_| {});
1511 let move_to_beg = MoveToBeginningOfLine {
1512 stop_at_soft_wraps: true,
1513 };
1514
1515 let move_to_end = MoveToEndOfLine {
1516 stop_at_soft_wraps: true,
1517 };
1518
1519 let editor = cx.add_window(|window, cx| {
1520 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1521 build_editor(buffer, window, cx)
1522 });
1523 _ = editor.update(cx, |editor, window, cx| {
1524 editor.change_selections(None, window, cx, |s| {
1525 s.select_display_ranges([
1526 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1527 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1528 ]);
1529 });
1530 });
1531
1532 _ = editor.update(cx, |editor, window, cx| {
1533 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1534 assert_eq!(
1535 editor.selections.display_ranges(cx),
1536 &[
1537 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1538 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1539 ]
1540 );
1541 });
1542
1543 _ = editor.update(cx, |editor, window, cx| {
1544 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[
1548 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1549 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1550 ]
1551 );
1552 });
1553
1554 _ = editor.update(cx, |editor, window, cx| {
1555 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[
1559 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1560 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1561 ]
1562 );
1563 });
1564
1565 _ = editor.update(cx, |editor, window, cx| {
1566 editor.move_to_end_of_line(&move_to_end, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[
1570 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1571 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1572 ]
1573 );
1574 });
1575
1576 // Moving to the end of line again is a no-op.
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 _ = editor.update(cx, |editor, window, cx| {
1589 editor.move_left(&MoveLeft, window, cx);
1590 editor.select_to_beginning_of_line(
1591 &SelectToBeginningOfLine {
1592 stop_at_soft_wraps: true,
1593 },
1594 window,
1595 cx,
1596 );
1597 assert_eq!(
1598 editor.selections.display_ranges(cx),
1599 &[
1600 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1601 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1602 ]
1603 );
1604 });
1605
1606 _ = editor.update(cx, |editor, window, cx| {
1607 editor.select_to_beginning_of_line(
1608 &SelectToBeginningOfLine {
1609 stop_at_soft_wraps: true,
1610 },
1611 window,
1612 cx,
1613 );
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[
1617 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1618 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1619 ]
1620 );
1621 });
1622
1623 _ = editor.update(cx, |editor, window, cx| {
1624 editor.select_to_beginning_of_line(
1625 &SelectToBeginningOfLine {
1626 stop_at_soft_wraps: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_end_of_line(
1642 &SelectToEndOfLine {
1643 stop_at_soft_wraps: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1659 assert_eq!(editor.display_text(cx), "ab\n de");
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[
1663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1664 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1665 ]
1666 );
1667 });
1668
1669 _ = editor.update(cx, |editor, window, cx| {
1670 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1671 assert_eq!(editor.display_text(cx), "\n");
1672 assert_eq!(
1673 editor.selections.display_ranges(cx),
1674 &[
1675 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1676 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1677 ]
1678 );
1679 });
1680}
1681
1682#[gpui::test]
1683fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1684 init_test(cx, |_| {});
1685 let move_to_beg = MoveToBeginningOfLine {
1686 stop_at_soft_wraps: false,
1687 };
1688
1689 let move_to_end = MoveToEndOfLine {
1690 stop_at_soft_wraps: false,
1691 };
1692
1693 let editor = cx.add_window(|window, cx| {
1694 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1695 build_editor(buffer, window, cx)
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.set_wrap_width(Some(140.0.into()), cx);
1700
1701 // We expect the following lines after wrapping
1702 // ```
1703 // thequickbrownfox
1704 // jumpedoverthelazydo
1705 // gs
1706 // ```
1707 // The final `gs` was soft-wrapped onto a new line.
1708 assert_eq!(
1709 "thequickbrownfox\njumpedoverthelaz\nydogs",
1710 editor.display_text(cx),
1711 );
1712
1713 // First, let's assert behavior on the first line, that was not soft-wrapped.
1714 // Start the cursor at the `k` on the first line
1715 editor.change_selections(None, window, cx, |s| {
1716 s.select_display_ranges([
1717 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1718 ]);
1719 });
1720
1721 // Moving to the beginning of the line should put us at the beginning of the line.
1722 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1723 assert_eq!(
1724 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1725 editor.selections.display_ranges(cx)
1726 );
1727
1728 // Moving to the end of the line should put us at the end of the line.
1729 editor.move_to_end_of_line(&move_to_end, window, cx);
1730 assert_eq!(
1731 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1732 editor.selections.display_ranges(cx)
1733 );
1734
1735 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1736 // Start the cursor at the last line (`y` that was wrapped to a new line)
1737 editor.change_selections(None, window, cx, |s| {
1738 s.select_display_ranges([
1739 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1740 ]);
1741 });
1742
1743 // Moving to the beginning of the line should put us at the start of the second line of
1744 // display text, i.e., the `j`.
1745 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Moving to the beginning of the line again should be a no-op.
1752 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1753 assert_eq!(
1754 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1755 editor.selections.display_ranges(cx)
1756 );
1757
1758 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1759 // next display line.
1760 editor.move_to_end_of_line(&move_to_end, window, cx);
1761 assert_eq!(
1762 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1763 editor.selections.display_ranges(cx)
1764 );
1765
1766 // Moving to the end of the line again should be a no-op.
1767 editor.move_to_end_of_line(&move_to_end, window, cx);
1768 assert_eq!(
1769 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1770 editor.selections.display_ranges(cx)
1771 );
1772 });
1773}
1774
1775#[gpui::test]
1776fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1777 init_test(cx, |_| {});
1778
1779 let editor = cx.add_window(|window, cx| {
1780 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1781 build_editor(buffer, window, cx)
1782 });
1783 _ = editor.update(cx, |editor, window, cx| {
1784 editor.change_selections(None, window, cx, |s| {
1785 s.select_display_ranges([
1786 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1787 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1788 ])
1789 });
1790
1791 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1792 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1793
1794 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1795 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1805
1806 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1807 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1810 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1814
1815 editor.move_right(&MoveRight, window, cx);
1816 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1817 assert_selection_ranges(
1818 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1819 editor,
1820 cx,
1821 );
1822
1823 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1824 assert_selection_ranges(
1825 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1826 editor,
1827 cx,
1828 );
1829
1830 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1831 assert_selection_ranges(
1832 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1833 editor,
1834 cx,
1835 );
1836 });
1837}
1838
1839#[gpui::test]
1840fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1841 init_test(cx, |_| {});
1842
1843 let editor = cx.add_window(|window, cx| {
1844 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1845 build_editor(buffer, window, cx)
1846 });
1847
1848 _ = editor.update(cx, |editor, window, cx| {
1849 editor.set_wrap_width(Some(140.0.into()), cx);
1850 assert_eq!(
1851 editor.display_text(cx),
1852 "use one::{\n two::three::\n four::five\n};"
1853 );
1854
1855 editor.change_selections(None, window, cx, |s| {
1856 s.select_display_ranges([
1857 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1858 ]);
1859 });
1860
1861 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1862 assert_eq!(
1863 editor.selections.display_ranges(cx),
1864 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1865 );
1866
1867 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1868 assert_eq!(
1869 editor.selections.display_ranges(cx),
1870 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1871 );
1872
1873 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1874 assert_eq!(
1875 editor.selections.display_ranges(cx),
1876 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1877 );
1878
1879 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1880 assert_eq!(
1881 editor.selections.display_ranges(cx),
1882 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1883 );
1884
1885 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1886 assert_eq!(
1887 editor.selections.display_ranges(cx),
1888 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1889 );
1890
1891 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1892 assert_eq!(
1893 editor.selections.display_ranges(cx),
1894 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1895 );
1896 });
1897}
1898
1899#[gpui::test]
1900async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1901 init_test(cx, |_| {});
1902 let mut cx = EditorTestContext::new(cx).await;
1903
1904 let line_height = cx.editor(|editor, window, _| {
1905 editor
1906 .style()
1907 .unwrap()
1908 .text
1909 .line_height_in_pixels(window.rem_size())
1910 });
1911 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1912
1913 cx.set_state(
1914 &r#"ˇone
1915 two
1916
1917 three
1918 fourˇ
1919 five
1920
1921 six"#
1922 .unindent(),
1923 );
1924
1925 cx.update_editor(|editor, window, cx| {
1926 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1927 });
1928 cx.assert_editor_state(
1929 &r#"one
1930 two
1931 ˇ
1932 three
1933 four
1934 five
1935 ˇ
1936 six"#
1937 .unindent(),
1938 );
1939
1940 cx.update_editor(|editor, window, cx| {
1941 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1942 });
1943 cx.assert_editor_state(
1944 &r#"one
1945 two
1946
1947 three
1948 four
1949 five
1950 ˇ
1951 sixˇ"#
1952 .unindent(),
1953 );
1954
1955 cx.update_editor(|editor, window, cx| {
1956 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1957 });
1958 cx.assert_editor_state(
1959 &r#"one
1960 two
1961
1962 three
1963 four
1964 five
1965
1966 sixˇ"#
1967 .unindent(),
1968 );
1969
1970 cx.update_editor(|editor, window, cx| {
1971 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1972 });
1973 cx.assert_editor_state(
1974 &r#"one
1975 two
1976
1977 three
1978 four
1979 five
1980 ˇ
1981 six"#
1982 .unindent(),
1983 );
1984
1985 cx.update_editor(|editor, window, cx| {
1986 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1987 });
1988 cx.assert_editor_state(
1989 &r#"one
1990 two
1991 ˇ
1992 three
1993 four
1994 five
1995
1996 six"#
1997 .unindent(),
1998 );
1999
2000 cx.update_editor(|editor, window, cx| {
2001 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2002 });
2003 cx.assert_editor_state(
2004 &r#"ˇone
2005 two
2006
2007 three
2008 four
2009 five
2010
2011 six"#
2012 .unindent(),
2013 );
2014}
2015
2016#[gpui::test]
2017async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020 let line_height = cx.editor(|editor, window, _| {
2021 editor
2022 .style()
2023 .unwrap()
2024 .text
2025 .line_height_in_pixels(window.rem_size())
2026 });
2027 let window = cx.window;
2028 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2029
2030 cx.set_state(
2031 r#"ˇone
2032 two
2033 three
2034 four
2035 five
2036 six
2037 seven
2038 eight
2039 nine
2040 ten
2041 "#,
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 assert_eq!(
2046 editor.snapshot(window, cx).scroll_position(),
2047 gpui::Point::new(0., 0.)
2048 );
2049 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2050 assert_eq!(
2051 editor.snapshot(window, cx).scroll_position(),
2052 gpui::Point::new(0., 3.)
2053 );
2054 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2055 assert_eq!(
2056 editor.snapshot(window, cx).scroll_position(),
2057 gpui::Point::new(0., 6.)
2058 );
2059 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2060 assert_eq!(
2061 editor.snapshot(window, cx).scroll_position(),
2062 gpui::Point::new(0., 3.)
2063 );
2064
2065 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2066 assert_eq!(
2067 editor.snapshot(window, cx).scroll_position(),
2068 gpui::Point::new(0., 1.)
2069 );
2070 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2071 assert_eq!(
2072 editor.snapshot(window, cx).scroll_position(),
2073 gpui::Point::new(0., 3.)
2074 );
2075 });
2076}
2077
2078#[gpui::test]
2079async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2080 init_test(cx, |_| {});
2081 let mut cx = EditorTestContext::new(cx).await;
2082
2083 let line_height = cx.update_editor(|editor, window, cx| {
2084 editor.set_vertical_scroll_margin(2, cx);
2085 editor
2086 .style()
2087 .unwrap()
2088 .text
2089 .line_height_in_pixels(window.rem_size())
2090 });
2091 let window = cx.window;
2092 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2093
2094 cx.set_state(
2095 r#"ˇone
2096 two
2097 three
2098 four
2099 five
2100 six
2101 seven
2102 eight
2103 nine
2104 ten
2105 "#,
2106 );
2107 cx.update_editor(|editor, window, cx| {
2108 assert_eq!(
2109 editor.snapshot(window, cx).scroll_position(),
2110 gpui::Point::new(0., 0.0)
2111 );
2112 });
2113
2114 // Add a cursor below the visible area. Since both cursors cannot fit
2115 // on screen, the editor autoscrolls to reveal the newest cursor, and
2116 // allows the vertical scroll margin below that cursor.
2117 cx.update_editor(|editor, window, cx| {
2118 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2119 selections.select_ranges([
2120 Point::new(0, 0)..Point::new(0, 0),
2121 Point::new(6, 0)..Point::new(6, 0),
2122 ]);
2123 })
2124 });
2125 cx.update_editor(|editor, window, cx| {
2126 assert_eq!(
2127 editor.snapshot(window, cx).scroll_position(),
2128 gpui::Point::new(0., 3.0)
2129 );
2130 });
2131
2132 // Move down. The editor cursor scrolls down to track the newest cursor.
2133 cx.update_editor(|editor, window, cx| {
2134 editor.move_down(&Default::default(), window, cx);
2135 });
2136 cx.update_editor(|editor, window, cx| {
2137 assert_eq!(
2138 editor.snapshot(window, cx).scroll_position(),
2139 gpui::Point::new(0., 4.0)
2140 );
2141 });
2142
2143 // Add a cursor above the visible area. Since both cursors fit on screen,
2144 // the editor scrolls to show both.
2145 cx.update_editor(|editor, window, cx| {
2146 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2147 selections.select_ranges([
2148 Point::new(1, 0)..Point::new(1, 0),
2149 Point::new(6, 0)..Point::new(6, 0),
2150 ]);
2151 })
2152 });
2153 cx.update_editor(|editor, window, cx| {
2154 assert_eq!(
2155 editor.snapshot(window, cx).scroll_position(),
2156 gpui::Point::new(0., 1.0)
2157 );
2158 });
2159}
2160
2161#[gpui::test]
2162async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2163 init_test(cx, |_| {});
2164 let mut cx = EditorTestContext::new(cx).await;
2165
2166 let line_height = cx.editor(|editor, window, _cx| {
2167 editor
2168 .style()
2169 .unwrap()
2170 .text
2171 .line_height_in_pixels(window.rem_size())
2172 });
2173 let window = cx.window;
2174 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2175 cx.set_state(
2176 &r#"
2177 ˇone
2178 two
2179 threeˇ
2180 four
2181 five
2182 six
2183 seven
2184 eight
2185 nine
2186 ten
2187 "#
2188 .unindent(),
2189 );
2190
2191 cx.update_editor(|editor, window, cx| {
2192 editor.move_page_down(&MovePageDown::default(), window, cx)
2193 });
2194 cx.assert_editor_state(
2195 &r#"
2196 one
2197 two
2198 three
2199 ˇfour
2200 five
2201 sixˇ
2202 seven
2203 eight
2204 nine
2205 ten
2206 "#
2207 .unindent(),
2208 );
2209
2210 cx.update_editor(|editor, window, cx| {
2211 editor.move_page_down(&MovePageDown::default(), window, cx)
2212 });
2213 cx.assert_editor_state(
2214 &r#"
2215 one
2216 two
2217 three
2218 four
2219 five
2220 six
2221 ˇseven
2222 eight
2223 nineˇ
2224 ten
2225 "#
2226 .unindent(),
2227 );
2228
2229 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2230 cx.assert_editor_state(
2231 &r#"
2232 one
2233 two
2234 three
2235 ˇfour
2236 five
2237 sixˇ
2238 seven
2239 eight
2240 nine
2241 ten
2242 "#
2243 .unindent(),
2244 );
2245
2246 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2247 cx.assert_editor_state(
2248 &r#"
2249 ˇone
2250 two
2251 threeˇ
2252 four
2253 five
2254 six
2255 seven
2256 eight
2257 nine
2258 ten
2259 "#
2260 .unindent(),
2261 );
2262
2263 // Test select collapsing
2264 cx.update_editor(|editor, window, cx| {
2265 editor.move_page_down(&MovePageDown::default(), window, cx);
2266 editor.move_page_down(&MovePageDown::default(), window, cx);
2267 editor.move_page_down(&MovePageDown::default(), window, cx);
2268 });
2269 cx.assert_editor_state(
2270 &r#"
2271 one
2272 two
2273 three
2274 four
2275 five
2276 six
2277 seven
2278 eight
2279 nine
2280 ˇten
2281 ˇ"#
2282 .unindent(),
2283 );
2284}
2285
2286#[gpui::test]
2287async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2288 init_test(cx, |_| {});
2289 let mut cx = EditorTestContext::new(cx).await;
2290 cx.set_state("one «two threeˇ» four");
2291 cx.update_editor(|editor, window, cx| {
2292 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2293 assert_eq!(editor.text(cx), " four");
2294 });
2295}
2296
2297#[gpui::test]
2298fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2299 init_test(cx, |_| {});
2300
2301 let editor = cx.add_window(|window, cx| {
2302 let buffer = MultiBuffer::build_simple("one two three four", cx);
2303 build_editor(buffer.clone(), window, cx)
2304 });
2305
2306 _ = editor.update(cx, |editor, window, cx| {
2307 editor.change_selections(None, window, cx, |s| {
2308 s.select_display_ranges([
2309 // an empty selection - the preceding word fragment is deleted
2310 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2311 // characters selected - they are deleted
2312 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2313 ])
2314 });
2315 editor.delete_to_previous_word_start(
2316 &DeleteToPreviousWordStart {
2317 ignore_newlines: false,
2318 },
2319 window,
2320 cx,
2321 );
2322 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2323 });
2324
2325 _ = editor.update(cx, |editor, window, cx| {
2326 editor.change_selections(None, window, cx, |s| {
2327 s.select_display_ranges([
2328 // an empty selection - the following word fragment is deleted
2329 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2330 // characters selected - they are deleted
2331 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2332 ])
2333 });
2334 editor.delete_to_next_word_end(
2335 &DeleteToNextWordEnd {
2336 ignore_newlines: false,
2337 },
2338 window,
2339 cx,
2340 );
2341 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2342 });
2343}
2344
2345#[gpui::test]
2346fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2347 init_test(cx, |_| {});
2348
2349 let editor = cx.add_window(|window, cx| {
2350 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2351 build_editor(buffer.clone(), window, cx)
2352 });
2353 let del_to_prev_word_start = DeleteToPreviousWordStart {
2354 ignore_newlines: false,
2355 };
2356 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2357 ignore_newlines: true,
2358 };
2359
2360 _ = editor.update(cx, |editor, window, cx| {
2361 editor.change_selections(None, window, cx, |s| {
2362 s.select_display_ranges([
2363 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2364 ])
2365 });
2366 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2367 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2368 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2369 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2370 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2371 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2372 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2373 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2374 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2375 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2376 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2377 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2378 });
2379}
2380
2381#[gpui::test]
2382fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2383 init_test(cx, |_| {});
2384
2385 let editor = cx.add_window(|window, cx| {
2386 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2387 build_editor(buffer.clone(), window, cx)
2388 });
2389 let del_to_next_word_end = DeleteToNextWordEnd {
2390 ignore_newlines: false,
2391 };
2392 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2393 ignore_newlines: true,
2394 };
2395
2396 _ = editor.update(cx, |editor, window, cx| {
2397 editor.change_selections(None, window, cx, |s| {
2398 s.select_display_ranges([
2399 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2400 ])
2401 });
2402 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2403 assert_eq!(
2404 editor.buffer.read(cx).read(cx).text(),
2405 "one\n two\nthree\n four"
2406 );
2407 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2408 assert_eq!(
2409 editor.buffer.read(cx).read(cx).text(),
2410 "\n two\nthree\n four"
2411 );
2412 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2413 assert_eq!(
2414 editor.buffer.read(cx).read(cx).text(),
2415 "two\nthree\n four"
2416 );
2417 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2418 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2419 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2420 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2421 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2422 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2423 });
2424}
2425
2426#[gpui::test]
2427fn test_newline(cx: &mut TestAppContext) {
2428 init_test(cx, |_| {});
2429
2430 let editor = cx.add_window(|window, cx| {
2431 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2432 build_editor(buffer.clone(), window, cx)
2433 });
2434
2435 _ = editor.update(cx, |editor, window, cx| {
2436 editor.change_selections(None, window, cx, |s| {
2437 s.select_display_ranges([
2438 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2439 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2440 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2441 ])
2442 });
2443
2444 editor.newline(&Newline, window, cx);
2445 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2446 });
2447}
2448
2449#[gpui::test]
2450fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2451 init_test(cx, |_| {});
2452
2453 let editor = cx.add_window(|window, cx| {
2454 let buffer = MultiBuffer::build_simple(
2455 "
2456 a
2457 b(
2458 X
2459 )
2460 c(
2461 X
2462 )
2463 "
2464 .unindent()
2465 .as_str(),
2466 cx,
2467 );
2468 let mut editor = build_editor(buffer.clone(), window, cx);
2469 editor.change_selections(None, window, cx, |s| {
2470 s.select_ranges([
2471 Point::new(2, 4)..Point::new(2, 5),
2472 Point::new(5, 4)..Point::new(5, 5),
2473 ])
2474 });
2475 editor
2476 });
2477
2478 _ = editor.update(cx, |editor, window, cx| {
2479 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2480 editor.buffer.update(cx, |buffer, cx| {
2481 buffer.edit(
2482 [
2483 (Point::new(1, 2)..Point::new(3, 0), ""),
2484 (Point::new(4, 2)..Point::new(6, 0), ""),
2485 ],
2486 None,
2487 cx,
2488 );
2489 assert_eq!(
2490 buffer.read(cx).text(),
2491 "
2492 a
2493 b()
2494 c()
2495 "
2496 .unindent()
2497 );
2498 });
2499 assert_eq!(
2500 editor.selections.ranges(cx),
2501 &[
2502 Point::new(1, 2)..Point::new(1, 2),
2503 Point::new(2, 2)..Point::new(2, 2),
2504 ],
2505 );
2506
2507 editor.newline(&Newline, window, cx);
2508 assert_eq!(
2509 editor.text(cx),
2510 "
2511 a
2512 b(
2513 )
2514 c(
2515 )
2516 "
2517 .unindent()
2518 );
2519
2520 // The selections are moved after the inserted newlines
2521 assert_eq!(
2522 editor.selections.ranges(cx),
2523 &[
2524 Point::new(2, 0)..Point::new(2, 0),
2525 Point::new(4, 0)..Point::new(4, 0),
2526 ],
2527 );
2528 });
2529}
2530
2531#[gpui::test]
2532async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2533 init_test(cx, |settings| {
2534 settings.defaults.tab_size = NonZeroU32::new(4)
2535 });
2536
2537 let language = Arc::new(
2538 Language::new(
2539 LanguageConfig::default(),
2540 Some(tree_sitter_rust::LANGUAGE.into()),
2541 )
2542 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2543 .unwrap(),
2544 );
2545
2546 let mut cx = EditorTestContext::new(cx).await;
2547 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2548 cx.set_state(indoc! {"
2549 const a: ˇA = (
2550 (ˇ
2551 «const_functionˇ»(ˇ),
2552 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2553 )ˇ
2554 ˇ);ˇ
2555 "});
2556
2557 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2558 cx.assert_editor_state(indoc! {"
2559 ˇ
2560 const a: A = (
2561 ˇ
2562 (
2563 ˇ
2564 ˇ
2565 const_function(),
2566 ˇ
2567 ˇ
2568 ˇ
2569 ˇ
2570 something_else,
2571 ˇ
2572 )
2573 ˇ
2574 ˇ
2575 );
2576 "});
2577}
2578
2579#[gpui::test]
2580async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2581 init_test(cx, |settings| {
2582 settings.defaults.tab_size = NonZeroU32::new(4)
2583 });
2584
2585 let language = Arc::new(
2586 Language::new(
2587 LanguageConfig::default(),
2588 Some(tree_sitter_rust::LANGUAGE.into()),
2589 )
2590 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2591 .unwrap(),
2592 );
2593
2594 let mut cx = EditorTestContext::new(cx).await;
2595 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2596 cx.set_state(indoc! {"
2597 const a: ˇA = (
2598 (ˇ
2599 «const_functionˇ»(ˇ),
2600 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2601 )ˇ
2602 ˇ);ˇ
2603 "});
2604
2605 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2606 cx.assert_editor_state(indoc! {"
2607 const a: A = (
2608 ˇ
2609 (
2610 ˇ
2611 const_function(),
2612 ˇ
2613 ˇ
2614 something_else,
2615 ˇ
2616 ˇ
2617 ˇ
2618 ˇ
2619 )
2620 ˇ
2621 );
2622 ˇ
2623 ˇ
2624 "});
2625}
2626
2627#[gpui::test]
2628async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2629 init_test(cx, |settings| {
2630 settings.defaults.tab_size = NonZeroU32::new(4)
2631 });
2632
2633 let language = Arc::new(Language::new(
2634 LanguageConfig {
2635 line_comments: vec!["//".into()],
2636 ..LanguageConfig::default()
2637 },
2638 None,
2639 ));
2640 {
2641 let mut cx = EditorTestContext::new(cx).await;
2642 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2643 cx.set_state(indoc! {"
2644 // Fooˇ
2645 "});
2646
2647 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2648 cx.assert_editor_state(indoc! {"
2649 // Foo
2650 //ˇ
2651 "});
2652 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2653 cx.set_state(indoc! {"
2654 ˇ// Foo
2655 "});
2656 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2657 cx.assert_editor_state(indoc! {"
2658
2659 ˇ// Foo
2660 "});
2661 }
2662 // Ensure that comment continuations can be disabled.
2663 update_test_language_settings(cx, |settings| {
2664 settings.defaults.extend_comment_on_newline = Some(false);
2665 });
2666 let mut cx = EditorTestContext::new(cx).await;
2667 cx.set_state(indoc! {"
2668 // Fooˇ
2669 "});
2670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2671 cx.assert_editor_state(indoc! {"
2672 // Foo
2673 ˇ
2674 "});
2675}
2676
2677#[gpui::test]
2678fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2679 init_test(cx, |_| {});
2680
2681 let editor = cx.add_window(|window, cx| {
2682 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2683 let mut editor = build_editor(buffer.clone(), window, cx);
2684 editor.change_selections(None, window, cx, |s| {
2685 s.select_ranges([3..4, 11..12, 19..20])
2686 });
2687 editor
2688 });
2689
2690 _ = editor.update(cx, |editor, window, cx| {
2691 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2692 editor.buffer.update(cx, |buffer, cx| {
2693 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2694 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2695 });
2696 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2697
2698 editor.insert("Z", window, cx);
2699 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2700
2701 // The selections are moved after the inserted characters
2702 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2703 });
2704}
2705
2706#[gpui::test]
2707async fn test_tab(cx: &mut gpui::TestAppContext) {
2708 init_test(cx, |settings| {
2709 settings.defaults.tab_size = NonZeroU32::new(3)
2710 });
2711
2712 let mut cx = EditorTestContext::new(cx).await;
2713 cx.set_state(indoc! {"
2714 ˇabˇc
2715 ˇ🏀ˇ🏀ˇefg
2716 dˇ
2717 "});
2718 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2719 cx.assert_editor_state(indoc! {"
2720 ˇab ˇc
2721 ˇ🏀 ˇ🏀 ˇefg
2722 d ˇ
2723 "});
2724
2725 cx.set_state(indoc! {"
2726 a
2727 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2728 "});
2729 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 a
2732 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2733 "});
2734}
2735
2736#[gpui::test]
2737async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2738 init_test(cx, |_| {});
2739
2740 let mut cx = EditorTestContext::new(cx).await;
2741 let language = Arc::new(
2742 Language::new(
2743 LanguageConfig::default(),
2744 Some(tree_sitter_rust::LANGUAGE.into()),
2745 )
2746 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2747 .unwrap(),
2748 );
2749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2750
2751 // cursors that are already at the suggested indent level insert
2752 // a soft tab. cursors that are to the left of the suggested indent
2753 // auto-indent their line.
2754 cx.set_state(indoc! {"
2755 ˇ
2756 const a: B = (
2757 c(
2758 d(
2759 ˇ
2760 )
2761 ˇ
2762 ˇ )
2763 );
2764 "});
2765 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2766 cx.assert_editor_state(indoc! {"
2767 ˇ
2768 const a: B = (
2769 c(
2770 d(
2771 ˇ
2772 )
2773 ˇ
2774 ˇ)
2775 );
2776 "});
2777
2778 // handle auto-indent when there are multiple cursors on the same line
2779 cx.set_state(indoc! {"
2780 const a: B = (
2781 c(
2782 ˇ ˇ
2783 ˇ )
2784 );
2785 "});
2786 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2787 cx.assert_editor_state(indoc! {"
2788 const a: B = (
2789 c(
2790 ˇ
2791 ˇ)
2792 );
2793 "});
2794}
2795
2796#[gpui::test]
2797async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2798 init_test(cx, |settings| {
2799 settings.defaults.tab_size = NonZeroU32::new(4)
2800 });
2801
2802 let language = Arc::new(
2803 Language::new(
2804 LanguageConfig::default(),
2805 Some(tree_sitter_rust::LANGUAGE.into()),
2806 )
2807 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2808 .unwrap(),
2809 );
2810
2811 let mut cx = EditorTestContext::new(cx).await;
2812 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2813 cx.set_state(indoc! {"
2814 fn a() {
2815 if b {
2816 \t ˇc
2817 }
2818 }
2819 "});
2820
2821 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2822 cx.assert_editor_state(indoc! {"
2823 fn a() {
2824 if b {
2825 ˇc
2826 }
2827 }
2828 "});
2829}
2830
2831#[gpui::test]
2832async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2833 init_test(cx, |settings| {
2834 settings.defaults.tab_size = NonZeroU32::new(4);
2835 });
2836
2837 let mut cx = EditorTestContext::new(cx).await;
2838
2839 cx.set_state(indoc! {"
2840 «oneˇ» «twoˇ»
2841 three
2842 four
2843 "});
2844 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 «oneˇ» «twoˇ»
2847 three
2848 four
2849 "});
2850
2851 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2852 cx.assert_editor_state(indoc! {"
2853 «oneˇ» «twoˇ»
2854 three
2855 four
2856 "});
2857
2858 // select across line ending
2859 cx.set_state(indoc! {"
2860 one two
2861 t«hree
2862 ˇ» four
2863 "});
2864 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2865 cx.assert_editor_state(indoc! {"
2866 one two
2867 t«hree
2868 ˇ» four
2869 "});
2870
2871 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2872 cx.assert_editor_state(indoc! {"
2873 one two
2874 t«hree
2875 ˇ» four
2876 "});
2877
2878 // Ensure that indenting/outdenting works when the cursor is at column 0.
2879 cx.set_state(indoc! {"
2880 one two
2881 ˇthree
2882 four
2883 "});
2884 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2885 cx.assert_editor_state(indoc! {"
2886 one two
2887 ˇthree
2888 four
2889 "});
2890
2891 cx.set_state(indoc! {"
2892 one two
2893 ˇ three
2894 four
2895 "});
2896 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2897 cx.assert_editor_state(indoc! {"
2898 one two
2899 ˇthree
2900 four
2901 "});
2902}
2903
2904#[gpui::test]
2905async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2906 init_test(cx, |settings| {
2907 settings.defaults.hard_tabs = Some(true);
2908 });
2909
2910 let mut cx = EditorTestContext::new(cx).await;
2911
2912 // select two ranges on one line
2913 cx.set_state(indoc! {"
2914 «oneˇ» «twoˇ»
2915 three
2916 four
2917 "});
2918 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2919 cx.assert_editor_state(indoc! {"
2920 \t«oneˇ» «twoˇ»
2921 three
2922 four
2923 "});
2924 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2925 cx.assert_editor_state(indoc! {"
2926 \t\t«oneˇ» «twoˇ»
2927 three
2928 four
2929 "});
2930 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 \t«oneˇ» «twoˇ»
2933 three
2934 four
2935 "});
2936 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2937 cx.assert_editor_state(indoc! {"
2938 «oneˇ» «twoˇ»
2939 three
2940 four
2941 "});
2942
2943 // select across a line ending
2944 cx.set_state(indoc! {"
2945 one two
2946 t«hree
2947 ˇ»four
2948 "});
2949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2950 cx.assert_editor_state(indoc! {"
2951 one two
2952 \tt«hree
2953 ˇ»four
2954 "});
2955 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2956 cx.assert_editor_state(indoc! {"
2957 one two
2958 \t\tt«hree
2959 ˇ»four
2960 "});
2961 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2962 cx.assert_editor_state(indoc! {"
2963 one two
2964 \tt«hree
2965 ˇ»four
2966 "});
2967 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2968 cx.assert_editor_state(indoc! {"
2969 one two
2970 t«hree
2971 ˇ»four
2972 "});
2973
2974 // Ensure that indenting/outdenting works when the cursor is at column 0.
2975 cx.set_state(indoc! {"
2976 one two
2977 ˇthree
2978 four
2979 "});
2980 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2981 cx.assert_editor_state(indoc! {"
2982 one two
2983 ˇthree
2984 four
2985 "});
2986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2987 cx.assert_editor_state(indoc! {"
2988 one two
2989 \tˇthree
2990 four
2991 "});
2992 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 one two
2995 ˇthree
2996 four
2997 "});
2998}
2999
3000#[gpui::test]
3001fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3002 init_test(cx, |settings| {
3003 settings.languages.extend([
3004 (
3005 "TOML".into(),
3006 LanguageSettingsContent {
3007 tab_size: NonZeroU32::new(2),
3008 ..Default::default()
3009 },
3010 ),
3011 (
3012 "Rust".into(),
3013 LanguageSettingsContent {
3014 tab_size: NonZeroU32::new(4),
3015 ..Default::default()
3016 },
3017 ),
3018 ]);
3019 });
3020
3021 let toml_language = Arc::new(Language::new(
3022 LanguageConfig {
3023 name: "TOML".into(),
3024 ..Default::default()
3025 },
3026 None,
3027 ));
3028 let rust_language = Arc::new(Language::new(
3029 LanguageConfig {
3030 name: "Rust".into(),
3031 ..Default::default()
3032 },
3033 None,
3034 ));
3035
3036 let toml_buffer =
3037 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3038 let rust_buffer =
3039 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3040 let multibuffer = cx.new(|cx| {
3041 let mut multibuffer = MultiBuffer::new(ReadWrite);
3042 multibuffer.push_excerpts(
3043 toml_buffer.clone(),
3044 [ExcerptRange {
3045 context: Point::new(0, 0)..Point::new(2, 0),
3046 primary: None,
3047 }],
3048 cx,
3049 );
3050 multibuffer.push_excerpts(
3051 rust_buffer.clone(),
3052 [ExcerptRange {
3053 context: Point::new(0, 0)..Point::new(1, 0),
3054 primary: None,
3055 }],
3056 cx,
3057 );
3058 multibuffer
3059 });
3060
3061 cx.add_window(|window, cx| {
3062 let mut editor = build_editor(multibuffer, window, cx);
3063
3064 assert_eq!(
3065 editor.text(cx),
3066 indoc! {"
3067 a = 1
3068 b = 2
3069
3070 const c: usize = 3;
3071 "}
3072 );
3073
3074 select_ranges(
3075 &mut editor,
3076 indoc! {"
3077 «aˇ» = 1
3078 b = 2
3079
3080 «const c:ˇ» usize = 3;
3081 "},
3082 window,
3083 cx,
3084 );
3085
3086 editor.tab(&Tab, window, cx);
3087 assert_text_with_selections(
3088 &mut editor,
3089 indoc! {"
3090 «aˇ» = 1
3091 b = 2
3092
3093 «const c:ˇ» usize = 3;
3094 "},
3095 cx,
3096 );
3097 editor.tab_prev(&TabPrev, window, cx);
3098 assert_text_with_selections(
3099 &mut editor,
3100 indoc! {"
3101 «aˇ» = 1
3102 b = 2
3103
3104 «const c:ˇ» usize = 3;
3105 "},
3106 cx,
3107 );
3108
3109 editor
3110 });
3111}
3112
3113#[gpui::test]
3114async fn test_backspace(cx: &mut gpui::TestAppContext) {
3115 init_test(cx, |_| {});
3116
3117 let mut cx = EditorTestContext::new(cx).await;
3118
3119 // Basic backspace
3120 cx.set_state(indoc! {"
3121 onˇe two three
3122 fou«rˇ» five six
3123 seven «ˇeight nine
3124 »ten
3125 "});
3126 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 oˇe two three
3129 fouˇ five six
3130 seven ˇten
3131 "});
3132
3133 // Test backspace inside and around indents
3134 cx.set_state(indoc! {"
3135 zero
3136 ˇone
3137 ˇtwo
3138 ˇ ˇ ˇ three
3139 ˇ ˇ four
3140 "});
3141 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3142 cx.assert_editor_state(indoc! {"
3143 zero
3144 ˇone
3145 ˇtwo
3146 ˇ threeˇ four
3147 "});
3148
3149 // Test backspace with line_mode set to true
3150 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3151 cx.set_state(indoc! {"
3152 The ˇquick ˇbrown
3153 fox jumps over
3154 the lazy dog
3155 ˇThe qu«ick bˇ»rown"});
3156 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3157 cx.assert_editor_state(indoc! {"
3158 ˇfox jumps over
3159 the lazy dogˇ"});
3160}
3161
3162#[gpui::test]
3163async fn test_delete(cx: &mut gpui::TestAppContext) {
3164 init_test(cx, |_| {});
3165
3166 let mut cx = EditorTestContext::new(cx).await;
3167 cx.set_state(indoc! {"
3168 onˇe two three
3169 fou«rˇ» five six
3170 seven «ˇeight nine
3171 »ten
3172 "});
3173 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3174 cx.assert_editor_state(indoc! {"
3175 onˇ two three
3176 fouˇ five six
3177 seven ˇten
3178 "});
3179
3180 // Test backspace with line_mode set to true
3181 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3182 cx.set_state(indoc! {"
3183 The ˇquick ˇbrown
3184 fox «ˇjum»ps over
3185 the lazy dog
3186 ˇThe qu«ick bˇ»rown"});
3187 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3188 cx.assert_editor_state("ˇthe lazy dogˇ");
3189}
3190
3191#[gpui::test]
3192fn test_delete_line(cx: &mut TestAppContext) {
3193 init_test(cx, |_| {});
3194
3195 let editor = cx.add_window(|window, cx| {
3196 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3197 build_editor(buffer, window, cx)
3198 });
3199 _ = editor.update(cx, |editor, window, cx| {
3200 editor.change_selections(None, window, cx, |s| {
3201 s.select_display_ranges([
3202 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3203 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3204 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3205 ])
3206 });
3207 editor.delete_line(&DeleteLine, window, cx);
3208 assert_eq!(editor.display_text(cx), "ghi");
3209 assert_eq!(
3210 editor.selections.display_ranges(cx),
3211 vec![
3212 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3213 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3214 ]
3215 );
3216 });
3217
3218 let editor = cx.add_window(|window, cx| {
3219 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3220 build_editor(buffer, window, cx)
3221 });
3222 _ = editor.update(cx, |editor, window, cx| {
3223 editor.change_selections(None, window, cx, |s| {
3224 s.select_display_ranges([
3225 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3226 ])
3227 });
3228 editor.delete_line(&DeleteLine, window, cx);
3229 assert_eq!(editor.display_text(cx), "ghi\n");
3230 assert_eq!(
3231 editor.selections.display_ranges(cx),
3232 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3233 );
3234 });
3235}
3236
3237#[gpui::test]
3238fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3239 init_test(cx, |_| {});
3240
3241 cx.add_window(|window, cx| {
3242 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3243 let mut editor = build_editor(buffer.clone(), window, cx);
3244 let buffer = buffer.read(cx).as_singleton().unwrap();
3245
3246 assert_eq!(
3247 editor.selections.ranges::<Point>(cx),
3248 &[Point::new(0, 0)..Point::new(0, 0)]
3249 );
3250
3251 // When on single line, replace newline at end by space
3252 editor.join_lines(&JoinLines, window, cx);
3253 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3254 assert_eq!(
3255 editor.selections.ranges::<Point>(cx),
3256 &[Point::new(0, 3)..Point::new(0, 3)]
3257 );
3258
3259 // When multiple lines are selected, remove newlines that are spanned by the selection
3260 editor.change_selections(None, window, cx, |s| {
3261 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3262 });
3263 editor.join_lines(&JoinLines, window, cx);
3264 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3265 assert_eq!(
3266 editor.selections.ranges::<Point>(cx),
3267 &[Point::new(0, 11)..Point::new(0, 11)]
3268 );
3269
3270 // Undo should be transactional
3271 editor.undo(&Undo, window, cx);
3272 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3273 assert_eq!(
3274 editor.selections.ranges::<Point>(cx),
3275 &[Point::new(0, 5)..Point::new(2, 2)]
3276 );
3277
3278 // When joining an empty line don't insert a space
3279 editor.change_selections(None, window, cx, |s| {
3280 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3281 });
3282 editor.join_lines(&JoinLines, window, cx);
3283 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3284 assert_eq!(
3285 editor.selections.ranges::<Point>(cx),
3286 [Point::new(2, 3)..Point::new(2, 3)]
3287 );
3288
3289 // We can remove trailing newlines
3290 editor.join_lines(&JoinLines, window, cx);
3291 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3292 assert_eq!(
3293 editor.selections.ranges::<Point>(cx),
3294 [Point::new(2, 3)..Point::new(2, 3)]
3295 );
3296
3297 // We don't blow up on the last line
3298 editor.join_lines(&JoinLines, window, cx);
3299 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3300 assert_eq!(
3301 editor.selections.ranges::<Point>(cx),
3302 [Point::new(2, 3)..Point::new(2, 3)]
3303 );
3304
3305 // reset to test indentation
3306 editor.buffer.update(cx, |buffer, cx| {
3307 buffer.edit(
3308 [
3309 (Point::new(1, 0)..Point::new(1, 2), " "),
3310 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3311 ],
3312 None,
3313 cx,
3314 )
3315 });
3316
3317 // We remove any leading spaces
3318 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3321 });
3322 editor.join_lines(&JoinLines, window, cx);
3323 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3324
3325 // We don't insert a space for a line containing only spaces
3326 editor.join_lines(&JoinLines, window, cx);
3327 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3328
3329 // We ignore any leading tabs
3330 editor.join_lines(&JoinLines, window, cx);
3331 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3332
3333 editor
3334 });
3335}
3336
3337#[gpui::test]
3338fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3339 init_test(cx, |_| {});
3340
3341 cx.add_window(|window, cx| {
3342 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3343 let mut editor = build_editor(buffer.clone(), window, cx);
3344 let buffer = buffer.read(cx).as_singleton().unwrap();
3345
3346 editor.change_selections(None, window, cx, |s| {
3347 s.select_ranges([
3348 Point::new(0, 2)..Point::new(1, 1),
3349 Point::new(1, 2)..Point::new(1, 2),
3350 Point::new(3, 1)..Point::new(3, 2),
3351 ])
3352 });
3353
3354 editor.join_lines(&JoinLines, window, cx);
3355 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3356
3357 assert_eq!(
3358 editor.selections.ranges::<Point>(cx),
3359 [
3360 Point::new(0, 7)..Point::new(0, 7),
3361 Point::new(1, 3)..Point::new(1, 3)
3362 ]
3363 );
3364 editor
3365 });
3366}
3367
3368#[gpui::test]
3369async fn test_join_lines_with_git_diff_base(
3370 executor: BackgroundExecutor,
3371 cx: &mut gpui::TestAppContext,
3372) {
3373 init_test(cx, |_| {});
3374
3375 let mut cx = EditorTestContext::new(cx).await;
3376
3377 let diff_base = r#"
3378 Line 0
3379 Line 1
3380 Line 2
3381 Line 3
3382 "#
3383 .unindent();
3384
3385 cx.set_state(
3386 &r#"
3387 ˇLine 0
3388 Line 1
3389 Line 2
3390 Line 3
3391 "#
3392 .unindent(),
3393 );
3394
3395 cx.set_diff_base(&diff_base);
3396 executor.run_until_parked();
3397
3398 // Join lines
3399 cx.update_editor(|editor, window, cx| {
3400 editor.join_lines(&JoinLines, window, cx);
3401 });
3402 executor.run_until_parked();
3403
3404 cx.assert_editor_state(
3405 &r#"
3406 Line 0ˇ Line 1
3407 Line 2
3408 Line 3
3409 "#
3410 .unindent(),
3411 );
3412 // Join again
3413 cx.update_editor(|editor, window, cx| {
3414 editor.join_lines(&JoinLines, window, cx);
3415 });
3416 executor.run_until_parked();
3417
3418 cx.assert_editor_state(
3419 &r#"
3420 Line 0 Line 1ˇ Line 2
3421 Line 3
3422 "#
3423 .unindent(),
3424 );
3425}
3426
3427#[gpui::test]
3428async fn test_custom_newlines_cause_no_false_positive_diffs(
3429 executor: BackgroundExecutor,
3430 cx: &mut gpui::TestAppContext,
3431) {
3432 init_test(cx, |_| {});
3433 let mut cx = EditorTestContext::new(cx).await;
3434 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3435 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3436 executor.run_until_parked();
3437
3438 cx.update_editor(|editor, window, cx| {
3439 let snapshot = editor.snapshot(window, cx);
3440 assert_eq!(
3441 snapshot
3442 .buffer_snapshot
3443 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3444 .collect::<Vec<_>>(),
3445 Vec::new(),
3446 "Should not have any diffs for files with custom newlines"
3447 );
3448 });
3449}
3450
3451#[gpui::test]
3452async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3453 init_test(cx, |_| {});
3454
3455 let mut cx = EditorTestContext::new(cx).await;
3456
3457 // Test sort_lines_case_insensitive()
3458 cx.set_state(indoc! {"
3459 «z
3460 y
3461 x
3462 Z
3463 Y
3464 Xˇ»
3465 "});
3466 cx.update_editor(|e, window, cx| {
3467 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3468 });
3469 cx.assert_editor_state(indoc! {"
3470 «x
3471 X
3472 y
3473 Y
3474 z
3475 Zˇ»
3476 "});
3477
3478 // Test reverse_lines()
3479 cx.set_state(indoc! {"
3480 «5
3481 4
3482 3
3483 2
3484 1ˇ»
3485 "});
3486 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3487 cx.assert_editor_state(indoc! {"
3488 «1
3489 2
3490 3
3491 4
3492 5ˇ»
3493 "});
3494
3495 // Skip testing shuffle_line()
3496
3497 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3498 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3499
3500 // Don't manipulate when cursor is on single line, but expand the selection
3501 cx.set_state(indoc! {"
3502 ddˇdd
3503 ccc
3504 bb
3505 a
3506 "});
3507 cx.update_editor(|e, window, cx| {
3508 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3509 });
3510 cx.assert_editor_state(indoc! {"
3511 «ddddˇ»
3512 ccc
3513 bb
3514 a
3515 "});
3516
3517 // Basic manipulate case
3518 // Start selection moves to column 0
3519 // End of selection shrinks to fit shorter line
3520 cx.set_state(indoc! {"
3521 dd«d
3522 ccc
3523 bb
3524 aaaaaˇ»
3525 "});
3526 cx.update_editor(|e, window, cx| {
3527 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3528 });
3529 cx.assert_editor_state(indoc! {"
3530 «aaaaa
3531 bb
3532 ccc
3533 dddˇ»
3534 "});
3535
3536 // Manipulate case with newlines
3537 cx.set_state(indoc! {"
3538 dd«d
3539 ccc
3540
3541 bb
3542 aaaaa
3543
3544 ˇ»
3545 "});
3546 cx.update_editor(|e, window, cx| {
3547 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3548 });
3549 cx.assert_editor_state(indoc! {"
3550 «
3551
3552 aaaaa
3553 bb
3554 ccc
3555 dddˇ»
3556
3557 "});
3558
3559 // Adding new line
3560 cx.set_state(indoc! {"
3561 aa«a
3562 bbˇ»b
3563 "});
3564 cx.update_editor(|e, window, cx| {
3565 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3566 });
3567 cx.assert_editor_state(indoc! {"
3568 «aaa
3569 bbb
3570 added_lineˇ»
3571 "});
3572
3573 // Removing line
3574 cx.set_state(indoc! {"
3575 aa«a
3576 bbbˇ»
3577 "});
3578 cx.update_editor(|e, window, cx| {
3579 e.manipulate_lines(window, cx, |lines| {
3580 lines.pop();
3581 })
3582 });
3583 cx.assert_editor_state(indoc! {"
3584 «aaaˇ»
3585 "});
3586
3587 // Removing all lines
3588 cx.set_state(indoc! {"
3589 aa«a
3590 bbbˇ»
3591 "});
3592 cx.update_editor(|e, window, cx| {
3593 e.manipulate_lines(window, cx, |lines| {
3594 lines.drain(..);
3595 })
3596 });
3597 cx.assert_editor_state(indoc! {"
3598 ˇ
3599 "});
3600}
3601
3602#[gpui::test]
3603async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3604 init_test(cx, |_| {});
3605
3606 let mut cx = EditorTestContext::new(cx).await;
3607
3608 // Consider continuous selection as single selection
3609 cx.set_state(indoc! {"
3610 Aaa«aa
3611 cˇ»c«c
3612 bb
3613 aaaˇ»aa
3614 "});
3615 cx.update_editor(|e, window, cx| {
3616 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3617 });
3618 cx.assert_editor_state(indoc! {"
3619 «Aaaaa
3620 ccc
3621 bb
3622 aaaaaˇ»
3623 "});
3624
3625 cx.set_state(indoc! {"
3626 Aaa«aa
3627 cˇ»c«c
3628 bb
3629 aaaˇ»aa
3630 "});
3631 cx.update_editor(|e, window, cx| {
3632 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3633 });
3634 cx.assert_editor_state(indoc! {"
3635 «Aaaaa
3636 ccc
3637 bbˇ»
3638 "});
3639
3640 // Consider non continuous selection as distinct dedup operations
3641 cx.set_state(indoc! {"
3642 «aaaaa
3643 bb
3644 aaaaa
3645 aaaaaˇ»
3646
3647 aaa«aaˇ»
3648 "});
3649 cx.update_editor(|e, window, cx| {
3650 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3651 });
3652 cx.assert_editor_state(indoc! {"
3653 «aaaaa
3654 bbˇ»
3655
3656 «aaaaaˇ»
3657 "});
3658}
3659
3660#[gpui::test]
3661async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3662 init_test(cx, |_| {});
3663
3664 let mut cx = EditorTestContext::new(cx).await;
3665
3666 cx.set_state(indoc! {"
3667 «Aaa
3668 aAa
3669 Aaaˇ»
3670 "});
3671 cx.update_editor(|e, window, cx| {
3672 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3673 });
3674 cx.assert_editor_state(indoc! {"
3675 «Aaa
3676 aAaˇ»
3677 "});
3678
3679 cx.set_state(indoc! {"
3680 «Aaa
3681 aAa
3682 aaAˇ»
3683 "});
3684 cx.update_editor(|e, window, cx| {
3685 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3686 });
3687 cx.assert_editor_state(indoc! {"
3688 «Aaaˇ»
3689 "});
3690}
3691
3692#[gpui::test]
3693async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3694 init_test(cx, |_| {});
3695
3696 let mut cx = EditorTestContext::new(cx).await;
3697
3698 // Manipulate with multiple selections on a single line
3699 cx.set_state(indoc! {"
3700 dd«dd
3701 cˇ»c«c
3702 bb
3703 aaaˇ»aa
3704 "});
3705 cx.update_editor(|e, window, cx| {
3706 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3707 });
3708 cx.assert_editor_state(indoc! {"
3709 «aaaaa
3710 bb
3711 ccc
3712 ddddˇ»
3713 "});
3714
3715 // Manipulate with multiple disjoin selections
3716 cx.set_state(indoc! {"
3717 5«
3718 4
3719 3
3720 2
3721 1ˇ»
3722
3723 dd«dd
3724 ccc
3725 bb
3726 aaaˇ»aa
3727 "});
3728 cx.update_editor(|e, window, cx| {
3729 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3730 });
3731 cx.assert_editor_state(indoc! {"
3732 «1
3733 2
3734 3
3735 4
3736 5ˇ»
3737
3738 «aaaaa
3739 bb
3740 ccc
3741 ddddˇ»
3742 "});
3743
3744 // Adding lines on each selection
3745 cx.set_state(indoc! {"
3746 2«
3747 1ˇ»
3748
3749 bb«bb
3750 aaaˇ»aa
3751 "});
3752 cx.update_editor(|e, window, cx| {
3753 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3754 });
3755 cx.assert_editor_state(indoc! {"
3756 «2
3757 1
3758 added lineˇ»
3759
3760 «bbbb
3761 aaaaa
3762 added lineˇ»
3763 "});
3764
3765 // Removing lines on each selection
3766 cx.set_state(indoc! {"
3767 2«
3768 1ˇ»
3769
3770 bb«bb
3771 aaaˇ»aa
3772 "});
3773 cx.update_editor(|e, window, cx| {
3774 e.manipulate_lines(window, cx, |lines| {
3775 lines.pop();
3776 })
3777 });
3778 cx.assert_editor_state(indoc! {"
3779 «2ˇ»
3780
3781 «bbbbˇ»
3782 "});
3783}
3784
3785#[gpui::test]
3786async fn test_manipulate_text(cx: &mut TestAppContext) {
3787 init_test(cx, |_| {});
3788
3789 let mut cx = EditorTestContext::new(cx).await;
3790
3791 // Test convert_to_upper_case()
3792 cx.set_state(indoc! {"
3793 «hello worldˇ»
3794 "});
3795 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 «HELLO WORLDˇ»
3798 "});
3799
3800 // Test convert_to_lower_case()
3801 cx.set_state(indoc! {"
3802 «HELLO WORLDˇ»
3803 "});
3804 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3805 cx.assert_editor_state(indoc! {"
3806 «hello worldˇ»
3807 "});
3808
3809 // Test multiple line, single selection case
3810 cx.set_state(indoc! {"
3811 «The quick brown
3812 fox jumps over
3813 the lazy dogˇ»
3814 "});
3815 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3816 cx.assert_editor_state(indoc! {"
3817 «The Quick Brown
3818 Fox Jumps Over
3819 The Lazy Dogˇ»
3820 "});
3821
3822 // Test multiple line, single selection case
3823 cx.set_state(indoc! {"
3824 «The quick brown
3825 fox jumps over
3826 the lazy dogˇ»
3827 "});
3828 cx.update_editor(|e, window, cx| {
3829 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3830 });
3831 cx.assert_editor_state(indoc! {"
3832 «TheQuickBrown
3833 FoxJumpsOver
3834 TheLazyDogˇ»
3835 "});
3836
3837 // From here on out, test more complex cases of manipulate_text()
3838
3839 // Test no selection case - should affect words cursors are in
3840 // Cursor at beginning, middle, and end of word
3841 cx.set_state(indoc! {"
3842 ˇhello big beauˇtiful worldˇ
3843 "});
3844 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3847 "});
3848
3849 // Test multiple selections on a single line and across multiple lines
3850 cx.set_state(indoc! {"
3851 «Theˇ» quick «brown
3852 foxˇ» jumps «overˇ»
3853 the «lazyˇ» dog
3854 "});
3855 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3856 cx.assert_editor_state(indoc! {"
3857 «THEˇ» quick «BROWN
3858 FOXˇ» jumps «OVERˇ»
3859 the «LAZYˇ» dog
3860 "});
3861
3862 // Test case where text length grows
3863 cx.set_state(indoc! {"
3864 «tschüߡ»
3865 "});
3866 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3867 cx.assert_editor_state(indoc! {"
3868 «TSCHÜSSˇ»
3869 "});
3870
3871 // Test to make sure we don't crash when text shrinks
3872 cx.set_state(indoc! {"
3873 aaa_bbbˇ
3874 "});
3875 cx.update_editor(|e, window, cx| {
3876 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3877 });
3878 cx.assert_editor_state(indoc! {"
3879 «aaaBbbˇ»
3880 "});
3881
3882 // Test to make sure we all aware of the fact that each word can grow and shrink
3883 // Final selections should be aware of this fact
3884 cx.set_state(indoc! {"
3885 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3886 "});
3887 cx.update_editor(|e, window, cx| {
3888 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3889 });
3890 cx.assert_editor_state(indoc! {"
3891 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3892 "});
3893
3894 cx.set_state(indoc! {"
3895 «hElLo, WoRld!ˇ»
3896 "});
3897 cx.update_editor(|e, window, cx| {
3898 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3899 });
3900 cx.assert_editor_state(indoc! {"
3901 «HeLlO, wOrLD!ˇ»
3902 "});
3903}
3904
3905#[gpui::test]
3906fn test_duplicate_line(cx: &mut TestAppContext) {
3907 init_test(cx, |_| {});
3908
3909 let editor = cx.add_window(|window, cx| {
3910 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3911 build_editor(buffer, window, cx)
3912 });
3913 _ = editor.update(cx, |editor, window, cx| {
3914 editor.change_selections(None, window, cx, |s| {
3915 s.select_display_ranges([
3916 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3917 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3918 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3919 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3920 ])
3921 });
3922 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3923 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3924 assert_eq!(
3925 editor.selections.display_ranges(cx),
3926 vec![
3927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3928 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3929 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3930 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3931 ]
3932 );
3933 });
3934
3935 let editor = cx.add_window(|window, cx| {
3936 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3937 build_editor(buffer, window, cx)
3938 });
3939 _ = editor.update(cx, |editor, window, cx| {
3940 editor.change_selections(None, window, cx, |s| {
3941 s.select_display_ranges([
3942 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3943 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3944 ])
3945 });
3946 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3947 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3948 assert_eq!(
3949 editor.selections.display_ranges(cx),
3950 vec![
3951 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3952 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3953 ]
3954 );
3955 });
3956
3957 // With `move_upwards` the selections stay in place, except for
3958 // the lines inserted above them
3959 let editor = cx.add_window(|window, cx| {
3960 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3961 build_editor(buffer, window, cx)
3962 });
3963 _ = editor.update(cx, |editor, window, cx| {
3964 editor.change_selections(None, window, cx, |s| {
3965 s.select_display_ranges([
3966 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3967 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3968 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3969 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3970 ])
3971 });
3972 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3973 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3974 assert_eq!(
3975 editor.selections.display_ranges(cx),
3976 vec![
3977 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3978 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3979 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3980 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3981 ]
3982 );
3983 });
3984
3985 let editor = cx.add_window(|window, cx| {
3986 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3987 build_editor(buffer, window, cx)
3988 });
3989 _ = editor.update(cx, |editor, window, cx| {
3990 editor.change_selections(None, window, cx, |s| {
3991 s.select_display_ranges([
3992 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3993 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3994 ])
3995 });
3996 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3997 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3998 assert_eq!(
3999 editor.selections.display_ranges(cx),
4000 vec![
4001 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4002 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4003 ]
4004 );
4005 });
4006
4007 let editor = cx.add_window(|window, cx| {
4008 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4009 build_editor(buffer, window, cx)
4010 });
4011 _ = editor.update(cx, |editor, window, cx| {
4012 editor.change_selections(None, window, cx, |s| {
4013 s.select_display_ranges([
4014 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4015 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4016 ])
4017 });
4018 editor.duplicate_selection(&DuplicateSelection, window, cx);
4019 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4020 assert_eq!(
4021 editor.selections.display_ranges(cx),
4022 vec![
4023 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4024 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4025 ]
4026 );
4027 });
4028}
4029
4030#[gpui::test]
4031fn test_move_line_up_down(cx: &mut TestAppContext) {
4032 init_test(cx, |_| {});
4033
4034 let editor = cx.add_window(|window, cx| {
4035 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4036 build_editor(buffer, window, cx)
4037 });
4038 _ = editor.update(cx, |editor, window, cx| {
4039 editor.fold_creases(
4040 vec![
4041 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4042 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4043 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4044 ],
4045 true,
4046 window,
4047 cx,
4048 );
4049 editor.change_selections(None, window, cx, |s| {
4050 s.select_display_ranges([
4051 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4052 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4053 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4054 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4055 ])
4056 });
4057 assert_eq!(
4058 editor.display_text(cx),
4059 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4060 );
4061
4062 editor.move_line_up(&MoveLineUp, window, cx);
4063 assert_eq!(
4064 editor.display_text(cx),
4065 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4066 );
4067 assert_eq!(
4068 editor.selections.display_ranges(cx),
4069 vec![
4070 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4071 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4072 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4073 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4074 ]
4075 );
4076 });
4077
4078 _ = editor.update(cx, |editor, window, cx| {
4079 editor.move_line_down(&MoveLineDown, window, cx);
4080 assert_eq!(
4081 editor.display_text(cx),
4082 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4083 );
4084 assert_eq!(
4085 editor.selections.display_ranges(cx),
4086 vec![
4087 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4088 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4089 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4090 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4091 ]
4092 );
4093 });
4094
4095 _ = editor.update(cx, |editor, window, cx| {
4096 editor.move_line_down(&MoveLineDown, window, cx);
4097 assert_eq!(
4098 editor.display_text(cx),
4099 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4100 );
4101 assert_eq!(
4102 editor.selections.display_ranges(cx),
4103 vec![
4104 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4105 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4106 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4107 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4108 ]
4109 );
4110 });
4111
4112 _ = editor.update(cx, |editor, window, cx| {
4113 editor.move_line_up(&MoveLineUp, window, cx);
4114 assert_eq!(
4115 editor.display_text(cx),
4116 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4117 );
4118 assert_eq!(
4119 editor.selections.display_ranges(cx),
4120 vec![
4121 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4122 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4123 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4124 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4125 ]
4126 );
4127 });
4128}
4129
4130#[gpui::test]
4131fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let editor = cx.add_window(|window, cx| {
4135 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4136 build_editor(buffer, window, cx)
4137 });
4138 _ = editor.update(cx, |editor, window, cx| {
4139 let snapshot = editor.buffer.read(cx).snapshot(cx);
4140 editor.insert_blocks(
4141 [BlockProperties {
4142 style: BlockStyle::Fixed,
4143 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4144 height: 1,
4145 render: Arc::new(|_| div().into_any()),
4146 priority: 0,
4147 }],
4148 Some(Autoscroll::fit()),
4149 cx,
4150 );
4151 editor.change_selections(None, window, cx, |s| {
4152 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4153 });
4154 editor.move_line_down(&MoveLineDown, window, cx);
4155 });
4156}
4157
4158#[gpui::test]
4159async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4160 init_test(cx, |_| {});
4161
4162 let mut cx = EditorTestContext::new(cx).await;
4163 cx.set_state(
4164 &"
4165 ˇzero
4166 one
4167 two
4168 three
4169 four
4170 five
4171 "
4172 .unindent(),
4173 );
4174
4175 // Create a four-line block that replaces three lines of text.
4176 cx.update_editor(|editor, window, cx| {
4177 let snapshot = editor.snapshot(window, cx);
4178 let snapshot = &snapshot.buffer_snapshot;
4179 let placement = BlockPlacement::Replace(
4180 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4181 );
4182 editor.insert_blocks(
4183 [BlockProperties {
4184 placement,
4185 height: 4,
4186 style: BlockStyle::Sticky,
4187 render: Arc::new(|_| gpui::div().into_any_element()),
4188 priority: 0,
4189 }],
4190 None,
4191 cx,
4192 );
4193 });
4194
4195 // Move down so that the cursor touches the block.
4196 cx.update_editor(|editor, window, cx| {
4197 editor.move_down(&Default::default(), window, cx);
4198 });
4199 cx.assert_editor_state(
4200 &"
4201 zero
4202 «one
4203 two
4204 threeˇ»
4205 four
4206 five
4207 "
4208 .unindent(),
4209 );
4210
4211 // Move down past the block.
4212 cx.update_editor(|editor, window, cx| {
4213 editor.move_down(&Default::default(), window, cx);
4214 });
4215 cx.assert_editor_state(
4216 &"
4217 zero
4218 one
4219 two
4220 three
4221 ˇfour
4222 five
4223 "
4224 .unindent(),
4225 );
4226}
4227
4228#[gpui::test]
4229fn test_transpose(cx: &mut TestAppContext) {
4230 init_test(cx, |_| {});
4231
4232 _ = cx.add_window(|window, cx| {
4233 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4234 editor.set_style(EditorStyle::default(), window, cx);
4235 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4236 editor.transpose(&Default::default(), window, cx);
4237 assert_eq!(editor.text(cx), "bac");
4238 assert_eq!(editor.selections.ranges(cx), [2..2]);
4239
4240 editor.transpose(&Default::default(), window, cx);
4241 assert_eq!(editor.text(cx), "bca");
4242 assert_eq!(editor.selections.ranges(cx), [3..3]);
4243
4244 editor.transpose(&Default::default(), window, cx);
4245 assert_eq!(editor.text(cx), "bac");
4246 assert_eq!(editor.selections.ranges(cx), [3..3]);
4247
4248 editor
4249 });
4250
4251 _ = cx.add_window(|window, cx| {
4252 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4253 editor.set_style(EditorStyle::default(), window, cx);
4254 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4255 editor.transpose(&Default::default(), window, cx);
4256 assert_eq!(editor.text(cx), "acb\nde");
4257 assert_eq!(editor.selections.ranges(cx), [3..3]);
4258
4259 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4260 editor.transpose(&Default::default(), window, cx);
4261 assert_eq!(editor.text(cx), "acbd\ne");
4262 assert_eq!(editor.selections.ranges(cx), [5..5]);
4263
4264 editor.transpose(&Default::default(), window, cx);
4265 assert_eq!(editor.text(cx), "acbde\n");
4266 assert_eq!(editor.selections.ranges(cx), [6..6]);
4267
4268 editor.transpose(&Default::default(), window, cx);
4269 assert_eq!(editor.text(cx), "acbd\ne");
4270 assert_eq!(editor.selections.ranges(cx), [6..6]);
4271
4272 editor
4273 });
4274
4275 _ = cx.add_window(|window, cx| {
4276 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4277 editor.set_style(EditorStyle::default(), window, cx);
4278 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4279 editor.transpose(&Default::default(), window, cx);
4280 assert_eq!(editor.text(cx), "bacd\ne");
4281 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4282
4283 editor.transpose(&Default::default(), window, cx);
4284 assert_eq!(editor.text(cx), "bcade\n");
4285 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4286
4287 editor.transpose(&Default::default(), window, cx);
4288 assert_eq!(editor.text(cx), "bcda\ne");
4289 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4290
4291 editor.transpose(&Default::default(), window, cx);
4292 assert_eq!(editor.text(cx), "bcade\n");
4293 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4294
4295 editor.transpose(&Default::default(), window, cx);
4296 assert_eq!(editor.text(cx), "bcaed\n");
4297 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4298
4299 editor
4300 });
4301
4302 _ = cx.add_window(|window, cx| {
4303 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4304 editor.set_style(EditorStyle::default(), window, cx);
4305 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4306 editor.transpose(&Default::default(), window, cx);
4307 assert_eq!(editor.text(cx), "🏀🍐✋");
4308 assert_eq!(editor.selections.ranges(cx), [8..8]);
4309
4310 editor.transpose(&Default::default(), window, cx);
4311 assert_eq!(editor.text(cx), "🏀✋🍐");
4312 assert_eq!(editor.selections.ranges(cx), [11..11]);
4313
4314 editor.transpose(&Default::default(), window, cx);
4315 assert_eq!(editor.text(cx), "🏀🍐✋");
4316 assert_eq!(editor.selections.ranges(cx), [11..11]);
4317
4318 editor
4319 });
4320}
4321
4322#[gpui::test]
4323async fn test_rewrap(cx: &mut TestAppContext) {
4324 init_test(cx, |_| {});
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 let language_with_c_comments = Arc::new(Language::new(
4329 LanguageConfig {
4330 line_comments: vec!["// ".into()],
4331 ..LanguageConfig::default()
4332 },
4333 None,
4334 ));
4335 let language_with_pound_comments = Arc::new(Language::new(
4336 LanguageConfig {
4337 line_comments: vec!["# ".into()],
4338 ..LanguageConfig::default()
4339 },
4340 None,
4341 ));
4342 let markdown_language = Arc::new(Language::new(
4343 LanguageConfig {
4344 name: "Markdown".into(),
4345 ..LanguageConfig::default()
4346 },
4347 None,
4348 ));
4349 let language_with_doc_comments = Arc::new(Language::new(
4350 LanguageConfig {
4351 line_comments: vec!["// ".into(), "/// ".into()],
4352 ..LanguageConfig::default()
4353 },
4354 Some(tree_sitter_rust::LANGUAGE.into()),
4355 ));
4356
4357 let plaintext_language = Arc::new(Language::new(
4358 LanguageConfig {
4359 name: "Plain Text".into(),
4360 ..LanguageConfig::default()
4361 },
4362 None,
4363 ));
4364
4365 assert_rewrap(
4366 indoc! {"
4367 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4368 "},
4369 indoc! {"
4370 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4371 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4372 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4373 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4374 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4375 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4376 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4377 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4378 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4379 // porttitor id. Aliquam id accumsan eros.
4380 "},
4381 language_with_c_comments.clone(),
4382 &mut cx,
4383 );
4384
4385 // Test that rewrapping works inside of a selection
4386 assert_rewrap(
4387 indoc! {"
4388 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4389 "},
4390 indoc! {"
4391 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4392 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4393 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4394 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4395 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4396 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4397 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4398 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4399 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4400 // porttitor id. Aliquam id accumsan eros.ˇ»
4401 "},
4402 language_with_c_comments.clone(),
4403 &mut cx,
4404 );
4405
4406 // Test that cursors that expand to the same region are collapsed.
4407 assert_rewrap(
4408 indoc! {"
4409 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4410 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4411 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4412 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4413 "},
4414 indoc! {"
4415 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4416 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4417 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4418 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4419 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4420 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4421 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4422 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4423 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4424 // porttitor id. Aliquam id accumsan eros.
4425 "},
4426 language_with_c_comments.clone(),
4427 &mut cx,
4428 );
4429
4430 // Test that non-contiguous selections are treated separately.
4431 assert_rewrap(
4432 indoc! {"
4433 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4434 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4435 //
4436 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4437 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4438 "},
4439 indoc! {"
4440 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4441 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4442 // auctor, eu lacinia sapien scelerisque.
4443 //
4444 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4445 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4446 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4447 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4448 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4449 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4450 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4451 "},
4452 language_with_c_comments.clone(),
4453 &mut cx,
4454 );
4455
4456 // Test that different comment prefixes are supported.
4457 assert_rewrap(
4458 indoc! {"
4459 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4460 "},
4461 indoc! {"
4462 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4463 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4464 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4465 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4466 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4467 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4468 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4469 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4470 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4471 # accumsan eros.
4472 "},
4473 language_with_pound_comments.clone(),
4474 &mut cx,
4475 );
4476
4477 // Test that rewrapping is ignored outside of comments in most languages.
4478 assert_rewrap(
4479 indoc! {"
4480 /// Adds two numbers.
4481 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4482 fn add(a: u32, b: u32) -> u32 {
4483 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4484 }
4485 "},
4486 indoc! {"
4487 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4488 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4489 fn add(a: u32, b: u32) -> u32 {
4490 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4491 }
4492 "},
4493 language_with_doc_comments.clone(),
4494 &mut cx,
4495 );
4496
4497 // Test that rewrapping works in Markdown and Plain Text languages.
4498 assert_rewrap(
4499 indoc! {"
4500 # Hello
4501
4502 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4503 "},
4504 indoc! {"
4505 # Hello
4506
4507 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4508 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4509 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4510 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4511 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4512 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4513 Integer sit amet scelerisque nisi.
4514 "},
4515 markdown_language,
4516 &mut cx,
4517 );
4518
4519 assert_rewrap(
4520 indoc! {"
4521 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4522 "},
4523 indoc! {"
4524 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4525 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4526 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4527 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4528 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4529 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4530 Integer sit amet scelerisque nisi.
4531 "},
4532 plaintext_language,
4533 &mut cx,
4534 );
4535
4536 // Test rewrapping unaligned comments in a selection.
4537 assert_rewrap(
4538 indoc! {"
4539 fn foo() {
4540 if true {
4541 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4542 // Praesent semper egestas tellus id dignissim.ˇ»
4543 do_something();
4544 } else {
4545 //
4546 }
4547 }
4548 "},
4549 indoc! {"
4550 fn foo() {
4551 if true {
4552 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4553 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4554 // egestas tellus id dignissim.ˇ»
4555 do_something();
4556 } else {
4557 //
4558 }
4559 }
4560 "},
4561 language_with_doc_comments.clone(),
4562 &mut cx,
4563 );
4564
4565 assert_rewrap(
4566 indoc! {"
4567 fn foo() {
4568 if true {
4569 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4570 // Praesent semper egestas tellus id dignissim.»
4571 do_something();
4572 } else {
4573 //
4574 }
4575
4576 }
4577 "},
4578 indoc! {"
4579 fn foo() {
4580 if true {
4581 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4582 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4583 // egestas tellus id dignissim.»
4584 do_something();
4585 } else {
4586 //
4587 }
4588
4589 }
4590 "},
4591 language_with_doc_comments.clone(),
4592 &mut cx,
4593 );
4594
4595 #[track_caller]
4596 fn assert_rewrap(
4597 unwrapped_text: &str,
4598 wrapped_text: &str,
4599 language: Arc<Language>,
4600 cx: &mut EditorTestContext,
4601 ) {
4602 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4603 cx.set_state(unwrapped_text);
4604 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4605 cx.assert_editor_state(wrapped_text);
4606 }
4607}
4608
4609#[gpui::test]
4610async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4611 init_test(cx, |_| {});
4612
4613 let mut cx = EditorTestContext::new(cx).await;
4614
4615 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4616 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4617 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4618
4619 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4620 cx.set_state("two ˇfour ˇsix ˇ");
4621 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4622 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4623
4624 // Paste again but with only two cursors. Since the number of cursors doesn't
4625 // match the number of slices in the clipboard, the entire clipboard text
4626 // is pasted at each cursor.
4627 cx.set_state("ˇtwo one✅ four three six five ˇ");
4628 cx.update_editor(|e, window, cx| {
4629 e.handle_input("( ", window, cx);
4630 e.paste(&Paste, window, cx);
4631 e.handle_input(") ", window, cx);
4632 });
4633 cx.assert_editor_state(
4634 &([
4635 "( one✅ ",
4636 "three ",
4637 "five ) ˇtwo one✅ four three six five ( one✅ ",
4638 "three ",
4639 "five ) ˇ",
4640 ]
4641 .join("\n")),
4642 );
4643
4644 // Cut with three selections, one of which is full-line.
4645 cx.set_state(indoc! {"
4646 1«2ˇ»3
4647 4ˇ567
4648 «8ˇ»9"});
4649 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4650 cx.assert_editor_state(indoc! {"
4651 1ˇ3
4652 ˇ9"});
4653
4654 // Paste with three selections, noticing how the copied selection that was full-line
4655 // gets inserted before the second cursor.
4656 cx.set_state(indoc! {"
4657 1ˇ3
4658 9ˇ
4659 «oˇ»ne"});
4660 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4661 cx.assert_editor_state(indoc! {"
4662 12ˇ3
4663 4567
4664 9ˇ
4665 8ˇne"});
4666
4667 // Copy with a single cursor only, which writes the whole line into the clipboard.
4668 cx.set_state(indoc! {"
4669 The quick brown
4670 fox juˇmps over
4671 the lazy dog"});
4672 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4673 assert_eq!(
4674 cx.read_from_clipboard()
4675 .and_then(|item| item.text().as_deref().map(str::to_string)),
4676 Some("fox jumps over\n".to_string())
4677 );
4678
4679 // Paste with three selections, noticing how the copied full-line selection is inserted
4680 // before the empty selections but replaces the selection that is non-empty.
4681 cx.set_state(indoc! {"
4682 Tˇhe quick brown
4683 «foˇ»x jumps over
4684 tˇhe lazy dog"});
4685 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4686 cx.assert_editor_state(indoc! {"
4687 fox jumps over
4688 Tˇhe quick brown
4689 fox jumps over
4690 ˇx jumps over
4691 fox jumps over
4692 tˇhe lazy dog"});
4693}
4694
4695#[gpui::test]
4696async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4697 init_test(cx, |_| {});
4698
4699 let mut cx = EditorTestContext::new(cx).await;
4700 let language = Arc::new(Language::new(
4701 LanguageConfig::default(),
4702 Some(tree_sitter_rust::LANGUAGE.into()),
4703 ));
4704 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4705
4706 // Cut an indented block, without the leading whitespace.
4707 cx.set_state(indoc! {"
4708 const a: B = (
4709 c(),
4710 «d(
4711 e,
4712 f
4713 )ˇ»
4714 );
4715 "});
4716 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4717 cx.assert_editor_state(indoc! {"
4718 const a: B = (
4719 c(),
4720 ˇ
4721 );
4722 "});
4723
4724 // Paste it at the same position.
4725 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4726 cx.assert_editor_state(indoc! {"
4727 const a: B = (
4728 c(),
4729 d(
4730 e,
4731 f
4732 )ˇ
4733 );
4734 "});
4735
4736 // Paste it at a line with a lower indent level.
4737 cx.set_state(indoc! {"
4738 ˇ
4739 const a: B = (
4740 c(),
4741 );
4742 "});
4743 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4744 cx.assert_editor_state(indoc! {"
4745 d(
4746 e,
4747 f
4748 )ˇ
4749 const a: B = (
4750 c(),
4751 );
4752 "});
4753
4754 // Cut an indented block, with the leading whitespace.
4755 cx.set_state(indoc! {"
4756 const a: B = (
4757 c(),
4758 « d(
4759 e,
4760 f
4761 )
4762 ˇ»);
4763 "});
4764 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4765 cx.assert_editor_state(indoc! {"
4766 const a: B = (
4767 c(),
4768 ˇ);
4769 "});
4770
4771 // Paste it at the same position.
4772 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4773 cx.assert_editor_state(indoc! {"
4774 const a: B = (
4775 c(),
4776 d(
4777 e,
4778 f
4779 )
4780 ˇ);
4781 "});
4782
4783 // Paste it at a line with a higher indent level.
4784 cx.set_state(indoc! {"
4785 const a: B = (
4786 c(),
4787 d(
4788 e,
4789 fˇ
4790 )
4791 );
4792 "});
4793 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4794 cx.assert_editor_state(indoc! {"
4795 const a: B = (
4796 c(),
4797 d(
4798 e,
4799 f d(
4800 e,
4801 f
4802 )
4803 ˇ
4804 )
4805 );
4806 "});
4807}
4808
4809#[gpui::test]
4810fn test_select_all(cx: &mut TestAppContext) {
4811 init_test(cx, |_| {});
4812
4813 let editor = cx.add_window(|window, cx| {
4814 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4815 build_editor(buffer, window, cx)
4816 });
4817 _ = editor.update(cx, |editor, window, cx| {
4818 editor.select_all(&SelectAll, window, cx);
4819 assert_eq!(
4820 editor.selections.display_ranges(cx),
4821 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4822 );
4823 });
4824}
4825
4826#[gpui::test]
4827fn test_select_line(cx: &mut TestAppContext) {
4828 init_test(cx, |_| {});
4829
4830 let editor = cx.add_window(|window, cx| {
4831 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4832 build_editor(buffer, window, cx)
4833 });
4834 _ = editor.update(cx, |editor, window, cx| {
4835 editor.change_selections(None, window, cx, |s| {
4836 s.select_display_ranges([
4837 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4838 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4839 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4840 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4841 ])
4842 });
4843 editor.select_line(&SelectLine, window, cx);
4844 assert_eq!(
4845 editor.selections.display_ranges(cx),
4846 vec![
4847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4848 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4849 ]
4850 );
4851 });
4852
4853 _ = editor.update(cx, |editor, window, cx| {
4854 editor.select_line(&SelectLine, window, cx);
4855 assert_eq!(
4856 editor.selections.display_ranges(cx),
4857 vec![
4858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4859 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4860 ]
4861 );
4862 });
4863
4864 _ = editor.update(cx, |editor, window, cx| {
4865 editor.select_line(&SelectLine, window, cx);
4866 assert_eq!(
4867 editor.selections.display_ranges(cx),
4868 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4869 );
4870 });
4871}
4872
4873#[gpui::test]
4874async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4875 init_test(cx, |_| {});
4876 let mut cx = EditorTestContext::new(cx).await;
4877
4878 #[track_caller]
4879 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
4880 cx.set_state(initial_state);
4881 cx.update_editor(|e, window, cx| {
4882 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
4883 });
4884 cx.assert_editor_state(expected_state);
4885 }
4886
4887 // Selection starts and ends at the middle of lines, left-to-right
4888 test(
4889 &mut cx,
4890 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
4891 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4892 );
4893 // Same thing, right-to-left
4894 test(
4895 &mut cx,
4896 "aa\nb«b\ncc\ndd\neˇ»e\nff",
4897 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4898 );
4899
4900 // Whole buffer, left-to-right, last line *doesn't* end with newline
4901 test(
4902 &mut cx,
4903 "«ˇaa\nbb\ncc\ndd\nee\nff»",
4904 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4905 );
4906 // Same thing, right-to-left
4907 test(
4908 &mut cx,
4909 "«aa\nbb\ncc\ndd\nee\nffˇ»",
4910 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4911 );
4912
4913 // Whole buffer, left-to-right, last line ends with newline
4914 test(
4915 &mut cx,
4916 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
4917 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4918 );
4919 // Same thing, right-to-left
4920 test(
4921 &mut cx,
4922 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
4923 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4924 );
4925
4926 // Starts at the end of a line, ends at the start of another
4927 test(
4928 &mut cx,
4929 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
4930 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
4931 );
4932}
4933
4934#[gpui::test]
4935async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
4936 init_test(cx, |_| {});
4937
4938 let editor = cx.add_window(|window, cx| {
4939 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4940 build_editor(buffer, window, cx)
4941 });
4942
4943 // setup
4944 _ = editor.update(cx, |editor, window, cx| {
4945 editor.fold_creases(
4946 vec![
4947 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4948 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4949 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4950 ],
4951 true,
4952 window,
4953 cx,
4954 );
4955 assert_eq!(
4956 editor.display_text(cx),
4957 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4958 );
4959 });
4960
4961 _ = editor.update(cx, |editor, window, cx| {
4962 editor.change_selections(None, window, cx, |s| {
4963 s.select_display_ranges([
4964 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4965 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4966 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4967 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4968 ])
4969 });
4970 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4971 assert_eq!(
4972 editor.display_text(cx),
4973 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4974 );
4975 });
4976 EditorTestContext::for_editor(editor, cx)
4977 .await
4978 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
4979
4980 _ = editor.update(cx, |editor, window, cx| {
4981 editor.change_selections(None, window, cx, |s| {
4982 s.select_display_ranges([
4983 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4984 ])
4985 });
4986 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4987 assert_eq!(
4988 editor.display_text(cx),
4989 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4990 );
4991 assert_eq!(
4992 editor.selections.display_ranges(cx),
4993 [
4994 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4995 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4996 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
4997 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
4998 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
4999 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5000 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5001 ]
5002 );
5003 });
5004 EditorTestContext::for_editor(editor, cx)
5005 .await
5006 .assert_editor_state(
5007 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5008 );
5009}
5010
5011#[gpui::test]
5012async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5013 init_test(cx, |_| {});
5014
5015 let mut cx = EditorTestContext::new(cx).await;
5016
5017 cx.set_state(indoc!(
5018 r#"abc
5019 defˇghi
5020
5021 jk
5022 nlmo
5023 "#
5024 ));
5025
5026 cx.update_editor(|editor, window, cx| {
5027 editor.add_selection_above(&Default::default(), window, cx);
5028 });
5029
5030 cx.assert_editor_state(indoc!(
5031 r#"abcˇ
5032 defˇghi
5033
5034 jk
5035 nlmo
5036 "#
5037 ));
5038
5039 cx.update_editor(|editor, window, cx| {
5040 editor.add_selection_above(&Default::default(), window, cx);
5041 });
5042
5043 cx.assert_editor_state(indoc!(
5044 r#"abcˇ
5045 defˇghi
5046
5047 jk
5048 nlmo
5049 "#
5050 ));
5051
5052 cx.update_editor(|editor, window, cx| {
5053 editor.add_selection_below(&Default::default(), window, cx);
5054 });
5055
5056 cx.assert_editor_state(indoc!(
5057 r#"abc
5058 defˇghi
5059
5060 jk
5061 nlmo
5062 "#
5063 ));
5064
5065 cx.update_editor(|editor, window, cx| {
5066 editor.undo_selection(&Default::default(), window, cx);
5067 });
5068
5069 cx.assert_editor_state(indoc!(
5070 r#"abcˇ
5071 defˇghi
5072
5073 jk
5074 nlmo
5075 "#
5076 ));
5077
5078 cx.update_editor(|editor, window, cx| {
5079 editor.redo_selection(&Default::default(), window, cx);
5080 });
5081
5082 cx.assert_editor_state(indoc!(
5083 r#"abc
5084 defˇghi
5085
5086 jk
5087 nlmo
5088 "#
5089 ));
5090
5091 cx.update_editor(|editor, window, cx| {
5092 editor.add_selection_below(&Default::default(), window, cx);
5093 });
5094
5095 cx.assert_editor_state(indoc!(
5096 r#"abc
5097 defˇghi
5098
5099 jk
5100 nlmˇo
5101 "#
5102 ));
5103
5104 cx.update_editor(|editor, window, cx| {
5105 editor.add_selection_below(&Default::default(), window, cx);
5106 });
5107
5108 cx.assert_editor_state(indoc!(
5109 r#"abc
5110 defˇghi
5111
5112 jk
5113 nlmˇo
5114 "#
5115 ));
5116
5117 // change selections
5118 cx.set_state(indoc!(
5119 r#"abc
5120 def«ˇg»hi
5121
5122 jk
5123 nlmo
5124 "#
5125 ));
5126
5127 cx.update_editor(|editor, window, cx| {
5128 editor.add_selection_below(&Default::default(), window, cx);
5129 });
5130
5131 cx.assert_editor_state(indoc!(
5132 r#"abc
5133 def«ˇg»hi
5134
5135 jk
5136 nlm«ˇo»
5137 "#
5138 ));
5139
5140 cx.update_editor(|editor, window, cx| {
5141 editor.add_selection_below(&Default::default(), window, cx);
5142 });
5143
5144 cx.assert_editor_state(indoc!(
5145 r#"abc
5146 def«ˇg»hi
5147
5148 jk
5149 nlm«ˇo»
5150 "#
5151 ));
5152
5153 cx.update_editor(|editor, window, cx| {
5154 editor.add_selection_above(&Default::default(), window, cx);
5155 });
5156
5157 cx.assert_editor_state(indoc!(
5158 r#"abc
5159 def«ˇg»hi
5160
5161 jk
5162 nlmo
5163 "#
5164 ));
5165
5166 cx.update_editor(|editor, window, cx| {
5167 editor.add_selection_above(&Default::default(), window, cx);
5168 });
5169
5170 cx.assert_editor_state(indoc!(
5171 r#"abc
5172 def«ˇg»hi
5173
5174 jk
5175 nlmo
5176 "#
5177 ));
5178
5179 // Change selections again
5180 cx.set_state(indoc!(
5181 r#"a«bc
5182 defgˇ»hi
5183
5184 jk
5185 nlmo
5186 "#
5187 ));
5188
5189 cx.update_editor(|editor, window, cx| {
5190 editor.add_selection_below(&Default::default(), window, cx);
5191 });
5192
5193 cx.assert_editor_state(indoc!(
5194 r#"a«bcˇ»
5195 d«efgˇ»hi
5196
5197 j«kˇ»
5198 nlmo
5199 "#
5200 ));
5201
5202 cx.update_editor(|editor, window, cx| {
5203 editor.add_selection_below(&Default::default(), window, cx);
5204 });
5205 cx.assert_editor_state(indoc!(
5206 r#"a«bcˇ»
5207 d«efgˇ»hi
5208
5209 j«kˇ»
5210 n«lmoˇ»
5211 "#
5212 ));
5213 cx.update_editor(|editor, window, cx| {
5214 editor.add_selection_above(&Default::default(), window, cx);
5215 });
5216
5217 cx.assert_editor_state(indoc!(
5218 r#"a«bcˇ»
5219 d«efgˇ»hi
5220
5221 j«kˇ»
5222 nlmo
5223 "#
5224 ));
5225
5226 // Change selections again
5227 cx.set_state(indoc!(
5228 r#"abc
5229 d«ˇefghi
5230
5231 jk
5232 nlm»o
5233 "#
5234 ));
5235
5236 cx.update_editor(|editor, window, cx| {
5237 editor.add_selection_above(&Default::default(), window, cx);
5238 });
5239
5240 cx.assert_editor_state(indoc!(
5241 r#"a«ˇbc»
5242 d«ˇef»ghi
5243
5244 j«ˇk»
5245 n«ˇlm»o
5246 "#
5247 ));
5248
5249 cx.update_editor(|editor, window, cx| {
5250 editor.add_selection_below(&Default::default(), window, cx);
5251 });
5252
5253 cx.assert_editor_state(indoc!(
5254 r#"abc
5255 d«ˇef»ghi
5256
5257 j«ˇk»
5258 n«ˇlm»o
5259 "#
5260 ));
5261}
5262
5263#[gpui::test]
5264async fn test_select_next(cx: &mut gpui::TestAppContext) {
5265 init_test(cx, |_| {});
5266
5267 let mut cx = EditorTestContext::new(cx).await;
5268 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5269
5270 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5271 .unwrap();
5272 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5273
5274 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5275 .unwrap();
5276 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5277
5278 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5279 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5280
5281 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5282 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5283
5284 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5285 .unwrap();
5286 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5287
5288 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5289 .unwrap();
5290 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5291}
5292
5293#[gpui::test]
5294async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5295 init_test(cx, |_| {});
5296
5297 let mut cx = EditorTestContext::new(cx).await;
5298
5299 // Test caret-only selections
5300 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5301 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5302 .unwrap();
5303 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5304
5305 // Test left-to-right selections
5306 cx.set_state("abc\n«abcˇ»\nabc");
5307 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5308 .unwrap();
5309 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5310
5311 // Test right-to-left selections
5312 cx.set_state("abc\n«ˇabc»\nabc");
5313 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5314 .unwrap();
5315 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5316
5317 // Test selecting whitespace with caret selection
5318 cx.set_state("abc\nˇ abc\nabc");
5319 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5320 .unwrap();
5321 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5322
5323 // Test selecting whitespace with left-to-right selection
5324 cx.set_state("abc\n«ˇ »abc\nabc");
5325 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5326 .unwrap();
5327 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5328
5329 // Test no matches with right-to-left selection
5330 cx.set_state("abc\n« ˇ»abc\nabc");
5331 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5332 .unwrap();
5333 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5334}
5335
5336#[gpui::test]
5337async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5338 init_test(cx, |_| {});
5339
5340 let mut cx = EditorTestContext::new(cx).await;
5341 cx.set_state(
5342 r#"let foo = 2;
5343lˇet foo = 2;
5344let fooˇ = 2;
5345let foo = 2;
5346let foo = ˇ2;"#,
5347 );
5348
5349 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5350 .unwrap();
5351 cx.assert_editor_state(
5352 r#"let foo = 2;
5353«letˇ» foo = 2;
5354let «fooˇ» = 2;
5355let foo = 2;
5356let foo = «2ˇ»;"#,
5357 );
5358
5359 // noop for multiple selections with different contents
5360 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5361 .unwrap();
5362 cx.assert_editor_state(
5363 r#"let foo = 2;
5364«letˇ» foo = 2;
5365let «fooˇ» = 2;
5366let foo = 2;
5367let foo = «2ˇ»;"#,
5368 );
5369}
5370
5371#[gpui::test]
5372async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5373 init_test(cx, |_| {});
5374
5375 let mut cx =
5376 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5377
5378 cx.assert_editor_state(indoc! {"
5379 ˇbbb
5380 ccc
5381
5382 bbb
5383 ccc
5384 "});
5385 cx.dispatch_action(SelectPrevious::default());
5386 cx.assert_editor_state(indoc! {"
5387 «bbbˇ»
5388 ccc
5389
5390 bbb
5391 ccc
5392 "});
5393 cx.dispatch_action(SelectPrevious::default());
5394 cx.assert_editor_state(indoc! {"
5395 «bbbˇ»
5396 ccc
5397
5398 «bbbˇ»
5399 ccc
5400 "});
5401}
5402
5403#[gpui::test]
5404async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5405 init_test(cx, |_| {});
5406
5407 let mut cx = EditorTestContext::new(cx).await;
5408 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5409
5410 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5411 .unwrap();
5412 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5413
5414 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5415 .unwrap();
5416 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5417
5418 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5419 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5420
5421 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5422 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5423
5424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5425 .unwrap();
5426 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5427
5428 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5429 .unwrap();
5430 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5431
5432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5433 .unwrap();
5434 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5435}
5436
5437#[gpui::test]
5438async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
5439 init_test(cx, |_| {});
5440
5441 let mut cx = EditorTestContext::new(cx).await;
5442 cx.set_state("aˇ");
5443
5444 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5445 .unwrap();
5446 cx.assert_editor_state("«aˇ»");
5447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5448 .unwrap();
5449 cx.assert_editor_state("«aˇ»");
5450}
5451
5452#[gpui::test]
5453async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5454 init_test(cx, |_| {});
5455
5456 let mut cx = EditorTestContext::new(cx).await;
5457 cx.set_state(
5458 r#"let foo = 2;
5459lˇet foo = 2;
5460let fooˇ = 2;
5461let foo = 2;
5462let foo = ˇ2;"#,
5463 );
5464
5465 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5466 .unwrap();
5467 cx.assert_editor_state(
5468 r#"let foo = 2;
5469«letˇ» foo = 2;
5470let «fooˇ» = 2;
5471let foo = 2;
5472let foo = «2ˇ»;"#,
5473 );
5474
5475 // noop for multiple selections with different contents
5476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5477 .unwrap();
5478 cx.assert_editor_state(
5479 r#"let foo = 2;
5480«letˇ» foo = 2;
5481let «fooˇ» = 2;
5482let foo = 2;
5483let foo = «2ˇ»;"#,
5484 );
5485}
5486
5487#[gpui::test]
5488async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5489 init_test(cx, |_| {});
5490
5491 let mut cx = EditorTestContext::new(cx).await;
5492 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5493
5494 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5495 .unwrap();
5496 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5497
5498 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5499 .unwrap();
5500 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5501
5502 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5503 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5504
5505 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5506 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5507
5508 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5509 .unwrap();
5510 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5511
5512 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5513 .unwrap();
5514 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5515}
5516
5517#[gpui::test]
5518async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5519 init_test(cx, |_| {});
5520
5521 let language = Arc::new(Language::new(
5522 LanguageConfig::default(),
5523 Some(tree_sitter_rust::LANGUAGE.into()),
5524 ));
5525
5526 let text = r#"
5527 use mod1::mod2::{mod3, mod4};
5528
5529 fn fn_1(param1: bool, param2: &str) {
5530 let var1 = "text";
5531 }
5532 "#
5533 .unindent();
5534
5535 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5536 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5537 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5538
5539 editor
5540 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5541 .await;
5542
5543 editor.update_in(cx, |editor, window, cx| {
5544 editor.change_selections(None, window, cx, |s| {
5545 s.select_display_ranges([
5546 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5547 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5548 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5549 ]);
5550 });
5551 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5552 });
5553 editor.update(cx, |editor, cx| {
5554 assert_text_with_selections(
5555 editor,
5556 indoc! {r#"
5557 use mod1::mod2::{mod3, «mod4ˇ»};
5558
5559 fn fn_1«ˇ(param1: bool, param2: &str)» {
5560 let var1 = "«textˇ»";
5561 }
5562 "#},
5563 cx,
5564 );
5565 });
5566
5567 editor.update_in(cx, |editor, window, cx| {
5568 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5569 });
5570 editor.update(cx, |editor, cx| {
5571 assert_text_with_selections(
5572 editor,
5573 indoc! {r#"
5574 use mod1::mod2::«{mod3, mod4}ˇ»;
5575
5576 «ˇfn fn_1(param1: bool, param2: &str) {
5577 let var1 = "text";
5578 }»
5579 "#},
5580 cx,
5581 );
5582 });
5583
5584 editor.update_in(cx, |editor, window, cx| {
5585 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5586 });
5587 assert_eq!(
5588 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5589 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5590 );
5591
5592 // Trying to expand the selected syntax node one more time has no effect.
5593 editor.update_in(cx, |editor, window, cx| {
5594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5595 });
5596 assert_eq!(
5597 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5598 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5599 );
5600
5601 editor.update_in(cx, |editor, window, cx| {
5602 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5603 });
5604 editor.update(cx, |editor, cx| {
5605 assert_text_with_selections(
5606 editor,
5607 indoc! {r#"
5608 use mod1::mod2::«{mod3, mod4}ˇ»;
5609
5610 «ˇfn fn_1(param1: bool, param2: &str) {
5611 let var1 = "text";
5612 }»
5613 "#},
5614 cx,
5615 );
5616 });
5617
5618 editor.update_in(cx, |editor, window, cx| {
5619 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5620 });
5621 editor.update(cx, |editor, cx| {
5622 assert_text_with_selections(
5623 editor,
5624 indoc! {r#"
5625 use mod1::mod2::{mod3, «mod4ˇ»};
5626
5627 fn fn_1«ˇ(param1: bool, param2: &str)» {
5628 let var1 = "«textˇ»";
5629 }
5630 "#},
5631 cx,
5632 );
5633 });
5634
5635 editor.update_in(cx, |editor, window, cx| {
5636 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5637 });
5638 editor.update(cx, |editor, cx| {
5639 assert_text_with_selections(
5640 editor,
5641 indoc! {r#"
5642 use mod1::mod2::{mod3, mo«ˇ»d4};
5643
5644 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5645 let var1 = "te«ˇ»xt";
5646 }
5647 "#},
5648 cx,
5649 );
5650 });
5651
5652 // Trying to shrink the selected syntax node one more time has no effect.
5653 editor.update_in(cx, |editor, window, cx| {
5654 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5655 });
5656 editor.update_in(cx, |editor, _, cx| {
5657 assert_text_with_selections(
5658 editor,
5659 indoc! {r#"
5660 use mod1::mod2::{mod3, mo«ˇ»d4};
5661
5662 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5663 let var1 = "te«ˇ»xt";
5664 }
5665 "#},
5666 cx,
5667 );
5668 });
5669
5670 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5671 // a fold.
5672 editor.update_in(cx, |editor, window, cx| {
5673 editor.fold_creases(
5674 vec![
5675 Crease::simple(
5676 Point::new(0, 21)..Point::new(0, 24),
5677 FoldPlaceholder::test(),
5678 ),
5679 Crease::simple(
5680 Point::new(3, 20)..Point::new(3, 22),
5681 FoldPlaceholder::test(),
5682 ),
5683 ],
5684 true,
5685 window,
5686 cx,
5687 );
5688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5689 });
5690 editor.update(cx, |editor, cx| {
5691 assert_text_with_selections(
5692 editor,
5693 indoc! {r#"
5694 use mod1::mod2::«{mod3, mod4}ˇ»;
5695
5696 fn fn_1«ˇ(param1: bool, param2: &str)» {
5697 «let var1 = "text";ˇ»
5698 }
5699 "#},
5700 cx,
5701 );
5702 });
5703}
5704
5705#[gpui::test]
5706async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5707 init_test(cx, |_| {});
5708
5709 let base_text = r#"
5710 impl A {
5711 // this is an uncommitted comment
5712
5713 fn b() {
5714 c();
5715 }
5716
5717 // this is another uncommitted comment
5718
5719 fn d() {
5720 // e
5721 // f
5722 }
5723 }
5724
5725 fn g() {
5726 // h
5727 }
5728 "#
5729 .unindent();
5730
5731 let text = r#"
5732 ˇimpl A {
5733
5734 fn b() {
5735 c();
5736 }
5737
5738 fn d() {
5739 // e
5740 // f
5741 }
5742 }
5743
5744 fn g() {
5745 // h
5746 }
5747 "#
5748 .unindent();
5749
5750 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5751 cx.set_state(&text);
5752 cx.set_diff_base(&base_text);
5753 cx.update_editor(|editor, window, cx| {
5754 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5755 });
5756
5757 cx.assert_state_with_diff(
5758 "
5759 ˇimpl A {
5760 - // this is an uncommitted comment
5761
5762 fn b() {
5763 c();
5764 }
5765
5766 - // this is another uncommitted comment
5767 -
5768 fn d() {
5769 // e
5770 // f
5771 }
5772 }
5773
5774 fn g() {
5775 // h
5776 }
5777 "
5778 .unindent(),
5779 );
5780
5781 let expected_display_text = "
5782 impl A {
5783 // this is an uncommitted comment
5784
5785 fn b() {
5786 ⋯
5787 }
5788
5789 // this is another uncommitted comment
5790
5791 fn d() {
5792 ⋯
5793 }
5794 }
5795
5796 fn g() {
5797 ⋯
5798 }
5799 "
5800 .unindent();
5801
5802 cx.update_editor(|editor, window, cx| {
5803 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5804 assert_eq!(editor.display_text(cx), expected_display_text);
5805 });
5806}
5807
5808#[gpui::test]
5809async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5810 init_test(cx, |_| {});
5811
5812 let language = Arc::new(
5813 Language::new(
5814 LanguageConfig {
5815 brackets: BracketPairConfig {
5816 pairs: vec![
5817 BracketPair {
5818 start: "{".to_string(),
5819 end: "}".to_string(),
5820 close: false,
5821 surround: false,
5822 newline: true,
5823 },
5824 BracketPair {
5825 start: "(".to_string(),
5826 end: ")".to_string(),
5827 close: false,
5828 surround: false,
5829 newline: true,
5830 },
5831 ],
5832 ..Default::default()
5833 },
5834 ..Default::default()
5835 },
5836 Some(tree_sitter_rust::LANGUAGE.into()),
5837 )
5838 .with_indents_query(
5839 r#"
5840 (_ "(" ")" @end) @indent
5841 (_ "{" "}" @end) @indent
5842 "#,
5843 )
5844 .unwrap(),
5845 );
5846
5847 let text = "fn a() {}";
5848
5849 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5850 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5851 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5852 editor
5853 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5854 .await;
5855
5856 editor.update_in(cx, |editor, window, cx| {
5857 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5858 editor.newline(&Newline, window, cx);
5859 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5860 assert_eq!(
5861 editor.selections.ranges(cx),
5862 &[
5863 Point::new(1, 4)..Point::new(1, 4),
5864 Point::new(3, 4)..Point::new(3, 4),
5865 Point::new(5, 0)..Point::new(5, 0)
5866 ]
5867 );
5868 });
5869}
5870
5871#[gpui::test]
5872async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5873 init_test(cx, |_| {});
5874
5875 {
5876 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5877 cx.set_state(indoc! {"
5878 impl A {
5879
5880 fn b() {}
5881
5882 «fn c() {
5883
5884 }ˇ»
5885 }
5886 "});
5887
5888 cx.update_editor(|editor, window, cx| {
5889 editor.autoindent(&Default::default(), window, cx);
5890 });
5891
5892 cx.assert_editor_state(indoc! {"
5893 impl A {
5894
5895 fn b() {}
5896
5897 «fn c() {
5898
5899 }ˇ»
5900 }
5901 "});
5902 }
5903
5904 {
5905 let mut cx = EditorTestContext::new_multibuffer(
5906 cx,
5907 [indoc! { "
5908 impl A {
5909 «
5910 // a
5911 fn b(){}
5912 »
5913 «
5914 }
5915 fn c(){}
5916 »
5917 "}],
5918 );
5919
5920 let buffer = cx.update_editor(|editor, _, cx| {
5921 let buffer = editor.buffer().update(cx, |buffer, _| {
5922 buffer.all_buffers().iter().next().unwrap().clone()
5923 });
5924 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5925 buffer
5926 });
5927
5928 cx.run_until_parked();
5929 cx.update_editor(|editor, window, cx| {
5930 editor.select_all(&Default::default(), window, cx);
5931 editor.autoindent(&Default::default(), window, cx)
5932 });
5933 cx.run_until_parked();
5934
5935 cx.update(|_, cx| {
5936 pretty_assertions::assert_eq!(
5937 buffer.read(cx).text(),
5938 indoc! { "
5939 impl A {
5940
5941 // a
5942 fn b(){}
5943
5944
5945 }
5946 fn c(){}
5947
5948 " }
5949 )
5950 });
5951 }
5952}
5953
5954#[gpui::test]
5955async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5956 init_test(cx, |_| {});
5957
5958 let mut cx = EditorTestContext::new(cx).await;
5959
5960 let language = Arc::new(Language::new(
5961 LanguageConfig {
5962 brackets: BracketPairConfig {
5963 pairs: vec![
5964 BracketPair {
5965 start: "{".to_string(),
5966 end: "}".to_string(),
5967 close: true,
5968 surround: true,
5969 newline: true,
5970 },
5971 BracketPair {
5972 start: "(".to_string(),
5973 end: ")".to_string(),
5974 close: true,
5975 surround: true,
5976 newline: true,
5977 },
5978 BracketPair {
5979 start: "/*".to_string(),
5980 end: " */".to_string(),
5981 close: true,
5982 surround: true,
5983 newline: true,
5984 },
5985 BracketPair {
5986 start: "[".to_string(),
5987 end: "]".to_string(),
5988 close: false,
5989 surround: false,
5990 newline: true,
5991 },
5992 BracketPair {
5993 start: "\"".to_string(),
5994 end: "\"".to_string(),
5995 close: true,
5996 surround: true,
5997 newline: false,
5998 },
5999 BracketPair {
6000 start: "<".to_string(),
6001 end: ">".to_string(),
6002 close: false,
6003 surround: true,
6004 newline: true,
6005 },
6006 ],
6007 ..Default::default()
6008 },
6009 autoclose_before: "})]".to_string(),
6010 ..Default::default()
6011 },
6012 Some(tree_sitter_rust::LANGUAGE.into()),
6013 ));
6014
6015 cx.language_registry().add(language.clone());
6016 cx.update_buffer(|buffer, cx| {
6017 buffer.set_language(Some(language), cx);
6018 });
6019
6020 cx.set_state(
6021 &r#"
6022 🏀ˇ
6023 εˇ
6024 ❤️ˇ
6025 "#
6026 .unindent(),
6027 );
6028
6029 // autoclose multiple nested brackets at multiple cursors
6030 cx.update_editor(|editor, window, cx| {
6031 editor.handle_input("{", window, cx);
6032 editor.handle_input("{", window, cx);
6033 editor.handle_input("{", window, cx);
6034 });
6035 cx.assert_editor_state(
6036 &"
6037 🏀{{{ˇ}}}
6038 ε{{{ˇ}}}
6039 ❤️{{{ˇ}}}
6040 "
6041 .unindent(),
6042 );
6043
6044 // insert a different closing bracket
6045 cx.update_editor(|editor, window, cx| {
6046 editor.handle_input(")", window, cx);
6047 });
6048 cx.assert_editor_state(
6049 &"
6050 🏀{{{)ˇ}}}
6051 ε{{{)ˇ}}}
6052 ❤️{{{)ˇ}}}
6053 "
6054 .unindent(),
6055 );
6056
6057 // skip over the auto-closed brackets when typing a closing bracket
6058 cx.update_editor(|editor, window, cx| {
6059 editor.move_right(&MoveRight, window, cx);
6060 editor.handle_input("}", window, cx);
6061 editor.handle_input("}", window, cx);
6062 editor.handle_input("}", window, cx);
6063 });
6064 cx.assert_editor_state(
6065 &"
6066 🏀{{{)}}}}ˇ
6067 ε{{{)}}}}ˇ
6068 ❤️{{{)}}}}ˇ
6069 "
6070 .unindent(),
6071 );
6072
6073 // autoclose multi-character pairs
6074 cx.set_state(
6075 &"
6076 ˇ
6077 ˇ
6078 "
6079 .unindent(),
6080 );
6081 cx.update_editor(|editor, window, cx| {
6082 editor.handle_input("/", window, cx);
6083 editor.handle_input("*", window, cx);
6084 });
6085 cx.assert_editor_state(
6086 &"
6087 /*ˇ */
6088 /*ˇ */
6089 "
6090 .unindent(),
6091 );
6092
6093 // one cursor autocloses a multi-character pair, one cursor
6094 // does not autoclose.
6095 cx.set_state(
6096 &"
6097 /ˇ
6098 ˇ
6099 "
6100 .unindent(),
6101 );
6102 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6103 cx.assert_editor_state(
6104 &"
6105 /*ˇ */
6106 *ˇ
6107 "
6108 .unindent(),
6109 );
6110
6111 // Don't autoclose if the next character isn't whitespace and isn't
6112 // listed in the language's "autoclose_before" section.
6113 cx.set_state("ˇa b");
6114 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6115 cx.assert_editor_state("{ˇa b");
6116
6117 // Don't autoclose if `close` is false for the bracket pair
6118 cx.set_state("ˇ");
6119 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6120 cx.assert_editor_state("[ˇ");
6121
6122 // Surround with brackets if text is selected
6123 cx.set_state("«aˇ» b");
6124 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6125 cx.assert_editor_state("{«aˇ»} b");
6126
6127 // Autclose pair where the start and end characters are the same
6128 cx.set_state("aˇ");
6129 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6130 cx.assert_editor_state("a\"ˇ\"");
6131 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6132 cx.assert_editor_state("a\"\"ˇ");
6133
6134 // Don't autoclose pair if autoclose is disabled
6135 cx.set_state("ˇ");
6136 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6137 cx.assert_editor_state("<ˇ");
6138
6139 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6140 cx.set_state("«aˇ» b");
6141 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6142 cx.assert_editor_state("<«aˇ»> b");
6143}
6144
6145#[gpui::test]
6146async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6147 init_test(cx, |settings| {
6148 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6149 });
6150
6151 let mut cx = EditorTestContext::new(cx).await;
6152
6153 let language = Arc::new(Language::new(
6154 LanguageConfig {
6155 brackets: BracketPairConfig {
6156 pairs: vec![
6157 BracketPair {
6158 start: "{".to_string(),
6159 end: "}".to_string(),
6160 close: true,
6161 surround: true,
6162 newline: true,
6163 },
6164 BracketPair {
6165 start: "(".to_string(),
6166 end: ")".to_string(),
6167 close: true,
6168 surround: true,
6169 newline: true,
6170 },
6171 BracketPair {
6172 start: "[".to_string(),
6173 end: "]".to_string(),
6174 close: false,
6175 surround: false,
6176 newline: true,
6177 },
6178 ],
6179 ..Default::default()
6180 },
6181 autoclose_before: "})]".to_string(),
6182 ..Default::default()
6183 },
6184 Some(tree_sitter_rust::LANGUAGE.into()),
6185 ));
6186
6187 cx.language_registry().add(language.clone());
6188 cx.update_buffer(|buffer, cx| {
6189 buffer.set_language(Some(language), cx);
6190 });
6191
6192 cx.set_state(
6193 &"
6194 ˇ
6195 ˇ
6196 ˇ
6197 "
6198 .unindent(),
6199 );
6200
6201 // ensure only matching closing brackets are skipped over
6202 cx.update_editor(|editor, window, cx| {
6203 editor.handle_input("}", window, cx);
6204 editor.move_left(&MoveLeft, window, cx);
6205 editor.handle_input(")", window, cx);
6206 editor.move_left(&MoveLeft, window, cx);
6207 });
6208 cx.assert_editor_state(
6209 &"
6210 ˇ)}
6211 ˇ)}
6212 ˇ)}
6213 "
6214 .unindent(),
6215 );
6216
6217 // skip-over closing brackets at multiple cursors
6218 cx.update_editor(|editor, window, cx| {
6219 editor.handle_input(")", window, cx);
6220 editor.handle_input("}", window, cx);
6221 });
6222 cx.assert_editor_state(
6223 &"
6224 )}ˇ
6225 )}ˇ
6226 )}ˇ
6227 "
6228 .unindent(),
6229 );
6230
6231 // ignore non-close brackets
6232 cx.update_editor(|editor, window, cx| {
6233 editor.handle_input("]", window, cx);
6234 editor.move_left(&MoveLeft, window, cx);
6235 editor.handle_input("]", window, cx);
6236 });
6237 cx.assert_editor_state(
6238 &"
6239 )}]ˇ]
6240 )}]ˇ]
6241 )}]ˇ]
6242 "
6243 .unindent(),
6244 );
6245}
6246
6247#[gpui::test]
6248async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6249 init_test(cx, |_| {});
6250
6251 let mut cx = EditorTestContext::new(cx).await;
6252
6253 let html_language = Arc::new(
6254 Language::new(
6255 LanguageConfig {
6256 name: "HTML".into(),
6257 brackets: BracketPairConfig {
6258 pairs: vec![
6259 BracketPair {
6260 start: "<".into(),
6261 end: ">".into(),
6262 close: true,
6263 ..Default::default()
6264 },
6265 BracketPair {
6266 start: "{".into(),
6267 end: "}".into(),
6268 close: true,
6269 ..Default::default()
6270 },
6271 BracketPair {
6272 start: "(".into(),
6273 end: ")".into(),
6274 close: true,
6275 ..Default::default()
6276 },
6277 ],
6278 ..Default::default()
6279 },
6280 autoclose_before: "})]>".into(),
6281 ..Default::default()
6282 },
6283 Some(tree_sitter_html::LANGUAGE.into()),
6284 )
6285 .with_injection_query(
6286 r#"
6287 (script_element
6288 (raw_text) @injection.content
6289 (#set! injection.language "javascript"))
6290 "#,
6291 )
6292 .unwrap(),
6293 );
6294
6295 let javascript_language = Arc::new(Language::new(
6296 LanguageConfig {
6297 name: "JavaScript".into(),
6298 brackets: BracketPairConfig {
6299 pairs: vec![
6300 BracketPair {
6301 start: "/*".into(),
6302 end: " */".into(),
6303 close: true,
6304 ..Default::default()
6305 },
6306 BracketPair {
6307 start: "{".into(),
6308 end: "}".into(),
6309 close: true,
6310 ..Default::default()
6311 },
6312 BracketPair {
6313 start: "(".into(),
6314 end: ")".into(),
6315 close: true,
6316 ..Default::default()
6317 },
6318 ],
6319 ..Default::default()
6320 },
6321 autoclose_before: "})]>".into(),
6322 ..Default::default()
6323 },
6324 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6325 ));
6326
6327 cx.language_registry().add(html_language.clone());
6328 cx.language_registry().add(javascript_language.clone());
6329
6330 cx.update_buffer(|buffer, cx| {
6331 buffer.set_language(Some(html_language), cx);
6332 });
6333
6334 cx.set_state(
6335 &r#"
6336 <body>ˇ
6337 <script>
6338 var x = 1;ˇ
6339 </script>
6340 </body>ˇ
6341 "#
6342 .unindent(),
6343 );
6344
6345 // Precondition: different languages are active at different locations.
6346 cx.update_editor(|editor, window, cx| {
6347 let snapshot = editor.snapshot(window, cx);
6348 let cursors = editor.selections.ranges::<usize>(cx);
6349 let languages = cursors
6350 .iter()
6351 .map(|c| snapshot.language_at(c.start).unwrap().name())
6352 .collect::<Vec<_>>();
6353 assert_eq!(
6354 languages,
6355 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6356 );
6357 });
6358
6359 // Angle brackets autoclose in HTML, but not JavaScript.
6360 cx.update_editor(|editor, window, cx| {
6361 editor.handle_input("<", window, cx);
6362 editor.handle_input("a", window, cx);
6363 });
6364 cx.assert_editor_state(
6365 &r#"
6366 <body><aˇ>
6367 <script>
6368 var x = 1;<aˇ
6369 </script>
6370 </body><aˇ>
6371 "#
6372 .unindent(),
6373 );
6374
6375 // Curly braces and parens autoclose in both HTML and JavaScript.
6376 cx.update_editor(|editor, window, cx| {
6377 editor.handle_input(" b=", window, cx);
6378 editor.handle_input("{", window, cx);
6379 editor.handle_input("c", window, cx);
6380 editor.handle_input("(", window, cx);
6381 });
6382 cx.assert_editor_state(
6383 &r#"
6384 <body><a b={c(ˇ)}>
6385 <script>
6386 var x = 1;<a b={c(ˇ)}
6387 </script>
6388 </body><a b={c(ˇ)}>
6389 "#
6390 .unindent(),
6391 );
6392
6393 // Brackets that were already autoclosed are skipped.
6394 cx.update_editor(|editor, window, cx| {
6395 editor.handle_input(")", window, cx);
6396 editor.handle_input("d", window, cx);
6397 editor.handle_input("}", window, cx);
6398 });
6399 cx.assert_editor_state(
6400 &r#"
6401 <body><a b={c()d}ˇ>
6402 <script>
6403 var x = 1;<a b={c()d}ˇ
6404 </script>
6405 </body><a b={c()d}ˇ>
6406 "#
6407 .unindent(),
6408 );
6409 cx.update_editor(|editor, window, cx| {
6410 editor.handle_input(">", window, cx);
6411 });
6412 cx.assert_editor_state(
6413 &r#"
6414 <body><a b={c()d}>ˇ
6415 <script>
6416 var x = 1;<a b={c()d}>ˇ
6417 </script>
6418 </body><a b={c()d}>ˇ
6419 "#
6420 .unindent(),
6421 );
6422
6423 // Reset
6424 cx.set_state(
6425 &r#"
6426 <body>ˇ
6427 <script>
6428 var x = 1;ˇ
6429 </script>
6430 </body>ˇ
6431 "#
6432 .unindent(),
6433 );
6434
6435 cx.update_editor(|editor, window, cx| {
6436 editor.handle_input("<", window, cx);
6437 });
6438 cx.assert_editor_state(
6439 &r#"
6440 <body><ˇ>
6441 <script>
6442 var x = 1;<ˇ
6443 </script>
6444 </body><ˇ>
6445 "#
6446 .unindent(),
6447 );
6448
6449 // When backspacing, the closing angle brackets are removed.
6450 cx.update_editor(|editor, window, cx| {
6451 editor.backspace(&Backspace, window, cx);
6452 });
6453 cx.assert_editor_state(
6454 &r#"
6455 <body>ˇ
6456 <script>
6457 var x = 1;ˇ
6458 </script>
6459 </body>ˇ
6460 "#
6461 .unindent(),
6462 );
6463
6464 // Block comments autoclose in JavaScript, but not HTML.
6465 cx.update_editor(|editor, window, cx| {
6466 editor.handle_input("/", window, cx);
6467 editor.handle_input("*", window, cx);
6468 });
6469 cx.assert_editor_state(
6470 &r#"
6471 <body>/*ˇ
6472 <script>
6473 var x = 1;/*ˇ */
6474 </script>
6475 </body>/*ˇ
6476 "#
6477 .unindent(),
6478 );
6479}
6480
6481#[gpui::test]
6482async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6483 init_test(cx, |_| {});
6484
6485 let mut cx = EditorTestContext::new(cx).await;
6486
6487 let rust_language = Arc::new(
6488 Language::new(
6489 LanguageConfig {
6490 name: "Rust".into(),
6491 brackets: serde_json::from_value(json!([
6492 { "start": "{", "end": "}", "close": true, "newline": true },
6493 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6494 ]))
6495 .unwrap(),
6496 autoclose_before: "})]>".into(),
6497 ..Default::default()
6498 },
6499 Some(tree_sitter_rust::LANGUAGE.into()),
6500 )
6501 .with_override_query("(string_literal) @string")
6502 .unwrap(),
6503 );
6504
6505 cx.language_registry().add(rust_language.clone());
6506 cx.update_buffer(|buffer, cx| {
6507 buffer.set_language(Some(rust_language), cx);
6508 });
6509
6510 cx.set_state(
6511 &r#"
6512 let x = ˇ
6513 "#
6514 .unindent(),
6515 );
6516
6517 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6518 cx.update_editor(|editor, window, cx| {
6519 editor.handle_input("\"", window, cx);
6520 });
6521 cx.assert_editor_state(
6522 &r#"
6523 let x = "ˇ"
6524 "#
6525 .unindent(),
6526 );
6527
6528 // Inserting another quotation mark. The cursor moves across the existing
6529 // automatically-inserted quotation mark.
6530 cx.update_editor(|editor, window, cx| {
6531 editor.handle_input("\"", window, cx);
6532 });
6533 cx.assert_editor_state(
6534 &r#"
6535 let x = ""ˇ
6536 "#
6537 .unindent(),
6538 );
6539
6540 // Reset
6541 cx.set_state(
6542 &r#"
6543 let x = ˇ
6544 "#
6545 .unindent(),
6546 );
6547
6548 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6549 cx.update_editor(|editor, window, cx| {
6550 editor.handle_input("\"", window, cx);
6551 editor.handle_input(" ", window, cx);
6552 editor.move_left(&Default::default(), window, cx);
6553 editor.handle_input("\\", window, cx);
6554 editor.handle_input("\"", window, cx);
6555 });
6556 cx.assert_editor_state(
6557 &r#"
6558 let x = "\"ˇ "
6559 "#
6560 .unindent(),
6561 );
6562
6563 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6564 // mark. Nothing is inserted.
6565 cx.update_editor(|editor, window, cx| {
6566 editor.move_right(&Default::default(), window, cx);
6567 editor.handle_input("\"", window, cx);
6568 });
6569 cx.assert_editor_state(
6570 &r#"
6571 let x = "\" "ˇ
6572 "#
6573 .unindent(),
6574 );
6575}
6576
6577#[gpui::test]
6578async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6579 init_test(cx, |_| {});
6580
6581 let language = Arc::new(Language::new(
6582 LanguageConfig {
6583 brackets: BracketPairConfig {
6584 pairs: vec![
6585 BracketPair {
6586 start: "{".to_string(),
6587 end: "}".to_string(),
6588 close: true,
6589 surround: true,
6590 newline: true,
6591 },
6592 BracketPair {
6593 start: "/* ".to_string(),
6594 end: "*/".to_string(),
6595 close: true,
6596 surround: true,
6597 ..Default::default()
6598 },
6599 ],
6600 ..Default::default()
6601 },
6602 ..Default::default()
6603 },
6604 Some(tree_sitter_rust::LANGUAGE.into()),
6605 ));
6606
6607 let text = r#"
6608 a
6609 b
6610 c
6611 "#
6612 .unindent();
6613
6614 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6615 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6616 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6617 editor
6618 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6619 .await;
6620
6621 editor.update_in(cx, |editor, window, cx| {
6622 editor.change_selections(None, window, cx, |s| {
6623 s.select_display_ranges([
6624 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6625 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6626 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6627 ])
6628 });
6629
6630 editor.handle_input("{", window, cx);
6631 editor.handle_input("{", window, cx);
6632 editor.handle_input("{", window, cx);
6633 assert_eq!(
6634 editor.text(cx),
6635 "
6636 {{{a}}}
6637 {{{b}}}
6638 {{{c}}}
6639 "
6640 .unindent()
6641 );
6642 assert_eq!(
6643 editor.selections.display_ranges(cx),
6644 [
6645 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6646 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6647 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6648 ]
6649 );
6650
6651 editor.undo(&Undo, window, cx);
6652 editor.undo(&Undo, window, cx);
6653 editor.undo(&Undo, window, cx);
6654 assert_eq!(
6655 editor.text(cx),
6656 "
6657 a
6658 b
6659 c
6660 "
6661 .unindent()
6662 );
6663 assert_eq!(
6664 editor.selections.display_ranges(cx),
6665 [
6666 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6667 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6668 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6669 ]
6670 );
6671
6672 // Ensure inserting the first character of a multi-byte bracket pair
6673 // doesn't surround the selections with the bracket.
6674 editor.handle_input("/", window, cx);
6675 assert_eq!(
6676 editor.text(cx),
6677 "
6678 /
6679 /
6680 /
6681 "
6682 .unindent()
6683 );
6684 assert_eq!(
6685 editor.selections.display_ranges(cx),
6686 [
6687 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6688 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6689 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6690 ]
6691 );
6692
6693 editor.undo(&Undo, window, cx);
6694 assert_eq!(
6695 editor.text(cx),
6696 "
6697 a
6698 b
6699 c
6700 "
6701 .unindent()
6702 );
6703 assert_eq!(
6704 editor.selections.display_ranges(cx),
6705 [
6706 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6707 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6708 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6709 ]
6710 );
6711
6712 // Ensure inserting the last character of a multi-byte bracket pair
6713 // doesn't surround the selections with the bracket.
6714 editor.handle_input("*", window, cx);
6715 assert_eq!(
6716 editor.text(cx),
6717 "
6718 *
6719 *
6720 *
6721 "
6722 .unindent()
6723 );
6724 assert_eq!(
6725 editor.selections.display_ranges(cx),
6726 [
6727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6728 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6729 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6730 ]
6731 );
6732 });
6733}
6734
6735#[gpui::test]
6736async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6737 init_test(cx, |_| {});
6738
6739 let language = Arc::new(Language::new(
6740 LanguageConfig {
6741 brackets: BracketPairConfig {
6742 pairs: vec![BracketPair {
6743 start: "{".to_string(),
6744 end: "}".to_string(),
6745 close: true,
6746 surround: true,
6747 newline: true,
6748 }],
6749 ..Default::default()
6750 },
6751 autoclose_before: "}".to_string(),
6752 ..Default::default()
6753 },
6754 Some(tree_sitter_rust::LANGUAGE.into()),
6755 ));
6756
6757 let text = r#"
6758 a
6759 b
6760 c
6761 "#
6762 .unindent();
6763
6764 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6765 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6766 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6767 editor
6768 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6769 .await;
6770
6771 editor.update_in(cx, |editor, window, cx| {
6772 editor.change_selections(None, window, cx, |s| {
6773 s.select_ranges([
6774 Point::new(0, 1)..Point::new(0, 1),
6775 Point::new(1, 1)..Point::new(1, 1),
6776 Point::new(2, 1)..Point::new(2, 1),
6777 ])
6778 });
6779
6780 editor.handle_input("{", window, cx);
6781 editor.handle_input("{", window, cx);
6782 editor.handle_input("_", window, cx);
6783 assert_eq!(
6784 editor.text(cx),
6785 "
6786 a{{_}}
6787 b{{_}}
6788 c{{_}}
6789 "
6790 .unindent()
6791 );
6792 assert_eq!(
6793 editor.selections.ranges::<Point>(cx),
6794 [
6795 Point::new(0, 4)..Point::new(0, 4),
6796 Point::new(1, 4)..Point::new(1, 4),
6797 Point::new(2, 4)..Point::new(2, 4)
6798 ]
6799 );
6800
6801 editor.backspace(&Default::default(), window, cx);
6802 editor.backspace(&Default::default(), window, cx);
6803 assert_eq!(
6804 editor.text(cx),
6805 "
6806 a{}
6807 b{}
6808 c{}
6809 "
6810 .unindent()
6811 );
6812 assert_eq!(
6813 editor.selections.ranges::<Point>(cx),
6814 [
6815 Point::new(0, 2)..Point::new(0, 2),
6816 Point::new(1, 2)..Point::new(1, 2),
6817 Point::new(2, 2)..Point::new(2, 2)
6818 ]
6819 );
6820
6821 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6822 assert_eq!(
6823 editor.text(cx),
6824 "
6825 a
6826 b
6827 c
6828 "
6829 .unindent()
6830 );
6831 assert_eq!(
6832 editor.selections.ranges::<Point>(cx),
6833 [
6834 Point::new(0, 1)..Point::new(0, 1),
6835 Point::new(1, 1)..Point::new(1, 1),
6836 Point::new(2, 1)..Point::new(2, 1)
6837 ]
6838 );
6839 });
6840}
6841
6842#[gpui::test]
6843async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6844 init_test(cx, |settings| {
6845 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6846 });
6847
6848 let mut cx = EditorTestContext::new(cx).await;
6849
6850 let language = Arc::new(Language::new(
6851 LanguageConfig {
6852 brackets: BracketPairConfig {
6853 pairs: vec![
6854 BracketPair {
6855 start: "{".to_string(),
6856 end: "}".to_string(),
6857 close: true,
6858 surround: true,
6859 newline: true,
6860 },
6861 BracketPair {
6862 start: "(".to_string(),
6863 end: ")".to_string(),
6864 close: true,
6865 surround: true,
6866 newline: true,
6867 },
6868 BracketPair {
6869 start: "[".to_string(),
6870 end: "]".to_string(),
6871 close: false,
6872 surround: true,
6873 newline: true,
6874 },
6875 ],
6876 ..Default::default()
6877 },
6878 autoclose_before: "})]".to_string(),
6879 ..Default::default()
6880 },
6881 Some(tree_sitter_rust::LANGUAGE.into()),
6882 ));
6883
6884 cx.language_registry().add(language.clone());
6885 cx.update_buffer(|buffer, cx| {
6886 buffer.set_language(Some(language), cx);
6887 });
6888
6889 cx.set_state(
6890 &"
6891 {(ˇ)}
6892 [[ˇ]]
6893 {(ˇ)}
6894 "
6895 .unindent(),
6896 );
6897
6898 cx.update_editor(|editor, window, cx| {
6899 editor.backspace(&Default::default(), window, cx);
6900 editor.backspace(&Default::default(), window, cx);
6901 });
6902
6903 cx.assert_editor_state(
6904 &"
6905 ˇ
6906 ˇ]]
6907 ˇ
6908 "
6909 .unindent(),
6910 );
6911
6912 cx.update_editor(|editor, window, cx| {
6913 editor.handle_input("{", window, cx);
6914 editor.handle_input("{", window, cx);
6915 editor.move_right(&MoveRight, window, cx);
6916 editor.move_right(&MoveRight, window, cx);
6917 editor.move_left(&MoveLeft, window, cx);
6918 editor.move_left(&MoveLeft, window, cx);
6919 editor.backspace(&Default::default(), window, cx);
6920 });
6921
6922 cx.assert_editor_state(
6923 &"
6924 {ˇ}
6925 {ˇ}]]
6926 {ˇ}
6927 "
6928 .unindent(),
6929 );
6930
6931 cx.update_editor(|editor, window, cx| {
6932 editor.backspace(&Default::default(), window, cx);
6933 });
6934
6935 cx.assert_editor_state(
6936 &"
6937 ˇ
6938 ˇ]]
6939 ˇ
6940 "
6941 .unindent(),
6942 );
6943}
6944
6945#[gpui::test]
6946async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6947 init_test(cx, |_| {});
6948
6949 let language = Arc::new(Language::new(
6950 LanguageConfig::default(),
6951 Some(tree_sitter_rust::LANGUAGE.into()),
6952 ));
6953
6954 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6955 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6956 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6957 editor
6958 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6959 .await;
6960
6961 editor.update_in(cx, |editor, window, cx| {
6962 editor.set_auto_replace_emoji_shortcode(true);
6963
6964 editor.handle_input("Hello ", window, cx);
6965 editor.handle_input(":wave", window, cx);
6966 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6967
6968 editor.handle_input(":", window, cx);
6969 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6970
6971 editor.handle_input(" :smile", window, cx);
6972 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6973
6974 editor.handle_input(":", window, cx);
6975 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6976
6977 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6978 editor.handle_input(":wave", window, cx);
6979 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6980
6981 editor.handle_input(":", window, cx);
6982 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6983
6984 editor.handle_input(":1", window, cx);
6985 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6986
6987 editor.handle_input(":", window, cx);
6988 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6989
6990 // Ensure shortcode does not get replaced when it is part of a word
6991 editor.handle_input(" Test:wave", window, cx);
6992 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6993
6994 editor.handle_input(":", window, cx);
6995 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6996
6997 editor.set_auto_replace_emoji_shortcode(false);
6998
6999 // Ensure shortcode does not get replaced when auto replace is off
7000 editor.handle_input(" :wave", window, cx);
7001 assert_eq!(
7002 editor.text(cx),
7003 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7004 );
7005
7006 editor.handle_input(":", window, cx);
7007 assert_eq!(
7008 editor.text(cx),
7009 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7010 );
7011 });
7012}
7013
7014#[gpui::test]
7015async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
7016 init_test(cx, |_| {});
7017
7018 let (text, insertion_ranges) = marked_text_ranges(
7019 indoc! {"
7020 ˇ
7021 "},
7022 false,
7023 );
7024
7025 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7026 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7027
7028 _ = editor.update_in(cx, |editor, window, cx| {
7029 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7030
7031 editor
7032 .insert_snippet(&insertion_ranges, snippet, window, cx)
7033 .unwrap();
7034
7035 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7036 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7037 assert_eq!(editor.text(cx), expected_text);
7038 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7039 }
7040
7041 assert(
7042 editor,
7043 cx,
7044 indoc! {"
7045 type «» =•
7046 "},
7047 );
7048
7049 assert!(editor.context_menu_visible(), "There should be a matches");
7050 });
7051}
7052
7053#[gpui::test]
7054async fn test_snippets(cx: &mut gpui::TestAppContext) {
7055 init_test(cx, |_| {});
7056
7057 let (text, insertion_ranges) = marked_text_ranges(
7058 indoc! {"
7059 a.ˇ b
7060 a.ˇ b
7061 a.ˇ b
7062 "},
7063 false,
7064 );
7065
7066 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7067 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7068
7069 editor.update_in(cx, |editor, window, cx| {
7070 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7071
7072 editor
7073 .insert_snippet(&insertion_ranges, snippet, window, cx)
7074 .unwrap();
7075
7076 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7077 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7078 assert_eq!(editor.text(cx), expected_text);
7079 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7080 }
7081
7082 assert(
7083 editor,
7084 cx,
7085 indoc! {"
7086 a.f(«one», two, «three») b
7087 a.f(«one», two, «three») b
7088 a.f(«one», two, «three») b
7089 "},
7090 );
7091
7092 // Can't move earlier than the first tab stop
7093 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7094 assert(
7095 editor,
7096 cx,
7097 indoc! {"
7098 a.f(«one», two, «three») b
7099 a.f(«one», two, «three») b
7100 a.f(«one», two, «three») b
7101 "},
7102 );
7103
7104 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7105 assert(
7106 editor,
7107 cx,
7108 indoc! {"
7109 a.f(one, «two», three) b
7110 a.f(one, «two», three) b
7111 a.f(one, «two», three) b
7112 "},
7113 );
7114
7115 editor.move_to_prev_snippet_tabstop(window, cx);
7116 assert(
7117 editor,
7118 cx,
7119 indoc! {"
7120 a.f(«one», two, «three») b
7121 a.f(«one», two, «three») b
7122 a.f(«one», two, «three») b
7123 "},
7124 );
7125
7126 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7127 assert(
7128 editor,
7129 cx,
7130 indoc! {"
7131 a.f(one, «two», three) b
7132 a.f(one, «two», three) b
7133 a.f(one, «two», three) b
7134 "},
7135 );
7136 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7137 assert(
7138 editor,
7139 cx,
7140 indoc! {"
7141 a.f(one, two, three)ˇ b
7142 a.f(one, two, three)ˇ b
7143 a.f(one, two, three)ˇ b
7144 "},
7145 );
7146
7147 // As soon as the last tab stop is reached, snippet state is gone
7148 editor.move_to_prev_snippet_tabstop(window, cx);
7149 assert(
7150 editor,
7151 cx,
7152 indoc! {"
7153 a.f(one, two, three)ˇ b
7154 a.f(one, two, three)ˇ b
7155 a.f(one, two, three)ˇ b
7156 "},
7157 );
7158 });
7159}
7160
7161#[gpui::test]
7162async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7163 init_test(cx, |_| {});
7164
7165 let fs = FakeFs::new(cx.executor());
7166 fs.insert_file(path!("/file.rs"), Default::default()).await;
7167
7168 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7169
7170 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7171 language_registry.add(rust_lang());
7172 let mut fake_servers = language_registry.register_fake_lsp(
7173 "Rust",
7174 FakeLspAdapter {
7175 capabilities: lsp::ServerCapabilities {
7176 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7177 ..Default::default()
7178 },
7179 ..Default::default()
7180 },
7181 );
7182
7183 let buffer = project
7184 .update(cx, |project, cx| {
7185 project.open_local_buffer(path!("/file.rs"), cx)
7186 })
7187 .await
7188 .unwrap();
7189
7190 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7191 let (editor, cx) = cx.add_window_view(|window, cx| {
7192 build_editor_with_project(project.clone(), buffer, window, cx)
7193 });
7194 editor.update_in(cx, |editor, window, cx| {
7195 editor.set_text("one\ntwo\nthree\n", window, cx)
7196 });
7197 assert!(cx.read(|cx| editor.is_dirty(cx)));
7198
7199 cx.executor().start_waiting();
7200 let fake_server = fake_servers.next().await.unwrap();
7201
7202 let save = editor
7203 .update_in(cx, |editor, window, cx| {
7204 editor.save(true, project.clone(), window, cx)
7205 })
7206 .unwrap();
7207 fake_server
7208 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7209 assert_eq!(
7210 params.text_document.uri,
7211 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7212 );
7213 assert_eq!(params.options.tab_size, 4);
7214 Ok(Some(vec![lsp::TextEdit::new(
7215 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7216 ", ".to_string(),
7217 )]))
7218 })
7219 .next()
7220 .await;
7221 cx.executor().start_waiting();
7222 save.await;
7223
7224 assert_eq!(
7225 editor.update(cx, |editor, cx| editor.text(cx)),
7226 "one, two\nthree\n"
7227 );
7228 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7229
7230 editor.update_in(cx, |editor, window, cx| {
7231 editor.set_text("one\ntwo\nthree\n", window, cx)
7232 });
7233 assert!(cx.read(|cx| editor.is_dirty(cx)));
7234
7235 // Ensure we can still save even if formatting hangs.
7236 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7237 assert_eq!(
7238 params.text_document.uri,
7239 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7240 );
7241 futures::future::pending::<()>().await;
7242 unreachable!()
7243 });
7244 let save = editor
7245 .update_in(cx, |editor, window, cx| {
7246 editor.save(true, project.clone(), window, cx)
7247 })
7248 .unwrap();
7249 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7250 cx.executor().start_waiting();
7251 save.await;
7252 assert_eq!(
7253 editor.update(cx, |editor, cx| editor.text(cx)),
7254 "one\ntwo\nthree\n"
7255 );
7256 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7257
7258 // For non-dirty buffer, no formatting request should be sent
7259 let save = editor
7260 .update_in(cx, |editor, window, cx| {
7261 editor.save(true, project.clone(), window, cx)
7262 })
7263 .unwrap();
7264 let _pending_format_request = fake_server
7265 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7266 panic!("Should not be invoked on non-dirty buffer");
7267 })
7268 .next();
7269 cx.executor().start_waiting();
7270 save.await;
7271
7272 // Set rust language override and assert overridden tabsize is sent to language server
7273 update_test_language_settings(cx, |settings| {
7274 settings.languages.insert(
7275 "Rust".into(),
7276 LanguageSettingsContent {
7277 tab_size: NonZeroU32::new(8),
7278 ..Default::default()
7279 },
7280 );
7281 });
7282
7283 editor.update_in(cx, |editor, window, cx| {
7284 editor.set_text("somehting_new\n", window, cx)
7285 });
7286 assert!(cx.read(|cx| editor.is_dirty(cx)));
7287 let save = editor
7288 .update_in(cx, |editor, window, cx| {
7289 editor.save(true, project.clone(), window, cx)
7290 })
7291 .unwrap();
7292 fake_server
7293 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7294 assert_eq!(
7295 params.text_document.uri,
7296 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7297 );
7298 assert_eq!(params.options.tab_size, 8);
7299 Ok(Some(vec![]))
7300 })
7301 .next()
7302 .await;
7303 cx.executor().start_waiting();
7304 save.await;
7305}
7306
7307#[gpui::test]
7308async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7309 init_test(cx, |_| {});
7310
7311 let cols = 4;
7312 let rows = 10;
7313 let sample_text_1 = sample_text(rows, cols, 'a');
7314 assert_eq!(
7315 sample_text_1,
7316 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7317 );
7318 let sample_text_2 = sample_text(rows, cols, 'l');
7319 assert_eq!(
7320 sample_text_2,
7321 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7322 );
7323 let sample_text_3 = sample_text(rows, cols, 'v');
7324 assert_eq!(
7325 sample_text_3,
7326 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7327 );
7328
7329 let fs = FakeFs::new(cx.executor());
7330 fs.insert_tree(
7331 path!("/a"),
7332 json!({
7333 "main.rs": sample_text_1,
7334 "other.rs": sample_text_2,
7335 "lib.rs": sample_text_3,
7336 }),
7337 )
7338 .await;
7339
7340 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7341 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7342 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7343
7344 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7345 language_registry.add(rust_lang());
7346 let mut fake_servers = language_registry.register_fake_lsp(
7347 "Rust",
7348 FakeLspAdapter {
7349 capabilities: lsp::ServerCapabilities {
7350 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7351 ..Default::default()
7352 },
7353 ..Default::default()
7354 },
7355 );
7356
7357 let worktree = project.update(cx, |project, cx| {
7358 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7359 assert_eq!(worktrees.len(), 1);
7360 worktrees.pop().unwrap()
7361 });
7362 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7363
7364 let buffer_1 = project
7365 .update(cx, |project, cx| {
7366 project.open_buffer((worktree_id, "main.rs"), cx)
7367 })
7368 .await
7369 .unwrap();
7370 let buffer_2 = project
7371 .update(cx, |project, cx| {
7372 project.open_buffer((worktree_id, "other.rs"), cx)
7373 })
7374 .await
7375 .unwrap();
7376 let buffer_3 = project
7377 .update(cx, |project, cx| {
7378 project.open_buffer((worktree_id, "lib.rs"), cx)
7379 })
7380 .await
7381 .unwrap();
7382
7383 let multi_buffer = cx.new(|cx| {
7384 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7385 multi_buffer.push_excerpts(
7386 buffer_1.clone(),
7387 [
7388 ExcerptRange {
7389 context: Point::new(0, 0)..Point::new(3, 0),
7390 primary: None,
7391 },
7392 ExcerptRange {
7393 context: Point::new(5, 0)..Point::new(7, 0),
7394 primary: None,
7395 },
7396 ExcerptRange {
7397 context: Point::new(9, 0)..Point::new(10, 4),
7398 primary: None,
7399 },
7400 ],
7401 cx,
7402 );
7403 multi_buffer.push_excerpts(
7404 buffer_2.clone(),
7405 [
7406 ExcerptRange {
7407 context: Point::new(0, 0)..Point::new(3, 0),
7408 primary: None,
7409 },
7410 ExcerptRange {
7411 context: Point::new(5, 0)..Point::new(7, 0),
7412 primary: None,
7413 },
7414 ExcerptRange {
7415 context: Point::new(9, 0)..Point::new(10, 4),
7416 primary: None,
7417 },
7418 ],
7419 cx,
7420 );
7421 multi_buffer.push_excerpts(
7422 buffer_3.clone(),
7423 [
7424 ExcerptRange {
7425 context: Point::new(0, 0)..Point::new(3, 0),
7426 primary: None,
7427 },
7428 ExcerptRange {
7429 context: Point::new(5, 0)..Point::new(7, 0),
7430 primary: None,
7431 },
7432 ExcerptRange {
7433 context: Point::new(9, 0)..Point::new(10, 4),
7434 primary: None,
7435 },
7436 ],
7437 cx,
7438 );
7439 multi_buffer
7440 });
7441 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7442 Editor::new(
7443 EditorMode::Full,
7444 multi_buffer,
7445 Some(project.clone()),
7446 true,
7447 window,
7448 cx,
7449 )
7450 });
7451
7452 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7453 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7454 s.select_ranges(Some(1..2))
7455 });
7456 editor.insert("|one|two|three|", window, cx);
7457 });
7458 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7459 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7460 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7461 s.select_ranges(Some(60..70))
7462 });
7463 editor.insert("|four|five|six|", window, cx);
7464 });
7465 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7466
7467 // First two buffers should be edited, but not the third one.
7468 assert_eq!(
7469 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7470 "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}",
7471 );
7472 buffer_1.update(cx, |buffer, _| {
7473 assert!(buffer.is_dirty());
7474 assert_eq!(
7475 buffer.text(),
7476 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7477 )
7478 });
7479 buffer_2.update(cx, |buffer, _| {
7480 assert!(buffer.is_dirty());
7481 assert_eq!(
7482 buffer.text(),
7483 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7484 )
7485 });
7486 buffer_3.update(cx, |buffer, _| {
7487 assert!(!buffer.is_dirty());
7488 assert_eq!(buffer.text(), sample_text_3,)
7489 });
7490 cx.executor().run_until_parked();
7491
7492 cx.executor().start_waiting();
7493 let save = multi_buffer_editor
7494 .update_in(cx, |editor, window, cx| {
7495 editor.save(true, project.clone(), window, cx)
7496 })
7497 .unwrap();
7498
7499 let fake_server = fake_servers.next().await.unwrap();
7500 fake_server
7501 .server
7502 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7503 Ok(Some(vec![lsp::TextEdit::new(
7504 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7505 format!("[{} formatted]", params.text_document.uri),
7506 )]))
7507 })
7508 .detach();
7509 save.await;
7510
7511 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7512 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7513 assert_eq!(
7514 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7515 uri!("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}"),
7516 );
7517 buffer_1.update(cx, |buffer, _| {
7518 assert!(!buffer.is_dirty());
7519 assert_eq!(
7520 buffer.text(),
7521 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7522 )
7523 });
7524 buffer_2.update(cx, |buffer, _| {
7525 assert!(!buffer.is_dirty());
7526 assert_eq!(
7527 buffer.text(),
7528 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7529 )
7530 });
7531 buffer_3.update(cx, |buffer, _| {
7532 assert!(!buffer.is_dirty());
7533 assert_eq!(buffer.text(), sample_text_3,)
7534 });
7535}
7536
7537#[gpui::test]
7538async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7539 init_test(cx, |_| {});
7540
7541 let fs = FakeFs::new(cx.executor());
7542 fs.insert_file(path!("/file.rs"), Default::default()).await;
7543
7544 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7545
7546 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7547 language_registry.add(rust_lang());
7548 let mut fake_servers = language_registry.register_fake_lsp(
7549 "Rust",
7550 FakeLspAdapter {
7551 capabilities: lsp::ServerCapabilities {
7552 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7553 ..Default::default()
7554 },
7555 ..Default::default()
7556 },
7557 );
7558
7559 let buffer = project
7560 .update(cx, |project, cx| {
7561 project.open_local_buffer(path!("/file.rs"), cx)
7562 })
7563 .await
7564 .unwrap();
7565
7566 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7567 let (editor, cx) = cx.add_window_view(|window, cx| {
7568 build_editor_with_project(project.clone(), buffer, window, cx)
7569 });
7570 editor.update_in(cx, |editor, window, cx| {
7571 editor.set_text("one\ntwo\nthree\n", window, cx)
7572 });
7573 assert!(cx.read(|cx| editor.is_dirty(cx)));
7574
7575 cx.executor().start_waiting();
7576 let fake_server = fake_servers.next().await.unwrap();
7577
7578 let save = editor
7579 .update_in(cx, |editor, window, cx| {
7580 editor.save(true, project.clone(), window, cx)
7581 })
7582 .unwrap();
7583 fake_server
7584 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7585 assert_eq!(
7586 params.text_document.uri,
7587 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7588 );
7589 assert_eq!(params.options.tab_size, 4);
7590 Ok(Some(vec![lsp::TextEdit::new(
7591 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7592 ", ".to_string(),
7593 )]))
7594 })
7595 .next()
7596 .await;
7597 cx.executor().start_waiting();
7598 save.await;
7599 assert_eq!(
7600 editor.update(cx, |editor, cx| editor.text(cx)),
7601 "one, two\nthree\n"
7602 );
7603 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7604
7605 editor.update_in(cx, |editor, window, cx| {
7606 editor.set_text("one\ntwo\nthree\n", window, cx)
7607 });
7608 assert!(cx.read(|cx| editor.is_dirty(cx)));
7609
7610 // Ensure we can still save even if formatting hangs.
7611 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7612 move |params, _| async move {
7613 assert_eq!(
7614 params.text_document.uri,
7615 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7616 );
7617 futures::future::pending::<()>().await;
7618 unreachable!()
7619 },
7620 );
7621 let save = editor
7622 .update_in(cx, |editor, window, cx| {
7623 editor.save(true, project.clone(), window, cx)
7624 })
7625 .unwrap();
7626 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7627 cx.executor().start_waiting();
7628 save.await;
7629 assert_eq!(
7630 editor.update(cx, |editor, cx| editor.text(cx)),
7631 "one\ntwo\nthree\n"
7632 );
7633 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7634
7635 // For non-dirty buffer, no formatting request should be sent
7636 let save = editor
7637 .update_in(cx, |editor, window, cx| {
7638 editor.save(true, project.clone(), window, cx)
7639 })
7640 .unwrap();
7641 let _pending_format_request = fake_server
7642 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7643 panic!("Should not be invoked on non-dirty buffer");
7644 })
7645 .next();
7646 cx.executor().start_waiting();
7647 save.await;
7648
7649 // Set Rust language override and assert overridden tabsize is sent to language server
7650 update_test_language_settings(cx, |settings| {
7651 settings.languages.insert(
7652 "Rust".into(),
7653 LanguageSettingsContent {
7654 tab_size: NonZeroU32::new(8),
7655 ..Default::default()
7656 },
7657 );
7658 });
7659
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.set_text("somehting_new\n", window, cx)
7662 });
7663 assert!(cx.read(|cx| editor.is_dirty(cx)));
7664 let save = editor
7665 .update_in(cx, |editor, window, cx| {
7666 editor.save(true, project.clone(), window, cx)
7667 })
7668 .unwrap();
7669 fake_server
7670 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7671 assert_eq!(
7672 params.text_document.uri,
7673 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7674 );
7675 assert_eq!(params.options.tab_size, 8);
7676 Ok(Some(vec![]))
7677 })
7678 .next()
7679 .await;
7680 cx.executor().start_waiting();
7681 save.await;
7682}
7683
7684#[gpui::test]
7685async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7686 init_test(cx, |settings| {
7687 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7688 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7689 ))
7690 });
7691
7692 let fs = FakeFs::new(cx.executor());
7693 fs.insert_file(path!("/file.rs"), Default::default()).await;
7694
7695 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7696
7697 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7698 language_registry.add(Arc::new(Language::new(
7699 LanguageConfig {
7700 name: "Rust".into(),
7701 matcher: LanguageMatcher {
7702 path_suffixes: vec!["rs".to_string()],
7703 ..Default::default()
7704 },
7705 ..LanguageConfig::default()
7706 },
7707 Some(tree_sitter_rust::LANGUAGE.into()),
7708 )));
7709 update_test_language_settings(cx, |settings| {
7710 // Enable Prettier formatting for the same buffer, and ensure
7711 // LSP is called instead of Prettier.
7712 settings.defaults.prettier = Some(PrettierSettings {
7713 allowed: true,
7714 ..PrettierSettings::default()
7715 });
7716 });
7717 let mut fake_servers = language_registry.register_fake_lsp(
7718 "Rust",
7719 FakeLspAdapter {
7720 capabilities: lsp::ServerCapabilities {
7721 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7722 ..Default::default()
7723 },
7724 ..Default::default()
7725 },
7726 );
7727
7728 let buffer = project
7729 .update(cx, |project, cx| {
7730 project.open_local_buffer(path!("/file.rs"), cx)
7731 })
7732 .await
7733 .unwrap();
7734
7735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7736 let (editor, cx) = cx.add_window_view(|window, cx| {
7737 build_editor_with_project(project.clone(), buffer, window, cx)
7738 });
7739 editor.update_in(cx, |editor, window, cx| {
7740 editor.set_text("one\ntwo\nthree\n", window, cx)
7741 });
7742
7743 cx.executor().start_waiting();
7744 let fake_server = fake_servers.next().await.unwrap();
7745
7746 let format = editor
7747 .update_in(cx, |editor, window, cx| {
7748 editor.perform_format(
7749 project.clone(),
7750 FormatTrigger::Manual,
7751 FormatTarget::Buffers,
7752 window,
7753 cx,
7754 )
7755 })
7756 .unwrap();
7757 fake_server
7758 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7759 assert_eq!(
7760 params.text_document.uri,
7761 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7762 );
7763 assert_eq!(params.options.tab_size, 4);
7764 Ok(Some(vec![lsp::TextEdit::new(
7765 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7766 ", ".to_string(),
7767 )]))
7768 })
7769 .next()
7770 .await;
7771 cx.executor().start_waiting();
7772 format.await;
7773 assert_eq!(
7774 editor.update(cx, |editor, cx| editor.text(cx)),
7775 "one, two\nthree\n"
7776 );
7777
7778 editor.update_in(cx, |editor, window, cx| {
7779 editor.set_text("one\ntwo\nthree\n", window, cx)
7780 });
7781 // Ensure we don't lock if formatting hangs.
7782 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7783 assert_eq!(
7784 params.text_document.uri,
7785 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7786 );
7787 futures::future::pending::<()>().await;
7788 unreachable!()
7789 });
7790 let format = editor
7791 .update_in(cx, |editor, window, cx| {
7792 editor.perform_format(
7793 project,
7794 FormatTrigger::Manual,
7795 FormatTarget::Buffers,
7796 window,
7797 cx,
7798 )
7799 })
7800 .unwrap();
7801 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7802 cx.executor().start_waiting();
7803 format.await;
7804 assert_eq!(
7805 editor.update(cx, |editor, cx| editor.text(cx)),
7806 "one\ntwo\nthree\n"
7807 );
7808}
7809
7810#[gpui::test]
7811async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7812 init_test(cx, |_| {});
7813
7814 let mut cx = EditorLspTestContext::new_rust(
7815 lsp::ServerCapabilities {
7816 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7817 ..Default::default()
7818 },
7819 cx,
7820 )
7821 .await;
7822
7823 cx.set_state(indoc! {"
7824 one.twoˇ
7825 "});
7826
7827 // The format request takes a long time. When it completes, it inserts
7828 // a newline and an indent before the `.`
7829 cx.lsp
7830 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7831 let executor = cx.background_executor().clone();
7832 async move {
7833 executor.timer(Duration::from_millis(100)).await;
7834 Ok(Some(vec![lsp::TextEdit {
7835 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7836 new_text: "\n ".into(),
7837 }]))
7838 }
7839 });
7840
7841 // Submit a format request.
7842 let format_1 = cx
7843 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7844 .unwrap();
7845 cx.executor().run_until_parked();
7846
7847 // Submit a second format request.
7848 let format_2 = cx
7849 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7850 .unwrap();
7851 cx.executor().run_until_parked();
7852
7853 // Wait for both format requests to complete
7854 cx.executor().advance_clock(Duration::from_millis(200));
7855 cx.executor().start_waiting();
7856 format_1.await.unwrap();
7857 cx.executor().start_waiting();
7858 format_2.await.unwrap();
7859
7860 // The formatting edits only happens once.
7861 cx.assert_editor_state(indoc! {"
7862 one
7863 .twoˇ
7864 "});
7865}
7866
7867#[gpui::test]
7868async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7869 init_test(cx, |settings| {
7870 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7871 });
7872
7873 let mut cx = EditorLspTestContext::new_rust(
7874 lsp::ServerCapabilities {
7875 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7876 ..Default::default()
7877 },
7878 cx,
7879 )
7880 .await;
7881
7882 // Set up a buffer white some trailing whitespace and no trailing newline.
7883 cx.set_state(
7884 &[
7885 "one ", //
7886 "twoˇ", //
7887 "three ", //
7888 "four", //
7889 ]
7890 .join("\n"),
7891 );
7892
7893 // Submit a format request.
7894 let format = cx
7895 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7896 .unwrap();
7897
7898 // Record which buffer changes have been sent to the language server
7899 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7900 cx.lsp
7901 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7902 let buffer_changes = buffer_changes.clone();
7903 move |params, _| {
7904 buffer_changes.lock().extend(
7905 params
7906 .content_changes
7907 .into_iter()
7908 .map(|e| (e.range.unwrap(), e.text)),
7909 );
7910 }
7911 });
7912
7913 // Handle formatting requests to the language server.
7914 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7915 let buffer_changes = buffer_changes.clone();
7916 move |_, _| {
7917 // When formatting is requested, trailing whitespace has already been stripped,
7918 // and the trailing newline has already been added.
7919 assert_eq!(
7920 &buffer_changes.lock()[1..],
7921 &[
7922 (
7923 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7924 "".into()
7925 ),
7926 (
7927 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7928 "".into()
7929 ),
7930 (
7931 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7932 "\n".into()
7933 ),
7934 ]
7935 );
7936
7937 // Insert blank lines between each line of the buffer.
7938 async move {
7939 Ok(Some(vec![
7940 lsp::TextEdit {
7941 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7942 new_text: "\n".into(),
7943 },
7944 lsp::TextEdit {
7945 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7946 new_text: "\n".into(),
7947 },
7948 ]))
7949 }
7950 }
7951 });
7952
7953 // After formatting the buffer, the trailing whitespace is stripped,
7954 // a newline is appended, and the edits provided by the language server
7955 // have been applied.
7956 format.await.unwrap();
7957 cx.assert_editor_state(
7958 &[
7959 "one", //
7960 "", //
7961 "twoˇ", //
7962 "", //
7963 "three", //
7964 "four", //
7965 "", //
7966 ]
7967 .join("\n"),
7968 );
7969
7970 // Undoing the formatting undoes the trailing whitespace removal, the
7971 // trailing newline, and the LSP edits.
7972 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7973 cx.assert_editor_state(
7974 &[
7975 "one ", //
7976 "twoˇ", //
7977 "three ", //
7978 "four", //
7979 ]
7980 .join("\n"),
7981 );
7982}
7983
7984#[gpui::test]
7985async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7986 cx: &mut gpui::TestAppContext,
7987) {
7988 init_test(cx, |_| {});
7989
7990 cx.update(|cx| {
7991 cx.update_global::<SettingsStore, _>(|settings, cx| {
7992 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7993 settings.auto_signature_help = Some(true);
7994 });
7995 });
7996 });
7997
7998 let mut cx = EditorLspTestContext::new_rust(
7999 lsp::ServerCapabilities {
8000 signature_help_provider: Some(lsp::SignatureHelpOptions {
8001 ..Default::default()
8002 }),
8003 ..Default::default()
8004 },
8005 cx,
8006 )
8007 .await;
8008
8009 let language = Language::new(
8010 LanguageConfig {
8011 name: "Rust".into(),
8012 brackets: BracketPairConfig {
8013 pairs: vec![
8014 BracketPair {
8015 start: "{".to_string(),
8016 end: "}".to_string(),
8017 close: true,
8018 surround: true,
8019 newline: true,
8020 },
8021 BracketPair {
8022 start: "(".to_string(),
8023 end: ")".to_string(),
8024 close: true,
8025 surround: true,
8026 newline: true,
8027 },
8028 BracketPair {
8029 start: "/*".to_string(),
8030 end: " */".to_string(),
8031 close: true,
8032 surround: true,
8033 newline: true,
8034 },
8035 BracketPair {
8036 start: "[".to_string(),
8037 end: "]".to_string(),
8038 close: false,
8039 surround: false,
8040 newline: true,
8041 },
8042 BracketPair {
8043 start: "\"".to_string(),
8044 end: "\"".to_string(),
8045 close: true,
8046 surround: true,
8047 newline: false,
8048 },
8049 BracketPair {
8050 start: "<".to_string(),
8051 end: ">".to_string(),
8052 close: false,
8053 surround: true,
8054 newline: true,
8055 },
8056 ],
8057 ..Default::default()
8058 },
8059 autoclose_before: "})]".to_string(),
8060 ..Default::default()
8061 },
8062 Some(tree_sitter_rust::LANGUAGE.into()),
8063 );
8064 let language = Arc::new(language);
8065
8066 cx.language_registry().add(language.clone());
8067 cx.update_buffer(|buffer, cx| {
8068 buffer.set_language(Some(language), cx);
8069 });
8070
8071 cx.set_state(
8072 &r#"
8073 fn main() {
8074 sampleˇ
8075 }
8076 "#
8077 .unindent(),
8078 );
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.handle_input("(", window, cx);
8082 });
8083 cx.assert_editor_state(
8084 &"
8085 fn main() {
8086 sample(ˇ)
8087 }
8088 "
8089 .unindent(),
8090 );
8091
8092 let mocked_response = lsp::SignatureHelp {
8093 signatures: vec![lsp::SignatureInformation {
8094 label: "fn sample(param1: u8, param2: u8)".to_string(),
8095 documentation: None,
8096 parameters: Some(vec![
8097 lsp::ParameterInformation {
8098 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8099 documentation: None,
8100 },
8101 lsp::ParameterInformation {
8102 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8103 documentation: None,
8104 },
8105 ]),
8106 active_parameter: None,
8107 }],
8108 active_signature: Some(0),
8109 active_parameter: Some(0),
8110 };
8111 handle_signature_help_request(&mut cx, mocked_response).await;
8112
8113 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8114 .await;
8115
8116 cx.editor(|editor, _, _| {
8117 let signature_help_state = editor.signature_help_state.popover().cloned();
8118 assert_eq!(
8119 signature_help_state.unwrap().label,
8120 "param1: u8, param2: u8"
8121 );
8122 });
8123}
8124
8125#[gpui::test]
8126async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8127 init_test(cx, |_| {});
8128
8129 cx.update(|cx| {
8130 cx.update_global::<SettingsStore, _>(|settings, cx| {
8131 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8132 settings.auto_signature_help = Some(false);
8133 settings.show_signature_help_after_edits = Some(false);
8134 });
8135 });
8136 });
8137
8138 let mut cx = EditorLspTestContext::new_rust(
8139 lsp::ServerCapabilities {
8140 signature_help_provider: Some(lsp::SignatureHelpOptions {
8141 ..Default::default()
8142 }),
8143 ..Default::default()
8144 },
8145 cx,
8146 )
8147 .await;
8148
8149 let language = Language::new(
8150 LanguageConfig {
8151 name: "Rust".into(),
8152 brackets: BracketPairConfig {
8153 pairs: vec![
8154 BracketPair {
8155 start: "{".to_string(),
8156 end: "}".to_string(),
8157 close: true,
8158 surround: true,
8159 newline: true,
8160 },
8161 BracketPair {
8162 start: "(".to_string(),
8163 end: ")".to_string(),
8164 close: true,
8165 surround: true,
8166 newline: true,
8167 },
8168 BracketPair {
8169 start: "/*".to_string(),
8170 end: " */".to_string(),
8171 close: true,
8172 surround: true,
8173 newline: true,
8174 },
8175 BracketPair {
8176 start: "[".to_string(),
8177 end: "]".to_string(),
8178 close: false,
8179 surround: false,
8180 newline: true,
8181 },
8182 BracketPair {
8183 start: "\"".to_string(),
8184 end: "\"".to_string(),
8185 close: true,
8186 surround: true,
8187 newline: false,
8188 },
8189 BracketPair {
8190 start: "<".to_string(),
8191 end: ">".to_string(),
8192 close: false,
8193 surround: true,
8194 newline: true,
8195 },
8196 ],
8197 ..Default::default()
8198 },
8199 autoclose_before: "})]".to_string(),
8200 ..Default::default()
8201 },
8202 Some(tree_sitter_rust::LANGUAGE.into()),
8203 );
8204 let language = Arc::new(language);
8205
8206 cx.language_registry().add(language.clone());
8207 cx.update_buffer(|buffer, cx| {
8208 buffer.set_language(Some(language), cx);
8209 });
8210
8211 // Ensure that signature_help is not called when no signature help is enabled.
8212 cx.set_state(
8213 &r#"
8214 fn main() {
8215 sampleˇ
8216 }
8217 "#
8218 .unindent(),
8219 );
8220 cx.update_editor(|editor, window, cx| {
8221 editor.handle_input("(", window, cx);
8222 });
8223 cx.assert_editor_state(
8224 &"
8225 fn main() {
8226 sample(ˇ)
8227 }
8228 "
8229 .unindent(),
8230 );
8231 cx.editor(|editor, _, _| {
8232 assert!(editor.signature_help_state.task().is_none());
8233 });
8234
8235 let mocked_response = lsp::SignatureHelp {
8236 signatures: vec![lsp::SignatureInformation {
8237 label: "fn sample(param1: u8, param2: u8)".to_string(),
8238 documentation: None,
8239 parameters: Some(vec![
8240 lsp::ParameterInformation {
8241 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8242 documentation: None,
8243 },
8244 lsp::ParameterInformation {
8245 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8246 documentation: None,
8247 },
8248 ]),
8249 active_parameter: None,
8250 }],
8251 active_signature: Some(0),
8252 active_parameter: Some(0),
8253 };
8254
8255 // Ensure that signature_help is called when enabled afte edits
8256 cx.update(|_, cx| {
8257 cx.update_global::<SettingsStore, _>(|settings, cx| {
8258 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8259 settings.auto_signature_help = Some(false);
8260 settings.show_signature_help_after_edits = Some(true);
8261 });
8262 });
8263 });
8264 cx.set_state(
8265 &r#"
8266 fn main() {
8267 sampleˇ
8268 }
8269 "#
8270 .unindent(),
8271 );
8272 cx.update_editor(|editor, window, cx| {
8273 editor.handle_input("(", window, cx);
8274 });
8275 cx.assert_editor_state(
8276 &"
8277 fn main() {
8278 sample(ˇ)
8279 }
8280 "
8281 .unindent(),
8282 );
8283 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8284 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8285 .await;
8286 cx.update_editor(|editor, _, _| {
8287 let signature_help_state = editor.signature_help_state.popover().cloned();
8288 assert!(signature_help_state.is_some());
8289 assert_eq!(
8290 signature_help_state.unwrap().label,
8291 "param1: u8, param2: u8"
8292 );
8293 editor.signature_help_state = SignatureHelpState::default();
8294 });
8295
8296 // Ensure that signature_help is called when auto signature help override is enabled
8297 cx.update(|_, cx| {
8298 cx.update_global::<SettingsStore, _>(|settings, cx| {
8299 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8300 settings.auto_signature_help = Some(true);
8301 settings.show_signature_help_after_edits = Some(false);
8302 });
8303 });
8304 });
8305 cx.set_state(
8306 &r#"
8307 fn main() {
8308 sampleˇ
8309 }
8310 "#
8311 .unindent(),
8312 );
8313 cx.update_editor(|editor, window, cx| {
8314 editor.handle_input("(", window, cx);
8315 });
8316 cx.assert_editor_state(
8317 &"
8318 fn main() {
8319 sample(ˇ)
8320 }
8321 "
8322 .unindent(),
8323 );
8324 handle_signature_help_request(&mut cx, mocked_response).await;
8325 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8326 .await;
8327 cx.editor(|editor, _, _| {
8328 let signature_help_state = editor.signature_help_state.popover().cloned();
8329 assert!(signature_help_state.is_some());
8330 assert_eq!(
8331 signature_help_state.unwrap().label,
8332 "param1: u8, param2: u8"
8333 );
8334 });
8335}
8336
8337#[gpui::test]
8338async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8339 init_test(cx, |_| {});
8340 cx.update(|cx| {
8341 cx.update_global::<SettingsStore, _>(|settings, cx| {
8342 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8343 settings.auto_signature_help = Some(true);
8344 });
8345 });
8346 });
8347
8348 let mut cx = EditorLspTestContext::new_rust(
8349 lsp::ServerCapabilities {
8350 signature_help_provider: Some(lsp::SignatureHelpOptions {
8351 ..Default::default()
8352 }),
8353 ..Default::default()
8354 },
8355 cx,
8356 )
8357 .await;
8358
8359 // A test that directly calls `show_signature_help`
8360 cx.update_editor(|editor, window, cx| {
8361 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8362 });
8363
8364 let mocked_response = lsp::SignatureHelp {
8365 signatures: vec![lsp::SignatureInformation {
8366 label: "fn sample(param1: u8, param2: u8)".to_string(),
8367 documentation: None,
8368 parameters: Some(vec![
8369 lsp::ParameterInformation {
8370 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8371 documentation: None,
8372 },
8373 lsp::ParameterInformation {
8374 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8375 documentation: None,
8376 },
8377 ]),
8378 active_parameter: None,
8379 }],
8380 active_signature: Some(0),
8381 active_parameter: Some(0),
8382 };
8383 handle_signature_help_request(&mut cx, mocked_response).await;
8384
8385 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8386 .await;
8387
8388 cx.editor(|editor, _, _| {
8389 let signature_help_state = editor.signature_help_state.popover().cloned();
8390 assert!(signature_help_state.is_some());
8391 assert_eq!(
8392 signature_help_state.unwrap().label,
8393 "param1: u8, param2: u8"
8394 );
8395 });
8396
8397 // When exiting outside from inside the brackets, `signature_help` is closed.
8398 cx.set_state(indoc! {"
8399 fn main() {
8400 sample(ˇ);
8401 }
8402
8403 fn sample(param1: u8, param2: u8) {}
8404 "});
8405
8406 cx.update_editor(|editor, window, cx| {
8407 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8408 });
8409
8410 let mocked_response = lsp::SignatureHelp {
8411 signatures: Vec::new(),
8412 active_signature: None,
8413 active_parameter: None,
8414 };
8415 handle_signature_help_request(&mut cx, mocked_response).await;
8416
8417 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8418 .await;
8419
8420 cx.editor(|editor, _, _| {
8421 assert!(!editor.signature_help_state.is_shown());
8422 });
8423
8424 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8425 cx.set_state(indoc! {"
8426 fn main() {
8427 sample(ˇ);
8428 }
8429
8430 fn sample(param1: u8, param2: u8) {}
8431 "});
8432
8433 let mocked_response = lsp::SignatureHelp {
8434 signatures: vec![lsp::SignatureInformation {
8435 label: "fn sample(param1: u8, param2: u8)".to_string(),
8436 documentation: None,
8437 parameters: Some(vec![
8438 lsp::ParameterInformation {
8439 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8440 documentation: None,
8441 },
8442 lsp::ParameterInformation {
8443 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8444 documentation: None,
8445 },
8446 ]),
8447 active_parameter: None,
8448 }],
8449 active_signature: Some(0),
8450 active_parameter: Some(0),
8451 };
8452 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8453 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8454 .await;
8455 cx.editor(|editor, _, _| {
8456 assert!(editor.signature_help_state.is_shown());
8457 });
8458
8459 // Restore the popover with more parameter input
8460 cx.set_state(indoc! {"
8461 fn main() {
8462 sample(param1, param2ˇ);
8463 }
8464
8465 fn sample(param1: u8, param2: u8) {}
8466 "});
8467
8468 let mocked_response = lsp::SignatureHelp {
8469 signatures: vec![lsp::SignatureInformation {
8470 label: "fn sample(param1: u8, param2: u8)".to_string(),
8471 documentation: None,
8472 parameters: Some(vec![
8473 lsp::ParameterInformation {
8474 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8475 documentation: None,
8476 },
8477 lsp::ParameterInformation {
8478 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8479 documentation: None,
8480 },
8481 ]),
8482 active_parameter: None,
8483 }],
8484 active_signature: Some(0),
8485 active_parameter: Some(1),
8486 };
8487 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8488 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8489 .await;
8490
8491 // When selecting a range, the popover is gone.
8492 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8493 cx.update_editor(|editor, window, cx| {
8494 editor.change_selections(None, window, cx, |s| {
8495 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8496 })
8497 });
8498 cx.assert_editor_state(indoc! {"
8499 fn main() {
8500 sample(param1, «ˇparam2»);
8501 }
8502
8503 fn sample(param1: u8, param2: u8) {}
8504 "});
8505 cx.editor(|editor, _, _| {
8506 assert!(!editor.signature_help_state.is_shown());
8507 });
8508
8509 // When unselecting again, the popover is back if within the brackets.
8510 cx.update_editor(|editor, window, cx| {
8511 editor.change_selections(None, window, cx, |s| {
8512 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8513 })
8514 });
8515 cx.assert_editor_state(indoc! {"
8516 fn main() {
8517 sample(param1, ˇparam2);
8518 }
8519
8520 fn sample(param1: u8, param2: u8) {}
8521 "});
8522 handle_signature_help_request(&mut cx, mocked_response).await;
8523 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8524 .await;
8525 cx.editor(|editor, _, _| {
8526 assert!(editor.signature_help_state.is_shown());
8527 });
8528
8529 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8530 cx.update_editor(|editor, window, cx| {
8531 editor.change_selections(None, window, cx, |s| {
8532 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8533 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8534 })
8535 });
8536 cx.assert_editor_state(indoc! {"
8537 fn main() {
8538 sample(param1, ˇparam2);
8539 }
8540
8541 fn sample(param1: u8, param2: u8) {}
8542 "});
8543
8544 let mocked_response = lsp::SignatureHelp {
8545 signatures: vec![lsp::SignatureInformation {
8546 label: "fn sample(param1: u8, param2: u8)".to_string(),
8547 documentation: None,
8548 parameters: Some(vec![
8549 lsp::ParameterInformation {
8550 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8551 documentation: None,
8552 },
8553 lsp::ParameterInformation {
8554 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8555 documentation: None,
8556 },
8557 ]),
8558 active_parameter: None,
8559 }],
8560 active_signature: Some(0),
8561 active_parameter: Some(1),
8562 };
8563 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8564 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8565 .await;
8566 cx.update_editor(|editor, _, cx| {
8567 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8568 });
8569 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8570 .await;
8571 cx.update_editor(|editor, window, cx| {
8572 editor.change_selections(None, window, cx, |s| {
8573 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8574 })
8575 });
8576 cx.assert_editor_state(indoc! {"
8577 fn main() {
8578 sample(param1, «ˇparam2»);
8579 }
8580
8581 fn sample(param1: u8, param2: u8) {}
8582 "});
8583 cx.update_editor(|editor, window, cx| {
8584 editor.change_selections(None, window, cx, |s| {
8585 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8586 })
8587 });
8588 cx.assert_editor_state(indoc! {"
8589 fn main() {
8590 sample(param1, ˇparam2);
8591 }
8592
8593 fn sample(param1: u8, param2: u8) {}
8594 "});
8595 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8596 .await;
8597}
8598
8599#[gpui::test]
8600async fn test_completion(cx: &mut gpui::TestAppContext) {
8601 init_test(cx, |_| {});
8602
8603 let mut cx = EditorLspTestContext::new_rust(
8604 lsp::ServerCapabilities {
8605 completion_provider: Some(lsp::CompletionOptions {
8606 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8607 resolve_provider: Some(true),
8608 ..Default::default()
8609 }),
8610 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8611 ..Default::default()
8612 },
8613 cx,
8614 )
8615 .await;
8616 let counter = Arc::new(AtomicUsize::new(0));
8617
8618 cx.set_state(indoc! {"
8619 oneˇ
8620 two
8621 three
8622 "});
8623 cx.simulate_keystroke(".");
8624 handle_completion_request(
8625 &mut cx,
8626 indoc! {"
8627 one.|<>
8628 two
8629 three
8630 "},
8631 vec!["first_completion", "second_completion"],
8632 counter.clone(),
8633 )
8634 .await;
8635 cx.condition(|editor, _| editor.context_menu_visible())
8636 .await;
8637 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8638
8639 let _handler = handle_signature_help_request(
8640 &mut cx,
8641 lsp::SignatureHelp {
8642 signatures: vec![lsp::SignatureInformation {
8643 label: "test signature".to_string(),
8644 documentation: None,
8645 parameters: Some(vec![lsp::ParameterInformation {
8646 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8647 documentation: None,
8648 }]),
8649 active_parameter: None,
8650 }],
8651 active_signature: None,
8652 active_parameter: None,
8653 },
8654 );
8655 cx.update_editor(|editor, window, cx| {
8656 assert!(
8657 !editor.signature_help_state.is_shown(),
8658 "No signature help was called for"
8659 );
8660 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8661 });
8662 cx.run_until_parked();
8663 cx.update_editor(|editor, _, _| {
8664 assert!(
8665 !editor.signature_help_state.is_shown(),
8666 "No signature help should be shown when completions menu is open"
8667 );
8668 });
8669
8670 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8671 editor.context_menu_next(&Default::default(), window, cx);
8672 editor
8673 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8674 .unwrap()
8675 });
8676 cx.assert_editor_state(indoc! {"
8677 one.second_completionˇ
8678 two
8679 three
8680 "});
8681
8682 handle_resolve_completion_request(
8683 &mut cx,
8684 Some(vec![
8685 (
8686 //This overlaps with the primary completion edit which is
8687 //misbehavior from the LSP spec, test that we filter it out
8688 indoc! {"
8689 one.second_ˇcompletion
8690 two
8691 threeˇ
8692 "},
8693 "overlapping additional edit",
8694 ),
8695 (
8696 indoc! {"
8697 one.second_completion
8698 two
8699 threeˇ
8700 "},
8701 "\nadditional edit",
8702 ),
8703 ]),
8704 )
8705 .await;
8706 apply_additional_edits.await.unwrap();
8707 cx.assert_editor_state(indoc! {"
8708 one.second_completionˇ
8709 two
8710 three
8711 additional edit
8712 "});
8713
8714 cx.set_state(indoc! {"
8715 one.second_completion
8716 twoˇ
8717 threeˇ
8718 additional edit
8719 "});
8720 cx.simulate_keystroke(" ");
8721 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8722 cx.simulate_keystroke("s");
8723 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8724
8725 cx.assert_editor_state(indoc! {"
8726 one.second_completion
8727 two sˇ
8728 three sˇ
8729 additional edit
8730 "});
8731 handle_completion_request(
8732 &mut cx,
8733 indoc! {"
8734 one.second_completion
8735 two s
8736 three <s|>
8737 additional edit
8738 "},
8739 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8740 counter.clone(),
8741 )
8742 .await;
8743 cx.condition(|editor, _| editor.context_menu_visible())
8744 .await;
8745 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8746
8747 cx.simulate_keystroke("i");
8748
8749 handle_completion_request(
8750 &mut cx,
8751 indoc! {"
8752 one.second_completion
8753 two si
8754 three <si|>
8755 additional edit
8756 "},
8757 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8758 counter.clone(),
8759 )
8760 .await;
8761 cx.condition(|editor, _| editor.context_menu_visible())
8762 .await;
8763 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8764
8765 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8766 editor
8767 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8768 .unwrap()
8769 });
8770 cx.assert_editor_state(indoc! {"
8771 one.second_completion
8772 two sixth_completionˇ
8773 three sixth_completionˇ
8774 additional edit
8775 "});
8776
8777 apply_additional_edits.await.unwrap();
8778
8779 update_test_language_settings(&mut cx, |settings| {
8780 settings.defaults.show_completions_on_input = Some(false);
8781 });
8782 cx.set_state("editorˇ");
8783 cx.simulate_keystroke(".");
8784 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8785 cx.simulate_keystroke("c");
8786 cx.simulate_keystroke("l");
8787 cx.simulate_keystroke("o");
8788 cx.assert_editor_state("editor.cloˇ");
8789 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8790 cx.update_editor(|editor, window, cx| {
8791 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8792 });
8793 handle_completion_request(
8794 &mut cx,
8795 "editor.<clo|>",
8796 vec!["close", "clobber"],
8797 counter.clone(),
8798 )
8799 .await;
8800 cx.condition(|editor, _| editor.context_menu_visible())
8801 .await;
8802 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8803
8804 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8805 editor
8806 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8807 .unwrap()
8808 });
8809 cx.assert_editor_state("editor.closeˇ");
8810 handle_resolve_completion_request(&mut cx, None).await;
8811 apply_additional_edits.await.unwrap();
8812}
8813
8814#[gpui::test]
8815async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8816 init_test(cx, |_| {});
8817
8818 let fs = FakeFs::new(cx.executor());
8819 fs.insert_tree(
8820 path!("/a"),
8821 json!({
8822 "main.ts": "a",
8823 }),
8824 )
8825 .await;
8826
8827 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8828 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8829 let typescript_language = Arc::new(Language::new(
8830 LanguageConfig {
8831 name: "TypeScript".into(),
8832 matcher: LanguageMatcher {
8833 path_suffixes: vec!["ts".to_string()],
8834 ..LanguageMatcher::default()
8835 },
8836 line_comments: vec!["// ".into()],
8837 ..LanguageConfig::default()
8838 },
8839 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8840 ));
8841 language_registry.add(typescript_language.clone());
8842 let mut fake_servers = language_registry.register_fake_lsp(
8843 "TypeScript",
8844 FakeLspAdapter {
8845 capabilities: lsp::ServerCapabilities {
8846 completion_provider: Some(lsp::CompletionOptions {
8847 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8848 ..lsp::CompletionOptions::default()
8849 }),
8850 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8851 ..lsp::ServerCapabilities::default()
8852 },
8853 // Emulate vtsls label generation
8854 label_for_completion: Some(Box::new(|item, _| {
8855 let text = if let Some(description) = item
8856 .label_details
8857 .as_ref()
8858 .and_then(|label_details| label_details.description.as_ref())
8859 {
8860 format!("{} {}", item.label, description)
8861 } else if let Some(detail) = &item.detail {
8862 format!("{} {}", item.label, detail)
8863 } else {
8864 item.label.clone()
8865 };
8866 let len = text.len();
8867 Some(language::CodeLabel {
8868 text,
8869 runs: Vec::new(),
8870 filter_range: 0..len,
8871 })
8872 })),
8873 ..FakeLspAdapter::default()
8874 },
8875 );
8876 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8877 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8878 let worktree_id = workspace
8879 .update(cx, |workspace, _window, cx| {
8880 workspace.project().update(cx, |project, cx| {
8881 project.worktrees(cx).next().unwrap().read(cx).id()
8882 })
8883 })
8884 .unwrap();
8885 let _buffer = project
8886 .update(cx, |project, cx| {
8887 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8888 })
8889 .await
8890 .unwrap();
8891 let editor = workspace
8892 .update(cx, |workspace, window, cx| {
8893 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8894 })
8895 .unwrap()
8896 .await
8897 .unwrap()
8898 .downcast::<Editor>()
8899 .unwrap();
8900 let fake_server = fake_servers.next().await.unwrap();
8901
8902 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8903 let multiline_label_2 = "a\nb\nc\n";
8904 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8905 let multiline_description = "d\ne\nf\n";
8906 let multiline_detail_2 = "g\nh\ni\n";
8907
8908 let mut completion_handle =
8909 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8910 Ok(Some(lsp::CompletionResponse::Array(vec![
8911 lsp::CompletionItem {
8912 label: multiline_label.to_string(),
8913 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8914 range: lsp::Range {
8915 start: lsp::Position {
8916 line: params.text_document_position.position.line,
8917 character: params.text_document_position.position.character,
8918 },
8919 end: lsp::Position {
8920 line: params.text_document_position.position.line,
8921 character: params.text_document_position.position.character,
8922 },
8923 },
8924 new_text: "new_text_1".to_string(),
8925 })),
8926 ..lsp::CompletionItem::default()
8927 },
8928 lsp::CompletionItem {
8929 label: "single line label 1".to_string(),
8930 detail: Some(multiline_detail.to_string()),
8931 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8932 range: lsp::Range {
8933 start: lsp::Position {
8934 line: params.text_document_position.position.line,
8935 character: params.text_document_position.position.character,
8936 },
8937 end: lsp::Position {
8938 line: params.text_document_position.position.line,
8939 character: params.text_document_position.position.character,
8940 },
8941 },
8942 new_text: "new_text_2".to_string(),
8943 })),
8944 ..lsp::CompletionItem::default()
8945 },
8946 lsp::CompletionItem {
8947 label: "single line label 2".to_string(),
8948 label_details: Some(lsp::CompletionItemLabelDetails {
8949 description: Some(multiline_description.to_string()),
8950 detail: None,
8951 }),
8952 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8953 range: lsp::Range {
8954 start: lsp::Position {
8955 line: params.text_document_position.position.line,
8956 character: params.text_document_position.position.character,
8957 },
8958 end: lsp::Position {
8959 line: params.text_document_position.position.line,
8960 character: params.text_document_position.position.character,
8961 },
8962 },
8963 new_text: "new_text_2".to_string(),
8964 })),
8965 ..lsp::CompletionItem::default()
8966 },
8967 lsp::CompletionItem {
8968 label: multiline_label_2.to_string(),
8969 detail: Some(multiline_detail_2.to_string()),
8970 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8971 range: lsp::Range {
8972 start: lsp::Position {
8973 line: params.text_document_position.position.line,
8974 character: params.text_document_position.position.character,
8975 },
8976 end: lsp::Position {
8977 line: params.text_document_position.position.line,
8978 character: params.text_document_position.position.character,
8979 },
8980 },
8981 new_text: "new_text_3".to_string(),
8982 })),
8983 ..lsp::CompletionItem::default()
8984 },
8985 lsp::CompletionItem {
8986 label: "Label with many spaces and \t but without newlines".to_string(),
8987 detail: Some(
8988 "Details with many spaces and \t but without newlines".to_string(),
8989 ),
8990 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8991 range: lsp::Range {
8992 start: lsp::Position {
8993 line: params.text_document_position.position.line,
8994 character: params.text_document_position.position.character,
8995 },
8996 end: lsp::Position {
8997 line: params.text_document_position.position.line,
8998 character: params.text_document_position.position.character,
8999 },
9000 },
9001 new_text: "new_text_4".to_string(),
9002 })),
9003 ..lsp::CompletionItem::default()
9004 },
9005 ])))
9006 });
9007
9008 editor.update_in(cx, |editor, window, cx| {
9009 cx.focus_self(window);
9010 editor.move_to_end(&MoveToEnd, window, cx);
9011 editor.handle_input(".", window, cx);
9012 });
9013 cx.run_until_parked();
9014 completion_handle.next().await.unwrap();
9015
9016 editor.update(cx, |editor, _| {
9017 assert!(editor.context_menu_visible());
9018 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9019 {
9020 let completion_labels = menu
9021 .completions
9022 .borrow()
9023 .iter()
9024 .map(|c| c.label.text.clone())
9025 .collect::<Vec<_>>();
9026 assert_eq!(
9027 completion_labels,
9028 &[
9029 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9030 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9031 "single line label 2 d e f ",
9032 "a b c g h i ",
9033 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9034 ],
9035 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9036 );
9037
9038 for completion in menu
9039 .completions
9040 .borrow()
9041 .iter() {
9042 assert_eq!(
9043 completion.label.filter_range,
9044 0..completion.label.text.len(),
9045 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9046 );
9047 }
9048
9049 } else {
9050 panic!("expected completion menu to be open");
9051 }
9052 });
9053}
9054
9055#[gpui::test]
9056async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
9057 init_test(cx, |_| {});
9058 let mut cx = EditorLspTestContext::new_rust(
9059 lsp::ServerCapabilities {
9060 completion_provider: Some(lsp::CompletionOptions {
9061 trigger_characters: Some(vec![".".to_string()]),
9062 ..Default::default()
9063 }),
9064 ..Default::default()
9065 },
9066 cx,
9067 )
9068 .await;
9069 cx.lsp
9070 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9071 Ok(Some(lsp::CompletionResponse::Array(vec![
9072 lsp::CompletionItem {
9073 label: "first".into(),
9074 ..Default::default()
9075 },
9076 lsp::CompletionItem {
9077 label: "last".into(),
9078 ..Default::default()
9079 },
9080 ])))
9081 });
9082 cx.set_state("variableˇ");
9083 cx.simulate_keystroke(".");
9084 cx.executor().run_until_parked();
9085
9086 cx.update_editor(|editor, _, _| {
9087 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9088 {
9089 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9090 } else {
9091 panic!("expected completion menu to be open");
9092 }
9093 });
9094
9095 cx.update_editor(|editor, window, cx| {
9096 editor.move_page_down(&MovePageDown::default(), window, cx);
9097 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9098 {
9099 assert!(
9100 menu.selected_item == 1,
9101 "expected PageDown to select the last item from the context menu"
9102 );
9103 } else {
9104 panic!("expected completion menu to stay open after PageDown");
9105 }
9106 });
9107
9108 cx.update_editor(|editor, window, cx| {
9109 editor.move_page_up(&MovePageUp::default(), window, cx);
9110 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9111 {
9112 assert!(
9113 menu.selected_item == 0,
9114 "expected PageUp to select the first item from the context menu"
9115 );
9116 } else {
9117 panic!("expected completion menu to stay open after PageUp");
9118 }
9119 });
9120}
9121
9122#[gpui::test]
9123async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9124 init_test(cx, |_| {});
9125 let mut cx = EditorLspTestContext::new_rust(
9126 lsp::ServerCapabilities {
9127 completion_provider: Some(lsp::CompletionOptions {
9128 trigger_characters: Some(vec![".".to_string()]),
9129 ..Default::default()
9130 }),
9131 ..Default::default()
9132 },
9133 cx,
9134 )
9135 .await;
9136 cx.lsp
9137 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9138 Ok(Some(lsp::CompletionResponse::Array(vec![
9139 lsp::CompletionItem {
9140 label: "Range".into(),
9141 sort_text: Some("a".into()),
9142 ..Default::default()
9143 },
9144 lsp::CompletionItem {
9145 label: "r".into(),
9146 sort_text: Some("b".into()),
9147 ..Default::default()
9148 },
9149 lsp::CompletionItem {
9150 label: "ret".into(),
9151 sort_text: Some("c".into()),
9152 ..Default::default()
9153 },
9154 lsp::CompletionItem {
9155 label: "return".into(),
9156 sort_text: Some("d".into()),
9157 ..Default::default()
9158 },
9159 lsp::CompletionItem {
9160 label: "slice".into(),
9161 sort_text: Some("d".into()),
9162 ..Default::default()
9163 },
9164 ])))
9165 });
9166 cx.set_state("rˇ");
9167 cx.executor().run_until_parked();
9168 cx.update_editor(|editor, window, cx| {
9169 editor.show_completions(
9170 &ShowCompletions {
9171 trigger: Some("r".into()),
9172 },
9173 window,
9174 cx,
9175 );
9176 });
9177 cx.executor().run_until_parked();
9178
9179 cx.update_editor(|editor, _, _| {
9180 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9181 {
9182 assert_eq!(
9183 completion_menu_entries(&menu),
9184 &["r", "ret", "Range", "return"]
9185 );
9186 } else {
9187 panic!("expected completion menu to be open");
9188 }
9189 });
9190}
9191
9192#[gpui::test]
9193async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9194 init_test(cx, |_| {});
9195
9196 let mut cx = EditorLspTestContext::new_rust(
9197 lsp::ServerCapabilities {
9198 completion_provider: Some(lsp::CompletionOptions {
9199 trigger_characters: Some(vec![".".to_string()]),
9200 resolve_provider: Some(true),
9201 ..Default::default()
9202 }),
9203 ..Default::default()
9204 },
9205 cx,
9206 )
9207 .await;
9208
9209 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9210 cx.simulate_keystroke(".");
9211 let completion_item = lsp::CompletionItem {
9212 label: "Some".into(),
9213 kind: Some(lsp::CompletionItemKind::SNIPPET),
9214 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9215 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9216 kind: lsp::MarkupKind::Markdown,
9217 value: "```rust\nSome(2)\n```".to_string(),
9218 })),
9219 deprecated: Some(false),
9220 sort_text: Some("Some".to_string()),
9221 filter_text: Some("Some".to_string()),
9222 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9223 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9224 range: lsp::Range {
9225 start: lsp::Position {
9226 line: 0,
9227 character: 22,
9228 },
9229 end: lsp::Position {
9230 line: 0,
9231 character: 22,
9232 },
9233 },
9234 new_text: "Some(2)".to_string(),
9235 })),
9236 additional_text_edits: Some(vec![lsp::TextEdit {
9237 range: lsp::Range {
9238 start: lsp::Position {
9239 line: 0,
9240 character: 20,
9241 },
9242 end: lsp::Position {
9243 line: 0,
9244 character: 22,
9245 },
9246 },
9247 new_text: "".to_string(),
9248 }]),
9249 ..Default::default()
9250 };
9251
9252 let closure_completion_item = completion_item.clone();
9253 let counter = Arc::new(AtomicUsize::new(0));
9254 let counter_clone = counter.clone();
9255 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9256 let task_completion_item = closure_completion_item.clone();
9257 counter_clone.fetch_add(1, atomic::Ordering::Release);
9258 async move {
9259 Ok(Some(lsp::CompletionResponse::Array(vec![
9260 task_completion_item,
9261 ])))
9262 }
9263 });
9264
9265 cx.condition(|editor, _| editor.context_menu_visible())
9266 .await;
9267 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9268 assert!(request.next().await.is_some());
9269 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9270
9271 cx.simulate_keystroke("S");
9272 cx.simulate_keystroke("o");
9273 cx.simulate_keystroke("m");
9274 cx.condition(|editor, _| editor.context_menu_visible())
9275 .await;
9276 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9277 assert!(request.next().await.is_some());
9278 assert!(request.next().await.is_some());
9279 assert!(request.next().await.is_some());
9280 request.close();
9281 assert!(request.next().await.is_none());
9282 assert_eq!(
9283 counter.load(atomic::Ordering::Acquire),
9284 4,
9285 "With the completions menu open, only one LSP request should happen per input"
9286 );
9287}
9288
9289#[gpui::test]
9290async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9291 init_test(cx, |_| {});
9292 let mut cx = EditorTestContext::new(cx).await;
9293 let language = Arc::new(Language::new(
9294 LanguageConfig {
9295 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9296 ..Default::default()
9297 },
9298 Some(tree_sitter_rust::LANGUAGE.into()),
9299 ));
9300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9301
9302 // If multiple selections intersect a line, the line is only toggled once.
9303 cx.set_state(indoc! {"
9304 fn a() {
9305 «//b();
9306 ˇ»// «c();
9307 //ˇ» d();
9308 }
9309 "});
9310
9311 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9312
9313 cx.assert_editor_state(indoc! {"
9314 fn a() {
9315 «b();
9316 c();
9317 ˇ» d();
9318 }
9319 "});
9320
9321 // The comment prefix is inserted at the same column for every line in a
9322 // selection.
9323 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9324
9325 cx.assert_editor_state(indoc! {"
9326 fn a() {
9327 // «b();
9328 // c();
9329 ˇ»// d();
9330 }
9331 "});
9332
9333 // If a selection ends at the beginning of a line, that line is not toggled.
9334 cx.set_selections_state(indoc! {"
9335 fn a() {
9336 // b();
9337 «// c();
9338 ˇ» // d();
9339 }
9340 "});
9341
9342 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9343
9344 cx.assert_editor_state(indoc! {"
9345 fn a() {
9346 // b();
9347 «c();
9348 ˇ» // d();
9349 }
9350 "});
9351
9352 // If a selection span a single line and is empty, the line is toggled.
9353 cx.set_state(indoc! {"
9354 fn a() {
9355 a();
9356 b();
9357 ˇ
9358 }
9359 "});
9360
9361 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9362
9363 cx.assert_editor_state(indoc! {"
9364 fn a() {
9365 a();
9366 b();
9367 //•ˇ
9368 }
9369 "});
9370
9371 // If a selection span multiple lines, empty lines are not toggled.
9372 cx.set_state(indoc! {"
9373 fn a() {
9374 «a();
9375
9376 c();ˇ»
9377 }
9378 "});
9379
9380 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9381
9382 cx.assert_editor_state(indoc! {"
9383 fn a() {
9384 // «a();
9385
9386 // c();ˇ»
9387 }
9388 "});
9389
9390 // If a selection includes multiple comment prefixes, all lines are uncommented.
9391 cx.set_state(indoc! {"
9392 fn a() {
9393 «// a();
9394 /// b();
9395 //! c();ˇ»
9396 }
9397 "});
9398
9399 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9400
9401 cx.assert_editor_state(indoc! {"
9402 fn a() {
9403 «a();
9404 b();
9405 c();ˇ»
9406 }
9407 "});
9408}
9409
9410#[gpui::test]
9411async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9412 init_test(cx, |_| {});
9413 let mut cx = EditorTestContext::new(cx).await;
9414 let language = Arc::new(Language::new(
9415 LanguageConfig {
9416 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9417 ..Default::default()
9418 },
9419 Some(tree_sitter_rust::LANGUAGE.into()),
9420 ));
9421 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9422
9423 let toggle_comments = &ToggleComments {
9424 advance_downwards: false,
9425 ignore_indent: true,
9426 };
9427
9428 // If multiple selections intersect a line, the line is only toggled once.
9429 cx.set_state(indoc! {"
9430 fn a() {
9431 // «b();
9432 // c();
9433 // ˇ» d();
9434 }
9435 "});
9436
9437 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9438
9439 cx.assert_editor_state(indoc! {"
9440 fn a() {
9441 «b();
9442 c();
9443 ˇ» d();
9444 }
9445 "});
9446
9447 // The comment prefix is inserted at the beginning of each line
9448 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9449
9450 cx.assert_editor_state(indoc! {"
9451 fn a() {
9452 // «b();
9453 // c();
9454 // ˇ» d();
9455 }
9456 "});
9457
9458 // If a selection ends at the beginning of a line, that line is not toggled.
9459 cx.set_selections_state(indoc! {"
9460 fn a() {
9461 // b();
9462 // «c();
9463 ˇ»// d();
9464 }
9465 "});
9466
9467 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9468
9469 cx.assert_editor_state(indoc! {"
9470 fn a() {
9471 // b();
9472 «c();
9473 ˇ»// d();
9474 }
9475 "});
9476
9477 // If a selection span a single line and is empty, the line is toggled.
9478 cx.set_state(indoc! {"
9479 fn a() {
9480 a();
9481 b();
9482 ˇ
9483 }
9484 "});
9485
9486 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9487
9488 cx.assert_editor_state(indoc! {"
9489 fn a() {
9490 a();
9491 b();
9492 //ˇ
9493 }
9494 "});
9495
9496 // If a selection span multiple lines, empty lines are not toggled.
9497 cx.set_state(indoc! {"
9498 fn a() {
9499 «a();
9500
9501 c();ˇ»
9502 }
9503 "});
9504
9505 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9506
9507 cx.assert_editor_state(indoc! {"
9508 fn a() {
9509 // «a();
9510
9511 // c();ˇ»
9512 }
9513 "});
9514
9515 // If a selection includes multiple comment prefixes, all lines are uncommented.
9516 cx.set_state(indoc! {"
9517 fn a() {
9518 // «a();
9519 /// b();
9520 //! c();ˇ»
9521 }
9522 "});
9523
9524 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9525
9526 cx.assert_editor_state(indoc! {"
9527 fn a() {
9528 «a();
9529 b();
9530 c();ˇ»
9531 }
9532 "});
9533}
9534
9535#[gpui::test]
9536async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9537 init_test(cx, |_| {});
9538
9539 let language = Arc::new(Language::new(
9540 LanguageConfig {
9541 line_comments: vec!["// ".into()],
9542 ..Default::default()
9543 },
9544 Some(tree_sitter_rust::LANGUAGE.into()),
9545 ));
9546
9547 let mut cx = EditorTestContext::new(cx).await;
9548
9549 cx.language_registry().add(language.clone());
9550 cx.update_buffer(|buffer, cx| {
9551 buffer.set_language(Some(language), cx);
9552 });
9553
9554 let toggle_comments = &ToggleComments {
9555 advance_downwards: true,
9556 ignore_indent: false,
9557 };
9558
9559 // Single cursor on one line -> advance
9560 // Cursor moves horizontally 3 characters as well on non-blank line
9561 cx.set_state(indoc!(
9562 "fn a() {
9563 ˇdog();
9564 cat();
9565 }"
9566 ));
9567 cx.update_editor(|editor, window, cx| {
9568 editor.toggle_comments(toggle_comments, window, cx);
9569 });
9570 cx.assert_editor_state(indoc!(
9571 "fn a() {
9572 // dog();
9573 catˇ();
9574 }"
9575 ));
9576
9577 // Single selection on one line -> don't advance
9578 cx.set_state(indoc!(
9579 "fn a() {
9580 «dog()ˇ»;
9581 cat();
9582 }"
9583 ));
9584 cx.update_editor(|editor, window, cx| {
9585 editor.toggle_comments(toggle_comments, window, cx);
9586 });
9587 cx.assert_editor_state(indoc!(
9588 "fn a() {
9589 // «dog()ˇ»;
9590 cat();
9591 }"
9592 ));
9593
9594 // Multiple cursors on one line -> advance
9595 cx.set_state(indoc!(
9596 "fn a() {
9597 ˇdˇog();
9598 cat();
9599 }"
9600 ));
9601 cx.update_editor(|editor, window, cx| {
9602 editor.toggle_comments(toggle_comments, window, cx);
9603 });
9604 cx.assert_editor_state(indoc!(
9605 "fn a() {
9606 // dog();
9607 catˇ(ˇ);
9608 }"
9609 ));
9610
9611 // Multiple cursors on one line, with selection -> don't advance
9612 cx.set_state(indoc!(
9613 "fn a() {
9614 ˇdˇog«()ˇ»;
9615 cat();
9616 }"
9617 ));
9618 cx.update_editor(|editor, window, cx| {
9619 editor.toggle_comments(toggle_comments, window, cx);
9620 });
9621 cx.assert_editor_state(indoc!(
9622 "fn a() {
9623 // ˇdˇog«()ˇ»;
9624 cat();
9625 }"
9626 ));
9627
9628 // Single cursor on one line -> advance
9629 // Cursor moves to column 0 on blank line
9630 cx.set_state(indoc!(
9631 "fn a() {
9632 ˇdog();
9633
9634 cat();
9635 }"
9636 ));
9637 cx.update_editor(|editor, window, cx| {
9638 editor.toggle_comments(toggle_comments, window, cx);
9639 });
9640 cx.assert_editor_state(indoc!(
9641 "fn a() {
9642 // dog();
9643 ˇ
9644 cat();
9645 }"
9646 ));
9647
9648 // Single cursor on one line -> advance
9649 // Cursor starts and ends at column 0
9650 cx.set_state(indoc!(
9651 "fn a() {
9652 ˇ dog();
9653 cat();
9654 }"
9655 ));
9656 cx.update_editor(|editor, window, cx| {
9657 editor.toggle_comments(toggle_comments, window, cx);
9658 });
9659 cx.assert_editor_state(indoc!(
9660 "fn a() {
9661 // dog();
9662 ˇ cat();
9663 }"
9664 ));
9665}
9666
9667#[gpui::test]
9668async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9669 init_test(cx, |_| {});
9670
9671 let mut cx = EditorTestContext::new(cx).await;
9672
9673 let html_language = Arc::new(
9674 Language::new(
9675 LanguageConfig {
9676 name: "HTML".into(),
9677 block_comment: Some(("<!-- ".into(), " -->".into())),
9678 ..Default::default()
9679 },
9680 Some(tree_sitter_html::LANGUAGE.into()),
9681 )
9682 .with_injection_query(
9683 r#"
9684 (script_element
9685 (raw_text) @injection.content
9686 (#set! injection.language "javascript"))
9687 "#,
9688 )
9689 .unwrap(),
9690 );
9691
9692 let javascript_language = Arc::new(Language::new(
9693 LanguageConfig {
9694 name: "JavaScript".into(),
9695 line_comments: vec!["// ".into()],
9696 ..Default::default()
9697 },
9698 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9699 ));
9700
9701 cx.language_registry().add(html_language.clone());
9702 cx.language_registry().add(javascript_language.clone());
9703 cx.update_buffer(|buffer, cx| {
9704 buffer.set_language(Some(html_language), cx);
9705 });
9706
9707 // Toggle comments for empty selections
9708 cx.set_state(
9709 &r#"
9710 <p>A</p>ˇ
9711 <p>B</p>ˇ
9712 <p>C</p>ˇ
9713 "#
9714 .unindent(),
9715 );
9716 cx.update_editor(|editor, window, cx| {
9717 editor.toggle_comments(&ToggleComments::default(), window, cx)
9718 });
9719 cx.assert_editor_state(
9720 &r#"
9721 <!-- <p>A</p>ˇ -->
9722 <!-- <p>B</p>ˇ -->
9723 <!-- <p>C</p>ˇ -->
9724 "#
9725 .unindent(),
9726 );
9727 cx.update_editor(|editor, window, cx| {
9728 editor.toggle_comments(&ToggleComments::default(), window, cx)
9729 });
9730 cx.assert_editor_state(
9731 &r#"
9732 <p>A</p>ˇ
9733 <p>B</p>ˇ
9734 <p>C</p>ˇ
9735 "#
9736 .unindent(),
9737 );
9738
9739 // Toggle comments for mixture of empty and non-empty selections, where
9740 // multiple selections occupy a given line.
9741 cx.set_state(
9742 &r#"
9743 <p>A«</p>
9744 <p>ˇ»B</p>ˇ
9745 <p>C«</p>
9746 <p>ˇ»D</p>ˇ
9747 "#
9748 .unindent(),
9749 );
9750
9751 cx.update_editor(|editor, window, cx| {
9752 editor.toggle_comments(&ToggleComments::default(), window, cx)
9753 });
9754 cx.assert_editor_state(
9755 &r#"
9756 <!-- <p>A«</p>
9757 <p>ˇ»B</p>ˇ -->
9758 <!-- <p>C«</p>
9759 <p>ˇ»D</p>ˇ -->
9760 "#
9761 .unindent(),
9762 );
9763 cx.update_editor(|editor, window, cx| {
9764 editor.toggle_comments(&ToggleComments::default(), window, cx)
9765 });
9766 cx.assert_editor_state(
9767 &r#"
9768 <p>A«</p>
9769 <p>ˇ»B</p>ˇ
9770 <p>C«</p>
9771 <p>ˇ»D</p>ˇ
9772 "#
9773 .unindent(),
9774 );
9775
9776 // Toggle comments when different languages are active for different
9777 // selections.
9778 cx.set_state(
9779 &r#"
9780 ˇ<script>
9781 ˇvar x = new Y();
9782 ˇ</script>
9783 "#
9784 .unindent(),
9785 );
9786 cx.executor().run_until_parked();
9787 cx.update_editor(|editor, window, cx| {
9788 editor.toggle_comments(&ToggleComments::default(), window, cx)
9789 });
9790 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9791 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9792 cx.assert_editor_state(
9793 &r#"
9794 <!-- ˇ<script> -->
9795 // ˇvar x = new Y();
9796 <!-- ˇ</script> -->
9797 "#
9798 .unindent(),
9799 );
9800}
9801
9802#[gpui::test]
9803fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9804 init_test(cx, |_| {});
9805
9806 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9807 let multibuffer = cx.new(|cx| {
9808 let mut multibuffer = MultiBuffer::new(ReadWrite);
9809 multibuffer.push_excerpts(
9810 buffer.clone(),
9811 [
9812 ExcerptRange {
9813 context: Point::new(0, 0)..Point::new(0, 4),
9814 primary: None,
9815 },
9816 ExcerptRange {
9817 context: Point::new(1, 0)..Point::new(1, 4),
9818 primary: None,
9819 },
9820 ],
9821 cx,
9822 );
9823 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9824 multibuffer
9825 });
9826
9827 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9828 editor.update_in(cx, |editor, window, cx| {
9829 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9830 editor.change_selections(None, window, cx, |s| {
9831 s.select_ranges([
9832 Point::new(0, 0)..Point::new(0, 0),
9833 Point::new(1, 0)..Point::new(1, 0),
9834 ])
9835 });
9836
9837 editor.handle_input("X", window, cx);
9838 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9839 assert_eq!(
9840 editor.selections.ranges(cx),
9841 [
9842 Point::new(0, 1)..Point::new(0, 1),
9843 Point::new(1, 1)..Point::new(1, 1),
9844 ]
9845 );
9846
9847 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9848 editor.change_selections(None, window, cx, |s| {
9849 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9850 });
9851 editor.backspace(&Default::default(), window, cx);
9852 assert_eq!(editor.text(cx), "Xa\nbbb");
9853 assert_eq!(
9854 editor.selections.ranges(cx),
9855 [Point::new(1, 0)..Point::new(1, 0)]
9856 );
9857
9858 editor.change_selections(None, window, cx, |s| {
9859 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9860 });
9861 editor.backspace(&Default::default(), window, cx);
9862 assert_eq!(editor.text(cx), "X\nbb");
9863 assert_eq!(
9864 editor.selections.ranges(cx),
9865 [Point::new(0, 1)..Point::new(0, 1)]
9866 );
9867 });
9868}
9869
9870#[gpui::test]
9871fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9872 init_test(cx, |_| {});
9873
9874 let markers = vec![('[', ']').into(), ('(', ')').into()];
9875 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9876 indoc! {"
9877 [aaaa
9878 (bbbb]
9879 cccc)",
9880 },
9881 markers.clone(),
9882 );
9883 let excerpt_ranges = markers.into_iter().map(|marker| {
9884 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9885 ExcerptRange {
9886 context,
9887 primary: None,
9888 }
9889 });
9890 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9891 let multibuffer = cx.new(|cx| {
9892 let mut multibuffer = MultiBuffer::new(ReadWrite);
9893 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9894 multibuffer
9895 });
9896
9897 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9898 editor.update_in(cx, |editor, window, cx| {
9899 let (expected_text, selection_ranges) = marked_text_ranges(
9900 indoc! {"
9901 aaaa
9902 bˇbbb
9903 bˇbbˇb
9904 cccc"
9905 },
9906 true,
9907 );
9908 assert_eq!(editor.text(cx), expected_text);
9909 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9910
9911 editor.handle_input("X", window, cx);
9912
9913 let (expected_text, expected_selections) = marked_text_ranges(
9914 indoc! {"
9915 aaaa
9916 bXˇbbXb
9917 bXˇbbXˇb
9918 cccc"
9919 },
9920 false,
9921 );
9922 assert_eq!(editor.text(cx), expected_text);
9923 assert_eq!(editor.selections.ranges(cx), expected_selections);
9924
9925 editor.newline(&Newline, window, cx);
9926 let (expected_text, expected_selections) = marked_text_ranges(
9927 indoc! {"
9928 aaaa
9929 bX
9930 ˇbbX
9931 b
9932 bX
9933 ˇbbX
9934 ˇb
9935 cccc"
9936 },
9937 false,
9938 );
9939 assert_eq!(editor.text(cx), expected_text);
9940 assert_eq!(editor.selections.ranges(cx), expected_selections);
9941 });
9942}
9943
9944#[gpui::test]
9945fn test_refresh_selections(cx: &mut TestAppContext) {
9946 init_test(cx, |_| {});
9947
9948 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9949 let mut excerpt1_id = None;
9950 let multibuffer = cx.new(|cx| {
9951 let mut multibuffer = MultiBuffer::new(ReadWrite);
9952 excerpt1_id = multibuffer
9953 .push_excerpts(
9954 buffer.clone(),
9955 [
9956 ExcerptRange {
9957 context: Point::new(0, 0)..Point::new(1, 4),
9958 primary: None,
9959 },
9960 ExcerptRange {
9961 context: Point::new(1, 0)..Point::new(2, 4),
9962 primary: None,
9963 },
9964 ],
9965 cx,
9966 )
9967 .into_iter()
9968 .next();
9969 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9970 multibuffer
9971 });
9972
9973 let editor = cx.add_window(|window, cx| {
9974 let mut editor = build_editor(multibuffer.clone(), window, cx);
9975 let snapshot = editor.snapshot(window, cx);
9976 editor.change_selections(None, window, cx, |s| {
9977 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9978 });
9979 editor.begin_selection(
9980 Point::new(2, 1).to_display_point(&snapshot),
9981 true,
9982 1,
9983 window,
9984 cx,
9985 );
9986 assert_eq!(
9987 editor.selections.ranges(cx),
9988 [
9989 Point::new(1, 3)..Point::new(1, 3),
9990 Point::new(2, 1)..Point::new(2, 1),
9991 ]
9992 );
9993 editor
9994 });
9995
9996 // Refreshing selections is a no-op when excerpts haven't changed.
9997 _ = editor.update(cx, |editor, window, cx| {
9998 editor.change_selections(None, window, cx, |s| s.refresh());
9999 assert_eq!(
10000 editor.selections.ranges(cx),
10001 [
10002 Point::new(1, 3)..Point::new(1, 3),
10003 Point::new(2, 1)..Point::new(2, 1),
10004 ]
10005 );
10006 });
10007
10008 multibuffer.update(cx, |multibuffer, cx| {
10009 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10010 });
10011 _ = editor.update(cx, |editor, window, cx| {
10012 // Removing an excerpt causes the first selection to become degenerate.
10013 assert_eq!(
10014 editor.selections.ranges(cx),
10015 [
10016 Point::new(0, 0)..Point::new(0, 0),
10017 Point::new(0, 1)..Point::new(0, 1)
10018 ]
10019 );
10020
10021 // Refreshing selections will relocate the first selection to the original buffer
10022 // location.
10023 editor.change_selections(None, window, cx, |s| s.refresh());
10024 assert_eq!(
10025 editor.selections.ranges(cx),
10026 [
10027 Point::new(0, 1)..Point::new(0, 1),
10028 Point::new(0, 3)..Point::new(0, 3)
10029 ]
10030 );
10031 assert!(editor.selections.pending_anchor().is_some());
10032 });
10033}
10034
10035#[gpui::test]
10036fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10037 init_test(cx, |_| {});
10038
10039 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10040 let mut excerpt1_id = None;
10041 let multibuffer = cx.new(|cx| {
10042 let mut multibuffer = MultiBuffer::new(ReadWrite);
10043 excerpt1_id = multibuffer
10044 .push_excerpts(
10045 buffer.clone(),
10046 [
10047 ExcerptRange {
10048 context: Point::new(0, 0)..Point::new(1, 4),
10049 primary: None,
10050 },
10051 ExcerptRange {
10052 context: Point::new(1, 0)..Point::new(2, 4),
10053 primary: None,
10054 },
10055 ],
10056 cx,
10057 )
10058 .into_iter()
10059 .next();
10060 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10061 multibuffer
10062 });
10063
10064 let editor = cx.add_window(|window, cx| {
10065 let mut editor = build_editor(multibuffer.clone(), window, cx);
10066 let snapshot = editor.snapshot(window, cx);
10067 editor.begin_selection(
10068 Point::new(1, 3).to_display_point(&snapshot),
10069 false,
10070 1,
10071 window,
10072 cx,
10073 );
10074 assert_eq!(
10075 editor.selections.ranges(cx),
10076 [Point::new(1, 3)..Point::new(1, 3)]
10077 );
10078 editor
10079 });
10080
10081 multibuffer.update(cx, |multibuffer, cx| {
10082 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10083 });
10084 _ = editor.update(cx, |editor, window, cx| {
10085 assert_eq!(
10086 editor.selections.ranges(cx),
10087 [Point::new(0, 0)..Point::new(0, 0)]
10088 );
10089
10090 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10091 editor.change_selections(None, window, cx, |s| s.refresh());
10092 assert_eq!(
10093 editor.selections.ranges(cx),
10094 [Point::new(0, 3)..Point::new(0, 3)]
10095 );
10096 assert!(editor.selections.pending_anchor().is_some());
10097 });
10098}
10099
10100#[gpui::test]
10101async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10102 init_test(cx, |_| {});
10103
10104 let language = Arc::new(
10105 Language::new(
10106 LanguageConfig {
10107 brackets: BracketPairConfig {
10108 pairs: vec![
10109 BracketPair {
10110 start: "{".to_string(),
10111 end: "}".to_string(),
10112 close: true,
10113 surround: true,
10114 newline: true,
10115 },
10116 BracketPair {
10117 start: "/* ".to_string(),
10118 end: " */".to_string(),
10119 close: true,
10120 surround: true,
10121 newline: true,
10122 },
10123 ],
10124 ..Default::default()
10125 },
10126 ..Default::default()
10127 },
10128 Some(tree_sitter_rust::LANGUAGE.into()),
10129 )
10130 .with_indents_query("")
10131 .unwrap(),
10132 );
10133
10134 let text = concat!(
10135 "{ }\n", //
10136 " x\n", //
10137 " /* */\n", //
10138 "x\n", //
10139 "{{} }\n", //
10140 );
10141
10142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10145 editor
10146 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10147 .await;
10148
10149 editor.update_in(cx, |editor, window, cx| {
10150 editor.change_selections(None, window, cx, |s| {
10151 s.select_display_ranges([
10152 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10153 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10154 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10155 ])
10156 });
10157 editor.newline(&Newline, window, cx);
10158
10159 assert_eq!(
10160 editor.buffer().read(cx).read(cx).text(),
10161 concat!(
10162 "{ \n", // Suppress rustfmt
10163 "\n", //
10164 "}\n", //
10165 " x\n", //
10166 " /* \n", //
10167 " \n", //
10168 " */\n", //
10169 "x\n", //
10170 "{{} \n", //
10171 "}\n", //
10172 )
10173 );
10174 });
10175}
10176
10177#[gpui::test]
10178fn test_highlighted_ranges(cx: &mut TestAppContext) {
10179 init_test(cx, |_| {});
10180
10181 let editor = cx.add_window(|window, cx| {
10182 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10183 build_editor(buffer.clone(), window, cx)
10184 });
10185
10186 _ = editor.update(cx, |editor, window, cx| {
10187 struct Type1;
10188 struct Type2;
10189
10190 let buffer = editor.buffer.read(cx).snapshot(cx);
10191
10192 let anchor_range =
10193 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10194
10195 editor.highlight_background::<Type1>(
10196 &[
10197 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10198 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10199 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10200 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10201 ],
10202 |_| Hsla::red(),
10203 cx,
10204 );
10205 editor.highlight_background::<Type2>(
10206 &[
10207 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10208 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10209 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10210 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10211 ],
10212 |_| Hsla::green(),
10213 cx,
10214 );
10215
10216 let snapshot = editor.snapshot(window, cx);
10217 let mut highlighted_ranges = editor.background_highlights_in_range(
10218 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10219 &snapshot,
10220 cx.theme().colors(),
10221 );
10222 // Enforce a consistent ordering based on color without relying on the ordering of the
10223 // highlight's `TypeId` which is non-executor.
10224 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10225 assert_eq!(
10226 highlighted_ranges,
10227 &[
10228 (
10229 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10230 Hsla::red(),
10231 ),
10232 (
10233 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10234 Hsla::red(),
10235 ),
10236 (
10237 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10238 Hsla::green(),
10239 ),
10240 (
10241 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10242 Hsla::green(),
10243 ),
10244 ]
10245 );
10246 assert_eq!(
10247 editor.background_highlights_in_range(
10248 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10249 &snapshot,
10250 cx.theme().colors(),
10251 ),
10252 &[(
10253 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10254 Hsla::red(),
10255 )]
10256 );
10257 });
10258}
10259
10260#[gpui::test]
10261async fn test_following(cx: &mut gpui::TestAppContext) {
10262 init_test(cx, |_| {});
10263
10264 let fs = FakeFs::new(cx.executor());
10265 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10266
10267 let buffer = project.update(cx, |project, cx| {
10268 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10269 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10270 });
10271 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10272 let follower = cx.update(|cx| {
10273 cx.open_window(
10274 WindowOptions {
10275 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10276 gpui::Point::new(px(0.), px(0.)),
10277 gpui::Point::new(px(10.), px(80.)),
10278 ))),
10279 ..Default::default()
10280 },
10281 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10282 )
10283 .unwrap()
10284 });
10285
10286 let is_still_following = Rc::new(RefCell::new(true));
10287 let follower_edit_event_count = Rc::new(RefCell::new(0));
10288 let pending_update = Rc::new(RefCell::new(None));
10289 let leader_entity = leader.root(cx).unwrap();
10290 let follower_entity = follower.root(cx).unwrap();
10291 _ = follower.update(cx, {
10292 let update = pending_update.clone();
10293 let is_still_following = is_still_following.clone();
10294 let follower_edit_event_count = follower_edit_event_count.clone();
10295 |_, window, cx| {
10296 cx.subscribe_in(
10297 &leader_entity,
10298 window,
10299 move |_, leader, event, window, cx| {
10300 leader.read(cx).add_event_to_update_proto(
10301 event,
10302 &mut update.borrow_mut(),
10303 window,
10304 cx,
10305 );
10306 },
10307 )
10308 .detach();
10309
10310 cx.subscribe_in(
10311 &follower_entity,
10312 window,
10313 move |_, _, event: &EditorEvent, _window, _cx| {
10314 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10315 *is_still_following.borrow_mut() = false;
10316 }
10317
10318 if let EditorEvent::BufferEdited = event {
10319 *follower_edit_event_count.borrow_mut() += 1;
10320 }
10321 },
10322 )
10323 .detach();
10324 }
10325 });
10326
10327 // Update the selections only
10328 _ = leader.update(cx, |leader, window, cx| {
10329 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10330 });
10331 follower
10332 .update(cx, |follower, window, cx| {
10333 follower.apply_update_proto(
10334 &project,
10335 pending_update.borrow_mut().take().unwrap(),
10336 window,
10337 cx,
10338 )
10339 })
10340 .unwrap()
10341 .await
10342 .unwrap();
10343 _ = follower.update(cx, |follower, _, cx| {
10344 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10345 });
10346 assert!(*is_still_following.borrow());
10347 assert_eq!(*follower_edit_event_count.borrow(), 0);
10348
10349 // Update the scroll position only
10350 _ = leader.update(cx, |leader, window, cx| {
10351 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10352 });
10353 follower
10354 .update(cx, |follower, window, cx| {
10355 follower.apply_update_proto(
10356 &project,
10357 pending_update.borrow_mut().take().unwrap(),
10358 window,
10359 cx,
10360 )
10361 })
10362 .unwrap()
10363 .await
10364 .unwrap();
10365 assert_eq!(
10366 follower
10367 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10368 .unwrap(),
10369 gpui::Point::new(1.5, 3.5)
10370 );
10371 assert!(*is_still_following.borrow());
10372 assert_eq!(*follower_edit_event_count.borrow(), 0);
10373
10374 // Update the selections and scroll position. The follower's scroll position is updated
10375 // via autoscroll, not via the leader's exact scroll position.
10376 _ = leader.update(cx, |leader, window, cx| {
10377 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10378 leader.request_autoscroll(Autoscroll::newest(), cx);
10379 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10380 });
10381 follower
10382 .update(cx, |follower, window, cx| {
10383 follower.apply_update_proto(
10384 &project,
10385 pending_update.borrow_mut().take().unwrap(),
10386 window,
10387 cx,
10388 )
10389 })
10390 .unwrap()
10391 .await
10392 .unwrap();
10393 _ = follower.update(cx, |follower, _, cx| {
10394 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10395 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10396 });
10397 assert!(*is_still_following.borrow());
10398
10399 // Creating a pending selection that precedes another selection
10400 _ = leader.update(cx, |leader, window, cx| {
10401 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10402 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10403 });
10404 follower
10405 .update(cx, |follower, window, cx| {
10406 follower.apply_update_proto(
10407 &project,
10408 pending_update.borrow_mut().take().unwrap(),
10409 window,
10410 cx,
10411 )
10412 })
10413 .unwrap()
10414 .await
10415 .unwrap();
10416 _ = follower.update(cx, |follower, _, cx| {
10417 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10418 });
10419 assert!(*is_still_following.borrow());
10420
10421 // Extend the pending selection so that it surrounds another selection
10422 _ = leader.update(cx, |leader, window, cx| {
10423 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10424 });
10425 follower
10426 .update(cx, |follower, window, cx| {
10427 follower.apply_update_proto(
10428 &project,
10429 pending_update.borrow_mut().take().unwrap(),
10430 window,
10431 cx,
10432 )
10433 })
10434 .unwrap()
10435 .await
10436 .unwrap();
10437 _ = follower.update(cx, |follower, _, cx| {
10438 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10439 });
10440
10441 // Scrolling locally breaks the follow
10442 _ = follower.update(cx, |follower, window, cx| {
10443 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10444 follower.set_scroll_anchor(
10445 ScrollAnchor {
10446 anchor: top_anchor,
10447 offset: gpui::Point::new(0.0, 0.5),
10448 },
10449 window,
10450 cx,
10451 );
10452 });
10453 assert!(!(*is_still_following.borrow()));
10454}
10455
10456#[gpui::test]
10457async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10458 init_test(cx, |_| {});
10459
10460 let fs = FakeFs::new(cx.executor());
10461 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10462 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10463 let pane = workspace
10464 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10465 .unwrap();
10466
10467 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10468
10469 let leader = pane.update_in(cx, |_, window, cx| {
10470 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10471 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10472 });
10473
10474 // Start following the editor when it has no excerpts.
10475 let mut state_message =
10476 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10477 let workspace_entity = workspace.root(cx).unwrap();
10478 let follower_1 = cx
10479 .update_window(*workspace.deref(), |_, window, cx| {
10480 Editor::from_state_proto(
10481 workspace_entity,
10482 ViewId {
10483 creator: Default::default(),
10484 id: 0,
10485 },
10486 &mut state_message,
10487 window,
10488 cx,
10489 )
10490 })
10491 .unwrap()
10492 .unwrap()
10493 .await
10494 .unwrap();
10495
10496 let update_message = Rc::new(RefCell::new(None));
10497 follower_1.update_in(cx, {
10498 let update = update_message.clone();
10499 |_, window, cx| {
10500 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10501 leader.read(cx).add_event_to_update_proto(
10502 event,
10503 &mut update.borrow_mut(),
10504 window,
10505 cx,
10506 );
10507 })
10508 .detach();
10509 }
10510 });
10511
10512 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10513 (
10514 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10515 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10516 )
10517 });
10518
10519 // Insert some excerpts.
10520 leader.update(cx, |leader, cx| {
10521 leader.buffer.update(cx, |multibuffer, cx| {
10522 let excerpt_ids = multibuffer.push_excerpts(
10523 buffer_1.clone(),
10524 [
10525 ExcerptRange {
10526 context: 1..6,
10527 primary: None,
10528 },
10529 ExcerptRange {
10530 context: 12..15,
10531 primary: None,
10532 },
10533 ExcerptRange {
10534 context: 0..3,
10535 primary: None,
10536 },
10537 ],
10538 cx,
10539 );
10540 multibuffer.insert_excerpts_after(
10541 excerpt_ids[0],
10542 buffer_2.clone(),
10543 [
10544 ExcerptRange {
10545 context: 8..12,
10546 primary: None,
10547 },
10548 ExcerptRange {
10549 context: 0..6,
10550 primary: None,
10551 },
10552 ],
10553 cx,
10554 );
10555 });
10556 });
10557
10558 // Apply the update of adding the excerpts.
10559 follower_1
10560 .update_in(cx, |follower, window, cx| {
10561 follower.apply_update_proto(
10562 &project,
10563 update_message.borrow().clone().unwrap(),
10564 window,
10565 cx,
10566 )
10567 })
10568 .await
10569 .unwrap();
10570 assert_eq!(
10571 follower_1.update(cx, |editor, cx| editor.text(cx)),
10572 leader.update(cx, |editor, cx| editor.text(cx))
10573 );
10574 update_message.borrow_mut().take();
10575
10576 // Start following separately after it already has excerpts.
10577 let mut state_message =
10578 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10579 let workspace_entity = workspace.root(cx).unwrap();
10580 let follower_2 = cx
10581 .update_window(*workspace.deref(), |_, window, cx| {
10582 Editor::from_state_proto(
10583 workspace_entity,
10584 ViewId {
10585 creator: Default::default(),
10586 id: 0,
10587 },
10588 &mut state_message,
10589 window,
10590 cx,
10591 )
10592 })
10593 .unwrap()
10594 .unwrap()
10595 .await
10596 .unwrap();
10597 assert_eq!(
10598 follower_2.update(cx, |editor, cx| editor.text(cx)),
10599 leader.update(cx, |editor, cx| editor.text(cx))
10600 );
10601
10602 // Remove some excerpts.
10603 leader.update(cx, |leader, cx| {
10604 leader.buffer.update(cx, |multibuffer, cx| {
10605 let excerpt_ids = multibuffer.excerpt_ids();
10606 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10607 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10608 });
10609 });
10610
10611 // Apply the update of removing the excerpts.
10612 follower_1
10613 .update_in(cx, |follower, window, cx| {
10614 follower.apply_update_proto(
10615 &project,
10616 update_message.borrow().clone().unwrap(),
10617 window,
10618 cx,
10619 )
10620 })
10621 .await
10622 .unwrap();
10623 follower_2
10624 .update_in(cx, |follower, window, cx| {
10625 follower.apply_update_proto(
10626 &project,
10627 update_message.borrow().clone().unwrap(),
10628 window,
10629 cx,
10630 )
10631 })
10632 .await
10633 .unwrap();
10634 update_message.borrow_mut().take();
10635 assert_eq!(
10636 follower_1.update(cx, |editor, cx| editor.text(cx)),
10637 leader.update(cx, |editor, cx| editor.text(cx))
10638 );
10639}
10640
10641#[gpui::test]
10642async fn go_to_prev_overlapping_diagnostic(
10643 executor: BackgroundExecutor,
10644 cx: &mut gpui::TestAppContext,
10645) {
10646 init_test(cx, |_| {});
10647
10648 let mut cx = EditorTestContext::new(cx).await;
10649 let lsp_store =
10650 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10651
10652 cx.set_state(indoc! {"
10653 ˇfn func(abc def: i32) -> u32 {
10654 }
10655 "});
10656
10657 cx.update(|_, cx| {
10658 lsp_store.update(cx, |lsp_store, cx| {
10659 lsp_store
10660 .update_diagnostics(
10661 LanguageServerId(0),
10662 lsp::PublishDiagnosticsParams {
10663 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10664 version: None,
10665 diagnostics: vec![
10666 lsp::Diagnostic {
10667 range: lsp::Range::new(
10668 lsp::Position::new(0, 11),
10669 lsp::Position::new(0, 12),
10670 ),
10671 severity: Some(lsp::DiagnosticSeverity::ERROR),
10672 ..Default::default()
10673 },
10674 lsp::Diagnostic {
10675 range: lsp::Range::new(
10676 lsp::Position::new(0, 12),
10677 lsp::Position::new(0, 15),
10678 ),
10679 severity: Some(lsp::DiagnosticSeverity::ERROR),
10680 ..Default::default()
10681 },
10682 lsp::Diagnostic {
10683 range: lsp::Range::new(
10684 lsp::Position::new(0, 25),
10685 lsp::Position::new(0, 28),
10686 ),
10687 severity: Some(lsp::DiagnosticSeverity::ERROR),
10688 ..Default::default()
10689 },
10690 ],
10691 },
10692 &[],
10693 cx,
10694 )
10695 .unwrap()
10696 });
10697 });
10698
10699 executor.run_until_parked();
10700
10701 cx.update_editor(|editor, window, cx| {
10702 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10703 });
10704
10705 cx.assert_editor_state(indoc! {"
10706 fn func(abc def: i32) -> ˇu32 {
10707 }
10708 "});
10709
10710 cx.update_editor(|editor, window, cx| {
10711 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10712 });
10713
10714 cx.assert_editor_state(indoc! {"
10715 fn func(abc ˇdef: i32) -> u32 {
10716 }
10717 "});
10718
10719 cx.update_editor(|editor, window, cx| {
10720 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10721 });
10722
10723 cx.assert_editor_state(indoc! {"
10724 fn func(abcˇ def: i32) -> u32 {
10725 }
10726 "});
10727
10728 cx.update_editor(|editor, window, cx| {
10729 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10730 });
10731
10732 cx.assert_editor_state(indoc! {"
10733 fn func(abc def: i32) -> ˇu32 {
10734 }
10735 "});
10736}
10737
10738#[gpui::test]
10739async fn cycle_through_same_place_diagnostics(
10740 executor: BackgroundExecutor,
10741 cx: &mut gpui::TestAppContext,
10742) {
10743 init_test(cx, |_| {});
10744
10745 let mut cx = EditorTestContext::new(cx).await;
10746 let lsp_store =
10747 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10748
10749 cx.set_state(indoc! {"
10750 ˇfn func(abc def: i32) -> u32 {
10751 }
10752 "});
10753
10754 cx.update(|_, cx| {
10755 lsp_store.update(cx, |lsp_store, cx| {
10756 lsp_store
10757 .update_diagnostics(
10758 LanguageServerId(0),
10759 lsp::PublishDiagnosticsParams {
10760 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10761 version: None,
10762 diagnostics: vec![
10763 lsp::Diagnostic {
10764 range: lsp::Range::new(
10765 lsp::Position::new(0, 11),
10766 lsp::Position::new(0, 12),
10767 ),
10768 severity: Some(lsp::DiagnosticSeverity::ERROR),
10769 ..Default::default()
10770 },
10771 lsp::Diagnostic {
10772 range: lsp::Range::new(
10773 lsp::Position::new(0, 12),
10774 lsp::Position::new(0, 15),
10775 ),
10776 severity: Some(lsp::DiagnosticSeverity::ERROR),
10777 ..Default::default()
10778 },
10779 lsp::Diagnostic {
10780 range: lsp::Range::new(
10781 lsp::Position::new(0, 12),
10782 lsp::Position::new(0, 15),
10783 ),
10784 severity: Some(lsp::DiagnosticSeverity::ERROR),
10785 ..Default::default()
10786 },
10787 lsp::Diagnostic {
10788 range: lsp::Range::new(
10789 lsp::Position::new(0, 25),
10790 lsp::Position::new(0, 28),
10791 ),
10792 severity: Some(lsp::DiagnosticSeverity::ERROR),
10793 ..Default::default()
10794 },
10795 ],
10796 },
10797 &[],
10798 cx,
10799 )
10800 .unwrap()
10801 });
10802 });
10803 executor.run_until_parked();
10804
10805 //// Backward
10806
10807 // Fourth diagnostic
10808 cx.update_editor(|editor, window, cx| {
10809 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10810 });
10811 cx.assert_editor_state(indoc! {"
10812 fn func(abc def: i32) -> ˇu32 {
10813 }
10814 "});
10815
10816 // Third diagnostic
10817 cx.update_editor(|editor, window, cx| {
10818 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10819 });
10820 cx.assert_editor_state(indoc! {"
10821 fn func(abc ˇdef: i32) -> u32 {
10822 }
10823 "});
10824
10825 // Second diagnostic, same place
10826 cx.update_editor(|editor, window, cx| {
10827 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10828 });
10829 cx.assert_editor_state(indoc! {"
10830 fn func(abc ˇdef: i32) -> u32 {
10831 }
10832 "});
10833
10834 // First diagnostic
10835 cx.update_editor(|editor, window, cx| {
10836 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10837 });
10838 cx.assert_editor_state(indoc! {"
10839 fn func(abcˇ def: i32) -> u32 {
10840 }
10841 "});
10842
10843 // Wrapped over, fourth diagnostic
10844 cx.update_editor(|editor, window, cx| {
10845 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10846 });
10847 cx.assert_editor_state(indoc! {"
10848 fn func(abc def: i32) -> ˇu32 {
10849 }
10850 "});
10851
10852 cx.update_editor(|editor, window, cx| {
10853 editor.move_to_beginning(&MoveToBeginning, window, cx);
10854 });
10855 cx.assert_editor_state(indoc! {"
10856 ˇfn func(abc def: i32) -> u32 {
10857 }
10858 "});
10859
10860 //// Forward
10861
10862 // First diagnostic
10863 cx.update_editor(|editor, window, cx| {
10864 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10865 });
10866 cx.assert_editor_state(indoc! {"
10867 fn func(abcˇ def: i32) -> u32 {
10868 }
10869 "});
10870
10871 // Second diagnostic
10872 cx.update_editor(|editor, window, cx| {
10873 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10874 });
10875 cx.assert_editor_state(indoc! {"
10876 fn func(abc ˇdef: i32) -> u32 {
10877 }
10878 "});
10879
10880 // Third diagnostic, same place
10881 cx.update_editor(|editor, window, cx| {
10882 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10883 });
10884 cx.assert_editor_state(indoc! {"
10885 fn func(abc ˇdef: i32) -> u32 {
10886 }
10887 "});
10888
10889 // Fourth diagnostic
10890 cx.update_editor(|editor, window, cx| {
10891 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10892 });
10893 cx.assert_editor_state(indoc! {"
10894 fn func(abc def: i32) -> ˇu32 {
10895 }
10896 "});
10897
10898 // Wrapped around, first diagnostic
10899 cx.update_editor(|editor, window, cx| {
10900 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10901 });
10902 cx.assert_editor_state(indoc! {"
10903 fn func(abcˇ def: i32) -> u32 {
10904 }
10905 "});
10906}
10907
10908#[gpui::test]
10909async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10910 init_test(cx, |_| {});
10911
10912 let mut cx = EditorTestContext::new(cx).await;
10913
10914 cx.set_state(indoc! {"
10915 fn func(abˇc def: i32) -> u32 {
10916 }
10917 "});
10918 let lsp_store =
10919 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10920
10921 cx.update(|_, cx| {
10922 lsp_store.update(cx, |lsp_store, cx| {
10923 lsp_store.update_diagnostics(
10924 LanguageServerId(0),
10925 lsp::PublishDiagnosticsParams {
10926 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10927 version: None,
10928 diagnostics: vec![lsp::Diagnostic {
10929 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10930 severity: Some(lsp::DiagnosticSeverity::ERROR),
10931 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10932 ..Default::default()
10933 }],
10934 },
10935 &[],
10936 cx,
10937 )
10938 })
10939 }).unwrap();
10940 cx.run_until_parked();
10941 cx.update_editor(|editor, window, cx| {
10942 hover_popover::hover(editor, &Default::default(), window, cx)
10943 });
10944 cx.run_until_parked();
10945 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10946}
10947
10948#[gpui::test]
10949async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10950 init_test(cx, |_| {});
10951
10952 let mut cx = EditorTestContext::new(cx).await;
10953
10954 let diff_base = r#"
10955 use some::mod;
10956
10957 const A: u32 = 42;
10958
10959 fn main() {
10960 println!("hello");
10961
10962 println!("world");
10963 }
10964 "#
10965 .unindent();
10966
10967 // Edits are modified, removed, modified, added
10968 cx.set_state(
10969 &r#"
10970 use some::modified;
10971
10972 ˇ
10973 fn main() {
10974 println!("hello there");
10975
10976 println!("around the");
10977 println!("world");
10978 }
10979 "#
10980 .unindent(),
10981 );
10982
10983 cx.set_diff_base(&diff_base);
10984 executor.run_until_parked();
10985
10986 cx.update_editor(|editor, window, cx| {
10987 //Wrap around the bottom of the buffer
10988 for _ in 0..3 {
10989 editor.go_to_next_hunk(&GoToHunk, window, cx);
10990 }
10991 });
10992
10993 cx.assert_editor_state(
10994 &r#"
10995 ˇuse some::modified;
10996
10997
10998 fn main() {
10999 println!("hello there");
11000
11001 println!("around the");
11002 println!("world");
11003 }
11004 "#
11005 .unindent(),
11006 );
11007
11008 cx.update_editor(|editor, window, cx| {
11009 //Wrap around the top of the buffer
11010 for _ in 0..2 {
11011 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11012 }
11013 });
11014
11015 cx.assert_editor_state(
11016 &r#"
11017 use some::modified;
11018
11019
11020 fn main() {
11021 ˇ println!("hello there");
11022
11023 println!("around the");
11024 println!("world");
11025 }
11026 "#
11027 .unindent(),
11028 );
11029
11030 cx.update_editor(|editor, window, cx| {
11031 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11032 });
11033
11034 cx.assert_editor_state(
11035 &r#"
11036 use some::modified;
11037
11038 ˇ
11039 fn main() {
11040 println!("hello there");
11041
11042 println!("around the");
11043 println!("world");
11044 }
11045 "#
11046 .unindent(),
11047 );
11048
11049 cx.update_editor(|editor, window, cx| {
11050 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11051 });
11052
11053 cx.assert_editor_state(
11054 &r#"
11055 ˇuse some::modified;
11056
11057
11058 fn main() {
11059 println!("hello there");
11060
11061 println!("around the");
11062 println!("world");
11063 }
11064 "#
11065 .unindent(),
11066 );
11067
11068 cx.update_editor(|editor, window, cx| {
11069 for _ in 0..2 {
11070 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11071 }
11072 });
11073
11074 cx.assert_editor_state(
11075 &r#"
11076 use some::modified;
11077
11078
11079 fn main() {
11080 ˇ println!("hello there");
11081
11082 println!("around the");
11083 println!("world");
11084 }
11085 "#
11086 .unindent(),
11087 );
11088
11089 cx.update_editor(|editor, window, cx| {
11090 editor.fold(&Fold, window, cx);
11091 });
11092
11093 cx.update_editor(|editor, window, cx| {
11094 editor.go_to_next_hunk(&GoToHunk, window, cx);
11095 });
11096
11097 cx.assert_editor_state(
11098 &r#"
11099 ˇuse some::modified;
11100
11101
11102 fn main() {
11103 println!("hello there");
11104
11105 println!("around the");
11106 println!("world");
11107 }
11108 "#
11109 .unindent(),
11110 );
11111}
11112
11113#[test]
11114fn test_split_words() {
11115 fn split(text: &str) -> Vec<&str> {
11116 split_words(text).collect()
11117 }
11118
11119 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11120 assert_eq!(split("hello_world"), &["hello_", "world"]);
11121 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11122 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11123 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11124 assert_eq!(split("helloworld"), &["helloworld"]);
11125
11126 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11127}
11128
11129#[gpui::test]
11130async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
11131 init_test(cx, |_| {});
11132
11133 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11134 let mut assert = |before, after| {
11135 let _state_context = cx.set_state(before);
11136 cx.run_until_parked();
11137 cx.update_editor(|editor, window, cx| {
11138 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11139 });
11140 cx.assert_editor_state(after);
11141 };
11142
11143 // Outside bracket jumps to outside of matching bracket
11144 assert("console.logˇ(var);", "console.log(var)ˇ;");
11145 assert("console.log(var)ˇ;", "console.logˇ(var);");
11146
11147 // Inside bracket jumps to inside of matching bracket
11148 assert("console.log(ˇvar);", "console.log(varˇ);");
11149 assert("console.log(varˇ);", "console.log(ˇvar);");
11150
11151 // When outside a bracket and inside, favor jumping to the inside bracket
11152 assert(
11153 "console.log('foo', [1, 2, 3]ˇ);",
11154 "console.log(ˇ'foo', [1, 2, 3]);",
11155 );
11156 assert(
11157 "console.log(ˇ'foo', [1, 2, 3]);",
11158 "console.log('foo', [1, 2, 3]ˇ);",
11159 );
11160
11161 // Bias forward if two options are equally likely
11162 assert(
11163 "let result = curried_fun()ˇ();",
11164 "let result = curried_fun()()ˇ;",
11165 );
11166
11167 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11168 assert(
11169 indoc! {"
11170 function test() {
11171 console.log('test')ˇ
11172 }"},
11173 indoc! {"
11174 function test() {
11175 console.logˇ('test')
11176 }"},
11177 );
11178}
11179
11180#[gpui::test]
11181async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
11182 init_test(cx, |_| {});
11183
11184 let fs = FakeFs::new(cx.executor());
11185 fs.insert_tree(
11186 path!("/a"),
11187 json!({
11188 "main.rs": "fn main() { let a = 5; }",
11189 "other.rs": "// Test file",
11190 }),
11191 )
11192 .await;
11193 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11194
11195 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11196 language_registry.add(Arc::new(Language::new(
11197 LanguageConfig {
11198 name: "Rust".into(),
11199 matcher: LanguageMatcher {
11200 path_suffixes: vec!["rs".to_string()],
11201 ..Default::default()
11202 },
11203 brackets: BracketPairConfig {
11204 pairs: vec![BracketPair {
11205 start: "{".to_string(),
11206 end: "}".to_string(),
11207 close: true,
11208 surround: true,
11209 newline: true,
11210 }],
11211 disabled_scopes_by_bracket_ix: Vec::new(),
11212 },
11213 ..Default::default()
11214 },
11215 Some(tree_sitter_rust::LANGUAGE.into()),
11216 )));
11217 let mut fake_servers = language_registry.register_fake_lsp(
11218 "Rust",
11219 FakeLspAdapter {
11220 capabilities: lsp::ServerCapabilities {
11221 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11222 first_trigger_character: "{".to_string(),
11223 more_trigger_character: None,
11224 }),
11225 ..Default::default()
11226 },
11227 ..Default::default()
11228 },
11229 );
11230
11231 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11232
11233 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11234
11235 let worktree_id = workspace
11236 .update(cx, |workspace, _, cx| {
11237 workspace.project().update(cx, |project, cx| {
11238 project.worktrees(cx).next().unwrap().read(cx).id()
11239 })
11240 })
11241 .unwrap();
11242
11243 let buffer = project
11244 .update(cx, |project, cx| {
11245 project.open_local_buffer(path!("/a/main.rs"), cx)
11246 })
11247 .await
11248 .unwrap();
11249 let editor_handle = workspace
11250 .update(cx, |workspace, window, cx| {
11251 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11252 })
11253 .unwrap()
11254 .await
11255 .unwrap()
11256 .downcast::<Editor>()
11257 .unwrap();
11258
11259 cx.executor().start_waiting();
11260 let fake_server = fake_servers.next().await.unwrap();
11261
11262 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11263 assert_eq!(
11264 params.text_document_position.text_document.uri,
11265 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11266 );
11267 assert_eq!(
11268 params.text_document_position.position,
11269 lsp::Position::new(0, 21),
11270 );
11271
11272 Ok(Some(vec![lsp::TextEdit {
11273 new_text: "]".to_string(),
11274 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11275 }]))
11276 });
11277
11278 editor_handle.update_in(cx, |editor, window, cx| {
11279 window.focus(&editor.focus_handle(cx));
11280 editor.change_selections(None, window, cx, |s| {
11281 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11282 });
11283 editor.handle_input("{", window, cx);
11284 });
11285
11286 cx.executor().run_until_parked();
11287
11288 buffer.update(cx, |buffer, _| {
11289 assert_eq!(
11290 buffer.text(),
11291 "fn main() { let a = {5}; }",
11292 "No extra braces from on type formatting should appear in the buffer"
11293 )
11294 });
11295}
11296
11297#[gpui::test]
11298async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11299 init_test(cx, |_| {});
11300
11301 let fs = FakeFs::new(cx.executor());
11302 fs.insert_tree(
11303 path!("/a"),
11304 json!({
11305 "main.rs": "fn main() { let a = 5; }",
11306 "other.rs": "// Test file",
11307 }),
11308 )
11309 .await;
11310
11311 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11312
11313 let server_restarts = Arc::new(AtomicUsize::new(0));
11314 let closure_restarts = Arc::clone(&server_restarts);
11315 let language_server_name = "test language server";
11316 let language_name: LanguageName = "Rust".into();
11317
11318 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11319 language_registry.add(Arc::new(Language::new(
11320 LanguageConfig {
11321 name: language_name.clone(),
11322 matcher: LanguageMatcher {
11323 path_suffixes: vec!["rs".to_string()],
11324 ..Default::default()
11325 },
11326 ..Default::default()
11327 },
11328 Some(tree_sitter_rust::LANGUAGE.into()),
11329 )));
11330 let mut fake_servers = language_registry.register_fake_lsp(
11331 "Rust",
11332 FakeLspAdapter {
11333 name: language_server_name,
11334 initialization_options: Some(json!({
11335 "testOptionValue": true
11336 })),
11337 initializer: Some(Box::new(move |fake_server| {
11338 let task_restarts = Arc::clone(&closure_restarts);
11339 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11340 task_restarts.fetch_add(1, atomic::Ordering::Release);
11341 futures::future::ready(Ok(()))
11342 });
11343 })),
11344 ..Default::default()
11345 },
11346 );
11347
11348 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11349 let _buffer = project
11350 .update(cx, |project, cx| {
11351 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11352 })
11353 .await
11354 .unwrap();
11355 let _fake_server = fake_servers.next().await.unwrap();
11356 update_test_language_settings(cx, |language_settings| {
11357 language_settings.languages.insert(
11358 language_name.clone(),
11359 LanguageSettingsContent {
11360 tab_size: NonZeroU32::new(8),
11361 ..Default::default()
11362 },
11363 );
11364 });
11365 cx.executor().run_until_parked();
11366 assert_eq!(
11367 server_restarts.load(atomic::Ordering::Acquire),
11368 0,
11369 "Should not restart LSP server on an unrelated change"
11370 );
11371
11372 update_test_project_settings(cx, |project_settings| {
11373 project_settings.lsp.insert(
11374 "Some other server name".into(),
11375 LspSettings {
11376 binary: None,
11377 settings: None,
11378 initialization_options: Some(json!({
11379 "some other init value": false
11380 })),
11381 },
11382 );
11383 });
11384 cx.executor().run_until_parked();
11385 assert_eq!(
11386 server_restarts.load(atomic::Ordering::Acquire),
11387 0,
11388 "Should not restart LSP server on an unrelated LSP settings change"
11389 );
11390
11391 update_test_project_settings(cx, |project_settings| {
11392 project_settings.lsp.insert(
11393 language_server_name.into(),
11394 LspSettings {
11395 binary: None,
11396 settings: None,
11397 initialization_options: Some(json!({
11398 "anotherInitValue": false
11399 })),
11400 },
11401 );
11402 });
11403 cx.executor().run_until_parked();
11404 assert_eq!(
11405 server_restarts.load(atomic::Ordering::Acquire),
11406 1,
11407 "Should restart LSP server on a related LSP settings change"
11408 );
11409
11410 update_test_project_settings(cx, |project_settings| {
11411 project_settings.lsp.insert(
11412 language_server_name.into(),
11413 LspSettings {
11414 binary: None,
11415 settings: None,
11416 initialization_options: Some(json!({
11417 "anotherInitValue": false
11418 })),
11419 },
11420 );
11421 });
11422 cx.executor().run_until_parked();
11423 assert_eq!(
11424 server_restarts.load(atomic::Ordering::Acquire),
11425 1,
11426 "Should not restart LSP server on a related LSP settings change that is the same"
11427 );
11428
11429 update_test_project_settings(cx, |project_settings| {
11430 project_settings.lsp.insert(
11431 language_server_name.into(),
11432 LspSettings {
11433 binary: None,
11434 settings: None,
11435 initialization_options: None,
11436 },
11437 );
11438 });
11439 cx.executor().run_until_parked();
11440 assert_eq!(
11441 server_restarts.load(atomic::Ordering::Acquire),
11442 2,
11443 "Should restart LSP server on another related LSP settings change"
11444 );
11445}
11446
11447#[gpui::test]
11448async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11449 init_test(cx, |_| {});
11450
11451 let mut cx = EditorLspTestContext::new_rust(
11452 lsp::ServerCapabilities {
11453 completion_provider: Some(lsp::CompletionOptions {
11454 trigger_characters: Some(vec![".".to_string()]),
11455 resolve_provider: Some(true),
11456 ..Default::default()
11457 }),
11458 ..Default::default()
11459 },
11460 cx,
11461 )
11462 .await;
11463
11464 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11465 cx.simulate_keystroke(".");
11466 let completion_item = lsp::CompletionItem {
11467 label: "some".into(),
11468 kind: Some(lsp::CompletionItemKind::SNIPPET),
11469 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11470 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11471 kind: lsp::MarkupKind::Markdown,
11472 value: "```rust\nSome(2)\n```".to_string(),
11473 })),
11474 deprecated: Some(false),
11475 sort_text: Some("fffffff2".to_string()),
11476 filter_text: Some("some".to_string()),
11477 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11478 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11479 range: lsp::Range {
11480 start: lsp::Position {
11481 line: 0,
11482 character: 22,
11483 },
11484 end: lsp::Position {
11485 line: 0,
11486 character: 22,
11487 },
11488 },
11489 new_text: "Some(2)".to_string(),
11490 })),
11491 additional_text_edits: Some(vec![lsp::TextEdit {
11492 range: lsp::Range {
11493 start: lsp::Position {
11494 line: 0,
11495 character: 20,
11496 },
11497 end: lsp::Position {
11498 line: 0,
11499 character: 22,
11500 },
11501 },
11502 new_text: "".to_string(),
11503 }]),
11504 ..Default::default()
11505 };
11506
11507 let closure_completion_item = completion_item.clone();
11508 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11509 let task_completion_item = closure_completion_item.clone();
11510 async move {
11511 Ok(Some(lsp::CompletionResponse::Array(vec![
11512 task_completion_item,
11513 ])))
11514 }
11515 });
11516
11517 request.next().await;
11518
11519 cx.condition(|editor, _| editor.context_menu_visible())
11520 .await;
11521 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11522 editor
11523 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11524 .unwrap()
11525 });
11526 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11527
11528 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11529 let task_completion_item = completion_item.clone();
11530 async move { Ok(task_completion_item) }
11531 })
11532 .next()
11533 .await
11534 .unwrap();
11535 apply_additional_edits.await.unwrap();
11536 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11537}
11538
11539#[gpui::test]
11540async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11541 cx: &mut gpui::TestAppContext,
11542) {
11543 init_test(cx, |_| {});
11544
11545 let mut cx = EditorLspTestContext::new_rust(
11546 lsp::ServerCapabilities {
11547 completion_provider: Some(lsp::CompletionOptions {
11548 trigger_characters: Some(vec![".".to_string()]),
11549 resolve_provider: Some(true),
11550 ..Default::default()
11551 }),
11552 ..Default::default()
11553 },
11554 cx,
11555 )
11556 .await;
11557
11558 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11559 cx.simulate_keystroke(".");
11560
11561 let item1 = lsp::CompletionItem {
11562 label: "method id()".to_string(),
11563 filter_text: Some("id".to_string()),
11564 detail: None,
11565 documentation: None,
11566 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11567 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11568 new_text: ".id".to_string(),
11569 })),
11570 ..lsp::CompletionItem::default()
11571 };
11572
11573 let item2 = lsp::CompletionItem {
11574 label: "other".to_string(),
11575 filter_text: Some("other".to_string()),
11576 detail: None,
11577 documentation: None,
11578 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11579 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11580 new_text: ".other".to_string(),
11581 })),
11582 ..lsp::CompletionItem::default()
11583 };
11584
11585 let item1 = item1.clone();
11586 cx.handle_request::<lsp::request::Completion, _, _>({
11587 let item1 = item1.clone();
11588 move |_, _, _| {
11589 let item1 = item1.clone();
11590 let item2 = item2.clone();
11591 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11592 }
11593 })
11594 .next()
11595 .await;
11596
11597 cx.condition(|editor, _| editor.context_menu_visible())
11598 .await;
11599 cx.update_editor(|editor, _, _| {
11600 let context_menu = editor.context_menu.borrow_mut();
11601 let context_menu = context_menu
11602 .as_ref()
11603 .expect("Should have the context menu deployed");
11604 match context_menu {
11605 CodeContextMenu::Completions(completions_menu) => {
11606 let completions = completions_menu.completions.borrow_mut();
11607 assert_eq!(
11608 completions
11609 .iter()
11610 .map(|completion| &completion.label.text)
11611 .collect::<Vec<_>>(),
11612 vec!["method id()", "other"]
11613 )
11614 }
11615 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11616 }
11617 });
11618
11619 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11620 let item1 = item1.clone();
11621 move |_, item_to_resolve, _| {
11622 let item1 = item1.clone();
11623 async move {
11624 if item1 == item_to_resolve {
11625 Ok(lsp::CompletionItem {
11626 label: "method id()".to_string(),
11627 filter_text: Some("id".to_string()),
11628 detail: Some("Now resolved!".to_string()),
11629 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11630 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11631 range: lsp::Range::new(
11632 lsp::Position::new(0, 22),
11633 lsp::Position::new(0, 22),
11634 ),
11635 new_text: ".id".to_string(),
11636 })),
11637 ..lsp::CompletionItem::default()
11638 })
11639 } else {
11640 Ok(item_to_resolve)
11641 }
11642 }
11643 }
11644 })
11645 .next()
11646 .await
11647 .unwrap();
11648 cx.run_until_parked();
11649
11650 cx.update_editor(|editor, window, cx| {
11651 editor.context_menu_next(&Default::default(), window, cx);
11652 });
11653
11654 cx.update_editor(|editor, _, _| {
11655 let context_menu = editor.context_menu.borrow_mut();
11656 let context_menu = context_menu
11657 .as_ref()
11658 .expect("Should have the context menu deployed");
11659 match context_menu {
11660 CodeContextMenu::Completions(completions_menu) => {
11661 let completions = completions_menu.completions.borrow_mut();
11662 assert_eq!(
11663 completions
11664 .iter()
11665 .map(|completion| &completion.label.text)
11666 .collect::<Vec<_>>(),
11667 vec!["method id() Now resolved!", "other"],
11668 "Should update first completion label, but not second as the filter text did not match."
11669 );
11670 }
11671 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11672 }
11673 });
11674}
11675
11676#[gpui::test]
11677async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11678 init_test(cx, |_| {});
11679
11680 let mut cx = EditorLspTestContext::new_rust(
11681 lsp::ServerCapabilities {
11682 completion_provider: Some(lsp::CompletionOptions {
11683 trigger_characters: Some(vec![".".to_string()]),
11684 resolve_provider: Some(true),
11685 ..Default::default()
11686 }),
11687 ..Default::default()
11688 },
11689 cx,
11690 )
11691 .await;
11692
11693 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11694 cx.simulate_keystroke(".");
11695
11696 let unresolved_item_1 = lsp::CompletionItem {
11697 label: "id".to_string(),
11698 filter_text: Some("id".to_string()),
11699 detail: None,
11700 documentation: None,
11701 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11702 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11703 new_text: ".id".to_string(),
11704 })),
11705 ..lsp::CompletionItem::default()
11706 };
11707 let resolved_item_1 = lsp::CompletionItem {
11708 additional_text_edits: Some(vec![lsp::TextEdit {
11709 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11710 new_text: "!!".to_string(),
11711 }]),
11712 ..unresolved_item_1.clone()
11713 };
11714 let unresolved_item_2 = lsp::CompletionItem {
11715 label: "other".to_string(),
11716 filter_text: Some("other".to_string()),
11717 detail: None,
11718 documentation: None,
11719 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11720 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11721 new_text: ".other".to_string(),
11722 })),
11723 ..lsp::CompletionItem::default()
11724 };
11725 let resolved_item_2 = lsp::CompletionItem {
11726 additional_text_edits: Some(vec![lsp::TextEdit {
11727 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11728 new_text: "??".to_string(),
11729 }]),
11730 ..unresolved_item_2.clone()
11731 };
11732
11733 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11734 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11735 cx.lsp
11736 .server
11737 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11738 let unresolved_item_1 = unresolved_item_1.clone();
11739 let resolved_item_1 = resolved_item_1.clone();
11740 let unresolved_item_2 = unresolved_item_2.clone();
11741 let resolved_item_2 = resolved_item_2.clone();
11742 let resolve_requests_1 = resolve_requests_1.clone();
11743 let resolve_requests_2 = resolve_requests_2.clone();
11744 move |unresolved_request, _| {
11745 let unresolved_item_1 = unresolved_item_1.clone();
11746 let resolved_item_1 = resolved_item_1.clone();
11747 let unresolved_item_2 = unresolved_item_2.clone();
11748 let resolved_item_2 = resolved_item_2.clone();
11749 let resolve_requests_1 = resolve_requests_1.clone();
11750 let resolve_requests_2 = resolve_requests_2.clone();
11751 async move {
11752 if unresolved_request == unresolved_item_1 {
11753 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11754 Ok(resolved_item_1.clone())
11755 } else if unresolved_request == unresolved_item_2 {
11756 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11757 Ok(resolved_item_2.clone())
11758 } else {
11759 panic!("Unexpected completion item {unresolved_request:?}")
11760 }
11761 }
11762 }
11763 })
11764 .detach();
11765
11766 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11767 let unresolved_item_1 = unresolved_item_1.clone();
11768 let unresolved_item_2 = unresolved_item_2.clone();
11769 async move {
11770 Ok(Some(lsp::CompletionResponse::Array(vec![
11771 unresolved_item_1,
11772 unresolved_item_2,
11773 ])))
11774 }
11775 })
11776 .next()
11777 .await;
11778
11779 cx.condition(|editor, _| editor.context_menu_visible())
11780 .await;
11781 cx.update_editor(|editor, _, _| {
11782 let context_menu = editor.context_menu.borrow_mut();
11783 let context_menu = context_menu
11784 .as_ref()
11785 .expect("Should have the context menu deployed");
11786 match context_menu {
11787 CodeContextMenu::Completions(completions_menu) => {
11788 let completions = completions_menu.completions.borrow_mut();
11789 assert_eq!(
11790 completions
11791 .iter()
11792 .map(|completion| &completion.label.text)
11793 .collect::<Vec<_>>(),
11794 vec!["id", "other"]
11795 )
11796 }
11797 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11798 }
11799 });
11800 cx.run_until_parked();
11801
11802 cx.update_editor(|editor, window, cx| {
11803 editor.context_menu_next(&ContextMenuNext, window, cx);
11804 });
11805 cx.run_until_parked();
11806 cx.update_editor(|editor, window, cx| {
11807 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11808 });
11809 cx.run_until_parked();
11810 cx.update_editor(|editor, window, cx| {
11811 editor.context_menu_next(&ContextMenuNext, window, cx);
11812 });
11813 cx.run_until_parked();
11814 cx.update_editor(|editor, window, cx| {
11815 editor
11816 .compose_completion(&ComposeCompletion::default(), window, cx)
11817 .expect("No task returned")
11818 })
11819 .await
11820 .expect("Completion failed");
11821 cx.run_until_parked();
11822
11823 cx.update_editor(|editor, _, cx| {
11824 assert_eq!(
11825 resolve_requests_1.load(atomic::Ordering::Acquire),
11826 1,
11827 "Should always resolve once despite multiple selections"
11828 );
11829 assert_eq!(
11830 resolve_requests_2.load(atomic::Ordering::Acquire),
11831 1,
11832 "Should always resolve once after multiple selections and applying the completion"
11833 );
11834 assert_eq!(
11835 editor.text(cx),
11836 "fn main() { let a = ??.other; }",
11837 "Should use resolved data when applying the completion"
11838 );
11839 });
11840}
11841
11842#[gpui::test]
11843async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11844 init_test(cx, |_| {});
11845
11846 let item_0 = lsp::CompletionItem {
11847 label: "abs".into(),
11848 insert_text: Some("abs".into()),
11849 data: Some(json!({ "very": "special"})),
11850 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11851 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11852 lsp::InsertReplaceEdit {
11853 new_text: "abs".to_string(),
11854 insert: lsp::Range::default(),
11855 replace: lsp::Range::default(),
11856 },
11857 )),
11858 ..lsp::CompletionItem::default()
11859 };
11860 let items = iter::once(item_0.clone())
11861 .chain((11..51).map(|i| lsp::CompletionItem {
11862 label: format!("item_{}", i),
11863 insert_text: Some(format!("item_{}", i)),
11864 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11865 ..lsp::CompletionItem::default()
11866 }))
11867 .collect::<Vec<_>>();
11868
11869 let default_commit_characters = vec!["?".to_string()];
11870 let default_data = json!({ "default": "data"});
11871 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11872 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11873 let default_edit_range = lsp::Range {
11874 start: lsp::Position {
11875 line: 0,
11876 character: 5,
11877 },
11878 end: lsp::Position {
11879 line: 0,
11880 character: 5,
11881 },
11882 };
11883
11884 let item_0_out = lsp::CompletionItem {
11885 commit_characters: Some(default_commit_characters.clone()),
11886 insert_text_format: Some(default_insert_text_format),
11887 ..item_0
11888 };
11889 let items_out = iter::once(item_0_out)
11890 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11891 commit_characters: Some(default_commit_characters.clone()),
11892 data: Some(default_data.clone()),
11893 insert_text_mode: Some(default_insert_text_mode),
11894 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11895 range: default_edit_range,
11896 new_text: item.label.clone(),
11897 })),
11898 ..item.clone()
11899 }))
11900 .collect::<Vec<lsp::CompletionItem>>();
11901
11902 let mut cx = EditorLspTestContext::new_rust(
11903 lsp::ServerCapabilities {
11904 completion_provider: Some(lsp::CompletionOptions {
11905 trigger_characters: Some(vec![".".to_string()]),
11906 resolve_provider: Some(true),
11907 ..Default::default()
11908 }),
11909 ..Default::default()
11910 },
11911 cx,
11912 )
11913 .await;
11914
11915 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11916 cx.simulate_keystroke(".");
11917
11918 let completion_data = default_data.clone();
11919 let completion_characters = default_commit_characters.clone();
11920 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11921 let default_data = completion_data.clone();
11922 let default_commit_characters = completion_characters.clone();
11923 let items = items.clone();
11924 async move {
11925 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11926 items,
11927 item_defaults: Some(lsp::CompletionListItemDefaults {
11928 data: Some(default_data.clone()),
11929 commit_characters: Some(default_commit_characters.clone()),
11930 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11931 default_edit_range,
11932 )),
11933 insert_text_format: Some(default_insert_text_format),
11934 insert_text_mode: Some(default_insert_text_mode),
11935 }),
11936 ..lsp::CompletionList::default()
11937 })))
11938 }
11939 })
11940 .next()
11941 .await;
11942
11943 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11944 cx.lsp
11945 .server
11946 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11947 let closure_resolved_items = resolved_items.clone();
11948 move |item_to_resolve, _| {
11949 let closure_resolved_items = closure_resolved_items.clone();
11950 async move {
11951 closure_resolved_items.lock().push(item_to_resolve.clone());
11952 Ok(item_to_resolve)
11953 }
11954 }
11955 })
11956 .detach();
11957
11958 cx.condition(|editor, _| editor.context_menu_visible())
11959 .await;
11960 cx.run_until_parked();
11961 cx.update_editor(|editor, _, _| {
11962 let menu = editor.context_menu.borrow_mut();
11963 match menu.as_ref().expect("should have the completions menu") {
11964 CodeContextMenu::Completions(completions_menu) => {
11965 assert_eq!(
11966 completions_menu
11967 .entries
11968 .borrow()
11969 .iter()
11970 .map(|mat| mat.string.clone())
11971 .collect::<Vec<String>>(),
11972 items_out
11973 .iter()
11974 .map(|completion| completion.label.clone())
11975 .collect::<Vec<String>>()
11976 );
11977 }
11978 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11979 }
11980 });
11981 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11982 // with 4 from the end.
11983 assert_eq!(
11984 *resolved_items.lock(),
11985 [
11986 &items_out[0..16],
11987 &items_out[items_out.len() - 4..items_out.len()]
11988 ]
11989 .concat()
11990 .iter()
11991 .cloned()
11992 .collect::<Vec<lsp::CompletionItem>>()
11993 );
11994 resolved_items.lock().clear();
11995
11996 cx.update_editor(|editor, window, cx| {
11997 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11998 });
11999 cx.run_until_parked();
12000 // Completions that have already been resolved are skipped.
12001 assert_eq!(
12002 *resolved_items.lock(),
12003 items_out[items_out.len() - 16..items_out.len() - 4]
12004 .iter()
12005 .cloned()
12006 .collect::<Vec<lsp::CompletionItem>>()
12007 );
12008 resolved_items.lock().clear();
12009}
12010
12011#[gpui::test]
12012async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
12013 init_test(cx, |_| {});
12014
12015 let mut cx = EditorLspTestContext::new(
12016 Language::new(
12017 LanguageConfig {
12018 matcher: LanguageMatcher {
12019 path_suffixes: vec!["jsx".into()],
12020 ..Default::default()
12021 },
12022 overrides: [(
12023 "element".into(),
12024 LanguageConfigOverride {
12025 word_characters: Override::Set(['-'].into_iter().collect()),
12026 ..Default::default()
12027 },
12028 )]
12029 .into_iter()
12030 .collect(),
12031 ..Default::default()
12032 },
12033 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12034 )
12035 .with_override_query("(jsx_self_closing_element) @element")
12036 .unwrap(),
12037 lsp::ServerCapabilities {
12038 completion_provider: Some(lsp::CompletionOptions {
12039 trigger_characters: Some(vec![":".to_string()]),
12040 ..Default::default()
12041 }),
12042 ..Default::default()
12043 },
12044 cx,
12045 )
12046 .await;
12047
12048 cx.lsp
12049 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12050 Ok(Some(lsp::CompletionResponse::Array(vec![
12051 lsp::CompletionItem {
12052 label: "bg-blue".into(),
12053 ..Default::default()
12054 },
12055 lsp::CompletionItem {
12056 label: "bg-red".into(),
12057 ..Default::default()
12058 },
12059 lsp::CompletionItem {
12060 label: "bg-yellow".into(),
12061 ..Default::default()
12062 },
12063 ])))
12064 });
12065
12066 cx.set_state(r#"<p class="bgˇ" />"#);
12067
12068 // Trigger completion when typing a dash, because the dash is an extra
12069 // word character in the 'element' scope, which contains the cursor.
12070 cx.simulate_keystroke("-");
12071 cx.executor().run_until_parked();
12072 cx.update_editor(|editor, _, _| {
12073 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12074 {
12075 assert_eq!(
12076 completion_menu_entries(&menu),
12077 &["bg-red", "bg-blue", "bg-yellow"]
12078 );
12079 } else {
12080 panic!("expected completion menu to be open");
12081 }
12082 });
12083
12084 cx.simulate_keystroke("l");
12085 cx.executor().run_until_parked();
12086 cx.update_editor(|editor, _, _| {
12087 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12088 {
12089 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12090 } else {
12091 panic!("expected completion menu to be open");
12092 }
12093 });
12094
12095 // When filtering completions, consider the character after the '-' to
12096 // be the start of a subword.
12097 cx.set_state(r#"<p class="yelˇ" />"#);
12098 cx.simulate_keystroke("l");
12099 cx.executor().run_until_parked();
12100 cx.update_editor(|editor, _, _| {
12101 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12102 {
12103 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12104 } else {
12105 panic!("expected completion menu to be open");
12106 }
12107 });
12108}
12109
12110fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12111 let entries = menu.entries.borrow();
12112 entries.iter().map(|mat| mat.string.clone()).collect()
12113}
12114
12115#[gpui::test]
12116async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
12117 init_test(cx, |settings| {
12118 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12119 FormatterList(vec![Formatter::Prettier].into()),
12120 ))
12121 });
12122
12123 let fs = FakeFs::new(cx.executor());
12124 fs.insert_file(path!("/file.ts"), Default::default()).await;
12125
12126 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12127 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12128
12129 language_registry.add(Arc::new(Language::new(
12130 LanguageConfig {
12131 name: "TypeScript".into(),
12132 matcher: LanguageMatcher {
12133 path_suffixes: vec!["ts".to_string()],
12134 ..Default::default()
12135 },
12136 ..Default::default()
12137 },
12138 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12139 )));
12140 update_test_language_settings(cx, |settings| {
12141 settings.defaults.prettier = Some(PrettierSettings {
12142 allowed: true,
12143 ..PrettierSettings::default()
12144 });
12145 });
12146
12147 let test_plugin = "test_plugin";
12148 let _ = language_registry.register_fake_lsp(
12149 "TypeScript",
12150 FakeLspAdapter {
12151 prettier_plugins: vec![test_plugin],
12152 ..Default::default()
12153 },
12154 );
12155
12156 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12157 let buffer = project
12158 .update(cx, |project, cx| {
12159 project.open_local_buffer(path!("/file.ts"), cx)
12160 })
12161 .await
12162 .unwrap();
12163
12164 let buffer_text = "one\ntwo\nthree\n";
12165 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12166 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12167 editor.update_in(cx, |editor, window, cx| {
12168 editor.set_text(buffer_text, window, cx)
12169 });
12170
12171 editor
12172 .update_in(cx, |editor, window, cx| {
12173 editor.perform_format(
12174 project.clone(),
12175 FormatTrigger::Manual,
12176 FormatTarget::Buffers,
12177 window,
12178 cx,
12179 )
12180 })
12181 .unwrap()
12182 .await;
12183 assert_eq!(
12184 editor.update(cx, |editor, cx| editor.text(cx)),
12185 buffer_text.to_string() + prettier_format_suffix,
12186 "Test prettier formatting was not applied to the original buffer text",
12187 );
12188
12189 update_test_language_settings(cx, |settings| {
12190 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12191 });
12192 let format = editor.update_in(cx, |editor, window, cx| {
12193 editor.perform_format(
12194 project.clone(),
12195 FormatTrigger::Manual,
12196 FormatTarget::Buffers,
12197 window,
12198 cx,
12199 )
12200 });
12201 format.await.unwrap();
12202 assert_eq!(
12203 editor.update(cx, |editor, cx| editor.text(cx)),
12204 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12205 "Autoformatting (via test prettier) was not applied to the original buffer text",
12206 );
12207}
12208
12209#[gpui::test]
12210async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
12211 init_test(cx, |_| {});
12212 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12213 let base_text = indoc! {r#"
12214 struct Row;
12215 struct Row1;
12216 struct Row2;
12217
12218 struct Row4;
12219 struct Row5;
12220 struct Row6;
12221
12222 struct Row8;
12223 struct Row9;
12224 struct Row10;"#};
12225
12226 // When addition hunks are not adjacent to carets, no hunk revert is performed
12227 assert_hunk_revert(
12228 indoc! {r#"struct Row;
12229 struct Row1;
12230 struct Row1.1;
12231 struct Row1.2;
12232 struct Row2;ˇ
12233
12234 struct Row4;
12235 struct Row5;
12236 struct Row6;
12237
12238 struct Row8;
12239 ˇstruct Row9;
12240 struct Row9.1;
12241 struct Row9.2;
12242 struct Row9.3;
12243 struct Row10;"#},
12244 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12245 indoc! {r#"struct Row;
12246 struct Row1;
12247 struct Row1.1;
12248 struct Row1.2;
12249 struct Row2;ˇ
12250
12251 struct Row4;
12252 struct Row5;
12253 struct Row6;
12254
12255 struct Row8;
12256 ˇstruct Row9;
12257 struct Row9.1;
12258 struct Row9.2;
12259 struct Row9.3;
12260 struct Row10;"#},
12261 base_text,
12262 &mut cx,
12263 );
12264 // Same for selections
12265 assert_hunk_revert(
12266 indoc! {r#"struct Row;
12267 struct Row1;
12268 struct Row2;
12269 struct Row2.1;
12270 struct Row2.2;
12271 «ˇ
12272 struct Row4;
12273 struct» Row5;
12274 «struct Row6;
12275 ˇ»
12276 struct Row9.1;
12277 struct Row9.2;
12278 struct Row9.3;
12279 struct Row8;
12280 struct Row9;
12281 struct Row10;"#},
12282 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12283 indoc! {r#"struct Row;
12284 struct Row1;
12285 struct Row2;
12286 struct Row2.1;
12287 struct Row2.2;
12288 «ˇ
12289 struct Row4;
12290 struct» Row5;
12291 «struct Row6;
12292 ˇ»
12293 struct Row9.1;
12294 struct Row9.2;
12295 struct Row9.3;
12296 struct Row8;
12297 struct Row9;
12298 struct Row10;"#},
12299 base_text,
12300 &mut cx,
12301 );
12302
12303 // When carets and selections intersect the addition hunks, those are reverted.
12304 // Adjacent carets got merged.
12305 assert_hunk_revert(
12306 indoc! {r#"struct Row;
12307 ˇ// something on the top
12308 struct Row1;
12309 struct Row2;
12310 struct Roˇw3.1;
12311 struct Row2.2;
12312 struct Row2.3;ˇ
12313
12314 struct Row4;
12315 struct ˇRow5.1;
12316 struct Row5.2;
12317 struct «Rowˇ»5.3;
12318 struct Row5;
12319 struct Row6;
12320 ˇ
12321 struct Row9.1;
12322 struct «Rowˇ»9.2;
12323 struct «ˇRow»9.3;
12324 struct Row8;
12325 struct Row9;
12326 «ˇ// something on bottom»
12327 struct Row10;"#},
12328 vec![
12329 DiffHunkStatus::added(),
12330 DiffHunkStatus::added(),
12331 DiffHunkStatus::added(),
12332 DiffHunkStatus::added(),
12333 DiffHunkStatus::added(),
12334 ],
12335 indoc! {r#"struct Row;
12336 ˇstruct Row1;
12337 struct Row2;
12338 ˇ
12339 struct Row4;
12340 ˇstruct Row5;
12341 struct Row6;
12342 ˇ
12343 ˇstruct Row8;
12344 struct Row9;
12345 ˇstruct Row10;"#},
12346 base_text,
12347 &mut cx,
12348 );
12349}
12350
12351#[gpui::test]
12352async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12353 init_test(cx, |_| {});
12354 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12355 let base_text = indoc! {r#"
12356 struct Row;
12357 struct Row1;
12358 struct Row2;
12359
12360 struct Row4;
12361 struct Row5;
12362 struct Row6;
12363
12364 struct Row8;
12365 struct Row9;
12366 struct Row10;"#};
12367
12368 // Modification hunks behave the same as the addition ones.
12369 assert_hunk_revert(
12370 indoc! {r#"struct Row;
12371 struct Row1;
12372 struct Row33;
12373 ˇ
12374 struct Row4;
12375 struct Row5;
12376 struct Row6;
12377 ˇ
12378 struct Row99;
12379 struct Row9;
12380 struct Row10;"#},
12381 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12382 indoc! {r#"struct Row;
12383 struct Row1;
12384 struct Row33;
12385 ˇ
12386 struct Row4;
12387 struct Row5;
12388 struct Row6;
12389 ˇ
12390 struct Row99;
12391 struct Row9;
12392 struct Row10;"#},
12393 base_text,
12394 &mut cx,
12395 );
12396 assert_hunk_revert(
12397 indoc! {r#"struct Row;
12398 struct Row1;
12399 struct Row33;
12400 «ˇ
12401 struct Row4;
12402 struct» Row5;
12403 «struct Row6;
12404 ˇ»
12405 struct Row99;
12406 struct Row9;
12407 struct Row10;"#},
12408 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12409 indoc! {r#"struct Row;
12410 struct Row1;
12411 struct Row33;
12412 «ˇ
12413 struct Row4;
12414 struct» Row5;
12415 «struct Row6;
12416 ˇ»
12417 struct Row99;
12418 struct Row9;
12419 struct Row10;"#},
12420 base_text,
12421 &mut cx,
12422 );
12423
12424 assert_hunk_revert(
12425 indoc! {r#"ˇstruct Row1.1;
12426 struct Row1;
12427 «ˇstr»uct Row22;
12428
12429 struct ˇRow44;
12430 struct Row5;
12431 struct «Rˇ»ow66;ˇ
12432
12433 «struˇ»ct Row88;
12434 struct Row9;
12435 struct Row1011;ˇ"#},
12436 vec![
12437 DiffHunkStatus::modified(),
12438 DiffHunkStatus::modified(),
12439 DiffHunkStatus::modified(),
12440 DiffHunkStatus::modified(),
12441 DiffHunkStatus::modified(),
12442 DiffHunkStatus::modified(),
12443 ],
12444 indoc! {r#"struct Row;
12445 ˇstruct Row1;
12446 struct Row2;
12447 ˇ
12448 struct Row4;
12449 ˇstruct Row5;
12450 struct Row6;
12451 ˇ
12452 struct Row8;
12453 ˇstruct Row9;
12454 struct Row10;ˇ"#},
12455 base_text,
12456 &mut cx,
12457 );
12458}
12459
12460#[gpui::test]
12461async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12462 init_test(cx, |_| {});
12463 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12464 let base_text = indoc! {r#"
12465 one
12466
12467 two
12468 three
12469 "#};
12470
12471 cx.set_diff_base(base_text);
12472 cx.set_state("\nˇ\n");
12473 cx.executor().run_until_parked();
12474 cx.update_editor(|editor, _window, cx| {
12475 editor.expand_selected_diff_hunks(cx);
12476 });
12477 cx.executor().run_until_parked();
12478 cx.update_editor(|editor, window, cx| {
12479 editor.backspace(&Default::default(), window, cx);
12480 });
12481 cx.run_until_parked();
12482 cx.assert_state_with_diff(
12483 indoc! {r#"
12484
12485 - two
12486 - threeˇ
12487 +
12488 "#}
12489 .to_string(),
12490 );
12491}
12492
12493#[gpui::test]
12494async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12495 init_test(cx, |_| {});
12496 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12497 let base_text = indoc! {r#"struct Row;
12498struct Row1;
12499struct Row2;
12500
12501struct Row4;
12502struct Row5;
12503struct Row6;
12504
12505struct Row8;
12506struct Row9;
12507struct Row10;"#};
12508
12509 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12510 assert_hunk_revert(
12511 indoc! {r#"struct Row;
12512 struct Row2;
12513
12514 ˇstruct Row4;
12515 struct Row5;
12516 struct Row6;
12517 ˇ
12518 struct Row8;
12519 struct Row10;"#},
12520 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12521 indoc! {r#"struct Row;
12522 struct Row2;
12523
12524 ˇstruct Row4;
12525 struct Row5;
12526 struct Row6;
12527 ˇ
12528 struct Row8;
12529 struct Row10;"#},
12530 base_text,
12531 &mut cx,
12532 );
12533 assert_hunk_revert(
12534 indoc! {r#"struct Row;
12535 struct Row2;
12536
12537 «ˇstruct Row4;
12538 struct» Row5;
12539 «struct Row6;
12540 ˇ»
12541 struct Row8;
12542 struct Row10;"#},
12543 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12544 indoc! {r#"struct Row;
12545 struct Row2;
12546
12547 «ˇstruct Row4;
12548 struct» Row5;
12549 «struct Row6;
12550 ˇ»
12551 struct Row8;
12552 struct Row10;"#},
12553 base_text,
12554 &mut cx,
12555 );
12556
12557 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12558 assert_hunk_revert(
12559 indoc! {r#"struct Row;
12560 ˇstruct Row2;
12561
12562 struct Row4;
12563 struct Row5;
12564 struct Row6;
12565
12566 struct Row8;ˇ
12567 struct Row10;"#},
12568 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12569 indoc! {r#"struct Row;
12570 struct Row1;
12571 ˇstruct Row2;
12572
12573 struct Row4;
12574 struct Row5;
12575 struct Row6;
12576
12577 struct Row8;ˇ
12578 struct Row9;
12579 struct Row10;"#},
12580 base_text,
12581 &mut cx,
12582 );
12583 assert_hunk_revert(
12584 indoc! {r#"struct Row;
12585 struct Row2«ˇ;
12586 struct Row4;
12587 struct» Row5;
12588 «struct Row6;
12589
12590 struct Row8;ˇ»
12591 struct Row10;"#},
12592 vec![
12593 DiffHunkStatus::removed(),
12594 DiffHunkStatus::removed(),
12595 DiffHunkStatus::removed(),
12596 ],
12597 indoc! {r#"struct Row;
12598 struct Row1;
12599 struct Row2«ˇ;
12600
12601 struct Row4;
12602 struct» Row5;
12603 «struct Row6;
12604
12605 struct Row8;ˇ»
12606 struct Row9;
12607 struct Row10;"#},
12608 base_text,
12609 &mut cx,
12610 );
12611}
12612
12613#[gpui::test]
12614async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12615 init_test(cx, |_| {});
12616
12617 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12618 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12619 let base_text_3 =
12620 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12621
12622 let text_1 = edit_first_char_of_every_line(base_text_1);
12623 let text_2 = edit_first_char_of_every_line(base_text_2);
12624 let text_3 = edit_first_char_of_every_line(base_text_3);
12625
12626 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12627 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12628 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12629
12630 let multibuffer = cx.new(|cx| {
12631 let mut multibuffer = MultiBuffer::new(ReadWrite);
12632 multibuffer.push_excerpts(
12633 buffer_1.clone(),
12634 [
12635 ExcerptRange {
12636 context: Point::new(0, 0)..Point::new(3, 0),
12637 primary: None,
12638 },
12639 ExcerptRange {
12640 context: Point::new(5, 0)..Point::new(7, 0),
12641 primary: None,
12642 },
12643 ExcerptRange {
12644 context: Point::new(9, 0)..Point::new(10, 4),
12645 primary: None,
12646 },
12647 ],
12648 cx,
12649 );
12650 multibuffer.push_excerpts(
12651 buffer_2.clone(),
12652 [
12653 ExcerptRange {
12654 context: Point::new(0, 0)..Point::new(3, 0),
12655 primary: None,
12656 },
12657 ExcerptRange {
12658 context: Point::new(5, 0)..Point::new(7, 0),
12659 primary: None,
12660 },
12661 ExcerptRange {
12662 context: Point::new(9, 0)..Point::new(10, 4),
12663 primary: None,
12664 },
12665 ],
12666 cx,
12667 );
12668 multibuffer.push_excerpts(
12669 buffer_3.clone(),
12670 [
12671 ExcerptRange {
12672 context: Point::new(0, 0)..Point::new(3, 0),
12673 primary: None,
12674 },
12675 ExcerptRange {
12676 context: Point::new(5, 0)..Point::new(7, 0),
12677 primary: None,
12678 },
12679 ExcerptRange {
12680 context: Point::new(9, 0)..Point::new(10, 4),
12681 primary: None,
12682 },
12683 ],
12684 cx,
12685 );
12686 multibuffer
12687 });
12688
12689 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12690 editor.update_in(cx, |editor, _window, cx| {
12691 for (buffer, diff_base) in [
12692 (buffer_1.clone(), base_text_1),
12693 (buffer_2.clone(), base_text_2),
12694 (buffer_3.clone(), base_text_3),
12695 ] {
12696 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12697 editor
12698 .buffer
12699 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12700 }
12701 });
12702 cx.executor().run_until_parked();
12703
12704 editor.update_in(cx, |editor, window, cx| {
12705 assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
12706 editor.select_all(&SelectAll, window, cx);
12707 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12708 });
12709 cx.executor().run_until_parked();
12710
12711 // When all ranges are selected, all buffer hunks are reverted.
12712 editor.update(cx, |editor, cx| {
12713 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");
12714 });
12715 buffer_1.update(cx, |buffer, _| {
12716 assert_eq!(buffer.text(), base_text_1);
12717 });
12718 buffer_2.update(cx, |buffer, _| {
12719 assert_eq!(buffer.text(), base_text_2);
12720 });
12721 buffer_3.update(cx, |buffer, _| {
12722 assert_eq!(buffer.text(), base_text_3);
12723 });
12724
12725 editor.update_in(cx, |editor, window, cx| {
12726 editor.undo(&Default::default(), window, cx);
12727 });
12728
12729 editor.update_in(cx, |editor, window, cx| {
12730 editor.change_selections(None, window, cx, |s| {
12731 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12732 });
12733 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12734 });
12735
12736 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12737 // but not affect buffer_2 and its related excerpts.
12738 editor.update(cx, |editor, cx| {
12739 assert_eq!(
12740 editor.text(cx),
12741 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
12742 );
12743 });
12744 buffer_1.update(cx, |buffer, _| {
12745 assert_eq!(buffer.text(), base_text_1);
12746 });
12747 buffer_2.update(cx, |buffer, _| {
12748 assert_eq!(
12749 buffer.text(),
12750 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12751 );
12752 });
12753 buffer_3.update(cx, |buffer, _| {
12754 assert_eq!(
12755 buffer.text(),
12756 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12757 );
12758 });
12759
12760 fn edit_first_char_of_every_line(text: &str) -> String {
12761 text.split('\n')
12762 .map(|line| format!("X{}", &line[1..]))
12763 .collect::<Vec<_>>()
12764 .join("\n")
12765 }
12766}
12767
12768#[gpui::test]
12769async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12770 init_test(cx, |_| {});
12771
12772 let cols = 4;
12773 let rows = 10;
12774 let sample_text_1 = sample_text(rows, cols, 'a');
12775 assert_eq!(
12776 sample_text_1,
12777 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12778 );
12779 let sample_text_2 = sample_text(rows, cols, 'l');
12780 assert_eq!(
12781 sample_text_2,
12782 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12783 );
12784 let sample_text_3 = sample_text(rows, cols, 'v');
12785 assert_eq!(
12786 sample_text_3,
12787 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12788 );
12789
12790 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12791 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12792 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12793
12794 let multi_buffer = cx.new(|cx| {
12795 let mut multibuffer = MultiBuffer::new(ReadWrite);
12796 multibuffer.push_excerpts(
12797 buffer_1.clone(),
12798 [
12799 ExcerptRange {
12800 context: Point::new(0, 0)..Point::new(3, 0),
12801 primary: None,
12802 },
12803 ExcerptRange {
12804 context: Point::new(5, 0)..Point::new(7, 0),
12805 primary: None,
12806 },
12807 ExcerptRange {
12808 context: Point::new(9, 0)..Point::new(10, 4),
12809 primary: None,
12810 },
12811 ],
12812 cx,
12813 );
12814 multibuffer.push_excerpts(
12815 buffer_2.clone(),
12816 [
12817 ExcerptRange {
12818 context: Point::new(0, 0)..Point::new(3, 0),
12819 primary: None,
12820 },
12821 ExcerptRange {
12822 context: Point::new(5, 0)..Point::new(7, 0),
12823 primary: None,
12824 },
12825 ExcerptRange {
12826 context: Point::new(9, 0)..Point::new(10, 4),
12827 primary: None,
12828 },
12829 ],
12830 cx,
12831 );
12832 multibuffer.push_excerpts(
12833 buffer_3.clone(),
12834 [
12835 ExcerptRange {
12836 context: Point::new(0, 0)..Point::new(3, 0),
12837 primary: None,
12838 },
12839 ExcerptRange {
12840 context: Point::new(5, 0)..Point::new(7, 0),
12841 primary: None,
12842 },
12843 ExcerptRange {
12844 context: Point::new(9, 0)..Point::new(10, 4),
12845 primary: None,
12846 },
12847 ],
12848 cx,
12849 );
12850 multibuffer
12851 });
12852
12853 let fs = FakeFs::new(cx.executor());
12854 fs.insert_tree(
12855 "/a",
12856 json!({
12857 "main.rs": sample_text_1,
12858 "other.rs": sample_text_2,
12859 "lib.rs": sample_text_3,
12860 }),
12861 )
12862 .await;
12863 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12864 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12865 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12866 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12867 Editor::new(
12868 EditorMode::Full,
12869 multi_buffer,
12870 Some(project.clone()),
12871 true,
12872 window,
12873 cx,
12874 )
12875 });
12876 let multibuffer_item_id = workspace
12877 .update(cx, |workspace, window, cx| {
12878 assert!(
12879 workspace.active_item(cx).is_none(),
12880 "active item should be None before the first item is added"
12881 );
12882 workspace.add_item_to_active_pane(
12883 Box::new(multi_buffer_editor.clone()),
12884 None,
12885 true,
12886 window,
12887 cx,
12888 );
12889 let active_item = workspace
12890 .active_item(cx)
12891 .expect("should have an active item after adding the multi buffer");
12892 assert!(
12893 !active_item.is_singleton(cx),
12894 "A multi buffer was expected to active after adding"
12895 );
12896 active_item.item_id()
12897 })
12898 .unwrap();
12899 cx.executor().run_until_parked();
12900
12901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12902 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12903 s.select_ranges(Some(1..2))
12904 });
12905 editor.open_excerpts(&OpenExcerpts, window, cx);
12906 });
12907 cx.executor().run_until_parked();
12908 let first_item_id = workspace
12909 .update(cx, |workspace, window, cx| {
12910 let active_item = workspace
12911 .active_item(cx)
12912 .expect("should have an active item after navigating into the 1st buffer");
12913 let first_item_id = active_item.item_id();
12914 assert_ne!(
12915 first_item_id, multibuffer_item_id,
12916 "Should navigate into the 1st buffer and activate it"
12917 );
12918 assert!(
12919 active_item.is_singleton(cx),
12920 "New active item should be a singleton buffer"
12921 );
12922 assert_eq!(
12923 active_item
12924 .act_as::<Editor>(cx)
12925 .expect("should have navigated into an editor for the 1st buffer")
12926 .read(cx)
12927 .text(cx),
12928 sample_text_1
12929 );
12930
12931 workspace
12932 .go_back(workspace.active_pane().downgrade(), window, cx)
12933 .detach_and_log_err(cx);
12934
12935 first_item_id
12936 })
12937 .unwrap();
12938 cx.executor().run_until_parked();
12939 workspace
12940 .update(cx, |workspace, _, cx| {
12941 let active_item = workspace
12942 .active_item(cx)
12943 .expect("should have an active item after navigating back");
12944 assert_eq!(
12945 active_item.item_id(),
12946 multibuffer_item_id,
12947 "Should navigate back to the multi buffer"
12948 );
12949 assert!(!active_item.is_singleton(cx));
12950 })
12951 .unwrap();
12952
12953 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12954 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12955 s.select_ranges(Some(39..40))
12956 });
12957 editor.open_excerpts(&OpenExcerpts, window, cx);
12958 });
12959 cx.executor().run_until_parked();
12960 let second_item_id = workspace
12961 .update(cx, |workspace, window, cx| {
12962 let active_item = workspace
12963 .active_item(cx)
12964 .expect("should have an active item after navigating into the 2nd buffer");
12965 let second_item_id = active_item.item_id();
12966 assert_ne!(
12967 second_item_id, multibuffer_item_id,
12968 "Should navigate away from the multibuffer"
12969 );
12970 assert_ne!(
12971 second_item_id, first_item_id,
12972 "Should navigate into the 2nd buffer and activate it"
12973 );
12974 assert!(
12975 active_item.is_singleton(cx),
12976 "New active item should be a singleton buffer"
12977 );
12978 assert_eq!(
12979 active_item
12980 .act_as::<Editor>(cx)
12981 .expect("should have navigated into an editor")
12982 .read(cx)
12983 .text(cx),
12984 sample_text_2
12985 );
12986
12987 workspace
12988 .go_back(workspace.active_pane().downgrade(), window, cx)
12989 .detach_and_log_err(cx);
12990
12991 second_item_id
12992 })
12993 .unwrap();
12994 cx.executor().run_until_parked();
12995 workspace
12996 .update(cx, |workspace, _, cx| {
12997 let active_item = workspace
12998 .active_item(cx)
12999 .expect("should have an active item after navigating back from the 2nd buffer");
13000 assert_eq!(
13001 active_item.item_id(),
13002 multibuffer_item_id,
13003 "Should navigate back from the 2nd buffer to the multi buffer"
13004 );
13005 assert!(!active_item.is_singleton(cx));
13006 })
13007 .unwrap();
13008
13009 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13010 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13011 s.select_ranges(Some(70..70))
13012 });
13013 editor.open_excerpts(&OpenExcerpts, window, cx);
13014 });
13015 cx.executor().run_until_parked();
13016 workspace
13017 .update(cx, |workspace, window, cx| {
13018 let active_item = workspace
13019 .active_item(cx)
13020 .expect("should have an active item after navigating into the 3rd buffer");
13021 let third_item_id = active_item.item_id();
13022 assert_ne!(
13023 third_item_id, multibuffer_item_id,
13024 "Should navigate into the 3rd buffer and activate it"
13025 );
13026 assert_ne!(third_item_id, first_item_id);
13027 assert_ne!(third_item_id, second_item_id);
13028 assert!(
13029 active_item.is_singleton(cx),
13030 "New active item should be a singleton buffer"
13031 );
13032 assert_eq!(
13033 active_item
13034 .act_as::<Editor>(cx)
13035 .expect("should have navigated into an editor")
13036 .read(cx)
13037 .text(cx),
13038 sample_text_3
13039 );
13040
13041 workspace
13042 .go_back(workspace.active_pane().downgrade(), window, cx)
13043 .detach_and_log_err(cx);
13044 })
13045 .unwrap();
13046 cx.executor().run_until_parked();
13047 workspace
13048 .update(cx, |workspace, _, cx| {
13049 let active_item = workspace
13050 .active_item(cx)
13051 .expect("should have an active item after navigating back from the 3rd buffer");
13052 assert_eq!(
13053 active_item.item_id(),
13054 multibuffer_item_id,
13055 "Should navigate back from the 3rd buffer to the multi buffer"
13056 );
13057 assert!(!active_item.is_singleton(cx));
13058 })
13059 .unwrap();
13060}
13061
13062#[gpui::test]
13063async fn test_toggle_selected_diff_hunks(
13064 executor: BackgroundExecutor,
13065 cx: &mut gpui::TestAppContext,
13066) {
13067 init_test(cx, |_| {});
13068
13069 let mut cx = EditorTestContext::new(cx).await;
13070
13071 let diff_base = r#"
13072 use some::mod;
13073
13074 const A: u32 = 42;
13075
13076 fn main() {
13077 println!("hello");
13078
13079 println!("world");
13080 }
13081 "#
13082 .unindent();
13083
13084 cx.set_state(
13085 &r#"
13086 use some::modified;
13087
13088 ˇ
13089 fn main() {
13090 println!("hello there");
13091
13092 println!("around the");
13093 println!("world");
13094 }
13095 "#
13096 .unindent(),
13097 );
13098
13099 cx.set_diff_base(&diff_base);
13100 executor.run_until_parked();
13101
13102 cx.update_editor(|editor, window, cx| {
13103 editor.go_to_next_hunk(&GoToHunk, window, cx);
13104 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13105 });
13106 executor.run_until_parked();
13107 cx.assert_state_with_diff(
13108 r#"
13109 use some::modified;
13110
13111
13112 fn main() {
13113 - println!("hello");
13114 + ˇ println!("hello there");
13115
13116 println!("around the");
13117 println!("world");
13118 }
13119 "#
13120 .unindent(),
13121 );
13122
13123 cx.update_editor(|editor, window, cx| {
13124 for _ in 0..2 {
13125 editor.go_to_next_hunk(&GoToHunk, window, cx);
13126 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13127 }
13128 });
13129 executor.run_until_parked();
13130 cx.assert_state_with_diff(
13131 r#"
13132 - use some::mod;
13133 + ˇuse some::modified;
13134
13135
13136 fn main() {
13137 - println!("hello");
13138 + println!("hello there");
13139
13140 + println!("around the");
13141 println!("world");
13142 }
13143 "#
13144 .unindent(),
13145 );
13146
13147 cx.update_editor(|editor, window, cx| {
13148 editor.go_to_next_hunk(&GoToHunk, window, cx);
13149 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13150 });
13151 executor.run_until_parked();
13152 cx.assert_state_with_diff(
13153 r#"
13154 - use some::mod;
13155 + use some::modified;
13156
13157 - const A: u32 = 42;
13158 ˇ
13159 fn main() {
13160 - println!("hello");
13161 + println!("hello there");
13162
13163 + println!("around the");
13164 println!("world");
13165 }
13166 "#
13167 .unindent(),
13168 );
13169
13170 cx.update_editor(|editor, window, cx| {
13171 editor.cancel(&Cancel, window, cx);
13172 });
13173
13174 cx.assert_state_with_diff(
13175 r#"
13176 use some::modified;
13177
13178 ˇ
13179 fn main() {
13180 println!("hello there");
13181
13182 println!("around the");
13183 println!("world");
13184 }
13185 "#
13186 .unindent(),
13187 );
13188}
13189
13190#[gpui::test]
13191async fn test_diff_base_change_with_expanded_diff_hunks(
13192 executor: BackgroundExecutor,
13193 cx: &mut gpui::TestAppContext,
13194) {
13195 init_test(cx, |_| {});
13196
13197 let mut cx = EditorTestContext::new(cx).await;
13198
13199 let diff_base = r#"
13200 use some::mod1;
13201 use some::mod2;
13202
13203 const A: u32 = 42;
13204 const B: u32 = 42;
13205 const C: u32 = 42;
13206
13207 fn main() {
13208 println!("hello");
13209
13210 println!("world");
13211 }
13212 "#
13213 .unindent();
13214
13215 cx.set_state(
13216 &r#"
13217 use some::mod2;
13218
13219 const A: u32 = 42;
13220 const C: u32 = 42;
13221
13222 fn main(ˇ) {
13223 //println!("hello");
13224
13225 println!("world");
13226 //
13227 //
13228 }
13229 "#
13230 .unindent(),
13231 );
13232
13233 cx.set_diff_base(&diff_base);
13234 executor.run_until_parked();
13235
13236 cx.update_editor(|editor, window, cx| {
13237 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13238 });
13239 executor.run_until_parked();
13240 cx.assert_state_with_diff(
13241 r#"
13242 - use some::mod1;
13243 use some::mod2;
13244
13245 const A: u32 = 42;
13246 - const B: u32 = 42;
13247 const C: u32 = 42;
13248
13249 fn main(ˇ) {
13250 - println!("hello");
13251 + //println!("hello");
13252
13253 println!("world");
13254 + //
13255 + //
13256 }
13257 "#
13258 .unindent(),
13259 );
13260
13261 cx.set_diff_base("new diff base!");
13262 executor.run_until_parked();
13263 cx.assert_state_with_diff(
13264 r#"
13265 - new diff base!
13266 + use some::mod2;
13267 +
13268 + const A: u32 = 42;
13269 + const C: u32 = 42;
13270 +
13271 + fn main(ˇ) {
13272 + //println!("hello");
13273 +
13274 + println!("world");
13275 + //
13276 + //
13277 + }
13278 "#
13279 .unindent(),
13280 );
13281}
13282
13283#[gpui::test]
13284async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13285 init_test(cx, |_| {});
13286
13287 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13288 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13289 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13290 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13291 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13292 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13293
13294 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13295 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13296 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13297
13298 let multi_buffer = cx.new(|cx| {
13299 let mut multibuffer = MultiBuffer::new(ReadWrite);
13300 multibuffer.push_excerpts(
13301 buffer_1.clone(),
13302 [
13303 ExcerptRange {
13304 context: Point::new(0, 0)..Point::new(3, 0),
13305 primary: None,
13306 },
13307 ExcerptRange {
13308 context: Point::new(5, 0)..Point::new(7, 0),
13309 primary: None,
13310 },
13311 ExcerptRange {
13312 context: Point::new(9, 0)..Point::new(10, 3),
13313 primary: None,
13314 },
13315 ],
13316 cx,
13317 );
13318 multibuffer.push_excerpts(
13319 buffer_2.clone(),
13320 [
13321 ExcerptRange {
13322 context: Point::new(0, 0)..Point::new(3, 0),
13323 primary: None,
13324 },
13325 ExcerptRange {
13326 context: Point::new(5, 0)..Point::new(7, 0),
13327 primary: None,
13328 },
13329 ExcerptRange {
13330 context: Point::new(9, 0)..Point::new(10, 3),
13331 primary: None,
13332 },
13333 ],
13334 cx,
13335 );
13336 multibuffer.push_excerpts(
13337 buffer_3.clone(),
13338 [
13339 ExcerptRange {
13340 context: Point::new(0, 0)..Point::new(3, 0),
13341 primary: None,
13342 },
13343 ExcerptRange {
13344 context: Point::new(5, 0)..Point::new(7, 0),
13345 primary: None,
13346 },
13347 ExcerptRange {
13348 context: Point::new(9, 0)..Point::new(10, 3),
13349 primary: None,
13350 },
13351 ],
13352 cx,
13353 );
13354 multibuffer
13355 });
13356
13357 let editor = cx.add_window(|window, cx| {
13358 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13359 });
13360 editor
13361 .update(cx, |editor, _window, cx| {
13362 for (buffer, diff_base) in [
13363 (buffer_1.clone(), file_1_old),
13364 (buffer_2.clone(), file_2_old),
13365 (buffer_3.clone(), file_3_old),
13366 ] {
13367 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13368 editor
13369 .buffer
13370 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13371 }
13372 })
13373 .unwrap();
13374
13375 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13376 cx.run_until_parked();
13377
13378 cx.assert_editor_state(
13379 &"
13380 ˇaaa
13381 ccc
13382 ddd
13383
13384 ggg
13385 hhh
13386
13387
13388 lll
13389 mmm
13390 NNN
13391
13392 qqq
13393 rrr
13394
13395 uuu
13396 111
13397 222
13398 333
13399
13400 666
13401 777
13402
13403 000
13404 !!!"
13405 .unindent(),
13406 );
13407
13408 cx.update_editor(|editor, window, cx| {
13409 editor.select_all(&SelectAll, window, cx);
13410 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13411 });
13412 cx.executor().run_until_parked();
13413
13414 cx.assert_state_with_diff(
13415 "
13416 «aaa
13417 - bbb
13418 ccc
13419 ddd
13420
13421 ggg
13422 hhh
13423
13424
13425 lll
13426 mmm
13427 - nnn
13428 + NNN
13429
13430 qqq
13431 rrr
13432
13433 uuu
13434 111
13435 222
13436 333
13437
13438 + 666
13439 777
13440
13441 000
13442 !!!ˇ»"
13443 .unindent(),
13444 );
13445}
13446
13447#[gpui::test]
13448async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13449 init_test(cx, |_| {});
13450
13451 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13452 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13453
13454 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13455 let multi_buffer = cx.new(|cx| {
13456 let mut multibuffer = MultiBuffer::new(ReadWrite);
13457 multibuffer.push_excerpts(
13458 buffer.clone(),
13459 [
13460 ExcerptRange {
13461 context: Point::new(0, 0)..Point::new(2, 0),
13462 primary: None,
13463 },
13464 ExcerptRange {
13465 context: Point::new(4, 0)..Point::new(7, 0),
13466 primary: None,
13467 },
13468 ExcerptRange {
13469 context: Point::new(9, 0)..Point::new(10, 0),
13470 primary: None,
13471 },
13472 ],
13473 cx,
13474 );
13475 multibuffer
13476 });
13477
13478 let editor = cx.add_window(|window, cx| {
13479 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13480 });
13481 editor
13482 .update(cx, |editor, _window, cx| {
13483 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13484 editor
13485 .buffer
13486 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13487 })
13488 .unwrap();
13489
13490 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13491 cx.run_until_parked();
13492
13493 cx.update_editor(|editor, window, cx| {
13494 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13495 });
13496 cx.executor().run_until_parked();
13497
13498 // When the start of a hunk coincides with the start of its excerpt,
13499 // the hunk is expanded. When the start of a a hunk is earlier than
13500 // the start of its excerpt, the hunk is not expanded.
13501 cx.assert_state_with_diff(
13502 "
13503 ˇaaa
13504 - bbb
13505 + BBB
13506
13507 - ddd
13508 - eee
13509 + DDD
13510 + EEE
13511 fff
13512
13513 iii
13514 "
13515 .unindent(),
13516 );
13517}
13518
13519#[gpui::test]
13520async fn test_edits_around_expanded_insertion_hunks(
13521 executor: BackgroundExecutor,
13522 cx: &mut gpui::TestAppContext,
13523) {
13524 init_test(cx, |_| {});
13525
13526 let mut cx = EditorTestContext::new(cx).await;
13527
13528 let diff_base = r#"
13529 use some::mod1;
13530 use some::mod2;
13531
13532 const A: u32 = 42;
13533
13534 fn main() {
13535 println!("hello");
13536
13537 println!("world");
13538 }
13539 "#
13540 .unindent();
13541 executor.run_until_parked();
13542 cx.set_state(
13543 &r#"
13544 use some::mod1;
13545 use some::mod2;
13546
13547 const A: u32 = 42;
13548 const B: u32 = 42;
13549 const C: u32 = 42;
13550 ˇ
13551
13552 fn main() {
13553 println!("hello");
13554
13555 println!("world");
13556 }
13557 "#
13558 .unindent(),
13559 );
13560
13561 cx.set_diff_base(&diff_base);
13562 executor.run_until_parked();
13563
13564 cx.update_editor(|editor, window, cx| {
13565 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13566 });
13567 executor.run_until_parked();
13568
13569 cx.assert_state_with_diff(
13570 r#"
13571 use some::mod1;
13572 use some::mod2;
13573
13574 const A: u32 = 42;
13575 + const B: u32 = 42;
13576 + const C: u32 = 42;
13577 + ˇ
13578
13579 fn main() {
13580 println!("hello");
13581
13582 println!("world");
13583 }
13584 "#
13585 .unindent(),
13586 );
13587
13588 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13589 executor.run_until_parked();
13590
13591 cx.assert_state_with_diff(
13592 r#"
13593 use some::mod1;
13594 use some::mod2;
13595
13596 const A: u32 = 42;
13597 + const B: u32 = 42;
13598 + const C: u32 = 42;
13599 + const D: u32 = 42;
13600 + ˇ
13601
13602 fn main() {
13603 println!("hello");
13604
13605 println!("world");
13606 }
13607 "#
13608 .unindent(),
13609 );
13610
13611 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13612 executor.run_until_parked();
13613
13614 cx.assert_state_with_diff(
13615 r#"
13616 use some::mod1;
13617 use some::mod2;
13618
13619 const A: u32 = 42;
13620 + const B: u32 = 42;
13621 + const C: u32 = 42;
13622 + const D: u32 = 42;
13623 + const E: u32 = 42;
13624 + ˇ
13625
13626 fn main() {
13627 println!("hello");
13628
13629 println!("world");
13630 }
13631 "#
13632 .unindent(),
13633 );
13634
13635 cx.update_editor(|editor, window, cx| {
13636 editor.delete_line(&DeleteLine, window, cx);
13637 });
13638 executor.run_until_parked();
13639
13640 cx.assert_state_with_diff(
13641 r#"
13642 use some::mod1;
13643 use some::mod2;
13644
13645 const A: u32 = 42;
13646 + const B: u32 = 42;
13647 + const C: u32 = 42;
13648 + const D: u32 = 42;
13649 + const E: u32 = 42;
13650 ˇ
13651 fn main() {
13652 println!("hello");
13653
13654 println!("world");
13655 }
13656 "#
13657 .unindent(),
13658 );
13659
13660 cx.update_editor(|editor, window, cx| {
13661 editor.move_up(&MoveUp, window, cx);
13662 editor.delete_line(&DeleteLine, window, cx);
13663 editor.move_up(&MoveUp, window, cx);
13664 editor.delete_line(&DeleteLine, window, cx);
13665 editor.move_up(&MoveUp, window, cx);
13666 editor.delete_line(&DeleteLine, window, cx);
13667 });
13668 executor.run_until_parked();
13669 cx.assert_state_with_diff(
13670 r#"
13671 use some::mod1;
13672 use some::mod2;
13673
13674 const A: u32 = 42;
13675 + const B: u32 = 42;
13676 ˇ
13677 fn main() {
13678 println!("hello");
13679
13680 println!("world");
13681 }
13682 "#
13683 .unindent(),
13684 );
13685
13686 cx.update_editor(|editor, window, cx| {
13687 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13688 editor.delete_line(&DeleteLine, window, cx);
13689 });
13690 executor.run_until_parked();
13691 cx.assert_state_with_diff(
13692 r#"
13693 ˇ
13694 fn main() {
13695 println!("hello");
13696
13697 println!("world");
13698 }
13699 "#
13700 .unindent(),
13701 );
13702}
13703
13704#[gpui::test]
13705async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13706 init_test(cx, |_| {});
13707
13708 let mut cx = EditorTestContext::new(cx).await;
13709 cx.set_diff_base(indoc! { "
13710 one
13711 two
13712 three
13713 four
13714 five
13715 "
13716 });
13717 cx.set_state(indoc! { "
13718 one
13719 ˇthree
13720 five
13721 "});
13722 cx.run_until_parked();
13723 cx.update_editor(|editor, window, cx| {
13724 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13725 });
13726 cx.assert_state_with_diff(
13727 indoc! { "
13728 one
13729 - two
13730 ˇthree
13731 - four
13732 five
13733 "}
13734 .to_string(),
13735 );
13736 cx.update_editor(|editor, window, cx| {
13737 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13738 });
13739
13740 cx.assert_state_with_diff(
13741 indoc! { "
13742 one
13743 ˇthree
13744 five
13745 "}
13746 .to_string(),
13747 );
13748
13749 cx.set_state(indoc! { "
13750 one
13751 ˇTWO
13752 three
13753 four
13754 five
13755 "});
13756 cx.run_until_parked();
13757 cx.update_editor(|editor, window, cx| {
13758 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13759 });
13760
13761 cx.assert_state_with_diff(
13762 indoc! { "
13763 one
13764 - two
13765 + ˇTWO
13766 three
13767 four
13768 five
13769 "}
13770 .to_string(),
13771 );
13772 cx.update_editor(|editor, window, cx| {
13773 editor.move_up(&Default::default(), window, cx);
13774 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13775 });
13776 cx.assert_state_with_diff(
13777 indoc! { "
13778 one
13779 ˇTWO
13780 three
13781 four
13782 five
13783 "}
13784 .to_string(),
13785 );
13786}
13787
13788#[gpui::test]
13789async fn test_edits_around_expanded_deletion_hunks(
13790 executor: BackgroundExecutor,
13791 cx: &mut gpui::TestAppContext,
13792) {
13793 init_test(cx, |_| {});
13794
13795 let mut cx = EditorTestContext::new(cx).await;
13796
13797 let diff_base = r#"
13798 use some::mod1;
13799 use some::mod2;
13800
13801 const A: u32 = 42;
13802 const B: u32 = 42;
13803 const C: u32 = 42;
13804
13805
13806 fn main() {
13807 println!("hello");
13808
13809 println!("world");
13810 }
13811 "#
13812 .unindent();
13813 executor.run_until_parked();
13814 cx.set_state(
13815 &r#"
13816 use some::mod1;
13817 use some::mod2;
13818
13819 ˇconst B: u32 = 42;
13820 const C: u32 = 42;
13821
13822
13823 fn main() {
13824 println!("hello");
13825
13826 println!("world");
13827 }
13828 "#
13829 .unindent(),
13830 );
13831
13832 cx.set_diff_base(&diff_base);
13833 executor.run_until_parked();
13834
13835 cx.update_editor(|editor, window, cx| {
13836 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13837 });
13838 executor.run_until_parked();
13839
13840 cx.assert_state_with_diff(
13841 r#"
13842 use some::mod1;
13843 use some::mod2;
13844
13845 - const A: u32 = 42;
13846 ˇconst B: u32 = 42;
13847 const C: u32 = 42;
13848
13849
13850 fn main() {
13851 println!("hello");
13852
13853 println!("world");
13854 }
13855 "#
13856 .unindent(),
13857 );
13858
13859 cx.update_editor(|editor, window, cx| {
13860 editor.delete_line(&DeleteLine, window, cx);
13861 });
13862 executor.run_until_parked();
13863 cx.assert_state_with_diff(
13864 r#"
13865 use some::mod1;
13866 use some::mod2;
13867
13868 - const A: u32 = 42;
13869 - const B: u32 = 42;
13870 ˇconst C: u32 = 42;
13871
13872
13873 fn main() {
13874 println!("hello");
13875
13876 println!("world");
13877 }
13878 "#
13879 .unindent(),
13880 );
13881
13882 cx.update_editor(|editor, window, cx| {
13883 editor.delete_line(&DeleteLine, window, cx);
13884 });
13885 executor.run_until_parked();
13886 cx.assert_state_with_diff(
13887 r#"
13888 use some::mod1;
13889 use some::mod2;
13890
13891 - const A: u32 = 42;
13892 - const B: u32 = 42;
13893 - const C: u32 = 42;
13894 ˇ
13895
13896 fn main() {
13897 println!("hello");
13898
13899 println!("world");
13900 }
13901 "#
13902 .unindent(),
13903 );
13904
13905 cx.update_editor(|editor, window, cx| {
13906 editor.handle_input("replacement", window, cx);
13907 });
13908 executor.run_until_parked();
13909 cx.assert_state_with_diff(
13910 r#"
13911 use some::mod1;
13912 use some::mod2;
13913
13914 - const A: u32 = 42;
13915 - const B: u32 = 42;
13916 - const C: u32 = 42;
13917 -
13918 + replacementˇ
13919
13920 fn main() {
13921 println!("hello");
13922
13923 println!("world");
13924 }
13925 "#
13926 .unindent(),
13927 );
13928}
13929
13930#[gpui::test]
13931async fn test_backspace_after_deletion_hunk(
13932 executor: BackgroundExecutor,
13933 cx: &mut gpui::TestAppContext,
13934) {
13935 init_test(cx, |_| {});
13936
13937 let mut cx = EditorTestContext::new(cx).await;
13938
13939 let base_text = r#"
13940 one
13941 two
13942 three
13943 four
13944 five
13945 "#
13946 .unindent();
13947 executor.run_until_parked();
13948 cx.set_state(
13949 &r#"
13950 one
13951 two
13952 fˇour
13953 five
13954 "#
13955 .unindent(),
13956 );
13957
13958 cx.set_diff_base(&base_text);
13959 executor.run_until_parked();
13960
13961 cx.update_editor(|editor, window, cx| {
13962 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13963 });
13964 executor.run_until_parked();
13965
13966 cx.assert_state_with_diff(
13967 r#"
13968 one
13969 two
13970 - three
13971 fˇour
13972 five
13973 "#
13974 .unindent(),
13975 );
13976
13977 cx.update_editor(|editor, window, cx| {
13978 editor.backspace(&Backspace, window, cx);
13979 editor.backspace(&Backspace, window, cx);
13980 });
13981 executor.run_until_parked();
13982 cx.assert_state_with_diff(
13983 r#"
13984 one
13985 two
13986 - threeˇ
13987 - four
13988 + our
13989 five
13990 "#
13991 .unindent(),
13992 );
13993}
13994
13995#[gpui::test]
13996async fn test_edit_after_expanded_modification_hunk(
13997 executor: BackgroundExecutor,
13998 cx: &mut gpui::TestAppContext,
13999) {
14000 init_test(cx, |_| {});
14001
14002 let mut cx = EditorTestContext::new(cx).await;
14003
14004 let diff_base = r#"
14005 use some::mod1;
14006 use some::mod2;
14007
14008 const A: u32 = 42;
14009 const B: u32 = 42;
14010 const C: u32 = 42;
14011 const D: u32 = 42;
14012
14013
14014 fn main() {
14015 println!("hello");
14016
14017 println!("world");
14018 }"#
14019 .unindent();
14020
14021 cx.set_state(
14022 &r#"
14023 use some::mod1;
14024 use some::mod2;
14025
14026 const A: u32 = 42;
14027 const B: u32 = 42;
14028 const C: u32 = 43ˇ
14029 const D: u32 = 42;
14030
14031
14032 fn main() {
14033 println!("hello");
14034
14035 println!("world");
14036 }"#
14037 .unindent(),
14038 );
14039
14040 cx.set_diff_base(&diff_base);
14041 executor.run_until_parked();
14042 cx.update_editor(|editor, window, cx| {
14043 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
14044 });
14045 executor.run_until_parked();
14046
14047 cx.assert_state_with_diff(
14048 r#"
14049 use some::mod1;
14050 use some::mod2;
14051
14052 const A: u32 = 42;
14053 const B: u32 = 42;
14054 - const C: u32 = 42;
14055 + const C: u32 = 43ˇ
14056 const D: u32 = 42;
14057
14058
14059 fn main() {
14060 println!("hello");
14061
14062 println!("world");
14063 }"#
14064 .unindent(),
14065 );
14066
14067 cx.update_editor(|editor, window, cx| {
14068 editor.handle_input("\nnew_line\n", window, cx);
14069 });
14070 executor.run_until_parked();
14071
14072 cx.assert_state_with_diff(
14073 r#"
14074 use some::mod1;
14075 use some::mod2;
14076
14077 const A: u32 = 42;
14078 const B: u32 = 42;
14079 - const C: u32 = 42;
14080 + const C: u32 = 43
14081 + new_line
14082 + ˇ
14083 const D: u32 = 42;
14084
14085
14086 fn main() {
14087 println!("hello");
14088
14089 println!("world");
14090 }"#
14091 .unindent(),
14092 );
14093}
14094
14095#[gpui::test]
14096async fn test_stage_and_unstage_added_file_hunk(
14097 executor: BackgroundExecutor,
14098 cx: &mut gpui::TestAppContext,
14099) {
14100 init_test(cx, |_| {});
14101
14102 let mut cx = EditorTestContext::new(cx).await;
14103 cx.update_editor(|editor, _, cx| {
14104 editor.set_expand_all_diff_hunks(cx);
14105 });
14106
14107 let working_copy = r#"
14108 ˇfn main() {
14109 println!("hello, world!");
14110 }
14111 "#
14112 .unindent();
14113
14114 cx.set_state(&working_copy);
14115 executor.run_until_parked();
14116
14117 cx.assert_state_with_diff(
14118 r#"
14119 + ˇfn main() {
14120 + println!("hello, world!");
14121 + }
14122 "#
14123 .unindent(),
14124 );
14125 cx.assert_index_text(None);
14126
14127 cx.update_editor(|editor, window, cx| {
14128 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14129 });
14130 executor.run_until_parked();
14131 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14132 cx.assert_state_with_diff(
14133 r#"
14134 + ˇfn main() {
14135 + println!("hello, world!");
14136 + }
14137 "#
14138 .unindent(),
14139 );
14140
14141 cx.update_editor(|editor, window, cx| {
14142 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14143 });
14144 executor.run_until_parked();
14145 cx.assert_index_text(None);
14146}
14147
14148async fn setup_indent_guides_editor(
14149 text: &str,
14150 cx: &mut gpui::TestAppContext,
14151) -> (BufferId, EditorTestContext) {
14152 init_test(cx, |_| {});
14153
14154 let mut cx = EditorTestContext::new(cx).await;
14155
14156 let buffer_id = cx.update_editor(|editor, window, cx| {
14157 editor.set_text(text, window, cx);
14158 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14159
14160 buffer_ids[0]
14161 });
14162
14163 (buffer_id, cx)
14164}
14165
14166fn assert_indent_guides(
14167 range: Range<u32>,
14168 expected: Vec<IndentGuide>,
14169 active_indices: Option<Vec<usize>>,
14170 cx: &mut EditorTestContext,
14171) {
14172 let indent_guides = cx.update_editor(|editor, window, cx| {
14173 let snapshot = editor.snapshot(window, cx).display_snapshot;
14174 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14175 editor,
14176 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14177 true,
14178 &snapshot,
14179 cx,
14180 );
14181
14182 indent_guides.sort_by(|a, b| {
14183 a.depth.cmp(&b.depth).then(
14184 a.start_row
14185 .cmp(&b.start_row)
14186 .then(a.end_row.cmp(&b.end_row)),
14187 )
14188 });
14189 indent_guides
14190 });
14191
14192 if let Some(expected) = active_indices {
14193 let active_indices = cx.update_editor(|editor, window, cx| {
14194 let snapshot = editor.snapshot(window, cx).display_snapshot;
14195 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14196 });
14197
14198 assert_eq!(
14199 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14200 expected,
14201 "Active indent guide indices do not match"
14202 );
14203 }
14204
14205 assert_eq!(indent_guides, expected, "Indent guides do not match");
14206}
14207
14208fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14209 IndentGuide {
14210 buffer_id,
14211 start_row: MultiBufferRow(start_row),
14212 end_row: MultiBufferRow(end_row),
14213 depth,
14214 tab_size: 4,
14215 settings: IndentGuideSettings {
14216 enabled: true,
14217 line_width: 1,
14218 active_line_width: 1,
14219 ..Default::default()
14220 },
14221 }
14222}
14223
14224#[gpui::test]
14225async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14226 let (buffer_id, mut cx) = setup_indent_guides_editor(
14227 &"
14228 fn main() {
14229 let a = 1;
14230 }"
14231 .unindent(),
14232 cx,
14233 )
14234 .await;
14235
14236 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14237}
14238
14239#[gpui::test]
14240async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
14241 let (buffer_id, mut cx) = setup_indent_guides_editor(
14242 &"
14243 fn main() {
14244 let a = 1;
14245 let b = 2;
14246 }"
14247 .unindent(),
14248 cx,
14249 )
14250 .await;
14251
14252 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14253}
14254
14255#[gpui::test]
14256async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
14257 let (buffer_id, mut cx) = setup_indent_guides_editor(
14258 &"
14259 fn main() {
14260 let a = 1;
14261 if a == 3 {
14262 let b = 2;
14263 } else {
14264 let c = 3;
14265 }
14266 }"
14267 .unindent(),
14268 cx,
14269 )
14270 .await;
14271
14272 assert_indent_guides(
14273 0..8,
14274 vec![
14275 indent_guide(buffer_id, 1, 6, 0),
14276 indent_guide(buffer_id, 3, 3, 1),
14277 indent_guide(buffer_id, 5, 5, 1),
14278 ],
14279 None,
14280 &mut cx,
14281 );
14282}
14283
14284#[gpui::test]
14285async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14286 let (buffer_id, mut cx) = setup_indent_guides_editor(
14287 &"
14288 fn main() {
14289 let a = 1;
14290 let b = 2;
14291 let c = 3;
14292 }"
14293 .unindent(),
14294 cx,
14295 )
14296 .await;
14297
14298 assert_indent_guides(
14299 0..5,
14300 vec![
14301 indent_guide(buffer_id, 1, 3, 0),
14302 indent_guide(buffer_id, 2, 2, 1),
14303 ],
14304 None,
14305 &mut cx,
14306 );
14307}
14308
14309#[gpui::test]
14310async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14311 let (buffer_id, mut cx) = setup_indent_guides_editor(
14312 &"
14313 fn main() {
14314 let a = 1;
14315
14316 let c = 3;
14317 }"
14318 .unindent(),
14319 cx,
14320 )
14321 .await;
14322
14323 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14324}
14325
14326#[gpui::test]
14327async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14328 let (buffer_id, mut cx) = setup_indent_guides_editor(
14329 &"
14330 fn main() {
14331 let a = 1;
14332
14333 let c = 3;
14334
14335 if a == 3 {
14336 let b = 2;
14337 } else {
14338 let c = 3;
14339 }
14340 }"
14341 .unindent(),
14342 cx,
14343 )
14344 .await;
14345
14346 assert_indent_guides(
14347 0..11,
14348 vec![
14349 indent_guide(buffer_id, 1, 9, 0),
14350 indent_guide(buffer_id, 6, 6, 1),
14351 indent_guide(buffer_id, 8, 8, 1),
14352 ],
14353 None,
14354 &mut cx,
14355 );
14356}
14357
14358#[gpui::test]
14359async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14360 let (buffer_id, mut cx) = setup_indent_guides_editor(
14361 &"
14362 fn main() {
14363 let a = 1;
14364
14365 let c = 3;
14366
14367 if a == 3 {
14368 let b = 2;
14369 } else {
14370 let c = 3;
14371 }
14372 }"
14373 .unindent(),
14374 cx,
14375 )
14376 .await;
14377
14378 assert_indent_guides(
14379 1..11,
14380 vec![
14381 indent_guide(buffer_id, 1, 9, 0),
14382 indent_guide(buffer_id, 6, 6, 1),
14383 indent_guide(buffer_id, 8, 8, 1),
14384 ],
14385 None,
14386 &mut cx,
14387 );
14388}
14389
14390#[gpui::test]
14391async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14392 let (buffer_id, mut cx) = setup_indent_guides_editor(
14393 &"
14394 fn main() {
14395 let a = 1;
14396
14397 let c = 3;
14398
14399 if a == 3 {
14400 let b = 2;
14401 } else {
14402 let c = 3;
14403 }
14404 }"
14405 .unindent(),
14406 cx,
14407 )
14408 .await;
14409
14410 assert_indent_guides(
14411 1..10,
14412 vec![
14413 indent_guide(buffer_id, 1, 9, 0),
14414 indent_guide(buffer_id, 6, 6, 1),
14415 indent_guide(buffer_id, 8, 8, 1),
14416 ],
14417 None,
14418 &mut cx,
14419 );
14420}
14421
14422#[gpui::test]
14423async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14424 let (buffer_id, mut cx) = setup_indent_guides_editor(
14425 &"
14426 block1
14427 block2
14428 block3
14429 block4
14430 block2
14431 block1
14432 block1"
14433 .unindent(),
14434 cx,
14435 )
14436 .await;
14437
14438 assert_indent_guides(
14439 1..10,
14440 vec![
14441 indent_guide(buffer_id, 1, 4, 0),
14442 indent_guide(buffer_id, 2, 3, 1),
14443 indent_guide(buffer_id, 3, 3, 2),
14444 ],
14445 None,
14446 &mut cx,
14447 );
14448}
14449
14450#[gpui::test]
14451async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14452 let (buffer_id, mut cx) = setup_indent_guides_editor(
14453 &"
14454 block1
14455 block2
14456 block3
14457
14458 block1
14459 block1"
14460 .unindent(),
14461 cx,
14462 )
14463 .await;
14464
14465 assert_indent_guides(
14466 0..6,
14467 vec![
14468 indent_guide(buffer_id, 1, 2, 0),
14469 indent_guide(buffer_id, 2, 2, 1),
14470 ],
14471 None,
14472 &mut cx,
14473 );
14474}
14475
14476#[gpui::test]
14477async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14478 let (buffer_id, mut cx) = setup_indent_guides_editor(
14479 &"
14480 block1
14481
14482
14483
14484 block2
14485 "
14486 .unindent(),
14487 cx,
14488 )
14489 .await;
14490
14491 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14492}
14493
14494#[gpui::test]
14495async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14496 let (buffer_id, mut cx) = setup_indent_guides_editor(
14497 &"
14498 def a:
14499 \tb = 3
14500 \tif True:
14501 \t\tc = 4
14502 \t\td = 5
14503 \tprint(b)
14504 "
14505 .unindent(),
14506 cx,
14507 )
14508 .await;
14509
14510 assert_indent_guides(
14511 0..6,
14512 vec![
14513 indent_guide(buffer_id, 1, 6, 0),
14514 indent_guide(buffer_id, 3, 4, 1),
14515 ],
14516 None,
14517 &mut cx,
14518 );
14519}
14520
14521#[gpui::test]
14522async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14523 let (buffer_id, mut cx) = setup_indent_guides_editor(
14524 &"
14525 fn main() {
14526 let a = 1;
14527 }"
14528 .unindent(),
14529 cx,
14530 )
14531 .await;
14532
14533 cx.update_editor(|editor, window, cx| {
14534 editor.change_selections(None, window, cx, |s| {
14535 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14536 });
14537 });
14538
14539 assert_indent_guides(
14540 0..3,
14541 vec![indent_guide(buffer_id, 1, 1, 0)],
14542 Some(vec![0]),
14543 &mut cx,
14544 );
14545}
14546
14547#[gpui::test]
14548async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14549 let (buffer_id, mut cx) = setup_indent_guides_editor(
14550 &"
14551 fn main() {
14552 if 1 == 2 {
14553 let a = 1;
14554 }
14555 }"
14556 .unindent(),
14557 cx,
14558 )
14559 .await;
14560
14561 cx.update_editor(|editor, window, cx| {
14562 editor.change_selections(None, window, cx, |s| {
14563 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14564 });
14565 });
14566
14567 assert_indent_guides(
14568 0..4,
14569 vec![
14570 indent_guide(buffer_id, 1, 3, 0),
14571 indent_guide(buffer_id, 2, 2, 1),
14572 ],
14573 Some(vec![1]),
14574 &mut cx,
14575 );
14576
14577 cx.update_editor(|editor, window, cx| {
14578 editor.change_selections(None, window, cx, |s| {
14579 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14580 });
14581 });
14582
14583 assert_indent_guides(
14584 0..4,
14585 vec![
14586 indent_guide(buffer_id, 1, 3, 0),
14587 indent_guide(buffer_id, 2, 2, 1),
14588 ],
14589 Some(vec![1]),
14590 &mut cx,
14591 );
14592
14593 cx.update_editor(|editor, window, cx| {
14594 editor.change_selections(None, window, cx, |s| {
14595 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14596 });
14597 });
14598
14599 assert_indent_guides(
14600 0..4,
14601 vec![
14602 indent_guide(buffer_id, 1, 3, 0),
14603 indent_guide(buffer_id, 2, 2, 1),
14604 ],
14605 Some(vec![0]),
14606 &mut cx,
14607 );
14608}
14609
14610#[gpui::test]
14611async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14612 let (buffer_id, mut cx) = setup_indent_guides_editor(
14613 &"
14614 fn main() {
14615 let a = 1;
14616
14617 let b = 2;
14618 }"
14619 .unindent(),
14620 cx,
14621 )
14622 .await;
14623
14624 cx.update_editor(|editor, window, cx| {
14625 editor.change_selections(None, window, cx, |s| {
14626 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14627 });
14628 });
14629
14630 assert_indent_guides(
14631 0..5,
14632 vec![indent_guide(buffer_id, 1, 3, 0)],
14633 Some(vec![0]),
14634 &mut cx,
14635 );
14636}
14637
14638#[gpui::test]
14639async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14640 let (buffer_id, mut cx) = setup_indent_guides_editor(
14641 &"
14642 def m:
14643 a = 1
14644 pass"
14645 .unindent(),
14646 cx,
14647 )
14648 .await;
14649
14650 cx.update_editor(|editor, window, cx| {
14651 editor.change_selections(None, window, cx, |s| {
14652 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14653 });
14654 });
14655
14656 assert_indent_guides(
14657 0..3,
14658 vec![indent_guide(buffer_id, 1, 2, 0)],
14659 Some(vec![0]),
14660 &mut cx,
14661 );
14662}
14663
14664#[gpui::test]
14665async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14666 init_test(cx, |_| {});
14667 let mut cx = EditorTestContext::new(cx).await;
14668 let text = indoc! {
14669 "
14670 impl A {
14671 fn b() {
14672 0;
14673 3;
14674 5;
14675 6;
14676 7;
14677 }
14678 }
14679 "
14680 };
14681 let base_text = indoc! {
14682 "
14683 impl A {
14684 fn b() {
14685 0;
14686 1;
14687 2;
14688 3;
14689 4;
14690 }
14691 fn c() {
14692 5;
14693 6;
14694 7;
14695 }
14696 }
14697 "
14698 };
14699
14700 cx.update_editor(|editor, window, cx| {
14701 editor.set_text(text, window, cx);
14702
14703 editor.buffer().update(cx, |multibuffer, cx| {
14704 let buffer = multibuffer.as_singleton().unwrap();
14705 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14706
14707 multibuffer.set_all_diff_hunks_expanded(cx);
14708 multibuffer.add_diff(diff, cx);
14709
14710 buffer.read(cx).remote_id()
14711 })
14712 });
14713 cx.run_until_parked();
14714
14715 cx.assert_state_with_diff(
14716 indoc! { "
14717 impl A {
14718 fn b() {
14719 0;
14720 - 1;
14721 - 2;
14722 3;
14723 - 4;
14724 - }
14725 - fn c() {
14726 5;
14727 6;
14728 7;
14729 }
14730 }
14731 ˇ"
14732 }
14733 .to_string(),
14734 );
14735
14736 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14737 editor
14738 .snapshot(window, cx)
14739 .buffer_snapshot
14740 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14741 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14742 .collect::<Vec<_>>()
14743 });
14744 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14745 assert_eq!(
14746 actual_guides,
14747 vec![
14748 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14749 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14750 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14751 ]
14752 );
14753}
14754
14755#[gpui::test]
14756fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14757 init_test(cx, |_| {});
14758
14759 let editor = cx.add_window(|window, cx| {
14760 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14761 build_editor(buffer, window, cx)
14762 });
14763
14764 let render_args = Arc::new(Mutex::new(None));
14765 let snapshot = editor
14766 .update(cx, |editor, window, cx| {
14767 let snapshot = editor.buffer().read(cx).snapshot(cx);
14768 let range =
14769 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14770
14771 struct RenderArgs {
14772 row: MultiBufferRow,
14773 folded: bool,
14774 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14775 }
14776
14777 let crease = Crease::inline(
14778 range,
14779 FoldPlaceholder::test(),
14780 {
14781 let toggle_callback = render_args.clone();
14782 move |row, folded, callback, _window, _cx| {
14783 *toggle_callback.lock() = Some(RenderArgs {
14784 row,
14785 folded,
14786 callback,
14787 });
14788 div()
14789 }
14790 },
14791 |_row, _folded, _window, _cx| div(),
14792 );
14793
14794 editor.insert_creases(Some(crease), cx);
14795 let snapshot = editor.snapshot(window, cx);
14796 let _div = snapshot.render_crease_toggle(
14797 MultiBufferRow(1),
14798 false,
14799 cx.entity().clone(),
14800 window,
14801 cx,
14802 );
14803 snapshot
14804 })
14805 .unwrap();
14806
14807 let render_args = render_args.lock().take().unwrap();
14808 assert_eq!(render_args.row, MultiBufferRow(1));
14809 assert!(!render_args.folded);
14810 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14811
14812 cx.update_window(*editor, |_, window, cx| {
14813 (render_args.callback)(true, window, cx)
14814 })
14815 .unwrap();
14816 let snapshot = editor
14817 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14818 .unwrap();
14819 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14820
14821 cx.update_window(*editor, |_, window, cx| {
14822 (render_args.callback)(false, window, cx)
14823 })
14824 .unwrap();
14825 let snapshot = editor
14826 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14827 .unwrap();
14828 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14829}
14830
14831#[gpui::test]
14832async fn test_input_text(cx: &mut gpui::TestAppContext) {
14833 init_test(cx, |_| {});
14834 let mut cx = EditorTestContext::new(cx).await;
14835
14836 cx.set_state(
14837 &r#"ˇone
14838 two
14839
14840 three
14841 fourˇ
14842 five
14843
14844 siˇx"#
14845 .unindent(),
14846 );
14847
14848 cx.dispatch_action(HandleInput(String::new()));
14849 cx.assert_editor_state(
14850 &r#"ˇone
14851 two
14852
14853 three
14854 fourˇ
14855 five
14856
14857 siˇx"#
14858 .unindent(),
14859 );
14860
14861 cx.dispatch_action(HandleInput("AAAA".to_string()));
14862 cx.assert_editor_state(
14863 &r#"AAAAˇone
14864 two
14865
14866 three
14867 fourAAAAˇ
14868 five
14869
14870 siAAAAˇx"#
14871 .unindent(),
14872 );
14873}
14874
14875#[gpui::test]
14876async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14877 init_test(cx, |_| {});
14878
14879 let mut cx = EditorTestContext::new(cx).await;
14880 cx.set_state(
14881 r#"let foo = 1;
14882let foo = 2;
14883let foo = 3;
14884let fooˇ = 4;
14885let foo = 5;
14886let foo = 6;
14887let foo = 7;
14888let foo = 8;
14889let foo = 9;
14890let foo = 10;
14891let foo = 11;
14892let foo = 12;
14893let foo = 13;
14894let foo = 14;
14895let foo = 15;"#,
14896 );
14897
14898 cx.update_editor(|e, window, cx| {
14899 assert_eq!(
14900 e.next_scroll_position,
14901 NextScrollCursorCenterTopBottom::Center,
14902 "Default next scroll direction is center",
14903 );
14904
14905 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14906 assert_eq!(
14907 e.next_scroll_position,
14908 NextScrollCursorCenterTopBottom::Top,
14909 "After center, next scroll direction should be top",
14910 );
14911
14912 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14913 assert_eq!(
14914 e.next_scroll_position,
14915 NextScrollCursorCenterTopBottom::Bottom,
14916 "After top, next scroll direction should be bottom",
14917 );
14918
14919 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14920 assert_eq!(
14921 e.next_scroll_position,
14922 NextScrollCursorCenterTopBottom::Center,
14923 "After bottom, scrolling should start over",
14924 );
14925
14926 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14927 assert_eq!(
14928 e.next_scroll_position,
14929 NextScrollCursorCenterTopBottom::Top,
14930 "Scrolling continues if retriggered fast enough"
14931 );
14932 });
14933
14934 cx.executor()
14935 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14936 cx.executor().run_until_parked();
14937 cx.update_editor(|e, _, _| {
14938 assert_eq!(
14939 e.next_scroll_position,
14940 NextScrollCursorCenterTopBottom::Center,
14941 "If scrolling is not triggered fast enough, it should reset"
14942 );
14943 });
14944}
14945
14946#[gpui::test]
14947async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14948 init_test(cx, |_| {});
14949 let mut cx = EditorLspTestContext::new_rust(
14950 lsp::ServerCapabilities {
14951 definition_provider: Some(lsp::OneOf::Left(true)),
14952 references_provider: Some(lsp::OneOf::Left(true)),
14953 ..lsp::ServerCapabilities::default()
14954 },
14955 cx,
14956 )
14957 .await;
14958
14959 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14960 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14961 move |params, _| async move {
14962 if empty_go_to_definition {
14963 Ok(None)
14964 } else {
14965 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14966 uri: params.text_document_position_params.text_document.uri,
14967 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14968 })))
14969 }
14970 },
14971 );
14972 let references =
14973 cx.lsp
14974 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14975 Ok(Some(vec![lsp::Location {
14976 uri: params.text_document_position.text_document.uri,
14977 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14978 }]))
14979 });
14980 (go_to_definition, references)
14981 };
14982
14983 cx.set_state(
14984 &r#"fn one() {
14985 let mut a = ˇtwo();
14986 }
14987
14988 fn two() {}"#
14989 .unindent(),
14990 );
14991 set_up_lsp_handlers(false, &mut cx);
14992 let navigated = cx
14993 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14994 .await
14995 .expect("Failed to navigate to definition");
14996 assert_eq!(
14997 navigated,
14998 Navigated::Yes,
14999 "Should have navigated to definition from the GetDefinition response"
15000 );
15001 cx.assert_editor_state(
15002 &r#"fn one() {
15003 let mut a = two();
15004 }
15005
15006 fn «twoˇ»() {}"#
15007 .unindent(),
15008 );
15009
15010 let editors = cx.update_workspace(|workspace, _, cx| {
15011 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15012 });
15013 cx.update_editor(|_, _, test_editor_cx| {
15014 assert_eq!(
15015 editors.len(),
15016 1,
15017 "Initially, only one, test, editor should be open in the workspace"
15018 );
15019 assert_eq!(
15020 test_editor_cx.entity(),
15021 editors.last().expect("Asserted len is 1").clone()
15022 );
15023 });
15024
15025 set_up_lsp_handlers(true, &mut cx);
15026 let navigated = cx
15027 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15028 .await
15029 .expect("Failed to navigate to lookup references");
15030 assert_eq!(
15031 navigated,
15032 Navigated::Yes,
15033 "Should have navigated to references as a fallback after empty GoToDefinition response"
15034 );
15035 // We should not change the selections in the existing file,
15036 // if opening another milti buffer with the references
15037 cx.assert_editor_state(
15038 &r#"fn one() {
15039 let mut a = two();
15040 }
15041
15042 fn «twoˇ»() {}"#
15043 .unindent(),
15044 );
15045 let editors = cx.update_workspace(|workspace, _, cx| {
15046 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15047 });
15048 cx.update_editor(|_, _, test_editor_cx| {
15049 assert_eq!(
15050 editors.len(),
15051 2,
15052 "After falling back to references search, we open a new editor with the results"
15053 );
15054 let references_fallback_text = editors
15055 .into_iter()
15056 .find(|new_editor| *new_editor != test_editor_cx.entity())
15057 .expect("Should have one non-test editor now")
15058 .read(test_editor_cx)
15059 .text(test_editor_cx);
15060 assert_eq!(
15061 references_fallback_text, "fn one() {\n let mut a = two();\n}",
15062 "Should use the range from the references response and not the GoToDefinition one"
15063 );
15064 });
15065}
15066
15067#[gpui::test]
15068async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
15069 init_test(cx, |_| {});
15070
15071 let language = Arc::new(Language::new(
15072 LanguageConfig::default(),
15073 Some(tree_sitter_rust::LANGUAGE.into()),
15074 ));
15075
15076 let text = r#"
15077 #[cfg(test)]
15078 mod tests() {
15079 #[test]
15080 fn runnable_1() {
15081 let a = 1;
15082 }
15083
15084 #[test]
15085 fn runnable_2() {
15086 let a = 1;
15087 let b = 2;
15088 }
15089 }
15090 "#
15091 .unindent();
15092
15093 let fs = FakeFs::new(cx.executor());
15094 fs.insert_file("/file.rs", Default::default()).await;
15095
15096 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15097 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15098 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15099 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15100 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
15101
15102 let editor = cx.new_window_entity(|window, cx| {
15103 Editor::new(
15104 EditorMode::Full,
15105 multi_buffer,
15106 Some(project.clone()),
15107 true,
15108 window,
15109 cx,
15110 )
15111 });
15112
15113 editor.update_in(cx, |editor, window, cx| {
15114 editor.tasks.insert(
15115 (buffer.read(cx).remote_id(), 3),
15116 RunnableTasks {
15117 templates: vec![],
15118 offset: MultiBufferOffset(43),
15119 column: 0,
15120 extra_variables: HashMap::default(),
15121 context_range: BufferOffset(43)..BufferOffset(85),
15122 },
15123 );
15124 editor.tasks.insert(
15125 (buffer.read(cx).remote_id(), 8),
15126 RunnableTasks {
15127 templates: vec![],
15128 offset: MultiBufferOffset(86),
15129 column: 0,
15130 extra_variables: HashMap::default(),
15131 context_range: BufferOffset(86)..BufferOffset(191),
15132 },
15133 );
15134
15135 // Test finding task when cursor is inside function body
15136 editor.change_selections(None, window, cx, |s| {
15137 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15138 });
15139 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15140 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15141
15142 // Test finding task when cursor is on function name
15143 editor.change_selections(None, window, cx, |s| {
15144 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15145 });
15146 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15147 assert_eq!(row, 8, "Should find task when cursor is on function name");
15148 });
15149}
15150
15151#[gpui::test]
15152async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
15153 init_test(cx, |_| {});
15154
15155 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15156 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15157 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15158
15159 let fs = FakeFs::new(cx.executor());
15160 fs.insert_tree(
15161 path!("/a"),
15162 json!({
15163 "first.rs": sample_text_1,
15164 "second.rs": sample_text_2,
15165 "third.rs": sample_text_3,
15166 }),
15167 )
15168 .await;
15169 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15170 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15171 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15172 let worktree = project.update(cx, |project, cx| {
15173 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15174 assert_eq!(worktrees.len(), 1);
15175 worktrees.pop().unwrap()
15176 });
15177 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15178
15179 let buffer_1 = project
15180 .update(cx, |project, cx| {
15181 project.open_buffer((worktree_id, "first.rs"), cx)
15182 })
15183 .await
15184 .unwrap();
15185 let buffer_2 = project
15186 .update(cx, |project, cx| {
15187 project.open_buffer((worktree_id, "second.rs"), cx)
15188 })
15189 .await
15190 .unwrap();
15191 let buffer_3 = project
15192 .update(cx, |project, cx| {
15193 project.open_buffer((worktree_id, "third.rs"), cx)
15194 })
15195 .await
15196 .unwrap();
15197
15198 let multi_buffer = cx.new(|cx| {
15199 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15200 multi_buffer.push_excerpts(
15201 buffer_1.clone(),
15202 [
15203 ExcerptRange {
15204 context: Point::new(0, 0)..Point::new(3, 0),
15205 primary: None,
15206 },
15207 ExcerptRange {
15208 context: Point::new(5, 0)..Point::new(7, 0),
15209 primary: None,
15210 },
15211 ExcerptRange {
15212 context: Point::new(9, 0)..Point::new(10, 4),
15213 primary: None,
15214 },
15215 ],
15216 cx,
15217 );
15218 multi_buffer.push_excerpts(
15219 buffer_2.clone(),
15220 [
15221 ExcerptRange {
15222 context: Point::new(0, 0)..Point::new(3, 0),
15223 primary: None,
15224 },
15225 ExcerptRange {
15226 context: Point::new(5, 0)..Point::new(7, 0),
15227 primary: None,
15228 },
15229 ExcerptRange {
15230 context: Point::new(9, 0)..Point::new(10, 4),
15231 primary: None,
15232 },
15233 ],
15234 cx,
15235 );
15236 multi_buffer.push_excerpts(
15237 buffer_3.clone(),
15238 [
15239 ExcerptRange {
15240 context: Point::new(0, 0)..Point::new(3, 0),
15241 primary: None,
15242 },
15243 ExcerptRange {
15244 context: Point::new(5, 0)..Point::new(7, 0),
15245 primary: None,
15246 },
15247 ExcerptRange {
15248 context: Point::new(9, 0)..Point::new(10, 4),
15249 primary: None,
15250 },
15251 ],
15252 cx,
15253 );
15254 multi_buffer
15255 });
15256 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15257 Editor::new(
15258 EditorMode::Full,
15259 multi_buffer,
15260 Some(project.clone()),
15261 true,
15262 window,
15263 cx,
15264 )
15265 });
15266
15267 let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
15268 assert_eq!(
15269 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15270 full_text,
15271 );
15272
15273 multi_buffer_editor.update(cx, |editor, cx| {
15274 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15275 });
15276 assert_eq!(
15277 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15278 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15279 "After folding the first buffer, its text should not be displayed"
15280 );
15281
15282 multi_buffer_editor.update(cx, |editor, cx| {
15283 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15284 });
15285 assert_eq!(
15286 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15287 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15288 "After folding the second buffer, its text should not be displayed"
15289 );
15290
15291 multi_buffer_editor.update(cx, |editor, cx| {
15292 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15293 });
15294 assert_eq!(
15295 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15296 "\n\n\n\n\n",
15297 "After folding the third buffer, its text should not be displayed"
15298 );
15299
15300 // Emulate selection inside the fold logic, that should work
15301 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15302 editor
15303 .snapshot(window, cx)
15304 .next_line_boundary(Point::new(0, 4));
15305 });
15306
15307 multi_buffer_editor.update(cx, |editor, cx| {
15308 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15309 });
15310 assert_eq!(
15311 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15312 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15313 "After unfolding the second buffer, its text should be displayed"
15314 );
15315
15316 multi_buffer_editor.update(cx, |editor, cx| {
15317 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15318 });
15319 assert_eq!(
15320 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15321 "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15322 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15323 );
15324
15325 multi_buffer_editor.update(cx, |editor, cx| {
15326 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15327 });
15328 assert_eq!(
15329 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15330 full_text,
15331 "After unfolding the all buffers, all original text should be displayed"
15332 );
15333}
15334
15335#[gpui::test]
15336async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15337 init_test(cx, |_| {});
15338
15339 let sample_text_1 = "1111\n2222\n3333".to_string();
15340 let sample_text_2 = "4444\n5555\n6666".to_string();
15341 let sample_text_3 = "7777\n8888\n9999".to_string();
15342
15343 let fs = FakeFs::new(cx.executor());
15344 fs.insert_tree(
15345 path!("/a"),
15346 json!({
15347 "first.rs": sample_text_1,
15348 "second.rs": sample_text_2,
15349 "third.rs": sample_text_3,
15350 }),
15351 )
15352 .await;
15353 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15355 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15356 let worktree = project.update(cx, |project, cx| {
15357 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15358 assert_eq!(worktrees.len(), 1);
15359 worktrees.pop().unwrap()
15360 });
15361 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15362
15363 let buffer_1 = project
15364 .update(cx, |project, cx| {
15365 project.open_buffer((worktree_id, "first.rs"), cx)
15366 })
15367 .await
15368 .unwrap();
15369 let buffer_2 = project
15370 .update(cx, |project, cx| {
15371 project.open_buffer((worktree_id, "second.rs"), cx)
15372 })
15373 .await
15374 .unwrap();
15375 let buffer_3 = project
15376 .update(cx, |project, cx| {
15377 project.open_buffer((worktree_id, "third.rs"), cx)
15378 })
15379 .await
15380 .unwrap();
15381
15382 let multi_buffer = cx.new(|cx| {
15383 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15384 multi_buffer.push_excerpts(
15385 buffer_1.clone(),
15386 [ExcerptRange {
15387 context: Point::new(0, 0)..Point::new(3, 0),
15388 primary: None,
15389 }],
15390 cx,
15391 );
15392 multi_buffer.push_excerpts(
15393 buffer_2.clone(),
15394 [ExcerptRange {
15395 context: Point::new(0, 0)..Point::new(3, 0),
15396 primary: None,
15397 }],
15398 cx,
15399 );
15400 multi_buffer.push_excerpts(
15401 buffer_3.clone(),
15402 [ExcerptRange {
15403 context: Point::new(0, 0)..Point::new(3, 0),
15404 primary: None,
15405 }],
15406 cx,
15407 );
15408 multi_buffer
15409 });
15410
15411 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15412 Editor::new(
15413 EditorMode::Full,
15414 multi_buffer,
15415 Some(project.clone()),
15416 true,
15417 window,
15418 cx,
15419 )
15420 });
15421
15422 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15423 assert_eq!(
15424 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15425 full_text,
15426 );
15427
15428 multi_buffer_editor.update(cx, |editor, cx| {
15429 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15430 });
15431 assert_eq!(
15432 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15433 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15434 "After folding the first buffer, its text should not be displayed"
15435 );
15436
15437 multi_buffer_editor.update(cx, |editor, cx| {
15438 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15439 });
15440
15441 assert_eq!(
15442 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15443 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15444 "After folding the second buffer, its text should not be displayed"
15445 );
15446
15447 multi_buffer_editor.update(cx, |editor, cx| {
15448 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15449 });
15450 assert_eq!(
15451 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15452 "\n\n\n\n\n",
15453 "After folding the third buffer, its text should not be displayed"
15454 );
15455
15456 multi_buffer_editor.update(cx, |editor, cx| {
15457 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15458 });
15459 assert_eq!(
15460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15461 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15462 "After unfolding the second buffer, its text should be displayed"
15463 );
15464
15465 multi_buffer_editor.update(cx, |editor, cx| {
15466 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15467 });
15468 assert_eq!(
15469 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15470 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15471 "After unfolding the first buffer, its text should be displayed"
15472 );
15473
15474 multi_buffer_editor.update(cx, |editor, cx| {
15475 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15476 });
15477 assert_eq!(
15478 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15479 full_text,
15480 "After unfolding all buffers, all original text should be displayed"
15481 );
15482}
15483
15484#[gpui::test]
15485async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15486 init_test(cx, |_| {});
15487
15488 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15489
15490 let fs = FakeFs::new(cx.executor());
15491 fs.insert_tree(
15492 path!("/a"),
15493 json!({
15494 "main.rs": sample_text,
15495 }),
15496 )
15497 .await;
15498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15500 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15501 let worktree = project.update(cx, |project, cx| {
15502 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15503 assert_eq!(worktrees.len(), 1);
15504 worktrees.pop().unwrap()
15505 });
15506 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15507
15508 let buffer_1 = project
15509 .update(cx, |project, cx| {
15510 project.open_buffer((worktree_id, "main.rs"), cx)
15511 })
15512 .await
15513 .unwrap();
15514
15515 let multi_buffer = cx.new(|cx| {
15516 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15517 multi_buffer.push_excerpts(
15518 buffer_1.clone(),
15519 [ExcerptRange {
15520 context: Point::new(0, 0)
15521 ..Point::new(
15522 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15523 0,
15524 ),
15525 primary: None,
15526 }],
15527 cx,
15528 );
15529 multi_buffer
15530 });
15531 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15532 Editor::new(
15533 EditorMode::Full,
15534 multi_buffer,
15535 Some(project.clone()),
15536 true,
15537 window,
15538 cx,
15539 )
15540 });
15541
15542 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15543 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15544 enum TestHighlight {}
15545 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15546 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15547 editor.highlight_text::<TestHighlight>(
15548 vec![highlight_range.clone()],
15549 HighlightStyle::color(Hsla::green()),
15550 cx,
15551 );
15552 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15553 });
15554
15555 let full_text = format!("\n\n\n{sample_text}\n");
15556 assert_eq!(
15557 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15558 full_text,
15559 );
15560}
15561
15562#[gpui::test]
15563async fn test_inline_completion_text(cx: &mut TestAppContext) {
15564 init_test(cx, |_| {});
15565
15566 // Simple insertion
15567 assert_highlighted_edits(
15568 "Hello, world!",
15569 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15570 true,
15571 cx,
15572 |highlighted_edits, cx| {
15573 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15574 assert_eq!(highlighted_edits.highlights.len(), 1);
15575 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15576 assert_eq!(
15577 highlighted_edits.highlights[0].1.background_color,
15578 Some(cx.theme().status().created_background)
15579 );
15580 },
15581 )
15582 .await;
15583
15584 // Replacement
15585 assert_highlighted_edits(
15586 "This is a test.",
15587 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15588 false,
15589 cx,
15590 |highlighted_edits, cx| {
15591 assert_eq!(highlighted_edits.text, "That is a test.");
15592 assert_eq!(highlighted_edits.highlights.len(), 1);
15593 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15594 assert_eq!(
15595 highlighted_edits.highlights[0].1.background_color,
15596 Some(cx.theme().status().created_background)
15597 );
15598 },
15599 )
15600 .await;
15601
15602 // Multiple edits
15603 assert_highlighted_edits(
15604 "Hello, world!",
15605 vec![
15606 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15607 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15608 ],
15609 false,
15610 cx,
15611 |highlighted_edits, cx| {
15612 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15613 assert_eq!(highlighted_edits.highlights.len(), 2);
15614 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15615 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15616 assert_eq!(
15617 highlighted_edits.highlights[0].1.background_color,
15618 Some(cx.theme().status().created_background)
15619 );
15620 assert_eq!(
15621 highlighted_edits.highlights[1].1.background_color,
15622 Some(cx.theme().status().created_background)
15623 );
15624 },
15625 )
15626 .await;
15627
15628 // Multiple lines with edits
15629 assert_highlighted_edits(
15630 "First line\nSecond line\nThird line\nFourth line",
15631 vec![
15632 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15633 (
15634 Point::new(2, 0)..Point::new(2, 10),
15635 "New third line".to_string(),
15636 ),
15637 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15638 ],
15639 false,
15640 cx,
15641 |highlighted_edits, cx| {
15642 assert_eq!(
15643 highlighted_edits.text,
15644 "Second modified\nNew third line\nFourth updated line"
15645 );
15646 assert_eq!(highlighted_edits.highlights.len(), 3);
15647 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15648 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15649 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15650 for highlight in &highlighted_edits.highlights {
15651 assert_eq!(
15652 highlight.1.background_color,
15653 Some(cx.theme().status().created_background)
15654 );
15655 }
15656 },
15657 )
15658 .await;
15659}
15660
15661#[gpui::test]
15662async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15663 init_test(cx, |_| {});
15664
15665 // Deletion
15666 assert_highlighted_edits(
15667 "Hello, world!",
15668 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15669 true,
15670 cx,
15671 |highlighted_edits, cx| {
15672 assert_eq!(highlighted_edits.text, "Hello, world!");
15673 assert_eq!(highlighted_edits.highlights.len(), 1);
15674 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15675 assert_eq!(
15676 highlighted_edits.highlights[0].1.background_color,
15677 Some(cx.theme().status().deleted_background)
15678 );
15679 },
15680 )
15681 .await;
15682
15683 // Insertion
15684 assert_highlighted_edits(
15685 "Hello, world!",
15686 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15687 true,
15688 cx,
15689 |highlighted_edits, cx| {
15690 assert_eq!(highlighted_edits.highlights.len(), 1);
15691 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15692 assert_eq!(
15693 highlighted_edits.highlights[0].1.background_color,
15694 Some(cx.theme().status().created_background)
15695 );
15696 },
15697 )
15698 .await;
15699}
15700
15701async fn assert_highlighted_edits(
15702 text: &str,
15703 edits: Vec<(Range<Point>, String)>,
15704 include_deletions: bool,
15705 cx: &mut TestAppContext,
15706 assertion_fn: impl Fn(HighlightedText, &App),
15707) {
15708 let window = cx.add_window(|window, cx| {
15709 let buffer = MultiBuffer::build_simple(text, cx);
15710 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15711 });
15712 let cx = &mut VisualTestContext::from_window(*window, cx);
15713
15714 let (buffer, snapshot) = window
15715 .update(cx, |editor, _window, cx| {
15716 (
15717 editor.buffer().clone(),
15718 editor.buffer().read(cx).snapshot(cx),
15719 )
15720 })
15721 .unwrap();
15722
15723 let edits = edits
15724 .into_iter()
15725 .map(|(range, edit)| {
15726 (
15727 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15728 edit,
15729 )
15730 })
15731 .collect::<Vec<_>>();
15732
15733 let text_anchor_edits = edits
15734 .clone()
15735 .into_iter()
15736 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15737 .collect::<Vec<_>>();
15738
15739 let edit_preview = window
15740 .update(cx, |_, _window, cx| {
15741 buffer
15742 .read(cx)
15743 .as_singleton()
15744 .unwrap()
15745 .read(cx)
15746 .preview_edits(text_anchor_edits.into(), cx)
15747 })
15748 .unwrap()
15749 .await;
15750
15751 cx.update(|_window, cx| {
15752 let highlighted_edits = inline_completion_edit_text(
15753 &snapshot.as_singleton().unwrap().2,
15754 &edits,
15755 &edit_preview,
15756 include_deletions,
15757 cx,
15758 );
15759 assertion_fn(highlighted_edits, cx)
15760 });
15761}
15762
15763#[gpui::test]
15764async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15765 init_test(cx, |_| {});
15766 let capabilities = lsp::ServerCapabilities {
15767 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15768 prepare_provider: Some(true),
15769 work_done_progress_options: Default::default(),
15770 })),
15771 ..Default::default()
15772 };
15773 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15774
15775 cx.set_state(indoc! {"
15776 struct Fˇoo {}
15777 "});
15778
15779 cx.update_editor(|editor, _, cx| {
15780 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15781 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15782 editor.highlight_background::<DocumentHighlightRead>(
15783 &[highlight_range],
15784 |c| c.editor_document_highlight_read_background,
15785 cx,
15786 );
15787 });
15788
15789 let mut prepare_rename_handler =
15790 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15791 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15792 start: lsp::Position {
15793 line: 0,
15794 character: 7,
15795 },
15796 end: lsp::Position {
15797 line: 0,
15798 character: 10,
15799 },
15800 })))
15801 });
15802 let prepare_rename_task = cx
15803 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15804 .expect("Prepare rename was not started");
15805 prepare_rename_handler.next().await.unwrap();
15806 prepare_rename_task.await.expect("Prepare rename failed");
15807
15808 let mut rename_handler =
15809 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15810 let edit = lsp::TextEdit {
15811 range: lsp::Range {
15812 start: lsp::Position {
15813 line: 0,
15814 character: 7,
15815 },
15816 end: lsp::Position {
15817 line: 0,
15818 character: 10,
15819 },
15820 },
15821 new_text: "FooRenamed".to_string(),
15822 };
15823 Ok(Some(lsp::WorkspaceEdit::new(
15824 // Specify the same edit twice
15825 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15826 )))
15827 });
15828 let rename_task = cx
15829 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15830 .expect("Confirm rename was not started");
15831 rename_handler.next().await.unwrap();
15832 rename_task.await.expect("Confirm rename failed");
15833 cx.run_until_parked();
15834
15835 // Despite two edits, only one is actually applied as those are identical
15836 cx.assert_editor_state(indoc! {"
15837 struct FooRenamedˇ {}
15838 "});
15839}
15840
15841#[gpui::test]
15842async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15843 init_test(cx, |_| {});
15844 // These capabilities indicate that the server does not support prepare rename.
15845 let capabilities = lsp::ServerCapabilities {
15846 rename_provider: Some(lsp::OneOf::Left(true)),
15847 ..Default::default()
15848 };
15849 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15850
15851 cx.set_state(indoc! {"
15852 struct Fˇoo {}
15853 "});
15854
15855 cx.update_editor(|editor, _window, cx| {
15856 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15857 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15858 editor.highlight_background::<DocumentHighlightRead>(
15859 &[highlight_range],
15860 |c| c.editor_document_highlight_read_background,
15861 cx,
15862 );
15863 });
15864
15865 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15866 .expect("Prepare rename was not started")
15867 .await
15868 .expect("Prepare rename failed");
15869
15870 let mut rename_handler =
15871 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15872 let edit = lsp::TextEdit {
15873 range: lsp::Range {
15874 start: lsp::Position {
15875 line: 0,
15876 character: 7,
15877 },
15878 end: lsp::Position {
15879 line: 0,
15880 character: 10,
15881 },
15882 },
15883 new_text: "FooRenamed".to_string(),
15884 };
15885 Ok(Some(lsp::WorkspaceEdit::new(
15886 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15887 )))
15888 });
15889 let rename_task = cx
15890 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15891 .expect("Confirm rename was not started");
15892 rename_handler.next().await.unwrap();
15893 rename_task.await.expect("Confirm rename failed");
15894 cx.run_until_parked();
15895
15896 // Correct range is renamed, as `surrounding_word` is used to find it.
15897 cx.assert_editor_state(indoc! {"
15898 struct FooRenamedˇ {}
15899 "});
15900}
15901
15902fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15903 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15904 point..point
15905}
15906
15907fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15908 let (text, ranges) = marked_text_ranges(marked_text, true);
15909 assert_eq!(editor.text(cx), text);
15910 assert_eq!(
15911 editor.selections.ranges(cx),
15912 ranges,
15913 "Assert selections are {}",
15914 marked_text
15915 );
15916}
15917
15918pub fn handle_signature_help_request(
15919 cx: &mut EditorLspTestContext,
15920 mocked_response: lsp::SignatureHelp,
15921) -> impl Future<Output = ()> {
15922 let mut request =
15923 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15924 let mocked_response = mocked_response.clone();
15925 async move { Ok(Some(mocked_response)) }
15926 });
15927
15928 async move {
15929 request.next().await;
15930 }
15931}
15932
15933/// Handle completion request passing a marked string specifying where the completion
15934/// should be triggered from using '|' character, what range should be replaced, and what completions
15935/// should be returned using '<' and '>' to delimit the range
15936pub fn handle_completion_request(
15937 cx: &mut EditorLspTestContext,
15938 marked_string: &str,
15939 completions: Vec<&'static str>,
15940 counter: Arc<AtomicUsize>,
15941) -> impl Future<Output = ()> {
15942 let complete_from_marker: TextRangeMarker = '|'.into();
15943 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15944 let (_, mut marked_ranges) = marked_text_ranges_by(
15945 marked_string,
15946 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15947 );
15948
15949 let complete_from_position =
15950 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15951 let replace_range =
15952 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15953
15954 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15955 let completions = completions.clone();
15956 counter.fetch_add(1, atomic::Ordering::Release);
15957 async move {
15958 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15959 assert_eq!(
15960 params.text_document_position.position,
15961 complete_from_position
15962 );
15963 Ok(Some(lsp::CompletionResponse::Array(
15964 completions
15965 .iter()
15966 .map(|completion_text| lsp::CompletionItem {
15967 label: completion_text.to_string(),
15968 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15969 range: replace_range,
15970 new_text: completion_text.to_string(),
15971 })),
15972 ..Default::default()
15973 })
15974 .collect(),
15975 )))
15976 }
15977 });
15978
15979 async move {
15980 request.next().await;
15981 }
15982}
15983
15984fn handle_resolve_completion_request(
15985 cx: &mut EditorLspTestContext,
15986 edits: Option<Vec<(&'static str, &'static str)>>,
15987) -> impl Future<Output = ()> {
15988 let edits = edits.map(|edits| {
15989 edits
15990 .iter()
15991 .map(|(marked_string, new_text)| {
15992 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15993 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15994 lsp::TextEdit::new(replace_range, new_text.to_string())
15995 })
15996 .collect::<Vec<_>>()
15997 });
15998
15999 let mut request =
16000 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16001 let edits = edits.clone();
16002 async move {
16003 Ok(lsp::CompletionItem {
16004 additional_text_edits: edits,
16005 ..Default::default()
16006 })
16007 }
16008 });
16009
16010 async move {
16011 request.next().await;
16012 }
16013}
16014
16015pub(crate) fn update_test_language_settings(
16016 cx: &mut TestAppContext,
16017 f: impl Fn(&mut AllLanguageSettingsContent),
16018) {
16019 cx.update(|cx| {
16020 SettingsStore::update_global(cx, |store, cx| {
16021 store.update_user_settings::<AllLanguageSettings>(cx, f);
16022 });
16023 });
16024}
16025
16026pub(crate) fn update_test_project_settings(
16027 cx: &mut TestAppContext,
16028 f: impl Fn(&mut ProjectSettings),
16029) {
16030 cx.update(|cx| {
16031 SettingsStore::update_global(cx, |store, cx| {
16032 store.update_user_settings::<ProjectSettings>(cx, f);
16033 });
16034 });
16035}
16036
16037pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
16038 cx.update(|cx| {
16039 assets::Assets.load_test_fonts(cx);
16040 let store = SettingsStore::test(cx);
16041 cx.set_global(store);
16042 theme::init(theme::LoadThemes::JustBase, cx);
16043 release_channel::init(SemanticVersion::default(), cx);
16044 client::init_settings(cx);
16045 language::init(cx);
16046 Project::init_settings(cx);
16047 workspace::init_settings(cx);
16048 crate::init(cx);
16049 });
16050
16051 update_test_language_settings(cx, f);
16052}
16053
16054#[track_caller]
16055fn assert_hunk_revert(
16056 not_reverted_text_with_selections: &str,
16057 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
16058 expected_reverted_text_with_selections: &str,
16059 base_text: &str,
16060 cx: &mut EditorLspTestContext,
16061) {
16062 cx.set_state(not_reverted_text_with_selections);
16063 cx.set_diff_base(base_text);
16064 cx.executor().run_until_parked();
16065
16066 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
16067 let snapshot = editor.snapshot(window, cx);
16068 let reverted_hunk_statuses = snapshot
16069 .buffer_snapshot
16070 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
16071 .map(|hunk| hunk.status())
16072 .collect::<Vec<_>>();
16073
16074 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
16075 reverted_hunk_statuses
16076 });
16077 cx.executor().run_until_parked();
16078 cx.assert_editor_state(expected_reverted_text_with_selections);
16079 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
16080}