1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
279 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
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(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(4)
2924 });
2925
2926 let language = Arc::new(
2927 Language::new(
2928 LanguageConfig::default(),
2929 Some(tree_sitter_rust::LANGUAGE.into()),
2930 )
2931 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2932 .unwrap(),
2933 );
2934
2935 let mut cx = EditorTestContext::new(cx).await;
2936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2937 cx.set_state(indoc! {"
2938 fn a() {
2939 if b {
2940 \t ˇc
2941 }
2942 }
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 fn a() {
2948 if b {
2949 ˇc
2950 }
2951 }
2952 "});
2953}
2954
2955#[gpui::test]
2956async fn test_indent_outdent(cx: &mut TestAppContext) {
2957 init_test(cx, |settings| {
2958 settings.defaults.tab_size = NonZeroU32::new(4);
2959 });
2960
2961 let mut cx = EditorTestContext::new(cx).await;
2962
2963 cx.set_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 «oneˇ» «twoˇ»
2971 three
2972 four
2973 "});
2974
2975 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 «oneˇ» «twoˇ»
2978 three
2979 four
2980 "});
2981
2982 // select across line ending
2983 cx.set_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2989 cx.assert_editor_state(indoc! {"
2990 one two
2991 t«hree
2992 ˇ» four
2993 "});
2994
2995 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 t«hree
2999 ˇ» four
3000 "});
3001
3002 // Ensure that indenting/outdenting works when the cursor is at column 0.
3003 cx.set_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 one two
3011 ˇthree
3012 four
3013 "});
3014
3015 cx.set_state(indoc! {"
3016 one two
3017 ˇ three
3018 four
3019 "});
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 ˇthree
3024 four
3025 "});
3026}
3027
3028#[gpui::test]
3029async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3030 init_test(cx, |settings| {
3031 settings.defaults.hard_tabs = Some(true);
3032 });
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035
3036 // select two ranges on one line
3037 cx.set_state(indoc! {"
3038 «oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t\t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 \t«oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «oneˇ» «twoˇ»
3063 three
3064 four
3065 "});
3066
3067 // select across a line ending
3068 cx.set_state(indoc! {"
3069 one two
3070 t«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \t\tt«hree
3083 ˇ»four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 one two
3088 \tt«hree
3089 ˇ»four
3090 "});
3091 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 one two
3094 t«hree
3095 ˇ»four
3096 "});
3097
3098 // Ensure that indenting/outdenting works when the cursor is at column 0.
3099 cx.set_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 ˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tˇthree
3114 four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 ˇthree
3120 four
3121 "});
3122}
3123
3124#[gpui::test]
3125fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3126 init_test(cx, |settings| {
3127 settings.languages.extend([
3128 (
3129 "TOML".into(),
3130 LanguageSettingsContent {
3131 tab_size: NonZeroU32::new(2),
3132 ..Default::default()
3133 },
3134 ),
3135 (
3136 "Rust".into(),
3137 LanguageSettingsContent {
3138 tab_size: NonZeroU32::new(4),
3139 ..Default::default()
3140 },
3141 ),
3142 ]);
3143 });
3144
3145 let toml_language = Arc::new(Language::new(
3146 LanguageConfig {
3147 name: "TOML".into(),
3148 ..Default::default()
3149 },
3150 None,
3151 ));
3152 let rust_language = Arc::new(Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 ..Default::default()
3156 },
3157 None,
3158 ));
3159
3160 let toml_buffer =
3161 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3162 let rust_buffer =
3163 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3164 let multibuffer = cx.new(|cx| {
3165 let mut multibuffer = MultiBuffer::new(ReadWrite);
3166 multibuffer.push_excerpts(
3167 toml_buffer.clone(),
3168 [ExcerptRange {
3169 context: Point::new(0, 0)..Point::new(2, 0),
3170 primary: None,
3171 }],
3172 cx,
3173 );
3174 multibuffer.push_excerpts(
3175 rust_buffer.clone(),
3176 [ExcerptRange {
3177 context: Point::new(0, 0)..Point::new(1, 0),
3178 primary: None,
3179 }],
3180 cx,
3181 );
3182 multibuffer
3183 });
3184
3185 cx.add_window(|window, cx| {
3186 let mut editor = build_editor(multibuffer, window, cx);
3187
3188 assert_eq!(
3189 editor.text(cx),
3190 indoc! {"
3191 a = 1
3192 b = 2
3193
3194 const c: usize = 3;
3195 "}
3196 );
3197
3198 select_ranges(
3199 &mut editor,
3200 indoc! {"
3201 «aˇ» = 1
3202 b = 2
3203
3204 «const c:ˇ» usize = 3;
3205 "},
3206 window,
3207 cx,
3208 );
3209
3210 editor.tab(&Tab, window, cx);
3211 assert_text_with_selections(
3212 &mut editor,
3213 indoc! {"
3214 «aˇ» = 1
3215 b = 2
3216
3217 «const c:ˇ» usize = 3;
3218 "},
3219 cx,
3220 );
3221 editor.backtab(&Backtab, window, cx);
3222 assert_text_with_selections(
3223 &mut editor,
3224 indoc! {"
3225 «aˇ» = 1
3226 b = 2
3227
3228 «const c:ˇ» usize = 3;
3229 "},
3230 cx,
3231 );
3232
3233 editor
3234 });
3235}
3236
3237#[gpui::test]
3238async fn test_backspace(cx: &mut TestAppContext) {
3239 init_test(cx, |_| {});
3240
3241 let mut cx = EditorTestContext::new(cx).await;
3242
3243 // Basic backspace
3244 cx.set_state(indoc! {"
3245 onˇe two three
3246 fou«rˇ» five six
3247 seven «ˇeight nine
3248 »ten
3249 "});
3250 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3251 cx.assert_editor_state(indoc! {"
3252 oˇe two three
3253 fouˇ five six
3254 seven ˇten
3255 "});
3256
3257 // Test backspace inside and around indents
3258 cx.set_state(indoc! {"
3259 zero
3260 ˇone
3261 ˇtwo
3262 ˇ ˇ ˇ three
3263 ˇ ˇ four
3264 "});
3265 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3266 cx.assert_editor_state(indoc! {"
3267 zero
3268 ˇone
3269 ˇtwo
3270 ˇ threeˇ four
3271 "});
3272}
3273
3274#[gpui::test]
3275async fn test_delete(cx: &mut TestAppContext) {
3276 init_test(cx, |_| {});
3277
3278 let mut cx = EditorTestContext::new(cx).await;
3279 cx.set_state(indoc! {"
3280 onˇe two three
3281 fou«rˇ» five six
3282 seven «ˇeight nine
3283 »ten
3284 "});
3285 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 onˇ two three
3288 fouˇ five six
3289 seven ˇten
3290 "});
3291}
3292
3293#[gpui::test]
3294fn test_delete_line(cx: &mut TestAppContext) {
3295 init_test(cx, |_| {});
3296
3297 let editor = cx.add_window(|window, cx| {
3298 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3299 build_editor(buffer, window, cx)
3300 });
3301 _ = editor.update(cx, |editor, window, cx| {
3302 editor.change_selections(None, window, cx, |s| {
3303 s.select_display_ranges([
3304 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3305 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3306 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3307 ])
3308 });
3309 editor.delete_line(&DeleteLine, window, cx);
3310 assert_eq!(editor.display_text(cx), "ghi");
3311 assert_eq!(
3312 editor.selections.display_ranges(cx),
3313 vec![
3314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3316 ]
3317 );
3318 });
3319
3320 let editor = cx.add_window(|window, cx| {
3321 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3322 build_editor(buffer, window, cx)
3323 });
3324 _ = editor.update(cx, |editor, window, cx| {
3325 editor.change_selections(None, window, cx, |s| {
3326 s.select_display_ranges([
3327 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3328 ])
3329 });
3330 editor.delete_line(&DeleteLine, window, cx);
3331 assert_eq!(editor.display_text(cx), "ghi\n");
3332 assert_eq!(
3333 editor.selections.display_ranges(cx),
3334 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3335 );
3336 });
3337}
3338
3339#[gpui::test]
3340fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3341 init_test(cx, |_| {});
3342
3343 cx.add_window(|window, cx| {
3344 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3345 let mut editor = build_editor(buffer.clone(), window, cx);
3346 let buffer = buffer.read(cx).as_singleton().unwrap();
3347
3348 assert_eq!(
3349 editor.selections.ranges::<Point>(cx),
3350 &[Point::new(0, 0)..Point::new(0, 0)]
3351 );
3352
3353 // When on single line, replace newline at end by space
3354 editor.join_lines(&JoinLines, window, cx);
3355 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3356 assert_eq!(
3357 editor.selections.ranges::<Point>(cx),
3358 &[Point::new(0, 3)..Point::new(0, 3)]
3359 );
3360
3361 // When multiple lines are selected, remove newlines that are spanned by the selection
3362 editor.change_selections(None, window, cx, |s| {
3363 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3364 });
3365 editor.join_lines(&JoinLines, window, cx);
3366 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3367 assert_eq!(
3368 editor.selections.ranges::<Point>(cx),
3369 &[Point::new(0, 11)..Point::new(0, 11)]
3370 );
3371
3372 // Undo should be transactional
3373 editor.undo(&Undo, window, cx);
3374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3375 assert_eq!(
3376 editor.selections.ranges::<Point>(cx),
3377 &[Point::new(0, 5)..Point::new(2, 2)]
3378 );
3379
3380 // When joining an empty line don't insert a space
3381 editor.change_selections(None, window, cx, |s| {
3382 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3383 });
3384 editor.join_lines(&JoinLines, window, cx);
3385 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3386 assert_eq!(
3387 editor.selections.ranges::<Point>(cx),
3388 [Point::new(2, 3)..Point::new(2, 3)]
3389 );
3390
3391 // We can remove trailing newlines
3392 editor.join_lines(&JoinLines, window, cx);
3393 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3394 assert_eq!(
3395 editor.selections.ranges::<Point>(cx),
3396 [Point::new(2, 3)..Point::new(2, 3)]
3397 );
3398
3399 // We don't blow up on the last line
3400 editor.join_lines(&JoinLines, window, cx);
3401 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3402 assert_eq!(
3403 editor.selections.ranges::<Point>(cx),
3404 [Point::new(2, 3)..Point::new(2, 3)]
3405 );
3406
3407 // reset to test indentation
3408 editor.buffer.update(cx, |buffer, cx| {
3409 buffer.edit(
3410 [
3411 (Point::new(1, 0)..Point::new(1, 2), " "),
3412 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3413 ],
3414 None,
3415 cx,
3416 )
3417 });
3418
3419 // We remove any leading spaces
3420 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3421 editor.change_selections(None, window, cx, |s| {
3422 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3423 });
3424 editor.join_lines(&JoinLines, window, cx);
3425 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3426
3427 // We don't insert a space for a line containing only spaces
3428 editor.join_lines(&JoinLines, window, cx);
3429 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3430
3431 // We ignore any leading tabs
3432 editor.join_lines(&JoinLines, window, cx);
3433 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3434
3435 editor
3436 });
3437}
3438
3439#[gpui::test]
3440fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3441 init_test(cx, |_| {});
3442
3443 cx.add_window(|window, cx| {
3444 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3445 let mut editor = build_editor(buffer.clone(), window, cx);
3446 let buffer = buffer.read(cx).as_singleton().unwrap();
3447
3448 editor.change_selections(None, window, cx, |s| {
3449 s.select_ranges([
3450 Point::new(0, 2)..Point::new(1, 1),
3451 Point::new(1, 2)..Point::new(1, 2),
3452 Point::new(3, 1)..Point::new(3, 2),
3453 ])
3454 });
3455
3456 editor.join_lines(&JoinLines, window, cx);
3457 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3458
3459 assert_eq!(
3460 editor.selections.ranges::<Point>(cx),
3461 [
3462 Point::new(0, 7)..Point::new(0, 7),
3463 Point::new(1, 3)..Point::new(1, 3)
3464 ]
3465 );
3466 editor
3467 });
3468}
3469
3470#[gpui::test]
3471async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3472 init_test(cx, |_| {});
3473
3474 let mut cx = EditorTestContext::new(cx).await;
3475
3476 let diff_base = r#"
3477 Line 0
3478 Line 1
3479 Line 2
3480 Line 3
3481 "#
3482 .unindent();
3483
3484 cx.set_state(
3485 &r#"
3486 ˇLine 0
3487 Line 1
3488 Line 2
3489 Line 3
3490 "#
3491 .unindent(),
3492 );
3493
3494 cx.set_head_text(&diff_base);
3495 executor.run_until_parked();
3496
3497 // Join lines
3498 cx.update_editor(|editor, window, cx| {
3499 editor.join_lines(&JoinLines, window, cx);
3500 });
3501 executor.run_until_parked();
3502
3503 cx.assert_editor_state(
3504 &r#"
3505 Line 0ˇ Line 1
3506 Line 2
3507 Line 3
3508 "#
3509 .unindent(),
3510 );
3511 // Join again
3512 cx.update_editor(|editor, window, cx| {
3513 editor.join_lines(&JoinLines, window, cx);
3514 });
3515 executor.run_until_parked();
3516
3517 cx.assert_editor_state(
3518 &r#"
3519 Line 0 Line 1ˇ Line 2
3520 Line 3
3521 "#
3522 .unindent(),
3523 );
3524}
3525
3526#[gpui::test]
3527async fn test_custom_newlines_cause_no_false_positive_diffs(
3528 executor: BackgroundExecutor,
3529 cx: &mut TestAppContext,
3530) {
3531 init_test(cx, |_| {});
3532 let mut cx = EditorTestContext::new(cx).await;
3533 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3534 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3535 executor.run_until_parked();
3536
3537 cx.update_editor(|editor, window, cx| {
3538 let snapshot = editor.snapshot(window, cx);
3539 assert_eq!(
3540 snapshot
3541 .buffer_snapshot
3542 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3543 .collect::<Vec<_>>(),
3544 Vec::new(),
3545 "Should not have any diffs for files with custom newlines"
3546 );
3547 });
3548}
3549
3550#[gpui::test]
3551async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3552 init_test(cx, |_| {});
3553
3554 let mut cx = EditorTestContext::new(cx).await;
3555
3556 // Test sort_lines_case_insensitive()
3557 cx.set_state(indoc! {"
3558 «z
3559 y
3560 x
3561 Z
3562 Y
3563 Xˇ»
3564 "});
3565 cx.update_editor(|e, window, cx| {
3566 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3567 });
3568 cx.assert_editor_state(indoc! {"
3569 «x
3570 X
3571 y
3572 Y
3573 z
3574 Zˇ»
3575 "});
3576
3577 // Test reverse_lines()
3578 cx.set_state(indoc! {"
3579 «5
3580 4
3581 3
3582 2
3583 1ˇ»
3584 "});
3585 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3586 cx.assert_editor_state(indoc! {"
3587 «1
3588 2
3589 3
3590 4
3591 5ˇ»
3592 "});
3593
3594 // Skip testing shuffle_line()
3595
3596 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3597 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3598
3599 // Don't manipulate when cursor is on single line, but expand the selection
3600 cx.set_state(indoc! {"
3601 ddˇdd
3602 ccc
3603 bb
3604 a
3605 "});
3606 cx.update_editor(|e, window, cx| {
3607 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3608 });
3609 cx.assert_editor_state(indoc! {"
3610 «ddddˇ»
3611 ccc
3612 bb
3613 a
3614 "});
3615
3616 // Basic manipulate case
3617 // Start selection moves to column 0
3618 // End of selection shrinks to fit shorter line
3619 cx.set_state(indoc! {"
3620 dd«d
3621 ccc
3622 bb
3623 aaaaaˇ»
3624 "});
3625 cx.update_editor(|e, window, cx| {
3626 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3627 });
3628 cx.assert_editor_state(indoc! {"
3629 «aaaaa
3630 bb
3631 ccc
3632 dddˇ»
3633 "});
3634
3635 // Manipulate case with newlines
3636 cx.set_state(indoc! {"
3637 dd«d
3638 ccc
3639
3640 bb
3641 aaaaa
3642
3643 ˇ»
3644 "});
3645 cx.update_editor(|e, window, cx| {
3646 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3647 });
3648 cx.assert_editor_state(indoc! {"
3649 «
3650
3651 aaaaa
3652 bb
3653 ccc
3654 dddˇ»
3655
3656 "});
3657
3658 // Adding new line
3659 cx.set_state(indoc! {"
3660 aa«a
3661 bbˇ»b
3662 "});
3663 cx.update_editor(|e, window, cx| {
3664 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3665 });
3666 cx.assert_editor_state(indoc! {"
3667 «aaa
3668 bbb
3669 added_lineˇ»
3670 "});
3671
3672 // Removing line
3673 cx.set_state(indoc! {"
3674 aa«a
3675 bbbˇ»
3676 "});
3677 cx.update_editor(|e, window, cx| {
3678 e.manipulate_lines(window, cx, |lines| {
3679 lines.pop();
3680 })
3681 });
3682 cx.assert_editor_state(indoc! {"
3683 «aaaˇ»
3684 "});
3685
3686 // Removing all lines
3687 cx.set_state(indoc! {"
3688 aa«a
3689 bbbˇ»
3690 "});
3691 cx.update_editor(|e, window, cx| {
3692 e.manipulate_lines(window, cx, |lines| {
3693 lines.drain(..);
3694 })
3695 });
3696 cx.assert_editor_state(indoc! {"
3697 ˇ
3698 "});
3699}
3700
3701#[gpui::test]
3702async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3703 init_test(cx, |_| {});
3704
3705 let mut cx = EditorTestContext::new(cx).await;
3706
3707 // Consider continuous selection as single selection
3708 cx.set_state(indoc! {"
3709 Aaa«aa
3710 cˇ»c«c
3711 bb
3712 aaaˇ»aa
3713 "});
3714 cx.update_editor(|e, window, cx| {
3715 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3716 });
3717 cx.assert_editor_state(indoc! {"
3718 «Aaaaa
3719 ccc
3720 bb
3721 aaaaaˇ»
3722 "});
3723
3724 cx.set_state(indoc! {"
3725 Aaa«aa
3726 cˇ»c«c
3727 bb
3728 aaaˇ»aa
3729 "});
3730 cx.update_editor(|e, window, cx| {
3731 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3732 });
3733 cx.assert_editor_state(indoc! {"
3734 «Aaaaa
3735 ccc
3736 bbˇ»
3737 "});
3738
3739 // Consider non continuous selection as distinct dedup operations
3740 cx.set_state(indoc! {"
3741 «aaaaa
3742 bb
3743 aaaaa
3744 aaaaaˇ»
3745
3746 aaa«aaˇ»
3747 "});
3748 cx.update_editor(|e, window, cx| {
3749 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3750 });
3751 cx.assert_editor_state(indoc! {"
3752 «aaaaa
3753 bbˇ»
3754
3755 «aaaaaˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764
3765 cx.set_state(indoc! {"
3766 «Aaa
3767 aAa
3768 Aaaˇ»
3769 "});
3770 cx.update_editor(|e, window, cx| {
3771 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3772 });
3773 cx.assert_editor_state(indoc! {"
3774 «Aaa
3775 aAaˇ»
3776 "});
3777
3778 cx.set_state(indoc! {"
3779 «Aaa
3780 aAa
3781 aaAˇ»
3782 "});
3783 cx.update_editor(|e, window, cx| {
3784 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3785 });
3786 cx.assert_editor_state(indoc! {"
3787 «Aaaˇ»
3788 "});
3789}
3790
3791#[gpui::test]
3792async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3793 init_test(cx, |_| {});
3794
3795 let mut cx = EditorTestContext::new(cx).await;
3796
3797 // Manipulate with multiple selections on a single line
3798 cx.set_state(indoc! {"
3799 dd«dd
3800 cˇ»c«c
3801 bb
3802 aaaˇ»aa
3803 "});
3804 cx.update_editor(|e, window, cx| {
3805 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3806 });
3807 cx.assert_editor_state(indoc! {"
3808 «aaaaa
3809 bb
3810 ccc
3811 ddddˇ»
3812 "});
3813
3814 // Manipulate with multiple disjoin selections
3815 cx.set_state(indoc! {"
3816 5«
3817 4
3818 3
3819 2
3820 1ˇ»
3821
3822 dd«dd
3823 ccc
3824 bb
3825 aaaˇ»aa
3826 "});
3827 cx.update_editor(|e, window, cx| {
3828 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3829 });
3830 cx.assert_editor_state(indoc! {"
3831 «1
3832 2
3833 3
3834 4
3835 5ˇ»
3836
3837 «aaaaa
3838 bb
3839 ccc
3840 ddddˇ»
3841 "});
3842
3843 // Adding lines on each selection
3844 cx.set_state(indoc! {"
3845 2«
3846 1ˇ»
3847
3848 bb«bb
3849 aaaˇ»aa
3850 "});
3851 cx.update_editor(|e, window, cx| {
3852 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3853 });
3854 cx.assert_editor_state(indoc! {"
3855 «2
3856 1
3857 added lineˇ»
3858
3859 «bbbb
3860 aaaaa
3861 added lineˇ»
3862 "});
3863
3864 // Removing lines on each selection
3865 cx.set_state(indoc! {"
3866 2«
3867 1ˇ»
3868
3869 bb«bb
3870 aaaˇ»aa
3871 "});
3872 cx.update_editor(|e, window, cx| {
3873 e.manipulate_lines(window, cx, |lines| {
3874 lines.pop();
3875 })
3876 });
3877 cx.assert_editor_state(indoc! {"
3878 «2ˇ»
3879
3880 «bbbbˇ»
3881 "});
3882}
3883
3884#[gpui::test]
3885async fn test_manipulate_text(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 let mut cx = EditorTestContext::new(cx).await;
3889
3890 // Test convert_to_upper_case()
3891 cx.set_state(indoc! {"
3892 «hello worldˇ»
3893 "});
3894 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3895 cx.assert_editor_state(indoc! {"
3896 «HELLO WORLDˇ»
3897 "});
3898
3899 // Test convert_to_lower_case()
3900 cx.set_state(indoc! {"
3901 «HELLO WORLDˇ»
3902 "});
3903 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3904 cx.assert_editor_state(indoc! {"
3905 «hello worldˇ»
3906 "});
3907
3908 // Test multiple line, single selection case
3909 cx.set_state(indoc! {"
3910 «The quick brown
3911 fox jumps over
3912 the lazy dogˇ»
3913 "});
3914 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3915 cx.assert_editor_state(indoc! {"
3916 «The Quick Brown
3917 Fox Jumps Over
3918 The Lazy Dogˇ»
3919 "});
3920
3921 // Test multiple line, single selection case
3922 cx.set_state(indoc! {"
3923 «The quick brown
3924 fox jumps over
3925 the lazy dogˇ»
3926 "});
3927 cx.update_editor(|e, window, cx| {
3928 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3929 });
3930 cx.assert_editor_state(indoc! {"
3931 «TheQuickBrown
3932 FoxJumpsOver
3933 TheLazyDogˇ»
3934 "});
3935
3936 // From here on out, test more complex cases of manipulate_text()
3937
3938 // Test no selection case - should affect words cursors are in
3939 // Cursor at beginning, middle, and end of word
3940 cx.set_state(indoc! {"
3941 ˇhello big beauˇtiful worldˇ
3942 "});
3943 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3944 cx.assert_editor_state(indoc! {"
3945 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3946 "});
3947
3948 // Test multiple selections on a single line and across multiple lines
3949 cx.set_state(indoc! {"
3950 «Theˇ» quick «brown
3951 foxˇ» jumps «overˇ»
3952 the «lazyˇ» dog
3953 "});
3954 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3955 cx.assert_editor_state(indoc! {"
3956 «THEˇ» quick «BROWN
3957 FOXˇ» jumps «OVERˇ»
3958 the «LAZYˇ» dog
3959 "});
3960
3961 // Test case where text length grows
3962 cx.set_state(indoc! {"
3963 «tschüߡ»
3964 "});
3965 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3966 cx.assert_editor_state(indoc! {"
3967 «TSCHÜSSˇ»
3968 "});
3969
3970 // Test to make sure we don't crash when text shrinks
3971 cx.set_state(indoc! {"
3972 aaa_bbbˇ
3973 "});
3974 cx.update_editor(|e, window, cx| {
3975 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3976 });
3977 cx.assert_editor_state(indoc! {"
3978 «aaaBbbˇ»
3979 "});
3980
3981 // Test to make sure we all aware of the fact that each word can grow and shrink
3982 // Final selections should be aware of this fact
3983 cx.set_state(indoc! {"
3984 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3985 "});
3986 cx.update_editor(|e, window, cx| {
3987 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3988 });
3989 cx.assert_editor_state(indoc! {"
3990 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3991 "});
3992
3993 cx.set_state(indoc! {"
3994 «hElLo, WoRld!ˇ»
3995 "});
3996 cx.update_editor(|e, window, cx| {
3997 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3998 });
3999 cx.assert_editor_state(indoc! {"
4000 «HeLlO, wOrLD!ˇ»
4001 "});
4002}
4003
4004#[gpui::test]
4005fn test_duplicate_line(cx: &mut TestAppContext) {
4006 init_test(cx, |_| {});
4007
4008 let editor = cx.add_window(|window, cx| {
4009 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4010 build_editor(buffer, window, cx)
4011 });
4012 _ = editor.update(cx, |editor, window, cx| {
4013 editor.change_selections(None, window, cx, |s| {
4014 s.select_display_ranges([
4015 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4016 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4017 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4018 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4019 ])
4020 });
4021 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4028 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4029 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4030 ]
4031 );
4032 });
4033
4034 let editor = cx.add_window(|window, cx| {
4035 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4036 build_editor(buffer, window, cx)
4037 });
4038 _ = editor.update(cx, |editor, window, cx| {
4039 editor.change_selections(None, window, cx, |s| {
4040 s.select_display_ranges([
4041 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4042 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4043 ])
4044 });
4045 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4046 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4047 assert_eq!(
4048 editor.selections.display_ranges(cx),
4049 vec![
4050 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4051 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4052 ]
4053 );
4054 });
4055
4056 // With `move_upwards` the selections stay in place, except for
4057 // the lines inserted above them
4058 let editor = cx.add_window(|window, cx| {
4059 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4060 build_editor(buffer, window, cx)
4061 });
4062 _ = editor.update(cx, |editor, window, cx| {
4063 editor.change_selections(None, window, cx, |s| {
4064 s.select_display_ranges([
4065 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4066 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4067 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4068 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4069 ])
4070 });
4071 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4072 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4073 assert_eq!(
4074 editor.selections.display_ranges(cx),
4075 vec![
4076 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4077 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4078 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4079 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4080 ]
4081 );
4082 });
4083
4084 let editor = cx.add_window(|window, cx| {
4085 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4086 build_editor(buffer, window, cx)
4087 });
4088 _ = editor.update(cx, |editor, window, cx| {
4089 editor.change_selections(None, window, cx, |s| {
4090 s.select_display_ranges([
4091 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4092 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4093 ])
4094 });
4095 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4096 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4097 assert_eq!(
4098 editor.selections.display_ranges(cx),
4099 vec![
4100 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4101 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4102 ]
4103 );
4104 });
4105
4106 let editor = cx.add_window(|window, cx| {
4107 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4108 build_editor(buffer, window, cx)
4109 });
4110 _ = editor.update(cx, |editor, window, cx| {
4111 editor.change_selections(None, window, cx, |s| {
4112 s.select_display_ranges([
4113 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4114 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4115 ])
4116 });
4117 editor.duplicate_selection(&DuplicateSelection, window, cx);
4118 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4119 assert_eq!(
4120 editor.selections.display_ranges(cx),
4121 vec![
4122 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4123 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4124 ]
4125 );
4126 });
4127}
4128
4129#[gpui::test]
4130fn test_move_line_up_down(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let editor = cx.add_window(|window, cx| {
4134 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4135 build_editor(buffer, window, cx)
4136 });
4137 _ = editor.update(cx, |editor, window, cx| {
4138 editor.fold_creases(
4139 vec![
4140 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4141 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4142 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4143 ],
4144 true,
4145 window,
4146 cx,
4147 );
4148 editor.change_selections(None, window, cx, |s| {
4149 s.select_display_ranges([
4150 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4151 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4152 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4153 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4154 ])
4155 });
4156 assert_eq!(
4157 editor.display_text(cx),
4158 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4159 );
4160
4161 editor.move_line_up(&MoveLineUp, window, cx);
4162 assert_eq!(
4163 editor.display_text(cx),
4164 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4165 );
4166 assert_eq!(
4167 editor.selections.display_ranges(cx),
4168 vec![
4169 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4170 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4171 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4172 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4173 ]
4174 );
4175 });
4176
4177 _ = editor.update(cx, |editor, window, cx| {
4178 editor.move_line_down(&MoveLineDown, window, cx);
4179 assert_eq!(
4180 editor.display_text(cx),
4181 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4182 );
4183 assert_eq!(
4184 editor.selections.display_ranges(cx),
4185 vec![
4186 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4187 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4188 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4189 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4190 ]
4191 );
4192 });
4193
4194 _ = editor.update(cx, |editor, window, cx| {
4195 editor.move_line_down(&MoveLineDown, window, cx);
4196 assert_eq!(
4197 editor.display_text(cx),
4198 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4199 );
4200 assert_eq!(
4201 editor.selections.display_ranges(cx),
4202 vec![
4203 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4204 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4205 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4206 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4207 ]
4208 );
4209 });
4210
4211 _ = editor.update(cx, |editor, window, cx| {
4212 editor.move_line_up(&MoveLineUp, window, cx);
4213 assert_eq!(
4214 editor.display_text(cx),
4215 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4216 );
4217 assert_eq!(
4218 editor.selections.display_ranges(cx),
4219 vec![
4220 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4221 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4222 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4223 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4224 ]
4225 );
4226 });
4227}
4228
4229#[gpui::test]
4230fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4231 init_test(cx, |_| {});
4232
4233 let editor = cx.add_window(|window, cx| {
4234 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4235 build_editor(buffer, window, cx)
4236 });
4237 _ = editor.update(cx, |editor, window, cx| {
4238 let snapshot = editor.buffer.read(cx).snapshot(cx);
4239 editor.insert_blocks(
4240 [BlockProperties {
4241 style: BlockStyle::Fixed,
4242 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4243 height: 1,
4244 render: Arc::new(|_| div().into_any()),
4245 priority: 0,
4246 }],
4247 Some(Autoscroll::fit()),
4248 cx,
4249 );
4250 editor.change_selections(None, window, cx, |s| {
4251 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4252 });
4253 editor.move_line_down(&MoveLineDown, window, cx);
4254 });
4255}
4256
4257#[gpui::test]
4258async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4259 init_test(cx, |_| {});
4260
4261 let mut cx = EditorTestContext::new(cx).await;
4262 cx.set_state(
4263 &"
4264 ˇzero
4265 one
4266 two
4267 three
4268 four
4269 five
4270 "
4271 .unindent(),
4272 );
4273
4274 // Create a four-line block that replaces three lines of text.
4275 cx.update_editor(|editor, window, cx| {
4276 let snapshot = editor.snapshot(window, cx);
4277 let snapshot = &snapshot.buffer_snapshot;
4278 let placement = BlockPlacement::Replace(
4279 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4280 );
4281 editor.insert_blocks(
4282 [BlockProperties {
4283 placement,
4284 height: 4,
4285 style: BlockStyle::Sticky,
4286 render: Arc::new(|_| gpui::div().into_any_element()),
4287 priority: 0,
4288 }],
4289 None,
4290 cx,
4291 );
4292 });
4293
4294 // Move down so that the cursor touches the block.
4295 cx.update_editor(|editor, window, cx| {
4296 editor.move_down(&Default::default(), window, cx);
4297 });
4298 cx.assert_editor_state(
4299 &"
4300 zero
4301 «one
4302 two
4303 threeˇ»
4304 four
4305 five
4306 "
4307 .unindent(),
4308 );
4309
4310 // Move down past the block.
4311 cx.update_editor(|editor, window, cx| {
4312 editor.move_down(&Default::default(), window, cx);
4313 });
4314 cx.assert_editor_state(
4315 &"
4316 zero
4317 one
4318 two
4319 three
4320 ˇfour
4321 five
4322 "
4323 .unindent(),
4324 );
4325}
4326
4327#[gpui::test]
4328fn test_transpose(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 _ = cx.add_window(|window, cx| {
4332 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4333 editor.set_style(EditorStyle::default(), window, cx);
4334 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4335 editor.transpose(&Default::default(), window, cx);
4336 assert_eq!(editor.text(cx), "bac");
4337 assert_eq!(editor.selections.ranges(cx), [2..2]);
4338
4339 editor.transpose(&Default::default(), window, cx);
4340 assert_eq!(editor.text(cx), "bca");
4341 assert_eq!(editor.selections.ranges(cx), [3..3]);
4342
4343 editor.transpose(&Default::default(), window, cx);
4344 assert_eq!(editor.text(cx), "bac");
4345 assert_eq!(editor.selections.ranges(cx), [3..3]);
4346
4347 editor
4348 });
4349
4350 _ = cx.add_window(|window, cx| {
4351 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4352 editor.set_style(EditorStyle::default(), window, cx);
4353 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4354 editor.transpose(&Default::default(), window, cx);
4355 assert_eq!(editor.text(cx), "acb\nde");
4356 assert_eq!(editor.selections.ranges(cx), [3..3]);
4357
4358 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4359 editor.transpose(&Default::default(), window, cx);
4360 assert_eq!(editor.text(cx), "acbd\ne");
4361 assert_eq!(editor.selections.ranges(cx), [5..5]);
4362
4363 editor.transpose(&Default::default(), window, cx);
4364 assert_eq!(editor.text(cx), "acbde\n");
4365 assert_eq!(editor.selections.ranges(cx), [6..6]);
4366
4367 editor.transpose(&Default::default(), window, cx);
4368 assert_eq!(editor.text(cx), "acbd\ne");
4369 assert_eq!(editor.selections.ranges(cx), [6..6]);
4370
4371 editor
4372 });
4373
4374 _ = cx.add_window(|window, cx| {
4375 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4376 editor.set_style(EditorStyle::default(), window, cx);
4377 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4378 editor.transpose(&Default::default(), window, cx);
4379 assert_eq!(editor.text(cx), "bacd\ne");
4380 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4381
4382 editor.transpose(&Default::default(), window, cx);
4383 assert_eq!(editor.text(cx), "bcade\n");
4384 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4385
4386 editor.transpose(&Default::default(), window, cx);
4387 assert_eq!(editor.text(cx), "bcda\ne");
4388 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4389
4390 editor.transpose(&Default::default(), window, cx);
4391 assert_eq!(editor.text(cx), "bcade\n");
4392 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4393
4394 editor.transpose(&Default::default(), window, cx);
4395 assert_eq!(editor.text(cx), "bcaed\n");
4396 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4397
4398 editor
4399 });
4400
4401 _ = cx.add_window(|window, cx| {
4402 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4403 editor.set_style(EditorStyle::default(), window, cx);
4404 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4405 editor.transpose(&Default::default(), window, cx);
4406 assert_eq!(editor.text(cx), "🏀🍐✋");
4407 assert_eq!(editor.selections.ranges(cx), [8..8]);
4408
4409 editor.transpose(&Default::default(), window, cx);
4410 assert_eq!(editor.text(cx), "🏀✋🍐");
4411 assert_eq!(editor.selections.ranges(cx), [11..11]);
4412
4413 editor.transpose(&Default::default(), window, cx);
4414 assert_eq!(editor.text(cx), "🏀🍐✋");
4415 assert_eq!(editor.selections.ranges(cx), [11..11]);
4416
4417 editor
4418 });
4419}
4420
4421#[gpui::test]
4422async fn test_rewrap(cx: &mut TestAppContext) {
4423 init_test(cx, |settings| {
4424 settings.languages.extend([
4425 (
4426 "Markdown".into(),
4427 LanguageSettingsContent {
4428 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4429 ..Default::default()
4430 },
4431 ),
4432 (
4433 "Plain Text".into(),
4434 LanguageSettingsContent {
4435 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4436 ..Default::default()
4437 },
4438 ),
4439 ])
4440 });
4441
4442 let mut cx = EditorTestContext::new(cx).await;
4443
4444 let language_with_c_comments = Arc::new(Language::new(
4445 LanguageConfig {
4446 line_comments: vec!["// ".into()],
4447 ..LanguageConfig::default()
4448 },
4449 None,
4450 ));
4451 let language_with_pound_comments = Arc::new(Language::new(
4452 LanguageConfig {
4453 line_comments: vec!["# ".into()],
4454 ..LanguageConfig::default()
4455 },
4456 None,
4457 ));
4458 let markdown_language = Arc::new(Language::new(
4459 LanguageConfig {
4460 name: "Markdown".into(),
4461 ..LanguageConfig::default()
4462 },
4463 None,
4464 ));
4465 let language_with_doc_comments = Arc::new(Language::new(
4466 LanguageConfig {
4467 line_comments: vec!["// ".into(), "/// ".into()],
4468 ..LanguageConfig::default()
4469 },
4470 Some(tree_sitter_rust::LANGUAGE.into()),
4471 ));
4472
4473 let plaintext_language = Arc::new(Language::new(
4474 LanguageConfig {
4475 name: "Plain Text".into(),
4476 ..LanguageConfig::default()
4477 },
4478 None,
4479 ));
4480
4481 assert_rewrap(
4482 indoc! {"
4483 // ˇ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.
4484 "},
4485 indoc! {"
4486 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4487 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4488 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4489 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4490 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4491 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4492 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4493 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4494 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4495 // porttitor id. Aliquam id accumsan eros.
4496 "},
4497 language_with_c_comments.clone(),
4498 &mut cx,
4499 );
4500
4501 // Test that rewrapping works inside of a selection
4502 assert_rewrap(
4503 indoc! {"
4504 «// 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.ˇ»
4505 "},
4506 indoc! {"
4507 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4508 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4509 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4510 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4511 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4512 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4513 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4514 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4515 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4516 // porttitor id. Aliquam id accumsan eros.ˇ»
4517 "},
4518 language_with_c_comments.clone(),
4519 &mut cx,
4520 );
4521
4522 // Test that cursors that expand to the same region are collapsed.
4523 assert_rewrap(
4524 indoc! {"
4525 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4526 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4527 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4528 // ˇ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.
4529 "},
4530 indoc! {"
4531 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4532 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4533 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4534 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4535 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4536 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4537 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4538 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4539 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4540 // porttitor id. Aliquam id accumsan eros.
4541 "},
4542 language_with_c_comments.clone(),
4543 &mut cx,
4544 );
4545
4546 // Test that non-contiguous selections are treated separately.
4547 assert_rewrap(
4548 indoc! {"
4549 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4550 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4551 //
4552 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4553 // ˇ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.
4554 "},
4555 indoc! {"
4556 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4557 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4558 // auctor, eu lacinia sapien scelerisque.
4559 //
4560 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4561 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4562 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4563 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4564 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4565 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4566 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4567 "},
4568 language_with_c_comments.clone(),
4569 &mut cx,
4570 );
4571
4572 // Test that different comment prefixes are supported.
4573 assert_rewrap(
4574 indoc! {"
4575 # ˇ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.
4576 "},
4577 indoc! {"
4578 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4579 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4580 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4581 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4582 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4583 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4584 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4585 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4586 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4587 # accumsan eros.
4588 "},
4589 language_with_pound_comments.clone(),
4590 &mut cx,
4591 );
4592
4593 // Test that rewrapping is ignored outside of comments in most languages.
4594 assert_rewrap(
4595 indoc! {"
4596 /// Adds two numbers.
4597 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4598 fn add(a: u32, b: u32) -> u32 {
4599 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ˇ
4600 }
4601 "},
4602 indoc! {"
4603 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4604 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4605 fn add(a: u32, b: u32) -> u32 {
4606 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ˇ
4607 }
4608 "},
4609 language_with_doc_comments.clone(),
4610 &mut cx,
4611 );
4612
4613 // Test that rewrapping works in Markdown and Plain Text languages.
4614 assert_rewrap(
4615 indoc! {"
4616 # Hello
4617
4618 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.
4619 "},
4620 indoc! {"
4621 # Hello
4622
4623 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4624 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4625 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4626 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4627 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4628 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4629 Integer sit amet scelerisque nisi.
4630 "},
4631 markdown_language,
4632 &mut cx,
4633 );
4634
4635 assert_rewrap(
4636 indoc! {"
4637 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.
4638 "},
4639 indoc! {"
4640 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4641 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4642 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4643 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4644 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4645 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4646 Integer sit amet scelerisque nisi.
4647 "},
4648 plaintext_language,
4649 &mut cx,
4650 );
4651
4652 // Test rewrapping unaligned comments in a selection.
4653 assert_rewrap(
4654 indoc! {"
4655 fn foo() {
4656 if true {
4657 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4658 // Praesent semper egestas tellus id dignissim.ˇ»
4659 do_something();
4660 } else {
4661 //
4662 }
4663 }
4664 "},
4665 indoc! {"
4666 fn foo() {
4667 if true {
4668 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4669 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4670 // egestas tellus id dignissim.ˇ»
4671 do_something();
4672 } else {
4673 //
4674 }
4675 }
4676 "},
4677 language_with_doc_comments.clone(),
4678 &mut cx,
4679 );
4680
4681 assert_rewrap(
4682 indoc! {"
4683 fn foo() {
4684 if true {
4685 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4686 // Praesent semper egestas tellus id dignissim.»
4687 do_something();
4688 } else {
4689 //
4690 }
4691
4692 }
4693 "},
4694 indoc! {"
4695 fn foo() {
4696 if true {
4697 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4698 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4699 // egestas tellus id dignissim.»
4700 do_something();
4701 } else {
4702 //
4703 }
4704
4705 }
4706 "},
4707 language_with_doc_comments.clone(),
4708 &mut cx,
4709 );
4710
4711 #[track_caller]
4712 fn assert_rewrap(
4713 unwrapped_text: &str,
4714 wrapped_text: &str,
4715 language: Arc<Language>,
4716 cx: &mut EditorTestContext,
4717 ) {
4718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4719 cx.set_state(unwrapped_text);
4720 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4721 cx.assert_editor_state(wrapped_text);
4722 }
4723}
4724
4725#[gpui::test]
4726async fn test_hard_wrap(cx: &mut TestAppContext) {
4727 init_test(cx, |_| {});
4728 let mut cx = EditorTestContext::new(cx).await;
4729
4730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4731 cx.update_editor(|editor, _, cx| {
4732 editor.set_hard_wrap(Some(14), cx);
4733 });
4734
4735 cx.set_state(indoc!(
4736 "
4737 one two three ˇ
4738 "
4739 ));
4740 cx.simulate_input("four");
4741 cx.run_until_parked();
4742
4743 cx.assert_editor_state(indoc!(
4744 "
4745 one two three
4746 fourˇ
4747 "
4748 ));
4749
4750 cx.update_editor(|editor, window, cx| {
4751 editor.newline(&Default::default(), window, cx);
4752 });
4753 cx.run_until_parked();
4754 cx.assert_editor_state(indoc!(
4755 "
4756 one two three
4757 four
4758 ˇ
4759 "
4760 ));
4761
4762 cx.simulate_input("five");
4763 cx.run_until_parked();
4764 cx.assert_editor_state(indoc!(
4765 "
4766 one two three
4767 four
4768 fiveˇ
4769 "
4770 ));
4771
4772 cx.update_editor(|editor, window, cx| {
4773 editor.newline(&Default::default(), window, cx);
4774 });
4775 cx.run_until_parked();
4776 cx.simulate_input("# ");
4777 cx.run_until_parked();
4778 cx.assert_editor_state(indoc!(
4779 "
4780 one two three
4781 four
4782 five
4783 # ˇ
4784 "
4785 ));
4786
4787 cx.update_editor(|editor, window, cx| {
4788 editor.newline(&Default::default(), window, cx);
4789 });
4790 cx.run_until_parked();
4791 cx.assert_editor_state(indoc!(
4792 "
4793 one two three
4794 four
4795 five
4796 #\x20
4797 #ˇ
4798 "
4799 ));
4800
4801 cx.simulate_input(" 6");
4802 cx.run_until_parked();
4803 cx.assert_editor_state(indoc!(
4804 "
4805 one two three
4806 four
4807 five
4808 #
4809 # 6ˇ
4810 "
4811 ));
4812}
4813
4814#[gpui::test]
4815async fn test_clipboard(cx: &mut TestAppContext) {
4816 init_test(cx, |_| {});
4817
4818 let mut cx = EditorTestContext::new(cx).await;
4819
4820 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4821 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4822 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4823
4824 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4825 cx.set_state("two ˇfour ˇsix ˇ");
4826 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4827 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4828
4829 // Paste again but with only two cursors. Since the number of cursors doesn't
4830 // match the number of slices in the clipboard, the entire clipboard text
4831 // is pasted at each cursor.
4832 cx.set_state("ˇtwo one✅ four three six five ˇ");
4833 cx.update_editor(|e, window, cx| {
4834 e.handle_input("( ", window, cx);
4835 e.paste(&Paste, window, cx);
4836 e.handle_input(") ", window, cx);
4837 });
4838 cx.assert_editor_state(
4839 &([
4840 "( one✅ ",
4841 "three ",
4842 "five ) ˇtwo one✅ four three six five ( one✅ ",
4843 "three ",
4844 "five ) ˇ",
4845 ]
4846 .join("\n")),
4847 );
4848
4849 // Cut with three selections, one of which is full-line.
4850 cx.set_state(indoc! {"
4851 1«2ˇ»3
4852 4ˇ567
4853 «8ˇ»9"});
4854 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4855 cx.assert_editor_state(indoc! {"
4856 1ˇ3
4857 ˇ9"});
4858
4859 // Paste with three selections, noticing how the copied selection that was full-line
4860 // gets inserted before the second cursor.
4861 cx.set_state(indoc! {"
4862 1ˇ3
4863 9ˇ
4864 «oˇ»ne"});
4865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4866 cx.assert_editor_state(indoc! {"
4867 12ˇ3
4868 4567
4869 9ˇ
4870 8ˇne"});
4871
4872 // Copy with a single cursor only, which writes the whole line into the clipboard.
4873 cx.set_state(indoc! {"
4874 The quick brown
4875 fox juˇmps over
4876 the lazy dog"});
4877 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4878 assert_eq!(
4879 cx.read_from_clipboard()
4880 .and_then(|item| item.text().as_deref().map(str::to_string)),
4881 Some("fox jumps over\n".to_string())
4882 );
4883
4884 // Paste with three selections, noticing how the copied full-line selection is inserted
4885 // before the empty selections but replaces the selection that is non-empty.
4886 cx.set_state(indoc! {"
4887 Tˇhe quick brown
4888 «foˇ»x jumps over
4889 tˇhe lazy dog"});
4890 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4891 cx.assert_editor_state(indoc! {"
4892 fox jumps over
4893 Tˇhe quick brown
4894 fox jumps over
4895 ˇx jumps over
4896 fox jumps over
4897 tˇhe lazy dog"});
4898}
4899
4900#[gpui::test]
4901async fn test_copy_trim(cx: &mut TestAppContext) {
4902 init_test(cx, |_| {});
4903
4904 let mut cx = EditorTestContext::new(cx).await;
4905 cx.set_state(
4906 r#" «for selection in selections.iter() {
4907 let mut start = selection.start;
4908 let mut end = selection.end;
4909 let is_entire_line = selection.is_empty();
4910 if is_entire_line {
4911 start = Point::new(start.row, 0);ˇ»
4912 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4913 }
4914 "#,
4915 );
4916 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4917 assert_eq!(
4918 cx.read_from_clipboard()
4919 .and_then(|item| item.text().as_deref().map(str::to_string)),
4920 Some(
4921 "for selection in selections.iter() {
4922 let mut start = selection.start;
4923 let mut end = selection.end;
4924 let is_entire_line = selection.is_empty();
4925 if is_entire_line {
4926 start = Point::new(start.row, 0);"
4927 .to_string()
4928 ),
4929 "Regular copying preserves all indentation selected",
4930 );
4931 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4932 assert_eq!(
4933 cx.read_from_clipboard()
4934 .and_then(|item| item.text().as_deref().map(str::to_string)),
4935 Some(
4936 "for selection in selections.iter() {
4937let mut start = selection.start;
4938let mut end = selection.end;
4939let is_entire_line = selection.is_empty();
4940if is_entire_line {
4941 start = Point::new(start.row, 0);"
4942 .to_string()
4943 ),
4944 "Copying with stripping should strip all leading whitespaces"
4945 );
4946
4947 cx.set_state(
4948 r#" « for selection in selections.iter() {
4949 let mut start = selection.start;
4950 let mut end = selection.end;
4951 let is_entire_line = selection.is_empty();
4952 if is_entire_line {
4953 start = Point::new(start.row, 0);ˇ»
4954 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4955 }
4956 "#,
4957 );
4958 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4959 assert_eq!(
4960 cx.read_from_clipboard()
4961 .and_then(|item| item.text().as_deref().map(str::to_string)),
4962 Some(
4963 " for selection in selections.iter() {
4964 let mut start = selection.start;
4965 let mut end = selection.end;
4966 let is_entire_line = selection.is_empty();
4967 if is_entire_line {
4968 start = Point::new(start.row, 0);"
4969 .to_string()
4970 ),
4971 "Regular copying preserves all indentation selected",
4972 );
4973 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4974 assert_eq!(
4975 cx.read_from_clipboard()
4976 .and_then(|item| item.text().as_deref().map(str::to_string)),
4977 Some(
4978 "for selection in selections.iter() {
4979let mut start = selection.start;
4980let mut end = selection.end;
4981let is_entire_line = selection.is_empty();
4982if is_entire_line {
4983 start = Point::new(start.row, 0);"
4984 .to_string()
4985 ),
4986 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
4987 );
4988
4989 cx.set_state(
4990 r#" «ˇ for selection in selections.iter() {
4991 let mut start = selection.start;
4992 let mut end = selection.end;
4993 let is_entire_line = selection.is_empty();
4994 if is_entire_line {
4995 start = Point::new(start.row, 0);»
4996 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4997 }
4998 "#,
4999 );
5000 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5001 assert_eq!(
5002 cx.read_from_clipboard()
5003 .and_then(|item| item.text().as_deref().map(str::to_string)),
5004 Some(
5005 " for selection in selections.iter() {
5006 let mut start = selection.start;
5007 let mut end = selection.end;
5008 let is_entire_line = selection.is_empty();
5009 if is_entire_line {
5010 start = Point::new(start.row, 0);"
5011 .to_string()
5012 ),
5013 "Regular copying for reverse selection works the same",
5014 );
5015 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5016 assert_eq!(
5017 cx.read_from_clipboard()
5018 .and_then(|item| item.text().as_deref().map(str::to_string)),
5019 Some(
5020 "for selection in selections.iter() {
5021let mut start = selection.start;
5022let mut end = selection.end;
5023let is_entire_line = selection.is_empty();
5024if is_entire_line {
5025 start = Point::new(start.row, 0);"
5026 .to_string()
5027 ),
5028 "Copying with stripping for reverse selection works the same"
5029 );
5030
5031 cx.set_state(
5032 r#" for selection «in selections.iter() {
5033 let mut start = selection.start;
5034 let mut end = selection.end;
5035 let is_entire_line = selection.is_empty();
5036 if is_entire_line {
5037 start = Point::new(start.row, 0);ˇ»
5038 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5039 }
5040 "#,
5041 );
5042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5043 assert_eq!(
5044 cx.read_from_clipboard()
5045 .and_then(|item| item.text().as_deref().map(str::to_string)),
5046 Some(
5047 "in selections.iter() {
5048 let mut start = selection.start;
5049 let mut end = selection.end;
5050 let is_entire_line = selection.is_empty();
5051 if is_entire_line {
5052 start = Point::new(start.row, 0);"
5053 .to_string()
5054 ),
5055 "When selecting past the indent, the copying works as usual",
5056 );
5057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5058 assert_eq!(
5059 cx.read_from_clipboard()
5060 .and_then(|item| item.text().as_deref().map(str::to_string)),
5061 Some(
5062 "in selections.iter() {
5063 let mut start = selection.start;
5064 let mut end = selection.end;
5065 let is_entire_line = selection.is_empty();
5066 if is_entire_line {
5067 start = Point::new(start.row, 0);"
5068 .to_string()
5069 ),
5070 "When selecting past the indent, nothing is trimmed"
5071 );
5072}
5073
5074#[gpui::test]
5075async fn test_paste_multiline(cx: &mut TestAppContext) {
5076 init_test(cx, |_| {});
5077
5078 let mut cx = EditorTestContext::new(cx).await;
5079 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5080
5081 // Cut an indented block, without the leading whitespace.
5082 cx.set_state(indoc! {"
5083 const a: B = (
5084 c(),
5085 «d(
5086 e,
5087 f
5088 )ˇ»
5089 );
5090 "});
5091 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5092 cx.assert_editor_state(indoc! {"
5093 const a: B = (
5094 c(),
5095 ˇ
5096 );
5097 "});
5098
5099 // Paste it at the same position.
5100 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5101 cx.assert_editor_state(indoc! {"
5102 const a: B = (
5103 c(),
5104 d(
5105 e,
5106 f
5107 )ˇ
5108 );
5109 "});
5110
5111 // Paste it at a line with a lower indent level.
5112 cx.set_state(indoc! {"
5113 ˇ
5114 const a: B = (
5115 c(),
5116 );
5117 "});
5118 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5119 cx.assert_editor_state(indoc! {"
5120 d(
5121 e,
5122 f
5123 )ˇ
5124 const a: B = (
5125 c(),
5126 );
5127 "});
5128
5129 // Cut an indented block, with the leading whitespace.
5130 cx.set_state(indoc! {"
5131 const a: B = (
5132 c(),
5133 « d(
5134 e,
5135 f
5136 )
5137 ˇ»);
5138 "});
5139 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5140 cx.assert_editor_state(indoc! {"
5141 const a: B = (
5142 c(),
5143 ˇ);
5144 "});
5145
5146 // Paste it at the same position.
5147 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5148 cx.assert_editor_state(indoc! {"
5149 const a: B = (
5150 c(),
5151 d(
5152 e,
5153 f
5154 )
5155 ˇ);
5156 "});
5157
5158 // Paste it at a line with a higher indent level.
5159 cx.set_state(indoc! {"
5160 const a: B = (
5161 c(),
5162 d(
5163 e,
5164 fˇ
5165 )
5166 );
5167 "});
5168 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5169 cx.assert_editor_state(indoc! {"
5170 const a: B = (
5171 c(),
5172 d(
5173 e,
5174 f d(
5175 e,
5176 f
5177 )
5178 ˇ
5179 )
5180 );
5181 "});
5182
5183 // Copy an indented block, starting mid-line
5184 cx.set_state(indoc! {"
5185 const a: B = (
5186 c(),
5187 somethin«g(
5188 e,
5189 f
5190 )ˇ»
5191 );
5192 "});
5193 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5194
5195 // Paste it on a line with a lower indent level
5196 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5197 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5198 cx.assert_editor_state(indoc! {"
5199 const a: B = (
5200 c(),
5201 something(
5202 e,
5203 f
5204 )
5205 );
5206 g(
5207 e,
5208 f
5209 )ˇ"});
5210}
5211
5212#[gpui::test]
5213async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5214 init_test(cx, |_| {});
5215
5216 cx.write_to_clipboard(ClipboardItem::new_string(
5217 " d(\n e\n );\n".into(),
5218 ));
5219
5220 let mut cx = EditorTestContext::new(cx).await;
5221 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5222
5223 cx.set_state(indoc! {"
5224 fn a() {
5225 b();
5226 if c() {
5227 ˇ
5228 }
5229 }
5230 "});
5231
5232 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5233 cx.assert_editor_state(indoc! {"
5234 fn a() {
5235 b();
5236 if c() {
5237 d(
5238 e
5239 );
5240 ˇ
5241 }
5242 }
5243 "});
5244
5245 cx.set_state(indoc! {"
5246 fn a() {
5247 b();
5248 ˇ
5249 }
5250 "});
5251
5252 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5253 cx.assert_editor_state(indoc! {"
5254 fn a() {
5255 b();
5256 d(
5257 e
5258 );
5259 ˇ
5260 }
5261 "});
5262}
5263
5264#[gpui::test]
5265fn test_select_all(cx: &mut TestAppContext) {
5266 init_test(cx, |_| {});
5267
5268 let editor = cx.add_window(|window, cx| {
5269 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5270 build_editor(buffer, window, cx)
5271 });
5272 _ = editor.update(cx, |editor, window, cx| {
5273 editor.select_all(&SelectAll, window, cx);
5274 assert_eq!(
5275 editor.selections.display_ranges(cx),
5276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5277 );
5278 });
5279}
5280
5281#[gpui::test]
5282fn test_select_line(cx: &mut TestAppContext) {
5283 init_test(cx, |_| {});
5284
5285 let editor = cx.add_window(|window, cx| {
5286 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5287 build_editor(buffer, window, cx)
5288 });
5289 _ = editor.update(cx, |editor, window, cx| {
5290 editor.change_selections(None, window, cx, |s| {
5291 s.select_display_ranges([
5292 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5293 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5294 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5295 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5296 ])
5297 });
5298 editor.select_line(&SelectLine, window, cx);
5299 assert_eq!(
5300 editor.selections.display_ranges(cx),
5301 vec![
5302 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5303 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5304 ]
5305 );
5306 });
5307
5308 _ = editor.update(cx, |editor, window, cx| {
5309 editor.select_line(&SelectLine, window, cx);
5310 assert_eq!(
5311 editor.selections.display_ranges(cx),
5312 vec![
5313 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5314 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5315 ]
5316 );
5317 });
5318
5319 _ = editor.update(cx, |editor, window, cx| {
5320 editor.select_line(&SelectLine, window, cx);
5321 assert_eq!(
5322 editor.selections.display_ranges(cx),
5323 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5324 );
5325 });
5326}
5327
5328#[gpui::test]
5329async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5330 init_test(cx, |_| {});
5331 let mut cx = EditorTestContext::new(cx).await;
5332
5333 #[track_caller]
5334 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5335 cx.set_state(initial_state);
5336 cx.update_editor(|e, window, cx| {
5337 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5338 });
5339 cx.assert_editor_state(expected_state);
5340 }
5341
5342 // Selection starts and ends at the middle of lines, left-to-right
5343 test(
5344 &mut cx,
5345 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5346 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5347 );
5348 // Same thing, right-to-left
5349 test(
5350 &mut cx,
5351 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5352 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5353 );
5354
5355 // Whole buffer, left-to-right, last line *doesn't* end with newline
5356 test(
5357 &mut cx,
5358 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5359 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5360 );
5361 // Same thing, right-to-left
5362 test(
5363 &mut cx,
5364 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5365 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5366 );
5367
5368 // Whole buffer, left-to-right, last line ends with newline
5369 test(
5370 &mut cx,
5371 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5373 );
5374 // Same thing, right-to-left
5375 test(
5376 &mut cx,
5377 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5378 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5379 );
5380
5381 // Starts at the end of a line, ends at the start of another
5382 test(
5383 &mut cx,
5384 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5385 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5386 );
5387}
5388
5389#[gpui::test]
5390async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5391 init_test(cx, |_| {});
5392
5393 let editor = cx.add_window(|window, cx| {
5394 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5395 build_editor(buffer, window, cx)
5396 });
5397
5398 // setup
5399 _ = editor.update(cx, |editor, window, cx| {
5400 editor.fold_creases(
5401 vec![
5402 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5403 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5404 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5405 ],
5406 true,
5407 window,
5408 cx,
5409 );
5410 assert_eq!(
5411 editor.display_text(cx),
5412 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5413 );
5414 });
5415
5416 _ = editor.update(cx, |editor, window, cx| {
5417 editor.change_selections(None, window, cx, |s| {
5418 s.select_display_ranges([
5419 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5420 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5421 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5422 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5423 ])
5424 });
5425 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5426 assert_eq!(
5427 editor.display_text(cx),
5428 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5429 );
5430 });
5431 EditorTestContext::for_editor(editor, cx)
5432 .await
5433 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5434
5435 _ = editor.update(cx, |editor, window, cx| {
5436 editor.change_selections(None, window, cx, |s| {
5437 s.select_display_ranges([
5438 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5439 ])
5440 });
5441 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5442 assert_eq!(
5443 editor.display_text(cx),
5444 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5445 );
5446 assert_eq!(
5447 editor.selections.display_ranges(cx),
5448 [
5449 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5450 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5451 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5452 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5453 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5454 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5455 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5456 ]
5457 );
5458 });
5459 EditorTestContext::for_editor(editor, cx)
5460 .await
5461 .assert_editor_state(
5462 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5463 );
5464}
5465
5466#[gpui::test]
5467async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5468 init_test(cx, |_| {});
5469
5470 let mut cx = EditorTestContext::new(cx).await;
5471
5472 cx.set_state(indoc!(
5473 r#"abc
5474 defˇghi
5475
5476 jk
5477 nlmo
5478 "#
5479 ));
5480
5481 cx.update_editor(|editor, window, cx| {
5482 editor.add_selection_above(&Default::default(), window, cx);
5483 });
5484
5485 cx.assert_editor_state(indoc!(
5486 r#"abcˇ
5487 defˇghi
5488
5489 jk
5490 nlmo
5491 "#
5492 ));
5493
5494 cx.update_editor(|editor, window, cx| {
5495 editor.add_selection_above(&Default::default(), window, cx);
5496 });
5497
5498 cx.assert_editor_state(indoc!(
5499 r#"abcˇ
5500 defˇghi
5501
5502 jk
5503 nlmo
5504 "#
5505 ));
5506
5507 cx.update_editor(|editor, window, cx| {
5508 editor.add_selection_below(&Default::default(), window, cx);
5509 });
5510
5511 cx.assert_editor_state(indoc!(
5512 r#"abc
5513 defˇghi
5514
5515 jk
5516 nlmo
5517 "#
5518 ));
5519
5520 cx.update_editor(|editor, window, cx| {
5521 editor.undo_selection(&Default::default(), window, cx);
5522 });
5523
5524 cx.assert_editor_state(indoc!(
5525 r#"abcˇ
5526 defˇghi
5527
5528 jk
5529 nlmo
5530 "#
5531 ));
5532
5533 cx.update_editor(|editor, window, cx| {
5534 editor.redo_selection(&Default::default(), window, cx);
5535 });
5536
5537 cx.assert_editor_state(indoc!(
5538 r#"abc
5539 defˇghi
5540
5541 jk
5542 nlmo
5543 "#
5544 ));
5545
5546 cx.update_editor(|editor, window, cx| {
5547 editor.add_selection_below(&Default::default(), window, cx);
5548 });
5549
5550 cx.assert_editor_state(indoc!(
5551 r#"abc
5552 defˇghi
5553
5554 jk
5555 nlmˇo
5556 "#
5557 ));
5558
5559 cx.update_editor(|editor, window, cx| {
5560 editor.add_selection_below(&Default::default(), window, cx);
5561 });
5562
5563 cx.assert_editor_state(indoc!(
5564 r#"abc
5565 defˇghi
5566
5567 jk
5568 nlmˇo
5569 "#
5570 ));
5571
5572 // change selections
5573 cx.set_state(indoc!(
5574 r#"abc
5575 def«ˇg»hi
5576
5577 jk
5578 nlmo
5579 "#
5580 ));
5581
5582 cx.update_editor(|editor, window, cx| {
5583 editor.add_selection_below(&Default::default(), window, cx);
5584 });
5585
5586 cx.assert_editor_state(indoc!(
5587 r#"abc
5588 def«ˇg»hi
5589
5590 jk
5591 nlm«ˇo»
5592 "#
5593 ));
5594
5595 cx.update_editor(|editor, window, cx| {
5596 editor.add_selection_below(&Default::default(), window, cx);
5597 });
5598
5599 cx.assert_editor_state(indoc!(
5600 r#"abc
5601 def«ˇg»hi
5602
5603 jk
5604 nlm«ˇo»
5605 "#
5606 ));
5607
5608 cx.update_editor(|editor, window, cx| {
5609 editor.add_selection_above(&Default::default(), window, cx);
5610 });
5611
5612 cx.assert_editor_state(indoc!(
5613 r#"abc
5614 def«ˇg»hi
5615
5616 jk
5617 nlmo
5618 "#
5619 ));
5620
5621 cx.update_editor(|editor, window, cx| {
5622 editor.add_selection_above(&Default::default(), window, cx);
5623 });
5624
5625 cx.assert_editor_state(indoc!(
5626 r#"abc
5627 def«ˇg»hi
5628
5629 jk
5630 nlmo
5631 "#
5632 ));
5633
5634 // Change selections again
5635 cx.set_state(indoc!(
5636 r#"a«bc
5637 defgˇ»hi
5638
5639 jk
5640 nlmo
5641 "#
5642 ));
5643
5644 cx.update_editor(|editor, window, cx| {
5645 editor.add_selection_below(&Default::default(), window, cx);
5646 });
5647
5648 cx.assert_editor_state(indoc!(
5649 r#"a«bcˇ»
5650 d«efgˇ»hi
5651
5652 j«kˇ»
5653 nlmo
5654 "#
5655 ));
5656
5657 cx.update_editor(|editor, window, cx| {
5658 editor.add_selection_below(&Default::default(), window, cx);
5659 });
5660 cx.assert_editor_state(indoc!(
5661 r#"a«bcˇ»
5662 d«efgˇ»hi
5663
5664 j«kˇ»
5665 n«lmoˇ»
5666 "#
5667 ));
5668 cx.update_editor(|editor, window, cx| {
5669 editor.add_selection_above(&Default::default(), window, cx);
5670 });
5671
5672 cx.assert_editor_state(indoc!(
5673 r#"a«bcˇ»
5674 d«efgˇ»hi
5675
5676 j«kˇ»
5677 nlmo
5678 "#
5679 ));
5680
5681 // Change selections again
5682 cx.set_state(indoc!(
5683 r#"abc
5684 d«ˇefghi
5685
5686 jk
5687 nlm»o
5688 "#
5689 ));
5690
5691 cx.update_editor(|editor, window, cx| {
5692 editor.add_selection_above(&Default::default(), window, cx);
5693 });
5694
5695 cx.assert_editor_state(indoc!(
5696 r#"a«ˇbc»
5697 d«ˇef»ghi
5698
5699 j«ˇk»
5700 n«ˇlm»o
5701 "#
5702 ));
5703
5704 cx.update_editor(|editor, window, cx| {
5705 editor.add_selection_below(&Default::default(), window, cx);
5706 });
5707
5708 cx.assert_editor_state(indoc!(
5709 r#"abc
5710 d«ˇef»ghi
5711
5712 j«ˇk»
5713 n«ˇlm»o
5714 "#
5715 ));
5716}
5717
5718#[gpui::test]
5719async fn test_select_next(cx: &mut TestAppContext) {
5720 init_test(cx, |_| {});
5721
5722 let mut cx = EditorTestContext::new(cx).await;
5723 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5724
5725 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5726 .unwrap();
5727 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5728
5729 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5730 .unwrap();
5731 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5732
5733 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5734 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5735
5736 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5737 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5738
5739 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5740 .unwrap();
5741 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5742
5743 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5744 .unwrap();
5745 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5746}
5747
5748#[gpui::test]
5749async fn test_select_all_matches(cx: &mut TestAppContext) {
5750 init_test(cx, |_| {});
5751
5752 let mut cx = EditorTestContext::new(cx).await;
5753
5754 // Test caret-only selections
5755 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5756 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5757 .unwrap();
5758 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5759
5760 // Test left-to-right selections
5761 cx.set_state("abc\n«abcˇ»\nabc");
5762 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5763 .unwrap();
5764 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5765
5766 // Test right-to-left selections
5767 cx.set_state("abc\n«ˇabc»\nabc");
5768 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5769 .unwrap();
5770 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5771
5772 // Test selecting whitespace with caret selection
5773 cx.set_state("abc\nˇ abc\nabc");
5774 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5775 .unwrap();
5776 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5777
5778 // Test selecting whitespace with left-to-right selection
5779 cx.set_state("abc\n«ˇ »abc\nabc");
5780 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5781 .unwrap();
5782 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5783
5784 // Test no matches with right-to-left selection
5785 cx.set_state("abc\n« ˇ»abc\nabc");
5786 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5787 .unwrap();
5788 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5789}
5790
5791#[gpui::test]
5792async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5793 init_test(cx, |_| {});
5794
5795 let mut cx = EditorTestContext::new(cx).await;
5796 cx.set_state(
5797 r#"let foo = 2;
5798lˇet foo = 2;
5799let fooˇ = 2;
5800let foo = 2;
5801let foo = ˇ2;"#,
5802 );
5803
5804 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5805 .unwrap();
5806 cx.assert_editor_state(
5807 r#"let foo = 2;
5808«letˇ» foo = 2;
5809let «fooˇ» = 2;
5810let foo = 2;
5811let foo = «2ˇ»;"#,
5812 );
5813
5814 // noop for multiple selections with different contents
5815 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5816 .unwrap();
5817 cx.assert_editor_state(
5818 r#"let foo = 2;
5819«letˇ» foo = 2;
5820let «fooˇ» = 2;
5821let foo = 2;
5822let foo = «2ˇ»;"#,
5823 );
5824}
5825
5826#[gpui::test]
5827async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5828 init_test(cx, |_| {});
5829
5830 let mut cx =
5831 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5832
5833 cx.assert_editor_state(indoc! {"
5834 ˇbbb
5835 ccc
5836
5837 bbb
5838 ccc
5839 "});
5840 cx.dispatch_action(SelectPrevious::default());
5841 cx.assert_editor_state(indoc! {"
5842 «bbbˇ»
5843 ccc
5844
5845 bbb
5846 ccc
5847 "});
5848 cx.dispatch_action(SelectPrevious::default());
5849 cx.assert_editor_state(indoc! {"
5850 «bbbˇ»
5851 ccc
5852
5853 «bbbˇ»
5854 ccc
5855 "});
5856}
5857
5858#[gpui::test]
5859async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5860 init_test(cx, |_| {});
5861
5862 let mut cx = EditorTestContext::new(cx).await;
5863 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5864
5865 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5866 .unwrap();
5867 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5868
5869 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5870 .unwrap();
5871 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5872
5873 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5874 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5875
5876 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5877 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5878
5879 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5880 .unwrap();
5881 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5882
5883 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5884 .unwrap();
5885 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5886
5887 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5888 .unwrap();
5889 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5890}
5891
5892#[gpui::test]
5893async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5894 init_test(cx, |_| {});
5895
5896 let mut cx = EditorTestContext::new(cx).await;
5897 cx.set_state("aˇ");
5898
5899 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5900 .unwrap();
5901 cx.assert_editor_state("«aˇ»");
5902 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5903 .unwrap();
5904 cx.assert_editor_state("«aˇ»");
5905}
5906
5907#[gpui::test]
5908async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5909 init_test(cx, |_| {});
5910
5911 let mut cx = EditorTestContext::new(cx).await;
5912 cx.set_state(
5913 r#"let foo = 2;
5914lˇet foo = 2;
5915let fooˇ = 2;
5916let foo = 2;
5917let foo = ˇ2;"#,
5918 );
5919
5920 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5921 .unwrap();
5922 cx.assert_editor_state(
5923 r#"let foo = 2;
5924«letˇ» foo = 2;
5925let «fooˇ» = 2;
5926let foo = 2;
5927let foo = «2ˇ»;"#,
5928 );
5929
5930 // noop for multiple selections with different contents
5931 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5932 .unwrap();
5933 cx.assert_editor_state(
5934 r#"let foo = 2;
5935«letˇ» foo = 2;
5936let «fooˇ» = 2;
5937let foo = 2;
5938let foo = «2ˇ»;"#,
5939 );
5940}
5941
5942#[gpui::test]
5943async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5944 init_test(cx, |_| {});
5945
5946 let mut cx = EditorTestContext::new(cx).await;
5947 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5948
5949 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5950 .unwrap();
5951 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5952
5953 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5954 .unwrap();
5955 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5956
5957 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5958 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5959
5960 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5961 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5962
5963 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5964 .unwrap();
5965 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5966
5967 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5968 .unwrap();
5969 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5970}
5971
5972#[gpui::test]
5973async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5974 init_test(cx, |_| {});
5975
5976 let language = Arc::new(Language::new(
5977 LanguageConfig::default(),
5978 Some(tree_sitter_rust::LANGUAGE.into()),
5979 ));
5980
5981 let text = r#"
5982 use mod1::mod2::{mod3, mod4};
5983
5984 fn fn_1(param1: bool, param2: &str) {
5985 let var1 = "text";
5986 }
5987 "#
5988 .unindent();
5989
5990 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5991 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5992 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5993
5994 editor
5995 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5996 .await;
5997
5998 editor.update_in(cx, |editor, window, cx| {
5999 editor.change_selections(None, window, cx, |s| {
6000 s.select_display_ranges([
6001 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6002 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6003 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6004 ]);
6005 });
6006 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6007 });
6008 editor.update(cx, |editor, cx| {
6009 assert_text_with_selections(
6010 editor,
6011 indoc! {r#"
6012 use mod1::mod2::{mod3, «mod4ˇ»};
6013
6014 fn fn_1«ˇ(param1: bool, param2: &str)» {
6015 let var1 = "«ˇtext»";
6016 }
6017 "#},
6018 cx,
6019 );
6020 });
6021
6022 editor.update_in(cx, |editor, window, cx| {
6023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6024 });
6025 editor.update(cx, |editor, cx| {
6026 assert_text_with_selections(
6027 editor,
6028 indoc! {r#"
6029 use mod1::mod2::«{mod3, mod4}ˇ»;
6030
6031 «ˇfn fn_1(param1: bool, param2: &str) {
6032 let var1 = "text";
6033 }»
6034 "#},
6035 cx,
6036 );
6037 });
6038
6039 editor.update_in(cx, |editor, window, cx| {
6040 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6041 });
6042 assert_eq!(
6043 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6044 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6045 );
6046
6047 // Trying to expand the selected syntax node one more time has no effect.
6048 editor.update_in(cx, |editor, window, cx| {
6049 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6050 });
6051 assert_eq!(
6052 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6053 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6054 );
6055
6056 editor.update_in(cx, |editor, window, cx| {
6057 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6058 });
6059 editor.update(cx, |editor, cx| {
6060 assert_text_with_selections(
6061 editor,
6062 indoc! {r#"
6063 use mod1::mod2::«{mod3, mod4}ˇ»;
6064
6065 «ˇfn fn_1(param1: bool, param2: &str) {
6066 let var1 = "text";
6067 }»
6068 "#},
6069 cx,
6070 );
6071 });
6072
6073 editor.update_in(cx, |editor, window, cx| {
6074 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6075 });
6076 editor.update(cx, |editor, cx| {
6077 assert_text_with_selections(
6078 editor,
6079 indoc! {r#"
6080 use mod1::mod2::{mod3, «mod4ˇ»};
6081
6082 fn fn_1«ˇ(param1: bool, param2: &str)» {
6083 let var1 = "«ˇtext»";
6084 }
6085 "#},
6086 cx,
6087 );
6088 });
6089
6090 editor.update_in(cx, |editor, window, cx| {
6091 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6092 });
6093 editor.update(cx, |editor, cx| {
6094 assert_text_with_selections(
6095 editor,
6096 indoc! {r#"
6097 use mod1::mod2::{mod3, mo«ˇ»d4};
6098
6099 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6100 let var1 = "te«ˇ»xt";
6101 }
6102 "#},
6103 cx,
6104 );
6105 });
6106
6107 // Trying to shrink the selected syntax node one more time has no effect.
6108 editor.update_in(cx, |editor, window, cx| {
6109 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6110 });
6111 editor.update_in(cx, |editor, _, cx| {
6112 assert_text_with_selections(
6113 editor,
6114 indoc! {r#"
6115 use mod1::mod2::{mod3, mo«ˇ»d4};
6116
6117 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6118 let var1 = "te«ˇ»xt";
6119 }
6120 "#},
6121 cx,
6122 );
6123 });
6124
6125 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6126 // a fold.
6127 editor.update_in(cx, |editor, window, cx| {
6128 editor.fold_creases(
6129 vec![
6130 Crease::simple(
6131 Point::new(0, 21)..Point::new(0, 24),
6132 FoldPlaceholder::test(),
6133 ),
6134 Crease::simple(
6135 Point::new(3, 20)..Point::new(3, 22),
6136 FoldPlaceholder::test(),
6137 ),
6138 ],
6139 true,
6140 window,
6141 cx,
6142 );
6143 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6144 });
6145 editor.update(cx, |editor, cx| {
6146 assert_text_with_selections(
6147 editor,
6148 indoc! {r#"
6149 use mod1::mod2::«{mod3, mod4}ˇ»;
6150
6151 fn fn_1«ˇ(param1: bool, param2: &str)» {
6152 «ˇlet var1 = "text";»
6153 }
6154 "#},
6155 cx,
6156 );
6157 });
6158}
6159
6160#[gpui::test]
6161async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6162 init_test(cx, |_| {});
6163
6164 let base_text = r#"
6165 impl A {
6166 // this is an uncommitted comment
6167
6168 fn b() {
6169 c();
6170 }
6171
6172 // this is another uncommitted comment
6173
6174 fn d() {
6175 // e
6176 // f
6177 }
6178 }
6179
6180 fn g() {
6181 // h
6182 }
6183 "#
6184 .unindent();
6185
6186 let text = r#"
6187 ˇimpl A {
6188
6189 fn b() {
6190 c();
6191 }
6192
6193 fn d() {
6194 // e
6195 // f
6196 }
6197 }
6198
6199 fn g() {
6200 // h
6201 }
6202 "#
6203 .unindent();
6204
6205 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6206 cx.set_state(&text);
6207 cx.set_head_text(&base_text);
6208 cx.update_editor(|editor, window, cx| {
6209 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6210 });
6211
6212 cx.assert_state_with_diff(
6213 "
6214 ˇimpl A {
6215 - // this is an uncommitted comment
6216
6217 fn b() {
6218 c();
6219 }
6220
6221 - // this is another uncommitted comment
6222 -
6223 fn d() {
6224 // e
6225 // f
6226 }
6227 }
6228
6229 fn g() {
6230 // h
6231 }
6232 "
6233 .unindent(),
6234 );
6235
6236 let expected_display_text = "
6237 impl A {
6238 // this is an uncommitted comment
6239
6240 fn b() {
6241 ⋯
6242 }
6243
6244 // this is another uncommitted comment
6245
6246 fn d() {
6247 ⋯
6248 }
6249 }
6250
6251 fn g() {
6252 ⋯
6253 }
6254 "
6255 .unindent();
6256
6257 cx.update_editor(|editor, window, cx| {
6258 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6259 assert_eq!(editor.display_text(cx), expected_display_text);
6260 });
6261}
6262
6263#[gpui::test]
6264async fn test_autoindent(cx: &mut TestAppContext) {
6265 init_test(cx, |_| {});
6266
6267 let language = Arc::new(
6268 Language::new(
6269 LanguageConfig {
6270 brackets: BracketPairConfig {
6271 pairs: vec![
6272 BracketPair {
6273 start: "{".to_string(),
6274 end: "}".to_string(),
6275 close: false,
6276 surround: false,
6277 newline: true,
6278 },
6279 BracketPair {
6280 start: "(".to_string(),
6281 end: ")".to_string(),
6282 close: false,
6283 surround: false,
6284 newline: true,
6285 },
6286 ],
6287 ..Default::default()
6288 },
6289 ..Default::default()
6290 },
6291 Some(tree_sitter_rust::LANGUAGE.into()),
6292 )
6293 .with_indents_query(
6294 r#"
6295 (_ "(" ")" @end) @indent
6296 (_ "{" "}" @end) @indent
6297 "#,
6298 )
6299 .unwrap(),
6300 );
6301
6302 let text = "fn a() {}";
6303
6304 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6305 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6306 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6307 editor
6308 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6309 .await;
6310
6311 editor.update_in(cx, |editor, window, cx| {
6312 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6313 editor.newline(&Newline, window, cx);
6314 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6315 assert_eq!(
6316 editor.selections.ranges(cx),
6317 &[
6318 Point::new(1, 4)..Point::new(1, 4),
6319 Point::new(3, 4)..Point::new(3, 4),
6320 Point::new(5, 0)..Point::new(5, 0)
6321 ]
6322 );
6323 });
6324}
6325
6326#[gpui::test]
6327async fn test_autoindent_selections(cx: &mut TestAppContext) {
6328 init_test(cx, |_| {});
6329
6330 {
6331 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6332 cx.set_state(indoc! {"
6333 impl A {
6334
6335 fn b() {}
6336
6337 «fn c() {
6338
6339 }ˇ»
6340 }
6341 "});
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.autoindent(&Default::default(), window, cx);
6345 });
6346
6347 cx.assert_editor_state(indoc! {"
6348 impl A {
6349
6350 fn b() {}
6351
6352 «fn c() {
6353
6354 }ˇ»
6355 }
6356 "});
6357 }
6358
6359 {
6360 let mut cx = EditorTestContext::new_multibuffer(
6361 cx,
6362 [indoc! { "
6363 impl A {
6364 «
6365 // a
6366 fn b(){}
6367 »
6368 «
6369 }
6370 fn c(){}
6371 »
6372 "}],
6373 );
6374
6375 let buffer = cx.update_editor(|editor, _, cx| {
6376 let buffer = editor.buffer().update(cx, |buffer, _| {
6377 buffer.all_buffers().iter().next().unwrap().clone()
6378 });
6379 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6380 buffer
6381 });
6382
6383 cx.run_until_parked();
6384 cx.update_editor(|editor, window, cx| {
6385 editor.select_all(&Default::default(), window, cx);
6386 editor.autoindent(&Default::default(), window, cx)
6387 });
6388 cx.run_until_parked();
6389
6390 cx.update(|_, cx| {
6391 pretty_assertions::assert_eq!(
6392 buffer.read(cx).text(),
6393 indoc! { "
6394 impl A {
6395
6396 // a
6397 fn b(){}
6398
6399
6400 }
6401 fn c(){}
6402
6403 " }
6404 )
6405 });
6406 }
6407}
6408
6409#[gpui::test]
6410async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6411 init_test(cx, |_| {});
6412
6413 let mut cx = EditorTestContext::new(cx).await;
6414
6415 let language = Arc::new(Language::new(
6416 LanguageConfig {
6417 brackets: BracketPairConfig {
6418 pairs: vec![
6419 BracketPair {
6420 start: "{".to_string(),
6421 end: "}".to_string(),
6422 close: true,
6423 surround: true,
6424 newline: true,
6425 },
6426 BracketPair {
6427 start: "(".to_string(),
6428 end: ")".to_string(),
6429 close: true,
6430 surround: true,
6431 newline: true,
6432 },
6433 BracketPair {
6434 start: "/*".to_string(),
6435 end: " */".to_string(),
6436 close: true,
6437 surround: true,
6438 newline: true,
6439 },
6440 BracketPair {
6441 start: "[".to_string(),
6442 end: "]".to_string(),
6443 close: false,
6444 surround: false,
6445 newline: true,
6446 },
6447 BracketPair {
6448 start: "\"".to_string(),
6449 end: "\"".to_string(),
6450 close: true,
6451 surround: true,
6452 newline: false,
6453 },
6454 BracketPair {
6455 start: "<".to_string(),
6456 end: ">".to_string(),
6457 close: false,
6458 surround: true,
6459 newline: true,
6460 },
6461 ],
6462 ..Default::default()
6463 },
6464 autoclose_before: "})]".to_string(),
6465 ..Default::default()
6466 },
6467 Some(tree_sitter_rust::LANGUAGE.into()),
6468 ));
6469
6470 cx.language_registry().add(language.clone());
6471 cx.update_buffer(|buffer, cx| {
6472 buffer.set_language(Some(language), cx);
6473 });
6474
6475 cx.set_state(
6476 &r#"
6477 🏀ˇ
6478 εˇ
6479 ❤️ˇ
6480 "#
6481 .unindent(),
6482 );
6483
6484 // autoclose multiple nested brackets at multiple cursors
6485 cx.update_editor(|editor, window, cx| {
6486 editor.handle_input("{", window, cx);
6487 editor.handle_input("{", window, cx);
6488 editor.handle_input("{", window, cx);
6489 });
6490 cx.assert_editor_state(
6491 &"
6492 🏀{{{ˇ}}}
6493 ε{{{ˇ}}}
6494 ❤️{{{ˇ}}}
6495 "
6496 .unindent(),
6497 );
6498
6499 // insert a different closing bracket
6500 cx.update_editor(|editor, window, cx| {
6501 editor.handle_input(")", window, cx);
6502 });
6503 cx.assert_editor_state(
6504 &"
6505 🏀{{{)ˇ}}}
6506 ε{{{)ˇ}}}
6507 ❤️{{{)ˇ}}}
6508 "
6509 .unindent(),
6510 );
6511
6512 // skip over the auto-closed brackets when typing a closing bracket
6513 cx.update_editor(|editor, window, cx| {
6514 editor.move_right(&MoveRight, window, cx);
6515 editor.handle_input("}", window, cx);
6516 editor.handle_input("}", window, cx);
6517 editor.handle_input("}", window, cx);
6518 });
6519 cx.assert_editor_state(
6520 &"
6521 🏀{{{)}}}}ˇ
6522 ε{{{)}}}}ˇ
6523 ❤️{{{)}}}}ˇ
6524 "
6525 .unindent(),
6526 );
6527
6528 // autoclose multi-character pairs
6529 cx.set_state(
6530 &"
6531 ˇ
6532 ˇ
6533 "
6534 .unindent(),
6535 );
6536 cx.update_editor(|editor, window, cx| {
6537 editor.handle_input("/", window, cx);
6538 editor.handle_input("*", window, cx);
6539 });
6540 cx.assert_editor_state(
6541 &"
6542 /*ˇ */
6543 /*ˇ */
6544 "
6545 .unindent(),
6546 );
6547
6548 // one cursor autocloses a multi-character pair, one cursor
6549 // does not autoclose.
6550 cx.set_state(
6551 &"
6552 /ˇ
6553 ˇ
6554 "
6555 .unindent(),
6556 );
6557 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6558 cx.assert_editor_state(
6559 &"
6560 /*ˇ */
6561 *ˇ
6562 "
6563 .unindent(),
6564 );
6565
6566 // Don't autoclose if the next character isn't whitespace and isn't
6567 // listed in the language's "autoclose_before" section.
6568 cx.set_state("ˇa b");
6569 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6570 cx.assert_editor_state("{ˇa b");
6571
6572 // Don't autoclose if `close` is false for the bracket pair
6573 cx.set_state("ˇ");
6574 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6575 cx.assert_editor_state("[ˇ");
6576
6577 // Surround with brackets if text is selected
6578 cx.set_state("«aˇ» b");
6579 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6580 cx.assert_editor_state("{«aˇ»} b");
6581
6582 // Autoclose when not immediately after a word character
6583 cx.set_state("a ˇ");
6584 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6585 cx.assert_editor_state("a \"ˇ\"");
6586
6587 // Autoclose pair where the start and end characters are the same
6588 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6589 cx.assert_editor_state("a \"\"ˇ");
6590
6591 // Don't autoclose when immediately after a word character
6592 cx.set_state("aˇ");
6593 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6594 cx.assert_editor_state("a\"ˇ");
6595
6596 // Do autoclose when after a non-word character
6597 cx.set_state("{ˇ");
6598 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6599 cx.assert_editor_state("{\"ˇ\"");
6600
6601 // Non identical pairs autoclose regardless of preceding character
6602 cx.set_state("aˇ");
6603 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6604 cx.assert_editor_state("a{ˇ}");
6605
6606 // Don't autoclose pair if autoclose is disabled
6607 cx.set_state("ˇ");
6608 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6609 cx.assert_editor_state("<ˇ");
6610
6611 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6612 cx.set_state("«aˇ» b");
6613 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6614 cx.assert_editor_state("<«aˇ»> b");
6615}
6616
6617#[gpui::test]
6618async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6619 init_test(cx, |settings| {
6620 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6621 });
6622
6623 let mut cx = EditorTestContext::new(cx).await;
6624
6625 let language = Arc::new(Language::new(
6626 LanguageConfig {
6627 brackets: BracketPairConfig {
6628 pairs: vec![
6629 BracketPair {
6630 start: "{".to_string(),
6631 end: "}".to_string(),
6632 close: true,
6633 surround: true,
6634 newline: true,
6635 },
6636 BracketPair {
6637 start: "(".to_string(),
6638 end: ")".to_string(),
6639 close: true,
6640 surround: true,
6641 newline: true,
6642 },
6643 BracketPair {
6644 start: "[".to_string(),
6645 end: "]".to_string(),
6646 close: false,
6647 surround: false,
6648 newline: true,
6649 },
6650 ],
6651 ..Default::default()
6652 },
6653 autoclose_before: "})]".to_string(),
6654 ..Default::default()
6655 },
6656 Some(tree_sitter_rust::LANGUAGE.into()),
6657 ));
6658
6659 cx.language_registry().add(language.clone());
6660 cx.update_buffer(|buffer, cx| {
6661 buffer.set_language(Some(language), cx);
6662 });
6663
6664 cx.set_state(
6665 &"
6666 ˇ
6667 ˇ
6668 ˇ
6669 "
6670 .unindent(),
6671 );
6672
6673 // ensure only matching closing brackets are skipped over
6674 cx.update_editor(|editor, window, cx| {
6675 editor.handle_input("}", window, cx);
6676 editor.move_left(&MoveLeft, window, cx);
6677 editor.handle_input(")", window, cx);
6678 editor.move_left(&MoveLeft, window, cx);
6679 });
6680 cx.assert_editor_state(
6681 &"
6682 ˇ)}
6683 ˇ)}
6684 ˇ)}
6685 "
6686 .unindent(),
6687 );
6688
6689 // skip-over closing brackets at multiple cursors
6690 cx.update_editor(|editor, window, cx| {
6691 editor.handle_input(")", window, cx);
6692 editor.handle_input("}", window, cx);
6693 });
6694 cx.assert_editor_state(
6695 &"
6696 )}ˇ
6697 )}ˇ
6698 )}ˇ
6699 "
6700 .unindent(),
6701 );
6702
6703 // ignore non-close brackets
6704 cx.update_editor(|editor, window, cx| {
6705 editor.handle_input("]", window, cx);
6706 editor.move_left(&MoveLeft, window, cx);
6707 editor.handle_input("]", window, cx);
6708 });
6709 cx.assert_editor_state(
6710 &"
6711 )}]ˇ]
6712 )}]ˇ]
6713 )}]ˇ]
6714 "
6715 .unindent(),
6716 );
6717}
6718
6719#[gpui::test]
6720async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722
6723 let mut cx = EditorTestContext::new(cx).await;
6724
6725 let html_language = Arc::new(
6726 Language::new(
6727 LanguageConfig {
6728 name: "HTML".into(),
6729 brackets: BracketPairConfig {
6730 pairs: vec![
6731 BracketPair {
6732 start: "<".into(),
6733 end: ">".into(),
6734 close: true,
6735 ..Default::default()
6736 },
6737 BracketPair {
6738 start: "{".into(),
6739 end: "}".into(),
6740 close: true,
6741 ..Default::default()
6742 },
6743 BracketPair {
6744 start: "(".into(),
6745 end: ")".into(),
6746 close: true,
6747 ..Default::default()
6748 },
6749 ],
6750 ..Default::default()
6751 },
6752 autoclose_before: "})]>".into(),
6753 ..Default::default()
6754 },
6755 Some(tree_sitter_html::LANGUAGE.into()),
6756 )
6757 .with_injection_query(
6758 r#"
6759 (script_element
6760 (raw_text) @injection.content
6761 (#set! injection.language "javascript"))
6762 "#,
6763 )
6764 .unwrap(),
6765 );
6766
6767 let javascript_language = Arc::new(Language::new(
6768 LanguageConfig {
6769 name: "JavaScript".into(),
6770 brackets: BracketPairConfig {
6771 pairs: vec![
6772 BracketPair {
6773 start: "/*".into(),
6774 end: " */".into(),
6775 close: true,
6776 ..Default::default()
6777 },
6778 BracketPair {
6779 start: "{".into(),
6780 end: "}".into(),
6781 close: true,
6782 ..Default::default()
6783 },
6784 BracketPair {
6785 start: "(".into(),
6786 end: ")".into(),
6787 close: true,
6788 ..Default::default()
6789 },
6790 ],
6791 ..Default::default()
6792 },
6793 autoclose_before: "})]>".into(),
6794 ..Default::default()
6795 },
6796 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6797 ));
6798
6799 cx.language_registry().add(html_language.clone());
6800 cx.language_registry().add(javascript_language.clone());
6801
6802 cx.update_buffer(|buffer, cx| {
6803 buffer.set_language(Some(html_language), cx);
6804 });
6805
6806 cx.set_state(
6807 &r#"
6808 <body>ˇ
6809 <script>
6810 var x = 1;ˇ
6811 </script>
6812 </body>ˇ
6813 "#
6814 .unindent(),
6815 );
6816
6817 // Precondition: different languages are active at different locations.
6818 cx.update_editor(|editor, window, cx| {
6819 let snapshot = editor.snapshot(window, cx);
6820 let cursors = editor.selections.ranges::<usize>(cx);
6821 let languages = cursors
6822 .iter()
6823 .map(|c| snapshot.language_at(c.start).unwrap().name())
6824 .collect::<Vec<_>>();
6825 assert_eq!(
6826 languages,
6827 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6828 );
6829 });
6830
6831 // Angle brackets autoclose in HTML, but not JavaScript.
6832 cx.update_editor(|editor, window, cx| {
6833 editor.handle_input("<", window, cx);
6834 editor.handle_input("a", window, cx);
6835 });
6836 cx.assert_editor_state(
6837 &r#"
6838 <body><aˇ>
6839 <script>
6840 var x = 1;<aˇ
6841 </script>
6842 </body><aˇ>
6843 "#
6844 .unindent(),
6845 );
6846
6847 // Curly braces and parens autoclose in both HTML and JavaScript.
6848 cx.update_editor(|editor, window, cx| {
6849 editor.handle_input(" b=", window, cx);
6850 editor.handle_input("{", window, cx);
6851 editor.handle_input("c", window, cx);
6852 editor.handle_input("(", window, cx);
6853 });
6854 cx.assert_editor_state(
6855 &r#"
6856 <body><a b={c(ˇ)}>
6857 <script>
6858 var x = 1;<a b={c(ˇ)}
6859 </script>
6860 </body><a b={c(ˇ)}>
6861 "#
6862 .unindent(),
6863 );
6864
6865 // Brackets that were already autoclosed are skipped.
6866 cx.update_editor(|editor, window, cx| {
6867 editor.handle_input(")", window, cx);
6868 editor.handle_input("d", window, cx);
6869 editor.handle_input("}", window, cx);
6870 });
6871 cx.assert_editor_state(
6872 &r#"
6873 <body><a b={c()d}ˇ>
6874 <script>
6875 var x = 1;<a b={c()d}ˇ
6876 </script>
6877 </body><a b={c()d}ˇ>
6878 "#
6879 .unindent(),
6880 );
6881 cx.update_editor(|editor, window, cx| {
6882 editor.handle_input(">", window, cx);
6883 });
6884 cx.assert_editor_state(
6885 &r#"
6886 <body><a b={c()d}>ˇ
6887 <script>
6888 var x = 1;<a b={c()d}>ˇ
6889 </script>
6890 </body><a b={c()d}>ˇ
6891 "#
6892 .unindent(),
6893 );
6894
6895 // Reset
6896 cx.set_state(
6897 &r#"
6898 <body>ˇ
6899 <script>
6900 var x = 1;ˇ
6901 </script>
6902 </body>ˇ
6903 "#
6904 .unindent(),
6905 );
6906
6907 cx.update_editor(|editor, window, cx| {
6908 editor.handle_input("<", window, cx);
6909 });
6910 cx.assert_editor_state(
6911 &r#"
6912 <body><ˇ>
6913 <script>
6914 var x = 1;<ˇ
6915 </script>
6916 </body><ˇ>
6917 "#
6918 .unindent(),
6919 );
6920
6921 // When backspacing, the closing angle brackets are removed.
6922 cx.update_editor(|editor, window, cx| {
6923 editor.backspace(&Backspace, window, cx);
6924 });
6925 cx.assert_editor_state(
6926 &r#"
6927 <body>ˇ
6928 <script>
6929 var x = 1;ˇ
6930 </script>
6931 </body>ˇ
6932 "#
6933 .unindent(),
6934 );
6935
6936 // Block comments autoclose in JavaScript, but not HTML.
6937 cx.update_editor(|editor, window, cx| {
6938 editor.handle_input("/", window, cx);
6939 editor.handle_input("*", window, cx);
6940 });
6941 cx.assert_editor_state(
6942 &r#"
6943 <body>/*ˇ
6944 <script>
6945 var x = 1;/*ˇ */
6946 </script>
6947 </body>/*ˇ
6948 "#
6949 .unindent(),
6950 );
6951}
6952
6953#[gpui::test]
6954async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6955 init_test(cx, |_| {});
6956
6957 let mut cx = EditorTestContext::new(cx).await;
6958
6959 let rust_language = Arc::new(
6960 Language::new(
6961 LanguageConfig {
6962 name: "Rust".into(),
6963 brackets: serde_json::from_value(json!([
6964 { "start": "{", "end": "}", "close": true, "newline": true },
6965 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6966 ]))
6967 .unwrap(),
6968 autoclose_before: "})]>".into(),
6969 ..Default::default()
6970 },
6971 Some(tree_sitter_rust::LANGUAGE.into()),
6972 )
6973 .with_override_query("(string_literal) @string")
6974 .unwrap(),
6975 );
6976
6977 cx.language_registry().add(rust_language.clone());
6978 cx.update_buffer(|buffer, cx| {
6979 buffer.set_language(Some(rust_language), cx);
6980 });
6981
6982 cx.set_state(
6983 &r#"
6984 let x = ˇ
6985 "#
6986 .unindent(),
6987 );
6988
6989 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6990 cx.update_editor(|editor, window, cx| {
6991 editor.handle_input("\"", window, cx);
6992 });
6993 cx.assert_editor_state(
6994 &r#"
6995 let x = "ˇ"
6996 "#
6997 .unindent(),
6998 );
6999
7000 // Inserting another quotation mark. The cursor moves across the existing
7001 // automatically-inserted quotation mark.
7002 cx.update_editor(|editor, window, cx| {
7003 editor.handle_input("\"", window, cx);
7004 });
7005 cx.assert_editor_state(
7006 &r#"
7007 let x = ""ˇ
7008 "#
7009 .unindent(),
7010 );
7011
7012 // Reset
7013 cx.set_state(
7014 &r#"
7015 let x = ˇ
7016 "#
7017 .unindent(),
7018 );
7019
7020 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7021 cx.update_editor(|editor, window, cx| {
7022 editor.handle_input("\"", window, cx);
7023 editor.handle_input(" ", window, cx);
7024 editor.move_left(&Default::default(), window, cx);
7025 editor.handle_input("\\", window, cx);
7026 editor.handle_input("\"", window, cx);
7027 });
7028 cx.assert_editor_state(
7029 &r#"
7030 let x = "\"ˇ "
7031 "#
7032 .unindent(),
7033 );
7034
7035 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7036 // mark. Nothing is inserted.
7037 cx.update_editor(|editor, window, cx| {
7038 editor.move_right(&Default::default(), window, cx);
7039 editor.handle_input("\"", window, cx);
7040 });
7041 cx.assert_editor_state(
7042 &r#"
7043 let x = "\" "ˇ
7044 "#
7045 .unindent(),
7046 );
7047}
7048
7049#[gpui::test]
7050async fn test_surround_with_pair(cx: &mut TestAppContext) {
7051 init_test(cx, |_| {});
7052
7053 let language = Arc::new(Language::new(
7054 LanguageConfig {
7055 brackets: BracketPairConfig {
7056 pairs: vec![
7057 BracketPair {
7058 start: "{".to_string(),
7059 end: "}".to_string(),
7060 close: true,
7061 surround: true,
7062 newline: true,
7063 },
7064 BracketPair {
7065 start: "/* ".to_string(),
7066 end: "*/".to_string(),
7067 close: true,
7068 surround: true,
7069 ..Default::default()
7070 },
7071 ],
7072 ..Default::default()
7073 },
7074 ..Default::default()
7075 },
7076 Some(tree_sitter_rust::LANGUAGE.into()),
7077 ));
7078
7079 let text = r#"
7080 a
7081 b
7082 c
7083 "#
7084 .unindent();
7085
7086 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7088 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7089 editor
7090 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7091 .await;
7092
7093 editor.update_in(cx, |editor, window, cx| {
7094 editor.change_selections(None, window, cx, |s| {
7095 s.select_display_ranges([
7096 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7097 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7098 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7099 ])
7100 });
7101
7102 editor.handle_input("{", window, cx);
7103 editor.handle_input("{", window, cx);
7104 editor.handle_input("{", window, cx);
7105 assert_eq!(
7106 editor.text(cx),
7107 "
7108 {{{a}}}
7109 {{{b}}}
7110 {{{c}}}
7111 "
7112 .unindent()
7113 );
7114 assert_eq!(
7115 editor.selections.display_ranges(cx),
7116 [
7117 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7118 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7119 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7120 ]
7121 );
7122
7123 editor.undo(&Undo, window, cx);
7124 editor.undo(&Undo, window, cx);
7125 editor.undo(&Undo, window, cx);
7126 assert_eq!(
7127 editor.text(cx),
7128 "
7129 a
7130 b
7131 c
7132 "
7133 .unindent()
7134 );
7135 assert_eq!(
7136 editor.selections.display_ranges(cx),
7137 [
7138 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7139 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7140 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7141 ]
7142 );
7143
7144 // Ensure inserting the first character of a multi-byte bracket pair
7145 // doesn't surround the selections with the bracket.
7146 editor.handle_input("/", window, cx);
7147 assert_eq!(
7148 editor.text(cx),
7149 "
7150 /
7151 /
7152 /
7153 "
7154 .unindent()
7155 );
7156 assert_eq!(
7157 editor.selections.display_ranges(cx),
7158 [
7159 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7160 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7161 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7162 ]
7163 );
7164
7165 editor.undo(&Undo, window, cx);
7166 assert_eq!(
7167 editor.text(cx),
7168 "
7169 a
7170 b
7171 c
7172 "
7173 .unindent()
7174 );
7175 assert_eq!(
7176 editor.selections.display_ranges(cx),
7177 [
7178 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7179 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7180 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7181 ]
7182 );
7183
7184 // Ensure inserting the last character of a multi-byte bracket pair
7185 // doesn't surround the selections with the bracket.
7186 editor.handle_input("*", window, cx);
7187 assert_eq!(
7188 editor.text(cx),
7189 "
7190 *
7191 *
7192 *
7193 "
7194 .unindent()
7195 );
7196 assert_eq!(
7197 editor.selections.display_ranges(cx),
7198 [
7199 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7200 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7201 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7202 ]
7203 );
7204 });
7205}
7206
7207#[gpui::test]
7208async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7209 init_test(cx, |_| {});
7210
7211 let language = Arc::new(Language::new(
7212 LanguageConfig {
7213 brackets: BracketPairConfig {
7214 pairs: vec![BracketPair {
7215 start: "{".to_string(),
7216 end: "}".to_string(),
7217 close: true,
7218 surround: true,
7219 newline: true,
7220 }],
7221 ..Default::default()
7222 },
7223 autoclose_before: "}".to_string(),
7224 ..Default::default()
7225 },
7226 Some(tree_sitter_rust::LANGUAGE.into()),
7227 ));
7228
7229 let text = r#"
7230 a
7231 b
7232 c
7233 "#
7234 .unindent();
7235
7236 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7237 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7238 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7239 editor
7240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7241 .await;
7242
7243 editor.update_in(cx, |editor, window, cx| {
7244 editor.change_selections(None, window, cx, |s| {
7245 s.select_ranges([
7246 Point::new(0, 1)..Point::new(0, 1),
7247 Point::new(1, 1)..Point::new(1, 1),
7248 Point::new(2, 1)..Point::new(2, 1),
7249 ])
7250 });
7251
7252 editor.handle_input("{", window, cx);
7253 editor.handle_input("{", window, cx);
7254 editor.handle_input("_", window, cx);
7255 assert_eq!(
7256 editor.text(cx),
7257 "
7258 a{{_}}
7259 b{{_}}
7260 c{{_}}
7261 "
7262 .unindent()
7263 );
7264 assert_eq!(
7265 editor.selections.ranges::<Point>(cx),
7266 [
7267 Point::new(0, 4)..Point::new(0, 4),
7268 Point::new(1, 4)..Point::new(1, 4),
7269 Point::new(2, 4)..Point::new(2, 4)
7270 ]
7271 );
7272
7273 editor.backspace(&Default::default(), window, cx);
7274 editor.backspace(&Default::default(), window, cx);
7275 assert_eq!(
7276 editor.text(cx),
7277 "
7278 a{}
7279 b{}
7280 c{}
7281 "
7282 .unindent()
7283 );
7284 assert_eq!(
7285 editor.selections.ranges::<Point>(cx),
7286 [
7287 Point::new(0, 2)..Point::new(0, 2),
7288 Point::new(1, 2)..Point::new(1, 2),
7289 Point::new(2, 2)..Point::new(2, 2)
7290 ]
7291 );
7292
7293 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7294 assert_eq!(
7295 editor.text(cx),
7296 "
7297 a
7298 b
7299 c
7300 "
7301 .unindent()
7302 );
7303 assert_eq!(
7304 editor.selections.ranges::<Point>(cx),
7305 [
7306 Point::new(0, 1)..Point::new(0, 1),
7307 Point::new(1, 1)..Point::new(1, 1),
7308 Point::new(2, 1)..Point::new(2, 1)
7309 ]
7310 );
7311 });
7312}
7313
7314#[gpui::test]
7315async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7316 init_test(cx, |settings| {
7317 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7318 });
7319
7320 let mut cx = EditorTestContext::new(cx).await;
7321
7322 let language = Arc::new(Language::new(
7323 LanguageConfig {
7324 brackets: BracketPairConfig {
7325 pairs: vec![
7326 BracketPair {
7327 start: "{".to_string(),
7328 end: "}".to_string(),
7329 close: true,
7330 surround: true,
7331 newline: true,
7332 },
7333 BracketPair {
7334 start: "(".to_string(),
7335 end: ")".to_string(),
7336 close: true,
7337 surround: true,
7338 newline: true,
7339 },
7340 BracketPair {
7341 start: "[".to_string(),
7342 end: "]".to_string(),
7343 close: false,
7344 surround: true,
7345 newline: true,
7346 },
7347 ],
7348 ..Default::default()
7349 },
7350 autoclose_before: "})]".to_string(),
7351 ..Default::default()
7352 },
7353 Some(tree_sitter_rust::LANGUAGE.into()),
7354 ));
7355
7356 cx.language_registry().add(language.clone());
7357 cx.update_buffer(|buffer, cx| {
7358 buffer.set_language(Some(language), cx);
7359 });
7360
7361 cx.set_state(
7362 &"
7363 {(ˇ)}
7364 [[ˇ]]
7365 {(ˇ)}
7366 "
7367 .unindent(),
7368 );
7369
7370 cx.update_editor(|editor, window, cx| {
7371 editor.backspace(&Default::default(), window, cx);
7372 editor.backspace(&Default::default(), window, cx);
7373 });
7374
7375 cx.assert_editor_state(
7376 &"
7377 ˇ
7378 ˇ]]
7379 ˇ
7380 "
7381 .unindent(),
7382 );
7383
7384 cx.update_editor(|editor, window, cx| {
7385 editor.handle_input("{", window, cx);
7386 editor.handle_input("{", window, cx);
7387 editor.move_right(&MoveRight, window, cx);
7388 editor.move_right(&MoveRight, window, cx);
7389 editor.move_left(&MoveLeft, window, cx);
7390 editor.move_left(&MoveLeft, window, cx);
7391 editor.backspace(&Default::default(), window, cx);
7392 });
7393
7394 cx.assert_editor_state(
7395 &"
7396 {ˇ}
7397 {ˇ}]]
7398 {ˇ}
7399 "
7400 .unindent(),
7401 );
7402
7403 cx.update_editor(|editor, window, cx| {
7404 editor.backspace(&Default::default(), window, cx);
7405 });
7406
7407 cx.assert_editor_state(
7408 &"
7409 ˇ
7410 ˇ]]
7411 ˇ
7412 "
7413 .unindent(),
7414 );
7415}
7416
7417#[gpui::test]
7418async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420
7421 let language = Arc::new(Language::new(
7422 LanguageConfig::default(),
7423 Some(tree_sitter_rust::LANGUAGE.into()),
7424 ));
7425
7426 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7429 editor
7430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7431 .await;
7432
7433 editor.update_in(cx, |editor, window, cx| {
7434 editor.set_auto_replace_emoji_shortcode(true);
7435
7436 editor.handle_input("Hello ", window, cx);
7437 editor.handle_input(":wave", window, cx);
7438 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7439
7440 editor.handle_input(":", window, cx);
7441 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7442
7443 editor.handle_input(" :smile", window, cx);
7444 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7445
7446 editor.handle_input(":", window, cx);
7447 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7448
7449 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7450 editor.handle_input(":wave", window, cx);
7451 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7452
7453 editor.handle_input(":", window, cx);
7454 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7455
7456 editor.handle_input(":1", window, cx);
7457 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7458
7459 editor.handle_input(":", window, cx);
7460 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7461
7462 // Ensure shortcode does not get replaced when it is part of a word
7463 editor.handle_input(" Test:wave", window, cx);
7464 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7465
7466 editor.handle_input(":", window, cx);
7467 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7468
7469 editor.set_auto_replace_emoji_shortcode(false);
7470
7471 // Ensure shortcode does not get replaced when auto replace is off
7472 editor.handle_input(" :wave", window, cx);
7473 assert_eq!(
7474 editor.text(cx),
7475 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7476 );
7477
7478 editor.handle_input(":", window, cx);
7479 assert_eq!(
7480 editor.text(cx),
7481 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7482 );
7483 });
7484}
7485
7486#[gpui::test]
7487async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7488 init_test(cx, |_| {});
7489
7490 let (text, insertion_ranges) = marked_text_ranges(
7491 indoc! {"
7492 ˇ
7493 "},
7494 false,
7495 );
7496
7497 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7498 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7499
7500 _ = editor.update_in(cx, |editor, window, cx| {
7501 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7502
7503 editor
7504 .insert_snippet(&insertion_ranges, snippet, window, cx)
7505 .unwrap();
7506
7507 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7508 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7509 assert_eq!(editor.text(cx), expected_text);
7510 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7511 }
7512
7513 assert(
7514 editor,
7515 cx,
7516 indoc! {"
7517 type «» =•
7518 "},
7519 );
7520
7521 assert!(editor.context_menu_visible(), "There should be a matches");
7522 });
7523}
7524
7525#[gpui::test]
7526async fn test_snippets(cx: &mut TestAppContext) {
7527 init_test(cx, |_| {});
7528
7529 let (text, insertion_ranges) = marked_text_ranges(
7530 indoc! {"
7531 a.ˇ b
7532 a.ˇ b
7533 a.ˇ b
7534 "},
7535 false,
7536 );
7537
7538 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7539 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7540
7541 editor.update_in(cx, |editor, window, cx| {
7542 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7543
7544 editor
7545 .insert_snippet(&insertion_ranges, snippet, window, cx)
7546 .unwrap();
7547
7548 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7549 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7550 assert_eq!(editor.text(cx), expected_text);
7551 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7552 }
7553
7554 assert(
7555 editor,
7556 cx,
7557 indoc! {"
7558 a.f(«one», two, «three») b
7559 a.f(«one», two, «three») b
7560 a.f(«one», two, «three») b
7561 "},
7562 );
7563
7564 // Can't move earlier than the first tab stop
7565 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7566 assert(
7567 editor,
7568 cx,
7569 indoc! {"
7570 a.f(«one», two, «three») b
7571 a.f(«one», two, «three») b
7572 a.f(«one», two, «three») b
7573 "},
7574 );
7575
7576 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7577 assert(
7578 editor,
7579 cx,
7580 indoc! {"
7581 a.f(one, «two», three) b
7582 a.f(one, «two», three) b
7583 a.f(one, «two», three) b
7584 "},
7585 );
7586
7587 editor.move_to_prev_snippet_tabstop(window, cx);
7588 assert(
7589 editor,
7590 cx,
7591 indoc! {"
7592 a.f(«one», two, «three») b
7593 a.f(«one», two, «three») b
7594 a.f(«one», two, «three») b
7595 "},
7596 );
7597
7598 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7599 assert(
7600 editor,
7601 cx,
7602 indoc! {"
7603 a.f(one, «two», three) b
7604 a.f(one, «two», three) b
7605 a.f(one, «two», three) b
7606 "},
7607 );
7608 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7609 assert(
7610 editor,
7611 cx,
7612 indoc! {"
7613 a.f(one, two, three)ˇ b
7614 a.f(one, two, three)ˇ b
7615 a.f(one, two, three)ˇ b
7616 "},
7617 );
7618
7619 // As soon as the last tab stop is reached, snippet state is gone
7620 editor.move_to_prev_snippet_tabstop(window, cx);
7621 assert(
7622 editor,
7623 cx,
7624 indoc! {"
7625 a.f(one, two, three)ˇ b
7626 a.f(one, two, three)ˇ b
7627 a.f(one, two, three)ˇ b
7628 "},
7629 );
7630 });
7631}
7632
7633#[gpui::test]
7634async fn test_document_format_during_save(cx: &mut TestAppContext) {
7635 init_test(cx, |_| {});
7636
7637 let fs = FakeFs::new(cx.executor());
7638 fs.insert_file(path!("/file.rs"), Default::default()).await;
7639
7640 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7641
7642 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7643 language_registry.add(rust_lang());
7644 let mut fake_servers = language_registry.register_fake_lsp(
7645 "Rust",
7646 FakeLspAdapter {
7647 capabilities: lsp::ServerCapabilities {
7648 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7649 ..Default::default()
7650 },
7651 ..Default::default()
7652 },
7653 );
7654
7655 let buffer = project
7656 .update(cx, |project, cx| {
7657 project.open_local_buffer(path!("/file.rs"), cx)
7658 })
7659 .await
7660 .unwrap();
7661
7662 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7663 let (editor, cx) = cx.add_window_view(|window, cx| {
7664 build_editor_with_project(project.clone(), buffer, window, cx)
7665 });
7666 editor.update_in(cx, |editor, window, cx| {
7667 editor.set_text("one\ntwo\nthree\n", window, cx)
7668 });
7669 assert!(cx.read(|cx| editor.is_dirty(cx)));
7670
7671 cx.executor().start_waiting();
7672 let fake_server = fake_servers.next().await.unwrap();
7673
7674 let save = editor
7675 .update_in(cx, |editor, window, cx| {
7676 editor.save(true, project.clone(), window, cx)
7677 })
7678 .unwrap();
7679 fake_server
7680 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7681 assert_eq!(
7682 params.text_document.uri,
7683 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7684 );
7685 assert_eq!(params.options.tab_size, 4);
7686 Ok(Some(vec![lsp::TextEdit::new(
7687 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7688 ", ".to_string(),
7689 )]))
7690 })
7691 .next()
7692 .await;
7693 cx.executor().start_waiting();
7694 save.await;
7695
7696 assert_eq!(
7697 editor.update(cx, |editor, cx| editor.text(cx)),
7698 "one, two\nthree\n"
7699 );
7700 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7701
7702 editor.update_in(cx, |editor, window, cx| {
7703 editor.set_text("one\ntwo\nthree\n", window, cx)
7704 });
7705 assert!(cx.read(|cx| editor.is_dirty(cx)));
7706
7707 // Ensure we can still save even if formatting hangs.
7708 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7709 move |params, _| async move {
7710 assert_eq!(
7711 params.text_document.uri,
7712 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7713 );
7714 futures::future::pending::<()>().await;
7715 unreachable!()
7716 },
7717 );
7718 let save = editor
7719 .update_in(cx, |editor, window, cx| {
7720 editor.save(true, project.clone(), window, cx)
7721 })
7722 .unwrap();
7723 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7724 cx.executor().start_waiting();
7725 save.await;
7726 assert_eq!(
7727 editor.update(cx, |editor, cx| editor.text(cx)),
7728 "one\ntwo\nthree\n"
7729 );
7730 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7731
7732 // For non-dirty buffer, no formatting request should be sent
7733 let save = editor
7734 .update_in(cx, |editor, window, cx| {
7735 editor.save(true, project.clone(), window, cx)
7736 })
7737 .unwrap();
7738 let _pending_format_request = fake_server
7739 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7740 panic!("Should not be invoked on non-dirty buffer");
7741 })
7742 .next();
7743 cx.executor().start_waiting();
7744 save.await;
7745
7746 // Set rust language override and assert overridden tabsize is sent to language server
7747 update_test_language_settings(cx, |settings| {
7748 settings.languages.insert(
7749 "Rust".into(),
7750 LanguageSettingsContent {
7751 tab_size: NonZeroU32::new(8),
7752 ..Default::default()
7753 },
7754 );
7755 });
7756
7757 editor.update_in(cx, |editor, window, cx| {
7758 editor.set_text("somehting_new\n", window, cx)
7759 });
7760 assert!(cx.read(|cx| editor.is_dirty(cx)));
7761 let save = editor
7762 .update_in(cx, |editor, window, cx| {
7763 editor.save(true, project.clone(), window, cx)
7764 })
7765 .unwrap();
7766 fake_server
7767 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7768 assert_eq!(
7769 params.text_document.uri,
7770 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7771 );
7772 assert_eq!(params.options.tab_size, 8);
7773 Ok(Some(vec![]))
7774 })
7775 .next()
7776 .await;
7777 cx.executor().start_waiting();
7778 save.await;
7779}
7780
7781#[gpui::test]
7782async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7783 init_test(cx, |_| {});
7784
7785 let cols = 4;
7786 let rows = 10;
7787 let sample_text_1 = sample_text(rows, cols, 'a');
7788 assert_eq!(
7789 sample_text_1,
7790 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7791 );
7792 let sample_text_2 = sample_text(rows, cols, 'l');
7793 assert_eq!(
7794 sample_text_2,
7795 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7796 );
7797 let sample_text_3 = sample_text(rows, cols, 'v');
7798 assert_eq!(
7799 sample_text_3,
7800 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7801 );
7802
7803 let fs = FakeFs::new(cx.executor());
7804 fs.insert_tree(
7805 path!("/a"),
7806 json!({
7807 "main.rs": sample_text_1,
7808 "other.rs": sample_text_2,
7809 "lib.rs": sample_text_3,
7810 }),
7811 )
7812 .await;
7813
7814 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7815 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7816 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7817
7818 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7819 language_registry.add(rust_lang());
7820 let mut fake_servers = language_registry.register_fake_lsp(
7821 "Rust",
7822 FakeLspAdapter {
7823 capabilities: lsp::ServerCapabilities {
7824 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7825 ..Default::default()
7826 },
7827 ..Default::default()
7828 },
7829 );
7830
7831 let worktree = project.update(cx, |project, cx| {
7832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7833 assert_eq!(worktrees.len(), 1);
7834 worktrees.pop().unwrap()
7835 });
7836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7837
7838 let buffer_1 = project
7839 .update(cx, |project, cx| {
7840 project.open_buffer((worktree_id, "main.rs"), cx)
7841 })
7842 .await
7843 .unwrap();
7844 let buffer_2 = project
7845 .update(cx, |project, cx| {
7846 project.open_buffer((worktree_id, "other.rs"), cx)
7847 })
7848 .await
7849 .unwrap();
7850 let buffer_3 = project
7851 .update(cx, |project, cx| {
7852 project.open_buffer((worktree_id, "lib.rs"), cx)
7853 })
7854 .await
7855 .unwrap();
7856
7857 let multi_buffer = cx.new(|cx| {
7858 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7859 multi_buffer.push_excerpts(
7860 buffer_1.clone(),
7861 [
7862 ExcerptRange {
7863 context: Point::new(0, 0)..Point::new(3, 0),
7864 primary: None,
7865 },
7866 ExcerptRange {
7867 context: Point::new(5, 0)..Point::new(7, 0),
7868 primary: None,
7869 },
7870 ExcerptRange {
7871 context: Point::new(9, 0)..Point::new(10, 4),
7872 primary: None,
7873 },
7874 ],
7875 cx,
7876 );
7877 multi_buffer.push_excerpts(
7878 buffer_2.clone(),
7879 [
7880 ExcerptRange {
7881 context: Point::new(0, 0)..Point::new(3, 0),
7882 primary: None,
7883 },
7884 ExcerptRange {
7885 context: Point::new(5, 0)..Point::new(7, 0),
7886 primary: None,
7887 },
7888 ExcerptRange {
7889 context: Point::new(9, 0)..Point::new(10, 4),
7890 primary: None,
7891 },
7892 ],
7893 cx,
7894 );
7895 multi_buffer.push_excerpts(
7896 buffer_3.clone(),
7897 [
7898 ExcerptRange {
7899 context: Point::new(0, 0)..Point::new(3, 0),
7900 primary: None,
7901 },
7902 ExcerptRange {
7903 context: Point::new(5, 0)..Point::new(7, 0),
7904 primary: None,
7905 },
7906 ExcerptRange {
7907 context: Point::new(9, 0)..Point::new(10, 4),
7908 primary: None,
7909 },
7910 ],
7911 cx,
7912 );
7913 multi_buffer
7914 });
7915 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7916 Editor::new(
7917 EditorMode::Full,
7918 multi_buffer,
7919 Some(project.clone()),
7920 window,
7921 cx,
7922 )
7923 });
7924
7925 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7926 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7927 s.select_ranges(Some(1..2))
7928 });
7929 editor.insert("|one|two|three|", window, cx);
7930 });
7931 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7932 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7933 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7934 s.select_ranges(Some(60..70))
7935 });
7936 editor.insert("|four|five|six|", window, cx);
7937 });
7938 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7939
7940 // First two buffers should be edited, but not the third one.
7941 assert_eq!(
7942 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7943 "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}",
7944 );
7945 buffer_1.update(cx, |buffer, _| {
7946 assert!(buffer.is_dirty());
7947 assert_eq!(
7948 buffer.text(),
7949 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7950 )
7951 });
7952 buffer_2.update(cx, |buffer, _| {
7953 assert!(buffer.is_dirty());
7954 assert_eq!(
7955 buffer.text(),
7956 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7957 )
7958 });
7959 buffer_3.update(cx, |buffer, _| {
7960 assert!(!buffer.is_dirty());
7961 assert_eq!(buffer.text(), sample_text_3,)
7962 });
7963 cx.executor().run_until_parked();
7964
7965 cx.executor().start_waiting();
7966 let save = multi_buffer_editor
7967 .update_in(cx, |editor, window, cx| {
7968 editor.save(true, project.clone(), window, cx)
7969 })
7970 .unwrap();
7971
7972 let fake_server = fake_servers.next().await.unwrap();
7973 fake_server
7974 .server
7975 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7976 Ok(Some(vec![lsp::TextEdit::new(
7977 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7978 format!("[{} formatted]", params.text_document.uri),
7979 )]))
7980 })
7981 .detach();
7982 save.await;
7983
7984 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7985 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7986 assert_eq!(
7987 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7988 uri!(
7989 "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}"
7990 ),
7991 );
7992 buffer_1.update(cx, |buffer, _| {
7993 assert!(!buffer.is_dirty());
7994 assert_eq!(
7995 buffer.text(),
7996 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7997 )
7998 });
7999 buffer_2.update(cx, |buffer, _| {
8000 assert!(!buffer.is_dirty());
8001 assert_eq!(
8002 buffer.text(),
8003 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8004 )
8005 });
8006 buffer_3.update(cx, |buffer, _| {
8007 assert!(!buffer.is_dirty());
8008 assert_eq!(buffer.text(), sample_text_3,)
8009 });
8010}
8011
8012#[gpui::test]
8013async fn test_range_format_during_save(cx: &mut TestAppContext) {
8014 init_test(cx, |_| {});
8015
8016 let fs = FakeFs::new(cx.executor());
8017 fs.insert_file(path!("/file.rs"), Default::default()).await;
8018
8019 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8020
8021 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8022 language_registry.add(rust_lang());
8023 let mut fake_servers = language_registry.register_fake_lsp(
8024 "Rust",
8025 FakeLspAdapter {
8026 capabilities: lsp::ServerCapabilities {
8027 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8028 ..Default::default()
8029 },
8030 ..Default::default()
8031 },
8032 );
8033
8034 let buffer = project
8035 .update(cx, |project, cx| {
8036 project.open_local_buffer(path!("/file.rs"), cx)
8037 })
8038 .await
8039 .unwrap();
8040
8041 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8042 let (editor, cx) = cx.add_window_view(|window, cx| {
8043 build_editor_with_project(project.clone(), buffer, window, cx)
8044 });
8045 editor.update_in(cx, |editor, window, cx| {
8046 editor.set_text("one\ntwo\nthree\n", window, cx)
8047 });
8048 assert!(cx.read(|cx| editor.is_dirty(cx)));
8049
8050 cx.executor().start_waiting();
8051 let fake_server = fake_servers.next().await.unwrap();
8052
8053 let save = editor
8054 .update_in(cx, |editor, window, cx| {
8055 editor.save(true, project.clone(), window, cx)
8056 })
8057 .unwrap();
8058 fake_server
8059 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8060 assert_eq!(
8061 params.text_document.uri,
8062 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8063 );
8064 assert_eq!(params.options.tab_size, 4);
8065 Ok(Some(vec![lsp::TextEdit::new(
8066 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8067 ", ".to_string(),
8068 )]))
8069 })
8070 .next()
8071 .await;
8072 cx.executor().start_waiting();
8073 save.await;
8074 assert_eq!(
8075 editor.update(cx, |editor, cx| editor.text(cx)),
8076 "one, two\nthree\n"
8077 );
8078 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8079
8080 editor.update_in(cx, |editor, window, cx| {
8081 editor.set_text("one\ntwo\nthree\n", window, cx)
8082 });
8083 assert!(cx.read(|cx| editor.is_dirty(cx)));
8084
8085 // Ensure we can still save even if formatting hangs.
8086 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8087 move |params, _| async move {
8088 assert_eq!(
8089 params.text_document.uri,
8090 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8091 );
8092 futures::future::pending::<()>().await;
8093 unreachable!()
8094 },
8095 );
8096 let save = editor
8097 .update_in(cx, |editor, window, cx| {
8098 editor.save(true, project.clone(), window, cx)
8099 })
8100 .unwrap();
8101 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8102 cx.executor().start_waiting();
8103 save.await;
8104 assert_eq!(
8105 editor.update(cx, |editor, cx| editor.text(cx)),
8106 "one\ntwo\nthree\n"
8107 );
8108 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8109
8110 // For non-dirty buffer, no formatting request should be sent
8111 let save = editor
8112 .update_in(cx, |editor, window, cx| {
8113 editor.save(true, project.clone(), window, cx)
8114 })
8115 .unwrap();
8116 let _pending_format_request = fake_server
8117 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8118 panic!("Should not be invoked on non-dirty buffer");
8119 })
8120 .next();
8121 cx.executor().start_waiting();
8122 save.await;
8123
8124 // Set Rust language override and assert overridden tabsize is sent to language server
8125 update_test_language_settings(cx, |settings| {
8126 settings.languages.insert(
8127 "Rust".into(),
8128 LanguageSettingsContent {
8129 tab_size: NonZeroU32::new(8),
8130 ..Default::default()
8131 },
8132 );
8133 });
8134
8135 editor.update_in(cx, |editor, window, cx| {
8136 editor.set_text("somehting_new\n", window, cx)
8137 });
8138 assert!(cx.read(|cx| editor.is_dirty(cx)));
8139 let save = editor
8140 .update_in(cx, |editor, window, cx| {
8141 editor.save(true, project.clone(), window, cx)
8142 })
8143 .unwrap();
8144 fake_server
8145 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8146 assert_eq!(
8147 params.text_document.uri,
8148 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8149 );
8150 assert_eq!(params.options.tab_size, 8);
8151 Ok(Some(vec![]))
8152 })
8153 .next()
8154 .await;
8155 cx.executor().start_waiting();
8156 save.await;
8157}
8158
8159#[gpui::test]
8160async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8161 init_test(cx, |settings| {
8162 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8163 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8164 ))
8165 });
8166
8167 let fs = FakeFs::new(cx.executor());
8168 fs.insert_file(path!("/file.rs"), Default::default()).await;
8169
8170 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8171
8172 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8173 language_registry.add(Arc::new(Language::new(
8174 LanguageConfig {
8175 name: "Rust".into(),
8176 matcher: LanguageMatcher {
8177 path_suffixes: vec!["rs".to_string()],
8178 ..Default::default()
8179 },
8180 ..LanguageConfig::default()
8181 },
8182 Some(tree_sitter_rust::LANGUAGE.into()),
8183 )));
8184 update_test_language_settings(cx, |settings| {
8185 // Enable Prettier formatting for the same buffer, and ensure
8186 // LSP is called instead of Prettier.
8187 settings.defaults.prettier = Some(PrettierSettings {
8188 allowed: true,
8189 ..PrettierSettings::default()
8190 });
8191 });
8192 let mut fake_servers = language_registry.register_fake_lsp(
8193 "Rust",
8194 FakeLspAdapter {
8195 capabilities: lsp::ServerCapabilities {
8196 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8197 ..Default::default()
8198 },
8199 ..Default::default()
8200 },
8201 );
8202
8203 let buffer = project
8204 .update(cx, |project, cx| {
8205 project.open_local_buffer(path!("/file.rs"), cx)
8206 })
8207 .await
8208 .unwrap();
8209
8210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8211 let (editor, cx) = cx.add_window_view(|window, cx| {
8212 build_editor_with_project(project.clone(), buffer, window, cx)
8213 });
8214 editor.update_in(cx, |editor, window, cx| {
8215 editor.set_text("one\ntwo\nthree\n", window, cx)
8216 });
8217
8218 cx.executor().start_waiting();
8219 let fake_server = fake_servers.next().await.unwrap();
8220
8221 let format = editor
8222 .update_in(cx, |editor, window, cx| {
8223 editor.perform_format(
8224 project.clone(),
8225 FormatTrigger::Manual,
8226 FormatTarget::Buffers,
8227 window,
8228 cx,
8229 )
8230 })
8231 .unwrap();
8232 fake_server
8233 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8234 assert_eq!(
8235 params.text_document.uri,
8236 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8237 );
8238 assert_eq!(params.options.tab_size, 4);
8239 Ok(Some(vec![lsp::TextEdit::new(
8240 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8241 ", ".to_string(),
8242 )]))
8243 })
8244 .next()
8245 .await;
8246 cx.executor().start_waiting();
8247 format.await;
8248 assert_eq!(
8249 editor.update(cx, |editor, cx| editor.text(cx)),
8250 "one, two\nthree\n"
8251 );
8252
8253 editor.update_in(cx, |editor, window, cx| {
8254 editor.set_text("one\ntwo\nthree\n", window, cx)
8255 });
8256 // Ensure we don't lock if formatting hangs.
8257 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8258 move |params, _| async move {
8259 assert_eq!(
8260 params.text_document.uri,
8261 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8262 );
8263 futures::future::pending::<()>().await;
8264 unreachable!()
8265 },
8266 );
8267 let format = editor
8268 .update_in(cx, |editor, window, cx| {
8269 editor.perform_format(
8270 project,
8271 FormatTrigger::Manual,
8272 FormatTarget::Buffers,
8273 window,
8274 cx,
8275 )
8276 })
8277 .unwrap();
8278 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8279 cx.executor().start_waiting();
8280 format.await;
8281 assert_eq!(
8282 editor.update(cx, |editor, cx| editor.text(cx)),
8283 "one\ntwo\nthree\n"
8284 );
8285}
8286
8287#[gpui::test]
8288async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8289 init_test(cx, |settings| {
8290 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8291 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8292 ))
8293 });
8294
8295 let fs = FakeFs::new(cx.executor());
8296 fs.insert_file(path!("/file.ts"), Default::default()).await;
8297
8298 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8299
8300 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8301 language_registry.add(Arc::new(Language::new(
8302 LanguageConfig {
8303 name: "TypeScript".into(),
8304 matcher: LanguageMatcher {
8305 path_suffixes: vec!["ts".to_string()],
8306 ..Default::default()
8307 },
8308 ..LanguageConfig::default()
8309 },
8310 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8311 )));
8312 update_test_language_settings(cx, |settings| {
8313 settings.defaults.prettier = Some(PrettierSettings {
8314 allowed: true,
8315 ..PrettierSettings::default()
8316 });
8317 });
8318 let mut fake_servers = language_registry.register_fake_lsp(
8319 "TypeScript",
8320 FakeLspAdapter {
8321 capabilities: lsp::ServerCapabilities {
8322 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8323 ..Default::default()
8324 },
8325 ..Default::default()
8326 },
8327 );
8328
8329 let buffer = project
8330 .update(cx, |project, cx| {
8331 project.open_local_buffer(path!("/file.ts"), cx)
8332 })
8333 .await
8334 .unwrap();
8335
8336 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8337 let (editor, cx) = cx.add_window_view(|window, cx| {
8338 build_editor_with_project(project.clone(), buffer, window, cx)
8339 });
8340 editor.update_in(cx, |editor, window, cx| {
8341 editor.set_text(
8342 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8343 window,
8344 cx,
8345 )
8346 });
8347
8348 cx.executor().start_waiting();
8349 let fake_server = fake_servers.next().await.unwrap();
8350
8351 let format = editor
8352 .update_in(cx, |editor, window, cx| {
8353 editor.perform_code_action_kind(
8354 project.clone(),
8355 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8356 window,
8357 cx,
8358 )
8359 })
8360 .unwrap();
8361 fake_server
8362 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8363 assert_eq!(
8364 params.text_document.uri,
8365 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8366 );
8367 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8368 lsp::CodeAction {
8369 title: "Organize Imports".to_string(),
8370 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8371 edit: Some(lsp::WorkspaceEdit {
8372 changes: Some(
8373 [(
8374 params.text_document.uri.clone(),
8375 vec![lsp::TextEdit::new(
8376 lsp::Range::new(
8377 lsp::Position::new(1, 0),
8378 lsp::Position::new(2, 0),
8379 ),
8380 "".to_string(),
8381 )],
8382 )]
8383 .into_iter()
8384 .collect(),
8385 ),
8386 ..Default::default()
8387 }),
8388 ..Default::default()
8389 },
8390 )]))
8391 })
8392 .next()
8393 .await;
8394 cx.executor().start_waiting();
8395 format.await;
8396 assert_eq!(
8397 editor.update(cx, |editor, cx| editor.text(cx)),
8398 "import { a } from 'module';\n\nconst x = a;\n"
8399 );
8400
8401 editor.update_in(cx, |editor, window, cx| {
8402 editor.set_text(
8403 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8404 window,
8405 cx,
8406 )
8407 });
8408 // Ensure we don't lock if code action hangs.
8409 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8410 move |params, _| async move {
8411 assert_eq!(
8412 params.text_document.uri,
8413 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8414 );
8415 futures::future::pending::<()>().await;
8416 unreachable!()
8417 },
8418 );
8419 let format = editor
8420 .update_in(cx, |editor, window, cx| {
8421 editor.perform_code_action_kind(
8422 project,
8423 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8424 window,
8425 cx,
8426 )
8427 })
8428 .unwrap();
8429 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8430 cx.executor().start_waiting();
8431 format.await;
8432 assert_eq!(
8433 editor.update(cx, |editor, cx| editor.text(cx)),
8434 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8435 );
8436}
8437
8438#[gpui::test]
8439async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8440 init_test(cx, |_| {});
8441
8442 let mut cx = EditorLspTestContext::new_rust(
8443 lsp::ServerCapabilities {
8444 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8445 ..Default::default()
8446 },
8447 cx,
8448 )
8449 .await;
8450
8451 cx.set_state(indoc! {"
8452 one.twoˇ
8453 "});
8454
8455 // The format request takes a long time. When it completes, it inserts
8456 // a newline and an indent before the `.`
8457 cx.lsp
8458 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8459 let executor = cx.background_executor().clone();
8460 async move {
8461 executor.timer(Duration::from_millis(100)).await;
8462 Ok(Some(vec![lsp::TextEdit {
8463 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8464 new_text: "\n ".into(),
8465 }]))
8466 }
8467 });
8468
8469 // Submit a format request.
8470 let format_1 = cx
8471 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8472 .unwrap();
8473 cx.executor().run_until_parked();
8474
8475 // Submit a second format request.
8476 let format_2 = cx
8477 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8478 .unwrap();
8479 cx.executor().run_until_parked();
8480
8481 // Wait for both format requests to complete
8482 cx.executor().advance_clock(Duration::from_millis(200));
8483 cx.executor().start_waiting();
8484 format_1.await.unwrap();
8485 cx.executor().start_waiting();
8486 format_2.await.unwrap();
8487
8488 // The formatting edits only happens once.
8489 cx.assert_editor_state(indoc! {"
8490 one
8491 .twoˇ
8492 "});
8493}
8494
8495#[gpui::test]
8496async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8497 init_test(cx, |settings| {
8498 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8499 });
8500
8501 let mut cx = EditorLspTestContext::new_rust(
8502 lsp::ServerCapabilities {
8503 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8504 ..Default::default()
8505 },
8506 cx,
8507 )
8508 .await;
8509
8510 // Set up a buffer white some trailing whitespace and no trailing newline.
8511 cx.set_state(
8512 &[
8513 "one ", //
8514 "twoˇ", //
8515 "three ", //
8516 "four", //
8517 ]
8518 .join("\n"),
8519 );
8520
8521 // Submit a format request.
8522 let format = cx
8523 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8524 .unwrap();
8525
8526 // Record which buffer changes have been sent to the language server
8527 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8528 cx.lsp
8529 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8530 let buffer_changes = buffer_changes.clone();
8531 move |params, _| {
8532 buffer_changes.lock().extend(
8533 params
8534 .content_changes
8535 .into_iter()
8536 .map(|e| (e.range.unwrap(), e.text)),
8537 );
8538 }
8539 });
8540
8541 // Handle formatting requests to the language server.
8542 cx.lsp
8543 .set_request_handler::<lsp::request::Formatting, _, _>({
8544 let buffer_changes = buffer_changes.clone();
8545 move |_, _| {
8546 // When formatting is requested, trailing whitespace has already been stripped,
8547 // and the trailing newline has already been added.
8548 assert_eq!(
8549 &buffer_changes.lock()[1..],
8550 &[
8551 (
8552 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8553 "".into()
8554 ),
8555 (
8556 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8557 "".into()
8558 ),
8559 (
8560 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8561 "\n".into()
8562 ),
8563 ]
8564 );
8565
8566 // Insert blank lines between each line of the buffer.
8567 async move {
8568 Ok(Some(vec![
8569 lsp::TextEdit {
8570 range: lsp::Range::new(
8571 lsp::Position::new(1, 0),
8572 lsp::Position::new(1, 0),
8573 ),
8574 new_text: "\n".into(),
8575 },
8576 lsp::TextEdit {
8577 range: lsp::Range::new(
8578 lsp::Position::new(2, 0),
8579 lsp::Position::new(2, 0),
8580 ),
8581 new_text: "\n".into(),
8582 },
8583 ]))
8584 }
8585 }
8586 });
8587
8588 // After formatting the buffer, the trailing whitespace is stripped,
8589 // a newline is appended, and the edits provided by the language server
8590 // have been applied.
8591 format.await.unwrap();
8592 cx.assert_editor_state(
8593 &[
8594 "one", //
8595 "", //
8596 "twoˇ", //
8597 "", //
8598 "three", //
8599 "four", //
8600 "", //
8601 ]
8602 .join("\n"),
8603 );
8604
8605 // Undoing the formatting undoes the trailing whitespace removal, the
8606 // trailing newline, and the LSP edits.
8607 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8608 cx.assert_editor_state(
8609 &[
8610 "one ", //
8611 "twoˇ", //
8612 "three ", //
8613 "four", //
8614 ]
8615 .join("\n"),
8616 );
8617}
8618
8619#[gpui::test]
8620async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8621 cx: &mut TestAppContext,
8622) {
8623 init_test(cx, |_| {});
8624
8625 cx.update(|cx| {
8626 cx.update_global::<SettingsStore, _>(|settings, cx| {
8627 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8628 settings.auto_signature_help = Some(true);
8629 });
8630 });
8631 });
8632
8633 let mut cx = EditorLspTestContext::new_rust(
8634 lsp::ServerCapabilities {
8635 signature_help_provider: Some(lsp::SignatureHelpOptions {
8636 ..Default::default()
8637 }),
8638 ..Default::default()
8639 },
8640 cx,
8641 )
8642 .await;
8643
8644 let language = Language::new(
8645 LanguageConfig {
8646 name: "Rust".into(),
8647 brackets: BracketPairConfig {
8648 pairs: vec![
8649 BracketPair {
8650 start: "{".to_string(),
8651 end: "}".to_string(),
8652 close: true,
8653 surround: true,
8654 newline: true,
8655 },
8656 BracketPair {
8657 start: "(".to_string(),
8658 end: ")".to_string(),
8659 close: true,
8660 surround: true,
8661 newline: true,
8662 },
8663 BracketPair {
8664 start: "/*".to_string(),
8665 end: " */".to_string(),
8666 close: true,
8667 surround: true,
8668 newline: true,
8669 },
8670 BracketPair {
8671 start: "[".to_string(),
8672 end: "]".to_string(),
8673 close: false,
8674 surround: false,
8675 newline: true,
8676 },
8677 BracketPair {
8678 start: "\"".to_string(),
8679 end: "\"".to_string(),
8680 close: true,
8681 surround: true,
8682 newline: false,
8683 },
8684 BracketPair {
8685 start: "<".to_string(),
8686 end: ">".to_string(),
8687 close: false,
8688 surround: true,
8689 newline: true,
8690 },
8691 ],
8692 ..Default::default()
8693 },
8694 autoclose_before: "})]".to_string(),
8695 ..Default::default()
8696 },
8697 Some(tree_sitter_rust::LANGUAGE.into()),
8698 );
8699 let language = Arc::new(language);
8700
8701 cx.language_registry().add(language.clone());
8702 cx.update_buffer(|buffer, cx| {
8703 buffer.set_language(Some(language), cx);
8704 });
8705
8706 cx.set_state(
8707 &r#"
8708 fn main() {
8709 sampleˇ
8710 }
8711 "#
8712 .unindent(),
8713 );
8714
8715 cx.update_editor(|editor, window, cx| {
8716 editor.handle_input("(", window, cx);
8717 });
8718 cx.assert_editor_state(
8719 &"
8720 fn main() {
8721 sample(ˇ)
8722 }
8723 "
8724 .unindent(),
8725 );
8726
8727 let mocked_response = lsp::SignatureHelp {
8728 signatures: vec![lsp::SignatureInformation {
8729 label: "fn sample(param1: u8, param2: u8)".to_string(),
8730 documentation: None,
8731 parameters: Some(vec![
8732 lsp::ParameterInformation {
8733 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8734 documentation: None,
8735 },
8736 lsp::ParameterInformation {
8737 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8738 documentation: None,
8739 },
8740 ]),
8741 active_parameter: None,
8742 }],
8743 active_signature: Some(0),
8744 active_parameter: Some(0),
8745 };
8746 handle_signature_help_request(&mut cx, mocked_response).await;
8747
8748 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8749 .await;
8750
8751 cx.editor(|editor, _, _| {
8752 let signature_help_state = editor.signature_help_state.popover().cloned();
8753 assert_eq!(
8754 signature_help_state.unwrap().label,
8755 "param1: u8, param2: u8"
8756 );
8757 });
8758}
8759
8760#[gpui::test]
8761async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8762 init_test(cx, |_| {});
8763
8764 cx.update(|cx| {
8765 cx.update_global::<SettingsStore, _>(|settings, cx| {
8766 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8767 settings.auto_signature_help = Some(false);
8768 settings.show_signature_help_after_edits = Some(false);
8769 });
8770 });
8771 });
8772
8773 let mut cx = EditorLspTestContext::new_rust(
8774 lsp::ServerCapabilities {
8775 signature_help_provider: Some(lsp::SignatureHelpOptions {
8776 ..Default::default()
8777 }),
8778 ..Default::default()
8779 },
8780 cx,
8781 )
8782 .await;
8783
8784 let language = Language::new(
8785 LanguageConfig {
8786 name: "Rust".into(),
8787 brackets: BracketPairConfig {
8788 pairs: vec![
8789 BracketPair {
8790 start: "{".to_string(),
8791 end: "}".to_string(),
8792 close: true,
8793 surround: true,
8794 newline: true,
8795 },
8796 BracketPair {
8797 start: "(".to_string(),
8798 end: ")".to_string(),
8799 close: true,
8800 surround: true,
8801 newline: true,
8802 },
8803 BracketPair {
8804 start: "/*".to_string(),
8805 end: " */".to_string(),
8806 close: true,
8807 surround: true,
8808 newline: true,
8809 },
8810 BracketPair {
8811 start: "[".to_string(),
8812 end: "]".to_string(),
8813 close: false,
8814 surround: false,
8815 newline: true,
8816 },
8817 BracketPair {
8818 start: "\"".to_string(),
8819 end: "\"".to_string(),
8820 close: true,
8821 surround: true,
8822 newline: false,
8823 },
8824 BracketPair {
8825 start: "<".to_string(),
8826 end: ">".to_string(),
8827 close: false,
8828 surround: true,
8829 newline: true,
8830 },
8831 ],
8832 ..Default::default()
8833 },
8834 autoclose_before: "})]".to_string(),
8835 ..Default::default()
8836 },
8837 Some(tree_sitter_rust::LANGUAGE.into()),
8838 );
8839 let language = Arc::new(language);
8840
8841 cx.language_registry().add(language.clone());
8842 cx.update_buffer(|buffer, cx| {
8843 buffer.set_language(Some(language), cx);
8844 });
8845
8846 // Ensure that signature_help is not called when no signature help is enabled.
8847 cx.set_state(
8848 &r#"
8849 fn main() {
8850 sampleˇ
8851 }
8852 "#
8853 .unindent(),
8854 );
8855 cx.update_editor(|editor, window, cx| {
8856 editor.handle_input("(", window, cx);
8857 });
8858 cx.assert_editor_state(
8859 &"
8860 fn main() {
8861 sample(ˇ)
8862 }
8863 "
8864 .unindent(),
8865 );
8866 cx.editor(|editor, _, _| {
8867 assert!(editor.signature_help_state.task().is_none());
8868 });
8869
8870 let mocked_response = lsp::SignatureHelp {
8871 signatures: vec![lsp::SignatureInformation {
8872 label: "fn sample(param1: u8, param2: u8)".to_string(),
8873 documentation: None,
8874 parameters: Some(vec![
8875 lsp::ParameterInformation {
8876 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8877 documentation: None,
8878 },
8879 lsp::ParameterInformation {
8880 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8881 documentation: None,
8882 },
8883 ]),
8884 active_parameter: None,
8885 }],
8886 active_signature: Some(0),
8887 active_parameter: Some(0),
8888 };
8889
8890 // Ensure that signature_help is called when enabled afte edits
8891 cx.update(|_, cx| {
8892 cx.update_global::<SettingsStore, _>(|settings, cx| {
8893 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8894 settings.auto_signature_help = Some(false);
8895 settings.show_signature_help_after_edits = Some(true);
8896 });
8897 });
8898 });
8899 cx.set_state(
8900 &r#"
8901 fn main() {
8902 sampleˇ
8903 }
8904 "#
8905 .unindent(),
8906 );
8907 cx.update_editor(|editor, window, cx| {
8908 editor.handle_input("(", window, cx);
8909 });
8910 cx.assert_editor_state(
8911 &"
8912 fn main() {
8913 sample(ˇ)
8914 }
8915 "
8916 .unindent(),
8917 );
8918 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8919 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8920 .await;
8921 cx.update_editor(|editor, _, _| {
8922 let signature_help_state = editor.signature_help_state.popover().cloned();
8923 assert!(signature_help_state.is_some());
8924 assert_eq!(
8925 signature_help_state.unwrap().label,
8926 "param1: u8, param2: u8"
8927 );
8928 editor.signature_help_state = SignatureHelpState::default();
8929 });
8930
8931 // Ensure that signature_help is called when auto signature help override is enabled
8932 cx.update(|_, cx| {
8933 cx.update_global::<SettingsStore, _>(|settings, cx| {
8934 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8935 settings.auto_signature_help = Some(true);
8936 settings.show_signature_help_after_edits = Some(false);
8937 });
8938 });
8939 });
8940 cx.set_state(
8941 &r#"
8942 fn main() {
8943 sampleˇ
8944 }
8945 "#
8946 .unindent(),
8947 );
8948 cx.update_editor(|editor, window, cx| {
8949 editor.handle_input("(", window, cx);
8950 });
8951 cx.assert_editor_state(
8952 &"
8953 fn main() {
8954 sample(ˇ)
8955 }
8956 "
8957 .unindent(),
8958 );
8959 handle_signature_help_request(&mut cx, mocked_response).await;
8960 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8961 .await;
8962 cx.editor(|editor, _, _| {
8963 let signature_help_state = editor.signature_help_state.popover().cloned();
8964 assert!(signature_help_state.is_some());
8965 assert_eq!(
8966 signature_help_state.unwrap().label,
8967 "param1: u8, param2: u8"
8968 );
8969 });
8970}
8971
8972#[gpui::test]
8973async fn test_signature_help(cx: &mut TestAppContext) {
8974 init_test(cx, |_| {});
8975 cx.update(|cx| {
8976 cx.update_global::<SettingsStore, _>(|settings, cx| {
8977 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8978 settings.auto_signature_help = Some(true);
8979 });
8980 });
8981 });
8982
8983 let mut cx = EditorLspTestContext::new_rust(
8984 lsp::ServerCapabilities {
8985 signature_help_provider: Some(lsp::SignatureHelpOptions {
8986 ..Default::default()
8987 }),
8988 ..Default::default()
8989 },
8990 cx,
8991 )
8992 .await;
8993
8994 // A test that directly calls `show_signature_help`
8995 cx.update_editor(|editor, window, cx| {
8996 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8997 });
8998
8999 let mocked_response = lsp::SignatureHelp {
9000 signatures: vec![lsp::SignatureInformation {
9001 label: "fn sample(param1: u8, param2: u8)".to_string(),
9002 documentation: None,
9003 parameters: Some(vec![
9004 lsp::ParameterInformation {
9005 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9006 documentation: None,
9007 },
9008 lsp::ParameterInformation {
9009 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9010 documentation: None,
9011 },
9012 ]),
9013 active_parameter: None,
9014 }],
9015 active_signature: Some(0),
9016 active_parameter: Some(0),
9017 };
9018 handle_signature_help_request(&mut cx, mocked_response).await;
9019
9020 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9021 .await;
9022
9023 cx.editor(|editor, _, _| {
9024 let signature_help_state = editor.signature_help_state.popover().cloned();
9025 assert!(signature_help_state.is_some());
9026 assert_eq!(
9027 signature_help_state.unwrap().label,
9028 "param1: u8, param2: u8"
9029 );
9030 });
9031
9032 // When exiting outside from inside the brackets, `signature_help` is closed.
9033 cx.set_state(indoc! {"
9034 fn main() {
9035 sample(ˇ);
9036 }
9037
9038 fn sample(param1: u8, param2: u8) {}
9039 "});
9040
9041 cx.update_editor(|editor, window, cx| {
9042 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9043 });
9044
9045 let mocked_response = lsp::SignatureHelp {
9046 signatures: Vec::new(),
9047 active_signature: None,
9048 active_parameter: None,
9049 };
9050 handle_signature_help_request(&mut cx, mocked_response).await;
9051
9052 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9053 .await;
9054
9055 cx.editor(|editor, _, _| {
9056 assert!(!editor.signature_help_state.is_shown());
9057 });
9058
9059 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9060 cx.set_state(indoc! {"
9061 fn main() {
9062 sample(ˇ);
9063 }
9064
9065 fn sample(param1: u8, param2: u8) {}
9066 "});
9067
9068 let mocked_response = lsp::SignatureHelp {
9069 signatures: vec![lsp::SignatureInformation {
9070 label: "fn sample(param1: u8, param2: u8)".to_string(),
9071 documentation: None,
9072 parameters: Some(vec![
9073 lsp::ParameterInformation {
9074 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9075 documentation: None,
9076 },
9077 lsp::ParameterInformation {
9078 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9079 documentation: None,
9080 },
9081 ]),
9082 active_parameter: None,
9083 }],
9084 active_signature: Some(0),
9085 active_parameter: Some(0),
9086 };
9087 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9088 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9089 .await;
9090 cx.editor(|editor, _, _| {
9091 assert!(editor.signature_help_state.is_shown());
9092 });
9093
9094 // Restore the popover with more parameter input
9095 cx.set_state(indoc! {"
9096 fn main() {
9097 sample(param1, param2ˇ);
9098 }
9099
9100 fn sample(param1: u8, param2: u8) {}
9101 "});
9102
9103 let mocked_response = lsp::SignatureHelp {
9104 signatures: vec![lsp::SignatureInformation {
9105 label: "fn sample(param1: u8, param2: u8)".to_string(),
9106 documentation: None,
9107 parameters: Some(vec![
9108 lsp::ParameterInformation {
9109 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9110 documentation: None,
9111 },
9112 lsp::ParameterInformation {
9113 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9114 documentation: None,
9115 },
9116 ]),
9117 active_parameter: None,
9118 }],
9119 active_signature: Some(0),
9120 active_parameter: Some(1),
9121 };
9122 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9123 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9124 .await;
9125
9126 // When selecting a range, the popover is gone.
9127 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9128 cx.update_editor(|editor, window, cx| {
9129 editor.change_selections(None, window, cx, |s| {
9130 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9131 })
9132 });
9133 cx.assert_editor_state(indoc! {"
9134 fn main() {
9135 sample(param1, «ˇparam2»);
9136 }
9137
9138 fn sample(param1: u8, param2: u8) {}
9139 "});
9140 cx.editor(|editor, _, _| {
9141 assert!(!editor.signature_help_state.is_shown());
9142 });
9143
9144 // When unselecting again, the popover is back if within the brackets.
9145 cx.update_editor(|editor, window, cx| {
9146 editor.change_selections(None, window, cx, |s| {
9147 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9148 })
9149 });
9150 cx.assert_editor_state(indoc! {"
9151 fn main() {
9152 sample(param1, ˇparam2);
9153 }
9154
9155 fn sample(param1: u8, param2: u8) {}
9156 "});
9157 handle_signature_help_request(&mut cx, mocked_response).await;
9158 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9159 .await;
9160 cx.editor(|editor, _, _| {
9161 assert!(editor.signature_help_state.is_shown());
9162 });
9163
9164 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9165 cx.update_editor(|editor, window, cx| {
9166 editor.change_selections(None, window, cx, |s| {
9167 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9168 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9169 })
9170 });
9171 cx.assert_editor_state(indoc! {"
9172 fn main() {
9173 sample(param1, ˇparam2);
9174 }
9175
9176 fn sample(param1: u8, param2: u8) {}
9177 "});
9178
9179 let mocked_response = lsp::SignatureHelp {
9180 signatures: vec![lsp::SignatureInformation {
9181 label: "fn sample(param1: u8, param2: u8)".to_string(),
9182 documentation: None,
9183 parameters: Some(vec![
9184 lsp::ParameterInformation {
9185 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9186 documentation: None,
9187 },
9188 lsp::ParameterInformation {
9189 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9190 documentation: None,
9191 },
9192 ]),
9193 active_parameter: None,
9194 }],
9195 active_signature: Some(0),
9196 active_parameter: Some(1),
9197 };
9198 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9199 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9200 .await;
9201 cx.update_editor(|editor, _, cx| {
9202 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9203 });
9204 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9205 .await;
9206 cx.update_editor(|editor, window, cx| {
9207 editor.change_selections(None, window, cx, |s| {
9208 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9209 })
9210 });
9211 cx.assert_editor_state(indoc! {"
9212 fn main() {
9213 sample(param1, «ˇparam2»);
9214 }
9215
9216 fn sample(param1: u8, param2: u8) {}
9217 "});
9218 cx.update_editor(|editor, window, cx| {
9219 editor.change_selections(None, window, cx, |s| {
9220 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9221 })
9222 });
9223 cx.assert_editor_state(indoc! {"
9224 fn main() {
9225 sample(param1, ˇparam2);
9226 }
9227
9228 fn sample(param1: u8, param2: u8) {}
9229 "});
9230 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9231 .await;
9232}
9233
9234#[gpui::test]
9235async fn test_completion(cx: &mut TestAppContext) {
9236 init_test(cx, |_| {});
9237
9238 let mut cx = EditorLspTestContext::new_rust(
9239 lsp::ServerCapabilities {
9240 completion_provider: Some(lsp::CompletionOptions {
9241 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9242 resolve_provider: Some(true),
9243 ..Default::default()
9244 }),
9245 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9246 ..Default::default()
9247 },
9248 cx,
9249 )
9250 .await;
9251 let counter = Arc::new(AtomicUsize::new(0));
9252
9253 cx.set_state(indoc! {"
9254 oneˇ
9255 two
9256 three
9257 "});
9258 cx.simulate_keystroke(".");
9259 handle_completion_request(
9260 &mut cx,
9261 indoc! {"
9262 one.|<>
9263 two
9264 three
9265 "},
9266 vec!["first_completion", "second_completion"],
9267 counter.clone(),
9268 )
9269 .await;
9270 cx.condition(|editor, _| editor.context_menu_visible())
9271 .await;
9272 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9273
9274 let _handler = handle_signature_help_request(
9275 &mut cx,
9276 lsp::SignatureHelp {
9277 signatures: vec![lsp::SignatureInformation {
9278 label: "test signature".to_string(),
9279 documentation: None,
9280 parameters: Some(vec![lsp::ParameterInformation {
9281 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9282 documentation: None,
9283 }]),
9284 active_parameter: None,
9285 }],
9286 active_signature: None,
9287 active_parameter: None,
9288 },
9289 );
9290 cx.update_editor(|editor, window, cx| {
9291 assert!(
9292 !editor.signature_help_state.is_shown(),
9293 "No signature help was called for"
9294 );
9295 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9296 });
9297 cx.run_until_parked();
9298 cx.update_editor(|editor, _, _| {
9299 assert!(
9300 !editor.signature_help_state.is_shown(),
9301 "No signature help should be shown when completions menu is open"
9302 );
9303 });
9304
9305 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9306 editor.context_menu_next(&Default::default(), window, cx);
9307 editor
9308 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9309 .unwrap()
9310 });
9311 cx.assert_editor_state(indoc! {"
9312 one.second_completionˇ
9313 two
9314 three
9315 "});
9316
9317 handle_resolve_completion_request(
9318 &mut cx,
9319 Some(vec![
9320 (
9321 //This overlaps with the primary completion edit which is
9322 //misbehavior from the LSP spec, test that we filter it out
9323 indoc! {"
9324 one.second_ˇcompletion
9325 two
9326 threeˇ
9327 "},
9328 "overlapping additional edit",
9329 ),
9330 (
9331 indoc! {"
9332 one.second_completion
9333 two
9334 threeˇ
9335 "},
9336 "\nadditional edit",
9337 ),
9338 ]),
9339 )
9340 .await;
9341 apply_additional_edits.await.unwrap();
9342 cx.assert_editor_state(indoc! {"
9343 one.second_completionˇ
9344 two
9345 three
9346 additional edit
9347 "});
9348
9349 cx.set_state(indoc! {"
9350 one.second_completion
9351 twoˇ
9352 threeˇ
9353 additional edit
9354 "});
9355 cx.simulate_keystroke(" ");
9356 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9357 cx.simulate_keystroke("s");
9358 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9359
9360 cx.assert_editor_state(indoc! {"
9361 one.second_completion
9362 two sˇ
9363 three sˇ
9364 additional edit
9365 "});
9366 handle_completion_request(
9367 &mut cx,
9368 indoc! {"
9369 one.second_completion
9370 two s
9371 three <s|>
9372 additional edit
9373 "},
9374 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9375 counter.clone(),
9376 )
9377 .await;
9378 cx.condition(|editor, _| editor.context_menu_visible())
9379 .await;
9380 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9381
9382 cx.simulate_keystroke("i");
9383
9384 handle_completion_request(
9385 &mut cx,
9386 indoc! {"
9387 one.second_completion
9388 two si
9389 three <si|>
9390 additional edit
9391 "},
9392 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9393 counter.clone(),
9394 )
9395 .await;
9396 cx.condition(|editor, _| editor.context_menu_visible())
9397 .await;
9398 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9399
9400 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9401 editor
9402 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9403 .unwrap()
9404 });
9405 cx.assert_editor_state(indoc! {"
9406 one.second_completion
9407 two sixth_completionˇ
9408 three sixth_completionˇ
9409 additional edit
9410 "});
9411
9412 apply_additional_edits.await.unwrap();
9413
9414 update_test_language_settings(&mut cx, |settings| {
9415 settings.defaults.show_completions_on_input = Some(false);
9416 });
9417 cx.set_state("editorˇ");
9418 cx.simulate_keystroke(".");
9419 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9420 cx.simulate_keystrokes("c l o");
9421 cx.assert_editor_state("editor.cloˇ");
9422 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9423 cx.update_editor(|editor, window, cx| {
9424 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9425 });
9426 handle_completion_request(
9427 &mut cx,
9428 "editor.<clo|>",
9429 vec!["close", "clobber"],
9430 counter.clone(),
9431 )
9432 .await;
9433 cx.condition(|editor, _| editor.context_menu_visible())
9434 .await;
9435 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9436
9437 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9438 editor
9439 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9440 .unwrap()
9441 });
9442 cx.assert_editor_state("editor.closeˇ");
9443 handle_resolve_completion_request(&mut cx, None).await;
9444 apply_additional_edits.await.unwrap();
9445}
9446
9447#[gpui::test]
9448async fn test_word_completion(cx: &mut TestAppContext) {
9449 let lsp_fetch_timeout_ms = 10;
9450 init_test(cx, |language_settings| {
9451 language_settings.defaults.completions = Some(CompletionSettings {
9452 words: WordsCompletionMode::Fallback,
9453 lsp: true,
9454 lsp_fetch_timeout_ms: 10,
9455 });
9456 });
9457
9458 let mut cx = EditorLspTestContext::new_rust(
9459 lsp::ServerCapabilities {
9460 completion_provider: Some(lsp::CompletionOptions {
9461 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9462 ..lsp::CompletionOptions::default()
9463 }),
9464 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9465 ..lsp::ServerCapabilities::default()
9466 },
9467 cx,
9468 )
9469 .await;
9470
9471 let throttle_completions = Arc::new(AtomicBool::new(false));
9472
9473 let lsp_throttle_completions = throttle_completions.clone();
9474 let _completion_requests_handler =
9475 cx.lsp
9476 .server
9477 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9478 let lsp_throttle_completions = lsp_throttle_completions.clone();
9479 let cx = cx.clone();
9480 async move {
9481 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9482 cx.background_executor()
9483 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9484 .await;
9485 }
9486 Ok(Some(lsp::CompletionResponse::Array(vec![
9487 lsp::CompletionItem {
9488 label: "first".into(),
9489 ..lsp::CompletionItem::default()
9490 },
9491 lsp::CompletionItem {
9492 label: "last".into(),
9493 ..lsp::CompletionItem::default()
9494 },
9495 ])))
9496 }
9497 });
9498
9499 cx.set_state(indoc! {"
9500 oneˇ
9501 two
9502 three
9503 "});
9504 cx.simulate_keystroke(".");
9505 cx.executor().run_until_parked();
9506 cx.condition(|editor, _| editor.context_menu_visible())
9507 .await;
9508 cx.update_editor(|editor, window, cx| {
9509 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9510 {
9511 assert_eq!(
9512 completion_menu_entries(&menu),
9513 &["first", "last"],
9514 "When LSP server is fast to reply, no fallback word completions are used"
9515 );
9516 } else {
9517 panic!("expected completion menu to be open");
9518 }
9519 editor.cancel(&Cancel, window, cx);
9520 });
9521 cx.executor().run_until_parked();
9522 cx.condition(|editor, _| !editor.context_menu_visible())
9523 .await;
9524
9525 throttle_completions.store(true, atomic::Ordering::Release);
9526 cx.simulate_keystroke(".");
9527 cx.executor()
9528 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9529 cx.executor().run_until_parked();
9530 cx.condition(|editor, _| editor.context_menu_visible())
9531 .await;
9532 cx.update_editor(|editor, _, _| {
9533 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9534 {
9535 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9536 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9537 } else {
9538 panic!("expected completion menu to be open");
9539 }
9540 });
9541}
9542
9543#[gpui::test]
9544async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9545 init_test(cx, |language_settings| {
9546 language_settings.defaults.completions = Some(CompletionSettings {
9547 words: WordsCompletionMode::Enabled,
9548 lsp: true,
9549 lsp_fetch_timeout_ms: 0,
9550 });
9551 });
9552
9553 let mut cx = EditorLspTestContext::new_rust(
9554 lsp::ServerCapabilities {
9555 completion_provider: Some(lsp::CompletionOptions {
9556 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9557 ..lsp::CompletionOptions::default()
9558 }),
9559 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9560 ..lsp::ServerCapabilities::default()
9561 },
9562 cx,
9563 )
9564 .await;
9565
9566 let _completion_requests_handler =
9567 cx.lsp
9568 .server
9569 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9570 Ok(Some(lsp::CompletionResponse::Array(vec![
9571 lsp::CompletionItem {
9572 label: "first".into(),
9573 ..lsp::CompletionItem::default()
9574 },
9575 lsp::CompletionItem {
9576 label: "last".into(),
9577 ..lsp::CompletionItem::default()
9578 },
9579 ])))
9580 });
9581
9582 cx.set_state(indoc! {"ˇ
9583 first
9584 last
9585 second
9586 "});
9587 cx.simulate_keystroke(".");
9588 cx.executor().run_until_parked();
9589 cx.condition(|editor, _| editor.context_menu_visible())
9590 .await;
9591 cx.update_editor(|editor, _, _| {
9592 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9593 {
9594 assert_eq!(
9595 completion_menu_entries(&menu),
9596 &["first", "last", "second"],
9597 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9598 );
9599 } else {
9600 panic!("expected completion menu to be open");
9601 }
9602 });
9603}
9604
9605#[gpui::test]
9606async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9607 init_test(cx, |language_settings| {
9608 language_settings.defaults.completions = Some(CompletionSettings {
9609 words: WordsCompletionMode::Disabled,
9610 lsp: true,
9611 lsp_fetch_timeout_ms: 0,
9612 });
9613 });
9614
9615 let mut cx = EditorLspTestContext::new_rust(
9616 lsp::ServerCapabilities {
9617 completion_provider: Some(lsp::CompletionOptions {
9618 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9619 ..lsp::CompletionOptions::default()
9620 }),
9621 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9622 ..lsp::ServerCapabilities::default()
9623 },
9624 cx,
9625 )
9626 .await;
9627
9628 let _completion_requests_handler =
9629 cx.lsp
9630 .server
9631 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9632 panic!("LSP completions should not be queried when dealing with word completions")
9633 });
9634
9635 cx.set_state(indoc! {"ˇ
9636 first
9637 last
9638 second
9639 "});
9640 cx.update_editor(|editor, window, cx| {
9641 editor.show_word_completions(&ShowWordCompletions, window, cx);
9642 });
9643 cx.executor().run_until_parked();
9644 cx.condition(|editor, _| editor.context_menu_visible())
9645 .await;
9646 cx.update_editor(|editor, _, _| {
9647 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9648 {
9649 assert_eq!(
9650 completion_menu_entries(&menu),
9651 &["first", "last", "second"],
9652 "`ShowWordCompletions` action should show word completions"
9653 );
9654 } else {
9655 panic!("expected completion menu to be open");
9656 }
9657 });
9658
9659 cx.simulate_keystroke("l");
9660 cx.executor().run_until_parked();
9661 cx.condition(|editor, _| editor.context_menu_visible())
9662 .await;
9663 cx.update_editor(|editor, _, _| {
9664 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9665 {
9666 assert_eq!(
9667 completion_menu_entries(&menu),
9668 &["last"],
9669 "After showing word completions, further editing should filter them and not query the LSP"
9670 );
9671 } else {
9672 panic!("expected completion menu to be open");
9673 }
9674 });
9675}
9676
9677#[gpui::test]
9678async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9679 init_test(cx, |language_settings| {
9680 language_settings.defaults.completions = Some(CompletionSettings {
9681 words: WordsCompletionMode::Fallback,
9682 lsp: false,
9683 lsp_fetch_timeout_ms: 0,
9684 });
9685 });
9686
9687 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9688
9689 cx.set_state(indoc! {"ˇ
9690 0_usize
9691 let
9692 33
9693 4.5f32
9694 "});
9695 cx.update_editor(|editor, window, cx| {
9696 editor.show_completions(&ShowCompletions::default(), window, cx);
9697 });
9698 cx.executor().run_until_parked();
9699 cx.condition(|editor, _| editor.context_menu_visible())
9700 .await;
9701 cx.update_editor(|editor, window, cx| {
9702 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9703 {
9704 assert_eq!(
9705 completion_menu_entries(&menu),
9706 &["let"],
9707 "With no digits in the completion query, no digits should be in the word completions"
9708 );
9709 } else {
9710 panic!("expected completion menu to be open");
9711 }
9712 editor.cancel(&Cancel, window, cx);
9713 });
9714
9715 cx.set_state(indoc! {"3ˇ
9716 0_usize
9717 let
9718 3
9719 33.35f32
9720 "});
9721 cx.update_editor(|editor, window, cx| {
9722 editor.show_completions(&ShowCompletions::default(), window, cx);
9723 });
9724 cx.executor().run_until_parked();
9725 cx.condition(|editor, _| editor.context_menu_visible())
9726 .await;
9727 cx.update_editor(|editor, _, _| {
9728 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9729 {
9730 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9731 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9732 } else {
9733 panic!("expected completion menu to be open");
9734 }
9735 });
9736}
9737
9738fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9739 let position = || lsp::Position {
9740 line: params.text_document_position.position.line,
9741 character: params.text_document_position.position.character,
9742 };
9743 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9744 range: lsp::Range {
9745 start: position(),
9746 end: position(),
9747 },
9748 new_text: text.to_string(),
9749 }))
9750}
9751
9752#[gpui::test]
9753async fn test_multiline_completion(cx: &mut TestAppContext) {
9754 init_test(cx, |_| {});
9755
9756 let fs = FakeFs::new(cx.executor());
9757 fs.insert_tree(
9758 path!("/a"),
9759 json!({
9760 "main.ts": "a",
9761 }),
9762 )
9763 .await;
9764
9765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9766 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9767 let typescript_language = Arc::new(Language::new(
9768 LanguageConfig {
9769 name: "TypeScript".into(),
9770 matcher: LanguageMatcher {
9771 path_suffixes: vec!["ts".to_string()],
9772 ..LanguageMatcher::default()
9773 },
9774 line_comments: vec!["// ".into()],
9775 ..LanguageConfig::default()
9776 },
9777 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9778 ));
9779 language_registry.add(typescript_language.clone());
9780 let mut fake_servers = language_registry.register_fake_lsp(
9781 "TypeScript",
9782 FakeLspAdapter {
9783 capabilities: lsp::ServerCapabilities {
9784 completion_provider: Some(lsp::CompletionOptions {
9785 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9786 ..lsp::CompletionOptions::default()
9787 }),
9788 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9789 ..lsp::ServerCapabilities::default()
9790 },
9791 // Emulate vtsls label generation
9792 label_for_completion: Some(Box::new(|item, _| {
9793 let text = if let Some(description) = item
9794 .label_details
9795 .as_ref()
9796 .and_then(|label_details| label_details.description.as_ref())
9797 {
9798 format!("{} {}", item.label, description)
9799 } else if let Some(detail) = &item.detail {
9800 format!("{} {}", item.label, detail)
9801 } else {
9802 item.label.clone()
9803 };
9804 let len = text.len();
9805 Some(language::CodeLabel {
9806 text,
9807 runs: Vec::new(),
9808 filter_range: 0..len,
9809 })
9810 })),
9811 ..FakeLspAdapter::default()
9812 },
9813 );
9814 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9815 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9816 let worktree_id = workspace
9817 .update(cx, |workspace, _window, cx| {
9818 workspace.project().update(cx, |project, cx| {
9819 project.worktrees(cx).next().unwrap().read(cx).id()
9820 })
9821 })
9822 .unwrap();
9823 let _buffer = project
9824 .update(cx, |project, cx| {
9825 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9826 })
9827 .await
9828 .unwrap();
9829 let editor = workspace
9830 .update(cx, |workspace, window, cx| {
9831 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9832 })
9833 .unwrap()
9834 .await
9835 .unwrap()
9836 .downcast::<Editor>()
9837 .unwrap();
9838 let fake_server = fake_servers.next().await.unwrap();
9839
9840 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9841 let multiline_label_2 = "a\nb\nc\n";
9842 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9843 let multiline_description = "d\ne\nf\n";
9844 let multiline_detail_2 = "g\nh\ni\n";
9845
9846 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
9847 move |params, _| async move {
9848 Ok(Some(lsp::CompletionResponse::Array(vec![
9849 lsp::CompletionItem {
9850 label: multiline_label.to_string(),
9851 text_edit: gen_text_edit(¶ms, "new_text_1"),
9852 ..lsp::CompletionItem::default()
9853 },
9854 lsp::CompletionItem {
9855 label: "single line label 1".to_string(),
9856 detail: Some(multiline_detail.to_string()),
9857 text_edit: gen_text_edit(¶ms, "new_text_2"),
9858 ..lsp::CompletionItem::default()
9859 },
9860 lsp::CompletionItem {
9861 label: "single line label 2".to_string(),
9862 label_details: Some(lsp::CompletionItemLabelDetails {
9863 description: Some(multiline_description.to_string()),
9864 detail: None,
9865 }),
9866 text_edit: gen_text_edit(¶ms, "new_text_2"),
9867 ..lsp::CompletionItem::default()
9868 },
9869 lsp::CompletionItem {
9870 label: multiline_label_2.to_string(),
9871 detail: Some(multiline_detail_2.to_string()),
9872 text_edit: gen_text_edit(¶ms, "new_text_3"),
9873 ..lsp::CompletionItem::default()
9874 },
9875 lsp::CompletionItem {
9876 label: "Label with many spaces and \t but without newlines".to_string(),
9877 detail: Some(
9878 "Details with many spaces and \t but without newlines".to_string(),
9879 ),
9880 text_edit: gen_text_edit(¶ms, "new_text_4"),
9881 ..lsp::CompletionItem::default()
9882 },
9883 ])))
9884 },
9885 );
9886
9887 editor.update_in(cx, |editor, window, cx| {
9888 cx.focus_self(window);
9889 editor.move_to_end(&MoveToEnd, window, cx);
9890 editor.handle_input(".", window, cx);
9891 });
9892 cx.run_until_parked();
9893 completion_handle.next().await.unwrap();
9894
9895 editor.update(cx, |editor, _| {
9896 assert!(editor.context_menu_visible());
9897 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9898 {
9899 let completion_labels = menu
9900 .completions
9901 .borrow()
9902 .iter()
9903 .map(|c| c.label.text.clone())
9904 .collect::<Vec<_>>();
9905 assert_eq!(
9906 completion_labels,
9907 &[
9908 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9909 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9910 "single line label 2 d e f ",
9911 "a b c g h i ",
9912 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9913 ],
9914 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9915 );
9916
9917 for completion in menu
9918 .completions
9919 .borrow()
9920 .iter() {
9921 assert_eq!(
9922 completion.label.filter_range,
9923 0..completion.label.text.len(),
9924 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9925 );
9926 }
9927 } else {
9928 panic!("expected completion menu to be open");
9929 }
9930 });
9931}
9932
9933#[gpui::test]
9934async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9935 init_test(cx, |_| {});
9936 let mut cx = EditorLspTestContext::new_rust(
9937 lsp::ServerCapabilities {
9938 completion_provider: Some(lsp::CompletionOptions {
9939 trigger_characters: Some(vec![".".to_string()]),
9940 ..Default::default()
9941 }),
9942 ..Default::default()
9943 },
9944 cx,
9945 )
9946 .await;
9947 cx.lsp
9948 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
9949 Ok(Some(lsp::CompletionResponse::Array(vec![
9950 lsp::CompletionItem {
9951 label: "first".into(),
9952 ..Default::default()
9953 },
9954 lsp::CompletionItem {
9955 label: "last".into(),
9956 ..Default::default()
9957 },
9958 ])))
9959 });
9960 cx.set_state("variableˇ");
9961 cx.simulate_keystroke(".");
9962 cx.executor().run_until_parked();
9963
9964 cx.update_editor(|editor, _, _| {
9965 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9966 {
9967 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9968 } else {
9969 panic!("expected completion menu to be open");
9970 }
9971 });
9972
9973 cx.update_editor(|editor, window, cx| {
9974 editor.move_page_down(&MovePageDown::default(), window, cx);
9975 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9976 {
9977 assert!(
9978 menu.selected_item == 1,
9979 "expected PageDown to select the last item from the context menu"
9980 );
9981 } else {
9982 panic!("expected completion menu to stay open after PageDown");
9983 }
9984 });
9985
9986 cx.update_editor(|editor, window, cx| {
9987 editor.move_page_up(&MovePageUp::default(), window, cx);
9988 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9989 {
9990 assert!(
9991 menu.selected_item == 0,
9992 "expected PageUp to select the first item from the context menu"
9993 );
9994 } else {
9995 panic!("expected completion menu to stay open after PageUp");
9996 }
9997 });
9998}
9999
10000#[gpui::test]
10001async fn test_completion_sort(cx: &mut TestAppContext) {
10002 init_test(cx, |_| {});
10003 let mut cx = EditorLspTestContext::new_rust(
10004 lsp::ServerCapabilities {
10005 completion_provider: Some(lsp::CompletionOptions {
10006 trigger_characters: Some(vec![".".to_string()]),
10007 ..Default::default()
10008 }),
10009 ..Default::default()
10010 },
10011 cx,
10012 )
10013 .await;
10014 cx.lsp
10015 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10016 Ok(Some(lsp::CompletionResponse::Array(vec![
10017 lsp::CompletionItem {
10018 label: "Range".into(),
10019 sort_text: Some("a".into()),
10020 ..Default::default()
10021 },
10022 lsp::CompletionItem {
10023 label: "r".into(),
10024 sort_text: Some("b".into()),
10025 ..Default::default()
10026 },
10027 lsp::CompletionItem {
10028 label: "ret".into(),
10029 sort_text: Some("c".into()),
10030 ..Default::default()
10031 },
10032 lsp::CompletionItem {
10033 label: "return".into(),
10034 sort_text: Some("d".into()),
10035 ..Default::default()
10036 },
10037 lsp::CompletionItem {
10038 label: "slice".into(),
10039 sort_text: Some("d".into()),
10040 ..Default::default()
10041 },
10042 ])))
10043 });
10044 cx.set_state("rˇ");
10045 cx.executor().run_until_parked();
10046 cx.update_editor(|editor, window, cx| {
10047 editor.show_completions(
10048 &ShowCompletions {
10049 trigger: Some("r".into()),
10050 },
10051 window,
10052 cx,
10053 );
10054 });
10055 cx.executor().run_until_parked();
10056
10057 cx.update_editor(|editor, _, _| {
10058 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10059 {
10060 assert_eq!(
10061 completion_menu_entries(&menu),
10062 &["r", "ret", "Range", "return"]
10063 );
10064 } else {
10065 panic!("expected completion menu to be open");
10066 }
10067 });
10068}
10069
10070#[gpui::test]
10071async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10072 init_test(cx, |_| {});
10073
10074 let mut cx = EditorLspTestContext::new_rust(
10075 lsp::ServerCapabilities {
10076 completion_provider: Some(lsp::CompletionOptions {
10077 trigger_characters: Some(vec![".".to_string()]),
10078 resolve_provider: Some(true),
10079 ..Default::default()
10080 }),
10081 ..Default::default()
10082 },
10083 cx,
10084 )
10085 .await;
10086
10087 cx.set_state("fn main() { let a = 2ˇ; }");
10088 cx.simulate_keystroke(".");
10089 let completion_item = lsp::CompletionItem {
10090 label: "Some".into(),
10091 kind: Some(lsp::CompletionItemKind::SNIPPET),
10092 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10093 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10094 kind: lsp::MarkupKind::Markdown,
10095 value: "```rust\nSome(2)\n```".to_string(),
10096 })),
10097 deprecated: Some(false),
10098 sort_text: Some("Some".to_string()),
10099 filter_text: Some("Some".to_string()),
10100 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10101 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10102 range: lsp::Range {
10103 start: lsp::Position {
10104 line: 0,
10105 character: 22,
10106 },
10107 end: lsp::Position {
10108 line: 0,
10109 character: 22,
10110 },
10111 },
10112 new_text: "Some(2)".to_string(),
10113 })),
10114 additional_text_edits: Some(vec![lsp::TextEdit {
10115 range: lsp::Range {
10116 start: lsp::Position {
10117 line: 0,
10118 character: 20,
10119 },
10120 end: lsp::Position {
10121 line: 0,
10122 character: 22,
10123 },
10124 },
10125 new_text: "".to_string(),
10126 }]),
10127 ..Default::default()
10128 };
10129
10130 let closure_completion_item = completion_item.clone();
10131 let counter = Arc::new(AtomicUsize::new(0));
10132 let counter_clone = counter.clone();
10133 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10134 let task_completion_item = closure_completion_item.clone();
10135 counter_clone.fetch_add(1, atomic::Ordering::Release);
10136 async move {
10137 Ok(Some(lsp::CompletionResponse::Array(vec![
10138 task_completion_item,
10139 ])))
10140 }
10141 });
10142
10143 cx.condition(|editor, _| editor.context_menu_visible())
10144 .await;
10145 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10146 assert!(request.next().await.is_some());
10147 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10148
10149 cx.simulate_keystrokes("S o m");
10150 cx.condition(|editor, _| editor.context_menu_visible())
10151 .await;
10152 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10153 assert!(request.next().await.is_some());
10154 assert!(request.next().await.is_some());
10155 assert!(request.next().await.is_some());
10156 request.close();
10157 assert!(request.next().await.is_none());
10158 assert_eq!(
10159 counter.load(atomic::Ordering::Acquire),
10160 4,
10161 "With the completions menu open, only one LSP request should happen per input"
10162 );
10163}
10164
10165#[gpui::test]
10166async fn test_toggle_comment(cx: &mut TestAppContext) {
10167 init_test(cx, |_| {});
10168 let mut cx = EditorTestContext::new(cx).await;
10169 let language = Arc::new(Language::new(
10170 LanguageConfig {
10171 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10172 ..Default::default()
10173 },
10174 Some(tree_sitter_rust::LANGUAGE.into()),
10175 ));
10176 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10177
10178 // If multiple selections intersect a line, the line is only toggled once.
10179 cx.set_state(indoc! {"
10180 fn a() {
10181 «//b();
10182 ˇ»// «c();
10183 //ˇ» d();
10184 }
10185 "});
10186
10187 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10188
10189 cx.assert_editor_state(indoc! {"
10190 fn a() {
10191 «b();
10192 c();
10193 ˇ» d();
10194 }
10195 "});
10196
10197 // The comment prefix is inserted at the same column for every line in a
10198 // selection.
10199 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10200
10201 cx.assert_editor_state(indoc! {"
10202 fn a() {
10203 // «b();
10204 // c();
10205 ˇ»// d();
10206 }
10207 "});
10208
10209 // If a selection ends at the beginning of a line, that line is not toggled.
10210 cx.set_selections_state(indoc! {"
10211 fn a() {
10212 // b();
10213 «// c();
10214 ˇ» // d();
10215 }
10216 "});
10217
10218 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10219
10220 cx.assert_editor_state(indoc! {"
10221 fn a() {
10222 // b();
10223 «c();
10224 ˇ» // d();
10225 }
10226 "});
10227
10228 // If a selection span a single line and is empty, the line is toggled.
10229 cx.set_state(indoc! {"
10230 fn a() {
10231 a();
10232 b();
10233 ˇ
10234 }
10235 "});
10236
10237 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10238
10239 cx.assert_editor_state(indoc! {"
10240 fn a() {
10241 a();
10242 b();
10243 //•ˇ
10244 }
10245 "});
10246
10247 // If a selection span multiple lines, empty lines are not toggled.
10248 cx.set_state(indoc! {"
10249 fn a() {
10250 «a();
10251
10252 c();ˇ»
10253 }
10254 "});
10255
10256 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10257
10258 cx.assert_editor_state(indoc! {"
10259 fn a() {
10260 // «a();
10261
10262 // c();ˇ»
10263 }
10264 "});
10265
10266 // If a selection includes multiple comment prefixes, all lines are uncommented.
10267 cx.set_state(indoc! {"
10268 fn a() {
10269 «// a();
10270 /// b();
10271 //! c();ˇ»
10272 }
10273 "});
10274
10275 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10276
10277 cx.assert_editor_state(indoc! {"
10278 fn a() {
10279 «a();
10280 b();
10281 c();ˇ»
10282 }
10283 "});
10284}
10285
10286#[gpui::test]
10287async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10288 init_test(cx, |_| {});
10289 let mut cx = EditorTestContext::new(cx).await;
10290 let language = Arc::new(Language::new(
10291 LanguageConfig {
10292 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_rust::LANGUAGE.into()),
10296 ));
10297 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10298
10299 let toggle_comments = &ToggleComments {
10300 advance_downwards: false,
10301 ignore_indent: true,
10302 };
10303
10304 // If multiple selections intersect a line, the line is only toggled once.
10305 cx.set_state(indoc! {"
10306 fn a() {
10307 // «b();
10308 // c();
10309 // ˇ» d();
10310 }
10311 "});
10312
10313 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10314
10315 cx.assert_editor_state(indoc! {"
10316 fn a() {
10317 «b();
10318 c();
10319 ˇ» d();
10320 }
10321 "});
10322
10323 // The comment prefix is inserted at the beginning of each line
10324 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10325
10326 cx.assert_editor_state(indoc! {"
10327 fn a() {
10328 // «b();
10329 // c();
10330 // ˇ» d();
10331 }
10332 "});
10333
10334 // If a selection ends at the beginning of a line, that line is not toggled.
10335 cx.set_selections_state(indoc! {"
10336 fn a() {
10337 // b();
10338 // «c();
10339 ˇ»// d();
10340 }
10341 "});
10342
10343 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10344
10345 cx.assert_editor_state(indoc! {"
10346 fn a() {
10347 // b();
10348 «c();
10349 ˇ»// d();
10350 }
10351 "});
10352
10353 // If a selection span a single line and is empty, the line is toggled.
10354 cx.set_state(indoc! {"
10355 fn a() {
10356 a();
10357 b();
10358 ˇ
10359 }
10360 "});
10361
10362 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10363
10364 cx.assert_editor_state(indoc! {"
10365 fn a() {
10366 a();
10367 b();
10368 //ˇ
10369 }
10370 "});
10371
10372 // If a selection span multiple lines, empty lines are not toggled.
10373 cx.set_state(indoc! {"
10374 fn a() {
10375 «a();
10376
10377 c();ˇ»
10378 }
10379 "});
10380
10381 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10382
10383 cx.assert_editor_state(indoc! {"
10384 fn a() {
10385 // «a();
10386
10387 // c();ˇ»
10388 }
10389 "});
10390
10391 // If a selection includes multiple comment prefixes, all lines are uncommented.
10392 cx.set_state(indoc! {"
10393 fn a() {
10394 // «a();
10395 /// b();
10396 //! c();ˇ»
10397 }
10398 "});
10399
10400 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10401
10402 cx.assert_editor_state(indoc! {"
10403 fn a() {
10404 «a();
10405 b();
10406 c();ˇ»
10407 }
10408 "});
10409}
10410
10411#[gpui::test]
10412async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10413 init_test(cx, |_| {});
10414
10415 let language = Arc::new(Language::new(
10416 LanguageConfig {
10417 line_comments: vec!["// ".into()],
10418 ..Default::default()
10419 },
10420 Some(tree_sitter_rust::LANGUAGE.into()),
10421 ));
10422
10423 let mut cx = EditorTestContext::new(cx).await;
10424
10425 cx.language_registry().add(language.clone());
10426 cx.update_buffer(|buffer, cx| {
10427 buffer.set_language(Some(language), cx);
10428 });
10429
10430 let toggle_comments = &ToggleComments {
10431 advance_downwards: true,
10432 ignore_indent: false,
10433 };
10434
10435 // Single cursor on one line -> advance
10436 // Cursor moves horizontally 3 characters as well on non-blank line
10437 cx.set_state(indoc!(
10438 "fn a() {
10439 ˇdog();
10440 cat();
10441 }"
10442 ));
10443 cx.update_editor(|editor, window, cx| {
10444 editor.toggle_comments(toggle_comments, window, cx);
10445 });
10446 cx.assert_editor_state(indoc!(
10447 "fn a() {
10448 // dog();
10449 catˇ();
10450 }"
10451 ));
10452
10453 // Single selection on one line -> don't advance
10454 cx.set_state(indoc!(
10455 "fn a() {
10456 «dog()ˇ»;
10457 cat();
10458 }"
10459 ));
10460 cx.update_editor(|editor, window, cx| {
10461 editor.toggle_comments(toggle_comments, window, cx);
10462 });
10463 cx.assert_editor_state(indoc!(
10464 "fn a() {
10465 // «dog()ˇ»;
10466 cat();
10467 }"
10468 ));
10469
10470 // Multiple cursors on one line -> advance
10471 cx.set_state(indoc!(
10472 "fn a() {
10473 ˇdˇog();
10474 cat();
10475 }"
10476 ));
10477 cx.update_editor(|editor, window, cx| {
10478 editor.toggle_comments(toggle_comments, window, cx);
10479 });
10480 cx.assert_editor_state(indoc!(
10481 "fn a() {
10482 // dog();
10483 catˇ(ˇ);
10484 }"
10485 ));
10486
10487 // Multiple cursors on one line, with selection -> don't advance
10488 cx.set_state(indoc!(
10489 "fn a() {
10490 ˇdˇog«()ˇ»;
10491 cat();
10492 }"
10493 ));
10494 cx.update_editor(|editor, window, cx| {
10495 editor.toggle_comments(toggle_comments, window, cx);
10496 });
10497 cx.assert_editor_state(indoc!(
10498 "fn a() {
10499 // ˇdˇog«()ˇ»;
10500 cat();
10501 }"
10502 ));
10503
10504 // Single cursor on one line -> advance
10505 // Cursor moves to column 0 on blank line
10506 cx.set_state(indoc!(
10507 "fn a() {
10508 ˇdog();
10509
10510 cat();
10511 }"
10512 ));
10513 cx.update_editor(|editor, window, cx| {
10514 editor.toggle_comments(toggle_comments, window, cx);
10515 });
10516 cx.assert_editor_state(indoc!(
10517 "fn a() {
10518 // dog();
10519 ˇ
10520 cat();
10521 }"
10522 ));
10523
10524 // Single cursor on one line -> advance
10525 // Cursor starts and ends at column 0
10526 cx.set_state(indoc!(
10527 "fn a() {
10528 ˇ dog();
10529 cat();
10530 }"
10531 ));
10532 cx.update_editor(|editor, window, cx| {
10533 editor.toggle_comments(toggle_comments, window, cx);
10534 });
10535 cx.assert_editor_state(indoc!(
10536 "fn a() {
10537 // dog();
10538 ˇ cat();
10539 }"
10540 ));
10541}
10542
10543#[gpui::test]
10544async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10545 init_test(cx, |_| {});
10546
10547 let mut cx = EditorTestContext::new(cx).await;
10548
10549 let html_language = Arc::new(
10550 Language::new(
10551 LanguageConfig {
10552 name: "HTML".into(),
10553 block_comment: Some(("<!-- ".into(), " -->".into())),
10554 ..Default::default()
10555 },
10556 Some(tree_sitter_html::LANGUAGE.into()),
10557 )
10558 .with_injection_query(
10559 r#"
10560 (script_element
10561 (raw_text) @injection.content
10562 (#set! injection.language "javascript"))
10563 "#,
10564 )
10565 .unwrap(),
10566 );
10567
10568 let javascript_language = Arc::new(Language::new(
10569 LanguageConfig {
10570 name: "JavaScript".into(),
10571 line_comments: vec!["// ".into()],
10572 ..Default::default()
10573 },
10574 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10575 ));
10576
10577 cx.language_registry().add(html_language.clone());
10578 cx.language_registry().add(javascript_language.clone());
10579 cx.update_buffer(|buffer, cx| {
10580 buffer.set_language(Some(html_language), cx);
10581 });
10582
10583 // Toggle comments for empty selections
10584 cx.set_state(
10585 &r#"
10586 <p>A</p>ˇ
10587 <p>B</p>ˇ
10588 <p>C</p>ˇ
10589 "#
10590 .unindent(),
10591 );
10592 cx.update_editor(|editor, window, cx| {
10593 editor.toggle_comments(&ToggleComments::default(), window, cx)
10594 });
10595 cx.assert_editor_state(
10596 &r#"
10597 <!-- <p>A</p>ˇ -->
10598 <!-- <p>B</p>ˇ -->
10599 <!-- <p>C</p>ˇ -->
10600 "#
10601 .unindent(),
10602 );
10603 cx.update_editor(|editor, window, cx| {
10604 editor.toggle_comments(&ToggleComments::default(), window, cx)
10605 });
10606 cx.assert_editor_state(
10607 &r#"
10608 <p>A</p>ˇ
10609 <p>B</p>ˇ
10610 <p>C</p>ˇ
10611 "#
10612 .unindent(),
10613 );
10614
10615 // Toggle comments for mixture of empty and non-empty selections, where
10616 // multiple selections occupy a given line.
10617 cx.set_state(
10618 &r#"
10619 <p>A«</p>
10620 <p>ˇ»B</p>ˇ
10621 <p>C«</p>
10622 <p>ˇ»D</p>ˇ
10623 "#
10624 .unindent(),
10625 );
10626
10627 cx.update_editor(|editor, window, cx| {
10628 editor.toggle_comments(&ToggleComments::default(), window, cx)
10629 });
10630 cx.assert_editor_state(
10631 &r#"
10632 <!-- <p>A«</p>
10633 <p>ˇ»B</p>ˇ -->
10634 <!-- <p>C«</p>
10635 <p>ˇ»D</p>ˇ -->
10636 "#
10637 .unindent(),
10638 );
10639 cx.update_editor(|editor, window, cx| {
10640 editor.toggle_comments(&ToggleComments::default(), window, cx)
10641 });
10642 cx.assert_editor_state(
10643 &r#"
10644 <p>A«</p>
10645 <p>ˇ»B</p>ˇ
10646 <p>C«</p>
10647 <p>ˇ»D</p>ˇ
10648 "#
10649 .unindent(),
10650 );
10651
10652 // Toggle comments when different languages are active for different
10653 // selections.
10654 cx.set_state(
10655 &r#"
10656 ˇ<script>
10657 ˇvar x = new Y();
10658 ˇ</script>
10659 "#
10660 .unindent(),
10661 );
10662 cx.executor().run_until_parked();
10663 cx.update_editor(|editor, window, cx| {
10664 editor.toggle_comments(&ToggleComments::default(), window, cx)
10665 });
10666 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10667 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10668 cx.assert_editor_state(
10669 &r#"
10670 <!-- ˇ<script> -->
10671 // ˇvar x = new Y();
10672 <!-- ˇ</script> -->
10673 "#
10674 .unindent(),
10675 );
10676}
10677
10678#[gpui::test]
10679fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10680 init_test(cx, |_| {});
10681
10682 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10683 let multibuffer = cx.new(|cx| {
10684 let mut multibuffer = MultiBuffer::new(ReadWrite);
10685 multibuffer.push_excerpts(
10686 buffer.clone(),
10687 [
10688 ExcerptRange {
10689 context: Point::new(0, 0)..Point::new(0, 4),
10690 primary: None,
10691 },
10692 ExcerptRange {
10693 context: Point::new(1, 0)..Point::new(1, 4),
10694 primary: None,
10695 },
10696 ],
10697 cx,
10698 );
10699 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10700 multibuffer
10701 });
10702
10703 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10704 editor.update_in(cx, |editor, window, cx| {
10705 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10706 editor.change_selections(None, window, cx, |s| {
10707 s.select_ranges([
10708 Point::new(0, 0)..Point::new(0, 0),
10709 Point::new(1, 0)..Point::new(1, 0),
10710 ])
10711 });
10712
10713 editor.handle_input("X", window, cx);
10714 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10715 assert_eq!(
10716 editor.selections.ranges(cx),
10717 [
10718 Point::new(0, 1)..Point::new(0, 1),
10719 Point::new(1, 1)..Point::new(1, 1),
10720 ]
10721 );
10722
10723 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10724 editor.change_selections(None, window, cx, |s| {
10725 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10726 });
10727 editor.backspace(&Default::default(), window, cx);
10728 assert_eq!(editor.text(cx), "Xa\nbbb");
10729 assert_eq!(
10730 editor.selections.ranges(cx),
10731 [Point::new(1, 0)..Point::new(1, 0)]
10732 );
10733
10734 editor.change_selections(None, window, cx, |s| {
10735 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10736 });
10737 editor.backspace(&Default::default(), window, cx);
10738 assert_eq!(editor.text(cx), "X\nbb");
10739 assert_eq!(
10740 editor.selections.ranges(cx),
10741 [Point::new(0, 1)..Point::new(0, 1)]
10742 );
10743 });
10744}
10745
10746#[gpui::test]
10747fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10748 init_test(cx, |_| {});
10749
10750 let markers = vec![('[', ']').into(), ('(', ')').into()];
10751 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10752 indoc! {"
10753 [aaaa
10754 (bbbb]
10755 cccc)",
10756 },
10757 markers.clone(),
10758 );
10759 let excerpt_ranges = markers.into_iter().map(|marker| {
10760 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10761 ExcerptRange {
10762 context,
10763 primary: None,
10764 }
10765 });
10766 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10767 let multibuffer = cx.new(|cx| {
10768 let mut multibuffer = MultiBuffer::new(ReadWrite);
10769 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10770 multibuffer
10771 });
10772
10773 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10774 editor.update_in(cx, |editor, window, cx| {
10775 let (expected_text, selection_ranges) = marked_text_ranges(
10776 indoc! {"
10777 aaaa
10778 bˇbbb
10779 bˇbbˇb
10780 cccc"
10781 },
10782 true,
10783 );
10784 assert_eq!(editor.text(cx), expected_text);
10785 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10786
10787 editor.handle_input("X", window, cx);
10788
10789 let (expected_text, expected_selections) = marked_text_ranges(
10790 indoc! {"
10791 aaaa
10792 bXˇbbXb
10793 bXˇbbXˇb
10794 cccc"
10795 },
10796 false,
10797 );
10798 assert_eq!(editor.text(cx), expected_text);
10799 assert_eq!(editor.selections.ranges(cx), expected_selections);
10800
10801 editor.newline(&Newline, window, cx);
10802 let (expected_text, expected_selections) = marked_text_ranges(
10803 indoc! {"
10804 aaaa
10805 bX
10806 ˇbbX
10807 b
10808 bX
10809 ˇbbX
10810 ˇb
10811 cccc"
10812 },
10813 false,
10814 );
10815 assert_eq!(editor.text(cx), expected_text);
10816 assert_eq!(editor.selections.ranges(cx), expected_selections);
10817 });
10818}
10819
10820#[gpui::test]
10821fn test_refresh_selections(cx: &mut TestAppContext) {
10822 init_test(cx, |_| {});
10823
10824 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10825 let mut excerpt1_id = None;
10826 let multibuffer = cx.new(|cx| {
10827 let mut multibuffer = MultiBuffer::new(ReadWrite);
10828 excerpt1_id = multibuffer
10829 .push_excerpts(
10830 buffer.clone(),
10831 [
10832 ExcerptRange {
10833 context: Point::new(0, 0)..Point::new(1, 4),
10834 primary: None,
10835 },
10836 ExcerptRange {
10837 context: Point::new(1, 0)..Point::new(2, 4),
10838 primary: None,
10839 },
10840 ],
10841 cx,
10842 )
10843 .into_iter()
10844 .next();
10845 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10846 multibuffer
10847 });
10848
10849 let editor = cx.add_window(|window, cx| {
10850 let mut editor = build_editor(multibuffer.clone(), window, cx);
10851 let snapshot = editor.snapshot(window, cx);
10852 editor.change_selections(None, window, cx, |s| {
10853 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10854 });
10855 editor.begin_selection(
10856 Point::new(2, 1).to_display_point(&snapshot),
10857 true,
10858 1,
10859 window,
10860 cx,
10861 );
10862 assert_eq!(
10863 editor.selections.ranges(cx),
10864 [
10865 Point::new(1, 3)..Point::new(1, 3),
10866 Point::new(2, 1)..Point::new(2, 1),
10867 ]
10868 );
10869 editor
10870 });
10871
10872 // Refreshing selections is a no-op when excerpts haven't changed.
10873 _ = editor.update(cx, |editor, window, cx| {
10874 editor.change_selections(None, window, cx, |s| s.refresh());
10875 assert_eq!(
10876 editor.selections.ranges(cx),
10877 [
10878 Point::new(1, 3)..Point::new(1, 3),
10879 Point::new(2, 1)..Point::new(2, 1),
10880 ]
10881 );
10882 });
10883
10884 multibuffer.update(cx, |multibuffer, cx| {
10885 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10886 });
10887 _ = editor.update(cx, |editor, window, cx| {
10888 // Removing an excerpt causes the first selection to become degenerate.
10889 assert_eq!(
10890 editor.selections.ranges(cx),
10891 [
10892 Point::new(0, 0)..Point::new(0, 0),
10893 Point::new(0, 1)..Point::new(0, 1)
10894 ]
10895 );
10896
10897 // Refreshing selections will relocate the first selection to the original buffer
10898 // location.
10899 editor.change_selections(None, window, cx, |s| s.refresh());
10900 assert_eq!(
10901 editor.selections.ranges(cx),
10902 [
10903 Point::new(0, 1)..Point::new(0, 1),
10904 Point::new(0, 3)..Point::new(0, 3)
10905 ]
10906 );
10907 assert!(editor.selections.pending_anchor().is_some());
10908 });
10909}
10910
10911#[gpui::test]
10912fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10913 init_test(cx, |_| {});
10914
10915 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10916 let mut excerpt1_id = None;
10917 let multibuffer = cx.new(|cx| {
10918 let mut multibuffer = MultiBuffer::new(ReadWrite);
10919 excerpt1_id = multibuffer
10920 .push_excerpts(
10921 buffer.clone(),
10922 [
10923 ExcerptRange {
10924 context: Point::new(0, 0)..Point::new(1, 4),
10925 primary: None,
10926 },
10927 ExcerptRange {
10928 context: Point::new(1, 0)..Point::new(2, 4),
10929 primary: None,
10930 },
10931 ],
10932 cx,
10933 )
10934 .into_iter()
10935 .next();
10936 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10937 multibuffer
10938 });
10939
10940 let editor = cx.add_window(|window, cx| {
10941 let mut editor = build_editor(multibuffer.clone(), window, cx);
10942 let snapshot = editor.snapshot(window, cx);
10943 editor.begin_selection(
10944 Point::new(1, 3).to_display_point(&snapshot),
10945 false,
10946 1,
10947 window,
10948 cx,
10949 );
10950 assert_eq!(
10951 editor.selections.ranges(cx),
10952 [Point::new(1, 3)..Point::new(1, 3)]
10953 );
10954 editor
10955 });
10956
10957 multibuffer.update(cx, |multibuffer, cx| {
10958 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10959 });
10960 _ = editor.update(cx, |editor, window, cx| {
10961 assert_eq!(
10962 editor.selections.ranges(cx),
10963 [Point::new(0, 0)..Point::new(0, 0)]
10964 );
10965
10966 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10967 editor.change_selections(None, window, cx, |s| s.refresh());
10968 assert_eq!(
10969 editor.selections.ranges(cx),
10970 [Point::new(0, 3)..Point::new(0, 3)]
10971 );
10972 assert!(editor.selections.pending_anchor().is_some());
10973 });
10974}
10975
10976#[gpui::test]
10977async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10978 init_test(cx, |_| {});
10979
10980 let language = Arc::new(
10981 Language::new(
10982 LanguageConfig {
10983 brackets: BracketPairConfig {
10984 pairs: vec![
10985 BracketPair {
10986 start: "{".to_string(),
10987 end: "}".to_string(),
10988 close: true,
10989 surround: true,
10990 newline: true,
10991 },
10992 BracketPair {
10993 start: "/* ".to_string(),
10994 end: " */".to_string(),
10995 close: true,
10996 surround: true,
10997 newline: true,
10998 },
10999 ],
11000 ..Default::default()
11001 },
11002 ..Default::default()
11003 },
11004 Some(tree_sitter_rust::LANGUAGE.into()),
11005 )
11006 .with_indents_query("")
11007 .unwrap(),
11008 );
11009
11010 let text = concat!(
11011 "{ }\n", //
11012 " x\n", //
11013 " /* */\n", //
11014 "x\n", //
11015 "{{} }\n", //
11016 );
11017
11018 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11019 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11020 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11021 editor
11022 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11023 .await;
11024
11025 editor.update_in(cx, |editor, window, cx| {
11026 editor.change_selections(None, window, cx, |s| {
11027 s.select_display_ranges([
11028 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11029 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11030 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11031 ])
11032 });
11033 editor.newline(&Newline, window, cx);
11034
11035 assert_eq!(
11036 editor.buffer().read(cx).read(cx).text(),
11037 concat!(
11038 "{ \n", // Suppress rustfmt
11039 "\n", //
11040 "}\n", //
11041 " x\n", //
11042 " /* \n", //
11043 " \n", //
11044 " */\n", //
11045 "x\n", //
11046 "{{} \n", //
11047 "}\n", //
11048 )
11049 );
11050 });
11051}
11052
11053#[gpui::test]
11054fn test_highlighted_ranges(cx: &mut TestAppContext) {
11055 init_test(cx, |_| {});
11056
11057 let editor = cx.add_window(|window, cx| {
11058 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11059 build_editor(buffer.clone(), window, cx)
11060 });
11061
11062 _ = editor.update(cx, |editor, window, cx| {
11063 struct Type1;
11064 struct Type2;
11065
11066 let buffer = editor.buffer.read(cx).snapshot(cx);
11067
11068 let anchor_range =
11069 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11070
11071 editor.highlight_background::<Type1>(
11072 &[
11073 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11074 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11075 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11076 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11077 ],
11078 |_| Hsla::red(),
11079 cx,
11080 );
11081 editor.highlight_background::<Type2>(
11082 &[
11083 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11084 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11085 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11086 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11087 ],
11088 |_| Hsla::green(),
11089 cx,
11090 );
11091
11092 let snapshot = editor.snapshot(window, cx);
11093 let mut highlighted_ranges = editor.background_highlights_in_range(
11094 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11095 &snapshot,
11096 cx.theme().colors(),
11097 );
11098 // Enforce a consistent ordering based on color without relying on the ordering of the
11099 // highlight's `TypeId` which is non-executor.
11100 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11101 assert_eq!(
11102 highlighted_ranges,
11103 &[
11104 (
11105 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11106 Hsla::red(),
11107 ),
11108 (
11109 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11110 Hsla::red(),
11111 ),
11112 (
11113 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11114 Hsla::green(),
11115 ),
11116 (
11117 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11118 Hsla::green(),
11119 ),
11120 ]
11121 );
11122 assert_eq!(
11123 editor.background_highlights_in_range(
11124 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11125 &snapshot,
11126 cx.theme().colors(),
11127 ),
11128 &[(
11129 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11130 Hsla::red(),
11131 )]
11132 );
11133 });
11134}
11135
11136#[gpui::test]
11137async fn test_following(cx: &mut TestAppContext) {
11138 init_test(cx, |_| {});
11139
11140 let fs = FakeFs::new(cx.executor());
11141 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11142
11143 let buffer = project.update(cx, |project, cx| {
11144 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11145 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11146 });
11147 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11148 let follower = cx.update(|cx| {
11149 cx.open_window(
11150 WindowOptions {
11151 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11152 gpui::Point::new(px(0.), px(0.)),
11153 gpui::Point::new(px(10.), px(80.)),
11154 ))),
11155 ..Default::default()
11156 },
11157 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11158 )
11159 .unwrap()
11160 });
11161
11162 let is_still_following = Rc::new(RefCell::new(true));
11163 let follower_edit_event_count = Rc::new(RefCell::new(0));
11164 let pending_update = Rc::new(RefCell::new(None));
11165 let leader_entity = leader.root(cx).unwrap();
11166 let follower_entity = follower.root(cx).unwrap();
11167 _ = follower.update(cx, {
11168 let update = pending_update.clone();
11169 let is_still_following = is_still_following.clone();
11170 let follower_edit_event_count = follower_edit_event_count.clone();
11171 |_, window, cx| {
11172 cx.subscribe_in(
11173 &leader_entity,
11174 window,
11175 move |_, leader, event, window, cx| {
11176 leader.read(cx).add_event_to_update_proto(
11177 event,
11178 &mut update.borrow_mut(),
11179 window,
11180 cx,
11181 );
11182 },
11183 )
11184 .detach();
11185
11186 cx.subscribe_in(
11187 &follower_entity,
11188 window,
11189 move |_, _, event: &EditorEvent, _window, _cx| {
11190 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11191 *is_still_following.borrow_mut() = false;
11192 }
11193
11194 if let EditorEvent::BufferEdited = event {
11195 *follower_edit_event_count.borrow_mut() += 1;
11196 }
11197 },
11198 )
11199 .detach();
11200 }
11201 });
11202
11203 // Update the selections only
11204 _ = leader.update(cx, |leader, window, cx| {
11205 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11206 });
11207 follower
11208 .update(cx, |follower, window, cx| {
11209 follower.apply_update_proto(
11210 &project,
11211 pending_update.borrow_mut().take().unwrap(),
11212 window,
11213 cx,
11214 )
11215 })
11216 .unwrap()
11217 .await
11218 .unwrap();
11219 _ = follower.update(cx, |follower, _, cx| {
11220 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11221 });
11222 assert!(*is_still_following.borrow());
11223 assert_eq!(*follower_edit_event_count.borrow(), 0);
11224
11225 // Update the scroll position only
11226 _ = leader.update(cx, |leader, window, cx| {
11227 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11228 });
11229 follower
11230 .update(cx, |follower, window, cx| {
11231 follower.apply_update_proto(
11232 &project,
11233 pending_update.borrow_mut().take().unwrap(),
11234 window,
11235 cx,
11236 )
11237 })
11238 .unwrap()
11239 .await
11240 .unwrap();
11241 assert_eq!(
11242 follower
11243 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11244 .unwrap(),
11245 gpui::Point::new(1.5, 3.5)
11246 );
11247 assert!(*is_still_following.borrow());
11248 assert_eq!(*follower_edit_event_count.borrow(), 0);
11249
11250 // Update the selections and scroll position. The follower's scroll position is updated
11251 // via autoscroll, not via the leader's exact scroll position.
11252 _ = leader.update(cx, |leader, window, cx| {
11253 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11254 leader.request_autoscroll(Autoscroll::newest(), cx);
11255 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11256 });
11257 follower
11258 .update(cx, |follower, window, cx| {
11259 follower.apply_update_proto(
11260 &project,
11261 pending_update.borrow_mut().take().unwrap(),
11262 window,
11263 cx,
11264 )
11265 })
11266 .unwrap()
11267 .await
11268 .unwrap();
11269 _ = follower.update(cx, |follower, _, cx| {
11270 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11271 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11272 });
11273 assert!(*is_still_following.borrow());
11274
11275 // Creating a pending selection that precedes another selection
11276 _ = leader.update(cx, |leader, window, cx| {
11277 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11278 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11279 });
11280 follower
11281 .update(cx, |follower, window, cx| {
11282 follower.apply_update_proto(
11283 &project,
11284 pending_update.borrow_mut().take().unwrap(),
11285 window,
11286 cx,
11287 )
11288 })
11289 .unwrap()
11290 .await
11291 .unwrap();
11292 _ = follower.update(cx, |follower, _, cx| {
11293 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11294 });
11295 assert!(*is_still_following.borrow());
11296
11297 // Extend the pending selection so that it surrounds another selection
11298 _ = leader.update(cx, |leader, window, cx| {
11299 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11300 });
11301 follower
11302 .update(cx, |follower, window, cx| {
11303 follower.apply_update_proto(
11304 &project,
11305 pending_update.borrow_mut().take().unwrap(),
11306 window,
11307 cx,
11308 )
11309 })
11310 .unwrap()
11311 .await
11312 .unwrap();
11313 _ = follower.update(cx, |follower, _, cx| {
11314 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11315 });
11316
11317 // Scrolling locally breaks the follow
11318 _ = follower.update(cx, |follower, window, cx| {
11319 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11320 follower.set_scroll_anchor(
11321 ScrollAnchor {
11322 anchor: top_anchor,
11323 offset: gpui::Point::new(0.0, 0.5),
11324 },
11325 window,
11326 cx,
11327 );
11328 });
11329 assert!(!(*is_still_following.borrow()));
11330}
11331
11332#[gpui::test]
11333async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11334 init_test(cx, |_| {});
11335
11336 let fs = FakeFs::new(cx.executor());
11337 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11338 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11339 let pane = workspace
11340 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11341 .unwrap();
11342
11343 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11344
11345 let leader = pane.update_in(cx, |_, window, cx| {
11346 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11347 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11348 });
11349
11350 // Start following the editor when it has no excerpts.
11351 let mut state_message =
11352 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11353 let workspace_entity = workspace.root(cx).unwrap();
11354 let follower_1 = cx
11355 .update_window(*workspace.deref(), |_, window, cx| {
11356 Editor::from_state_proto(
11357 workspace_entity,
11358 ViewId {
11359 creator: Default::default(),
11360 id: 0,
11361 },
11362 &mut state_message,
11363 window,
11364 cx,
11365 )
11366 })
11367 .unwrap()
11368 .unwrap()
11369 .await
11370 .unwrap();
11371
11372 let update_message = Rc::new(RefCell::new(None));
11373 follower_1.update_in(cx, {
11374 let update = update_message.clone();
11375 |_, window, cx| {
11376 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11377 leader.read(cx).add_event_to_update_proto(
11378 event,
11379 &mut update.borrow_mut(),
11380 window,
11381 cx,
11382 );
11383 })
11384 .detach();
11385 }
11386 });
11387
11388 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11389 (
11390 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11391 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11392 )
11393 });
11394
11395 // Insert some excerpts.
11396 leader.update(cx, |leader, cx| {
11397 leader.buffer.update(cx, |multibuffer, cx| {
11398 let excerpt_ids = multibuffer.push_excerpts(
11399 buffer_1.clone(),
11400 [
11401 ExcerptRange {
11402 context: 1..6,
11403 primary: None,
11404 },
11405 ExcerptRange {
11406 context: 12..15,
11407 primary: None,
11408 },
11409 ExcerptRange {
11410 context: 0..3,
11411 primary: None,
11412 },
11413 ],
11414 cx,
11415 );
11416 multibuffer.insert_excerpts_after(
11417 excerpt_ids[0],
11418 buffer_2.clone(),
11419 [
11420 ExcerptRange {
11421 context: 8..12,
11422 primary: None,
11423 },
11424 ExcerptRange {
11425 context: 0..6,
11426 primary: None,
11427 },
11428 ],
11429 cx,
11430 );
11431 });
11432 });
11433
11434 // Apply the update of adding the excerpts.
11435 follower_1
11436 .update_in(cx, |follower, window, cx| {
11437 follower.apply_update_proto(
11438 &project,
11439 update_message.borrow().clone().unwrap(),
11440 window,
11441 cx,
11442 )
11443 })
11444 .await
11445 .unwrap();
11446 assert_eq!(
11447 follower_1.update(cx, |editor, cx| editor.text(cx)),
11448 leader.update(cx, |editor, cx| editor.text(cx))
11449 );
11450 update_message.borrow_mut().take();
11451
11452 // Start following separately after it already has excerpts.
11453 let mut state_message =
11454 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11455 let workspace_entity = workspace.root(cx).unwrap();
11456 let follower_2 = cx
11457 .update_window(*workspace.deref(), |_, window, cx| {
11458 Editor::from_state_proto(
11459 workspace_entity,
11460 ViewId {
11461 creator: Default::default(),
11462 id: 0,
11463 },
11464 &mut state_message,
11465 window,
11466 cx,
11467 )
11468 })
11469 .unwrap()
11470 .unwrap()
11471 .await
11472 .unwrap();
11473 assert_eq!(
11474 follower_2.update(cx, |editor, cx| editor.text(cx)),
11475 leader.update(cx, |editor, cx| editor.text(cx))
11476 );
11477
11478 // Remove some excerpts.
11479 leader.update(cx, |leader, cx| {
11480 leader.buffer.update(cx, |multibuffer, cx| {
11481 let excerpt_ids = multibuffer.excerpt_ids();
11482 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11483 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11484 });
11485 });
11486
11487 // Apply the update of removing the excerpts.
11488 follower_1
11489 .update_in(cx, |follower, window, cx| {
11490 follower.apply_update_proto(
11491 &project,
11492 update_message.borrow().clone().unwrap(),
11493 window,
11494 cx,
11495 )
11496 })
11497 .await
11498 .unwrap();
11499 follower_2
11500 .update_in(cx, |follower, window, cx| {
11501 follower.apply_update_proto(
11502 &project,
11503 update_message.borrow().clone().unwrap(),
11504 window,
11505 cx,
11506 )
11507 })
11508 .await
11509 .unwrap();
11510 update_message.borrow_mut().take();
11511 assert_eq!(
11512 follower_1.update(cx, |editor, cx| editor.text(cx)),
11513 leader.update(cx, |editor, cx| editor.text(cx))
11514 );
11515}
11516
11517#[gpui::test]
11518async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11519 init_test(cx, |_| {});
11520
11521 let mut cx = EditorTestContext::new(cx).await;
11522 let lsp_store =
11523 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11524
11525 cx.set_state(indoc! {"
11526 ˇfn func(abc def: i32) -> u32 {
11527 }
11528 "});
11529
11530 cx.update(|_, cx| {
11531 lsp_store.update(cx, |lsp_store, cx| {
11532 lsp_store
11533 .update_diagnostics(
11534 LanguageServerId(0),
11535 lsp::PublishDiagnosticsParams {
11536 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11537 version: None,
11538 diagnostics: vec![
11539 lsp::Diagnostic {
11540 range: lsp::Range::new(
11541 lsp::Position::new(0, 11),
11542 lsp::Position::new(0, 12),
11543 ),
11544 severity: Some(lsp::DiagnosticSeverity::ERROR),
11545 ..Default::default()
11546 },
11547 lsp::Diagnostic {
11548 range: lsp::Range::new(
11549 lsp::Position::new(0, 12),
11550 lsp::Position::new(0, 15),
11551 ),
11552 severity: Some(lsp::DiagnosticSeverity::ERROR),
11553 ..Default::default()
11554 },
11555 lsp::Diagnostic {
11556 range: lsp::Range::new(
11557 lsp::Position::new(0, 25),
11558 lsp::Position::new(0, 28),
11559 ),
11560 severity: Some(lsp::DiagnosticSeverity::ERROR),
11561 ..Default::default()
11562 },
11563 ],
11564 },
11565 &[],
11566 cx,
11567 )
11568 .unwrap()
11569 });
11570 });
11571
11572 executor.run_until_parked();
11573
11574 cx.update_editor(|editor, window, cx| {
11575 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11576 });
11577
11578 cx.assert_editor_state(indoc! {"
11579 fn func(abc def: i32) -> ˇu32 {
11580 }
11581 "});
11582
11583 cx.update_editor(|editor, window, cx| {
11584 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11585 });
11586
11587 cx.assert_editor_state(indoc! {"
11588 fn func(abc ˇdef: i32) -> u32 {
11589 }
11590 "});
11591
11592 cx.update_editor(|editor, window, cx| {
11593 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11594 });
11595
11596 cx.assert_editor_state(indoc! {"
11597 fn func(abcˇ def: i32) -> u32 {
11598 }
11599 "});
11600
11601 cx.update_editor(|editor, window, cx| {
11602 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11603 });
11604
11605 cx.assert_editor_state(indoc! {"
11606 fn func(abc def: i32) -> ˇu32 {
11607 }
11608 "});
11609}
11610
11611#[gpui::test]
11612async fn cycle_through_same_place_diagnostics(
11613 executor: BackgroundExecutor,
11614 cx: &mut TestAppContext,
11615) {
11616 init_test(cx, |_| {});
11617
11618 let mut cx = EditorTestContext::new(cx).await;
11619 let lsp_store =
11620 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11621
11622 cx.set_state(indoc! {"
11623 ˇfn func(abc def: i32) -> u32 {
11624 }
11625 "});
11626
11627 cx.update(|_, cx| {
11628 lsp_store.update(cx, |lsp_store, cx| {
11629 lsp_store
11630 .update_diagnostics(
11631 LanguageServerId(0),
11632 lsp::PublishDiagnosticsParams {
11633 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11634 version: None,
11635 diagnostics: vec![
11636 lsp::Diagnostic {
11637 range: lsp::Range::new(
11638 lsp::Position::new(0, 11),
11639 lsp::Position::new(0, 12),
11640 ),
11641 severity: Some(lsp::DiagnosticSeverity::ERROR),
11642 ..Default::default()
11643 },
11644 lsp::Diagnostic {
11645 range: lsp::Range::new(
11646 lsp::Position::new(0, 12),
11647 lsp::Position::new(0, 15),
11648 ),
11649 severity: Some(lsp::DiagnosticSeverity::ERROR),
11650 ..Default::default()
11651 },
11652 lsp::Diagnostic {
11653 range: lsp::Range::new(
11654 lsp::Position::new(0, 12),
11655 lsp::Position::new(0, 15),
11656 ),
11657 severity: Some(lsp::DiagnosticSeverity::ERROR),
11658 ..Default::default()
11659 },
11660 lsp::Diagnostic {
11661 range: lsp::Range::new(
11662 lsp::Position::new(0, 25),
11663 lsp::Position::new(0, 28),
11664 ),
11665 severity: Some(lsp::DiagnosticSeverity::ERROR),
11666 ..Default::default()
11667 },
11668 ],
11669 },
11670 &[],
11671 cx,
11672 )
11673 .unwrap()
11674 });
11675 });
11676 executor.run_until_parked();
11677
11678 //// Backward
11679
11680 // Fourth diagnostic
11681 cx.update_editor(|editor, window, cx| {
11682 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11683 });
11684 cx.assert_editor_state(indoc! {"
11685 fn func(abc def: i32) -> ˇu32 {
11686 }
11687 "});
11688
11689 // Third diagnostic
11690 cx.update_editor(|editor, window, cx| {
11691 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11692 });
11693 cx.assert_editor_state(indoc! {"
11694 fn func(abc ˇdef: i32) -> u32 {
11695 }
11696 "});
11697
11698 // Second diagnostic, same place
11699 cx.update_editor(|editor, window, cx| {
11700 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11701 });
11702 cx.assert_editor_state(indoc! {"
11703 fn func(abc ˇdef: i32) -> u32 {
11704 }
11705 "});
11706
11707 // First diagnostic
11708 cx.update_editor(|editor, window, cx| {
11709 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11710 });
11711 cx.assert_editor_state(indoc! {"
11712 fn func(abcˇ def: i32) -> u32 {
11713 }
11714 "});
11715
11716 // Wrapped over, fourth diagnostic
11717 cx.update_editor(|editor, window, cx| {
11718 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11719 });
11720 cx.assert_editor_state(indoc! {"
11721 fn func(abc def: i32) -> ˇu32 {
11722 }
11723 "});
11724
11725 cx.update_editor(|editor, window, cx| {
11726 editor.move_to_beginning(&MoveToBeginning, window, cx);
11727 });
11728 cx.assert_editor_state(indoc! {"
11729 ˇfn func(abc def: i32) -> u32 {
11730 }
11731 "});
11732
11733 //// Forward
11734
11735 // First diagnostic
11736 cx.update_editor(|editor, window, cx| {
11737 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11738 });
11739 cx.assert_editor_state(indoc! {"
11740 fn func(abcˇ def: i32) -> u32 {
11741 }
11742 "});
11743
11744 // Second diagnostic
11745 cx.update_editor(|editor, window, cx| {
11746 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11747 });
11748 cx.assert_editor_state(indoc! {"
11749 fn func(abc ˇdef: i32) -> u32 {
11750 }
11751 "});
11752
11753 // Third diagnostic, same place
11754 cx.update_editor(|editor, window, cx| {
11755 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11756 });
11757 cx.assert_editor_state(indoc! {"
11758 fn func(abc ˇdef: i32) -> u32 {
11759 }
11760 "});
11761
11762 // Fourth diagnostic
11763 cx.update_editor(|editor, window, cx| {
11764 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11765 });
11766 cx.assert_editor_state(indoc! {"
11767 fn func(abc def: i32) -> ˇu32 {
11768 }
11769 "});
11770
11771 // Wrapped around, first diagnostic
11772 cx.update_editor(|editor, window, cx| {
11773 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11774 });
11775 cx.assert_editor_state(indoc! {"
11776 fn func(abcˇ def: i32) -> u32 {
11777 }
11778 "});
11779}
11780
11781#[gpui::test]
11782async fn active_diagnostics_dismiss_after_invalidation(
11783 executor: BackgroundExecutor,
11784 cx: &mut TestAppContext,
11785) {
11786 init_test(cx, |_| {});
11787
11788 let mut cx = EditorTestContext::new(cx).await;
11789 let lsp_store =
11790 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11791
11792 cx.set_state(indoc! {"
11793 ˇfn func(abc def: i32) -> u32 {
11794 }
11795 "});
11796
11797 let message = "Something's wrong!";
11798 cx.update(|_, cx| {
11799 lsp_store.update(cx, |lsp_store, cx| {
11800 lsp_store
11801 .update_diagnostics(
11802 LanguageServerId(0),
11803 lsp::PublishDiagnosticsParams {
11804 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11805 version: None,
11806 diagnostics: vec![lsp::Diagnostic {
11807 range: lsp::Range::new(
11808 lsp::Position::new(0, 11),
11809 lsp::Position::new(0, 12),
11810 ),
11811 severity: Some(lsp::DiagnosticSeverity::ERROR),
11812 message: message.to_string(),
11813 ..Default::default()
11814 }],
11815 },
11816 &[],
11817 cx,
11818 )
11819 .unwrap()
11820 });
11821 });
11822 executor.run_until_parked();
11823
11824 cx.update_editor(|editor, window, cx| {
11825 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11826 assert_eq!(
11827 editor
11828 .active_diagnostics
11829 .as_ref()
11830 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11831 Some(message),
11832 "Should have a diagnostics group activated"
11833 );
11834 });
11835 cx.assert_editor_state(indoc! {"
11836 fn func(abcˇ def: i32) -> u32 {
11837 }
11838 "});
11839
11840 cx.update(|_, cx| {
11841 lsp_store.update(cx, |lsp_store, cx| {
11842 lsp_store
11843 .update_diagnostics(
11844 LanguageServerId(0),
11845 lsp::PublishDiagnosticsParams {
11846 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11847 version: None,
11848 diagnostics: Vec::new(),
11849 },
11850 &[],
11851 cx,
11852 )
11853 .unwrap()
11854 });
11855 });
11856 executor.run_until_parked();
11857 cx.update_editor(|editor, _, _| {
11858 assert_eq!(
11859 editor.active_diagnostics, None,
11860 "After no diagnostics set to the editor, no diagnostics should be active"
11861 );
11862 });
11863 cx.assert_editor_state(indoc! {"
11864 fn func(abcˇ def: i32) -> u32 {
11865 }
11866 "});
11867
11868 cx.update_editor(|editor, window, cx| {
11869 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11870 assert_eq!(
11871 editor.active_diagnostics, None,
11872 "Should be no diagnostics to go to and activate"
11873 );
11874 });
11875 cx.assert_editor_state(indoc! {"
11876 fn func(abcˇ def: i32) -> u32 {
11877 }
11878 "});
11879}
11880
11881#[gpui::test]
11882async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11883 init_test(cx, |_| {});
11884
11885 let mut cx = EditorTestContext::new(cx).await;
11886
11887 cx.set_state(indoc! {"
11888 fn func(abˇc def: i32) -> u32 {
11889 }
11890 "});
11891 let lsp_store =
11892 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11893
11894 cx.update(|_, cx| {
11895 lsp_store.update(cx, |lsp_store, cx| {
11896 lsp_store.update_diagnostics(
11897 LanguageServerId(0),
11898 lsp::PublishDiagnosticsParams {
11899 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11900 version: None,
11901 diagnostics: vec![lsp::Diagnostic {
11902 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11903 severity: Some(lsp::DiagnosticSeverity::ERROR),
11904 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11905 ..Default::default()
11906 }],
11907 },
11908 &[],
11909 cx,
11910 )
11911 })
11912 }).unwrap();
11913 cx.run_until_parked();
11914 cx.update_editor(|editor, window, cx| {
11915 hover_popover::hover(editor, &Default::default(), window, cx)
11916 });
11917 cx.run_until_parked();
11918 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11919}
11920
11921#[gpui::test]
11922async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11923 init_test(cx, |_| {});
11924
11925 let mut cx = EditorTestContext::new(cx).await;
11926
11927 let diff_base = r#"
11928 use some::mod;
11929
11930 const A: u32 = 42;
11931
11932 fn main() {
11933 println!("hello");
11934
11935 println!("world");
11936 }
11937 "#
11938 .unindent();
11939
11940 // Edits are modified, removed, modified, added
11941 cx.set_state(
11942 &r#"
11943 use some::modified;
11944
11945 ˇ
11946 fn main() {
11947 println!("hello there");
11948
11949 println!("around the");
11950 println!("world");
11951 }
11952 "#
11953 .unindent(),
11954 );
11955
11956 cx.set_head_text(&diff_base);
11957 executor.run_until_parked();
11958
11959 cx.update_editor(|editor, window, cx| {
11960 //Wrap around the bottom of the buffer
11961 for _ in 0..3 {
11962 editor.go_to_next_hunk(&GoToHunk, window, cx);
11963 }
11964 });
11965
11966 cx.assert_editor_state(
11967 &r#"
11968 ˇuse some::modified;
11969
11970
11971 fn main() {
11972 println!("hello there");
11973
11974 println!("around the");
11975 println!("world");
11976 }
11977 "#
11978 .unindent(),
11979 );
11980
11981 cx.update_editor(|editor, window, cx| {
11982 //Wrap around the top of the buffer
11983 for _ in 0..2 {
11984 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11985 }
11986 });
11987
11988 cx.assert_editor_state(
11989 &r#"
11990 use some::modified;
11991
11992
11993 fn main() {
11994 ˇ println!("hello there");
11995
11996 println!("around the");
11997 println!("world");
11998 }
11999 "#
12000 .unindent(),
12001 );
12002
12003 cx.update_editor(|editor, window, cx| {
12004 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12005 });
12006
12007 cx.assert_editor_state(
12008 &r#"
12009 use some::modified;
12010
12011 ˇ
12012 fn main() {
12013 println!("hello there");
12014
12015 println!("around the");
12016 println!("world");
12017 }
12018 "#
12019 .unindent(),
12020 );
12021
12022 cx.update_editor(|editor, window, cx| {
12023 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12024 });
12025
12026 cx.assert_editor_state(
12027 &r#"
12028 ˇuse some::modified;
12029
12030
12031 fn main() {
12032 println!("hello there");
12033
12034 println!("around the");
12035 println!("world");
12036 }
12037 "#
12038 .unindent(),
12039 );
12040
12041 cx.update_editor(|editor, window, cx| {
12042 for _ in 0..2 {
12043 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12044 }
12045 });
12046
12047 cx.assert_editor_state(
12048 &r#"
12049 use some::modified;
12050
12051
12052 fn main() {
12053 ˇ println!("hello there");
12054
12055 println!("around the");
12056 println!("world");
12057 }
12058 "#
12059 .unindent(),
12060 );
12061
12062 cx.update_editor(|editor, window, cx| {
12063 editor.fold(&Fold, window, cx);
12064 });
12065
12066 cx.update_editor(|editor, window, cx| {
12067 editor.go_to_next_hunk(&GoToHunk, window, cx);
12068 });
12069
12070 cx.assert_editor_state(
12071 &r#"
12072 ˇuse some::modified;
12073
12074
12075 fn main() {
12076 println!("hello there");
12077
12078 println!("around the");
12079 println!("world");
12080 }
12081 "#
12082 .unindent(),
12083 );
12084}
12085
12086#[test]
12087fn test_split_words() {
12088 fn split(text: &str) -> Vec<&str> {
12089 split_words(text).collect()
12090 }
12091
12092 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12093 assert_eq!(split("hello_world"), &["hello_", "world"]);
12094 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12095 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12096 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12097 assert_eq!(split("helloworld"), &["helloworld"]);
12098
12099 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12100}
12101
12102#[gpui::test]
12103async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12104 init_test(cx, |_| {});
12105
12106 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12107 let mut assert = |before, after| {
12108 let _state_context = cx.set_state(before);
12109 cx.run_until_parked();
12110 cx.update_editor(|editor, window, cx| {
12111 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12112 });
12113 cx.run_until_parked();
12114 cx.assert_editor_state(after);
12115 };
12116
12117 // Outside bracket jumps to outside of matching bracket
12118 assert("console.logˇ(var);", "console.log(var)ˇ;");
12119 assert("console.log(var)ˇ;", "console.logˇ(var);");
12120
12121 // Inside bracket jumps to inside of matching bracket
12122 assert("console.log(ˇvar);", "console.log(varˇ);");
12123 assert("console.log(varˇ);", "console.log(ˇvar);");
12124
12125 // When outside a bracket and inside, favor jumping to the inside bracket
12126 assert(
12127 "console.log('foo', [1, 2, 3]ˇ);",
12128 "console.log(ˇ'foo', [1, 2, 3]);",
12129 );
12130 assert(
12131 "console.log(ˇ'foo', [1, 2, 3]);",
12132 "console.log('foo', [1, 2, 3]ˇ);",
12133 );
12134
12135 // Bias forward if two options are equally likely
12136 assert(
12137 "let result = curried_fun()ˇ();",
12138 "let result = curried_fun()()ˇ;",
12139 );
12140
12141 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12142 assert(
12143 indoc! {"
12144 function test() {
12145 console.log('test')ˇ
12146 }"},
12147 indoc! {"
12148 function test() {
12149 console.logˇ('test')
12150 }"},
12151 );
12152}
12153
12154#[gpui::test]
12155async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12156 init_test(cx, |_| {});
12157
12158 let fs = FakeFs::new(cx.executor());
12159 fs.insert_tree(
12160 path!("/a"),
12161 json!({
12162 "main.rs": "fn main() { let a = 5; }",
12163 "other.rs": "// Test file",
12164 }),
12165 )
12166 .await;
12167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12168
12169 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12170 language_registry.add(Arc::new(Language::new(
12171 LanguageConfig {
12172 name: "Rust".into(),
12173 matcher: LanguageMatcher {
12174 path_suffixes: vec!["rs".to_string()],
12175 ..Default::default()
12176 },
12177 brackets: BracketPairConfig {
12178 pairs: vec![BracketPair {
12179 start: "{".to_string(),
12180 end: "}".to_string(),
12181 close: true,
12182 surround: true,
12183 newline: true,
12184 }],
12185 disabled_scopes_by_bracket_ix: Vec::new(),
12186 },
12187 ..Default::default()
12188 },
12189 Some(tree_sitter_rust::LANGUAGE.into()),
12190 )));
12191 let mut fake_servers = language_registry.register_fake_lsp(
12192 "Rust",
12193 FakeLspAdapter {
12194 capabilities: lsp::ServerCapabilities {
12195 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12196 first_trigger_character: "{".to_string(),
12197 more_trigger_character: None,
12198 }),
12199 ..Default::default()
12200 },
12201 ..Default::default()
12202 },
12203 );
12204
12205 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12206
12207 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12208
12209 let worktree_id = workspace
12210 .update(cx, |workspace, _, cx| {
12211 workspace.project().update(cx, |project, cx| {
12212 project.worktrees(cx).next().unwrap().read(cx).id()
12213 })
12214 })
12215 .unwrap();
12216
12217 let buffer = project
12218 .update(cx, |project, cx| {
12219 project.open_local_buffer(path!("/a/main.rs"), cx)
12220 })
12221 .await
12222 .unwrap();
12223 let editor_handle = workspace
12224 .update(cx, |workspace, window, cx| {
12225 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12226 })
12227 .unwrap()
12228 .await
12229 .unwrap()
12230 .downcast::<Editor>()
12231 .unwrap();
12232
12233 cx.executor().start_waiting();
12234 let fake_server = fake_servers.next().await.unwrap();
12235
12236 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12237 |params, _| async move {
12238 assert_eq!(
12239 params.text_document_position.text_document.uri,
12240 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12241 );
12242 assert_eq!(
12243 params.text_document_position.position,
12244 lsp::Position::new(0, 21),
12245 );
12246
12247 Ok(Some(vec![lsp::TextEdit {
12248 new_text: "]".to_string(),
12249 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12250 }]))
12251 },
12252 );
12253
12254 editor_handle.update_in(cx, |editor, window, cx| {
12255 window.focus(&editor.focus_handle(cx));
12256 editor.change_selections(None, window, cx, |s| {
12257 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12258 });
12259 editor.handle_input("{", window, cx);
12260 });
12261
12262 cx.executor().run_until_parked();
12263
12264 buffer.update(cx, |buffer, _| {
12265 assert_eq!(
12266 buffer.text(),
12267 "fn main() { let a = {5}; }",
12268 "No extra braces from on type formatting should appear in the buffer"
12269 )
12270 });
12271}
12272
12273#[gpui::test]
12274async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12275 init_test(cx, |_| {});
12276
12277 let fs = FakeFs::new(cx.executor());
12278 fs.insert_tree(
12279 path!("/a"),
12280 json!({
12281 "main.rs": "fn main() { let a = 5; }",
12282 "other.rs": "// Test file",
12283 }),
12284 )
12285 .await;
12286
12287 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12288
12289 let server_restarts = Arc::new(AtomicUsize::new(0));
12290 let closure_restarts = Arc::clone(&server_restarts);
12291 let language_server_name = "test language server";
12292 let language_name: LanguageName = "Rust".into();
12293
12294 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12295 language_registry.add(Arc::new(Language::new(
12296 LanguageConfig {
12297 name: language_name.clone(),
12298 matcher: LanguageMatcher {
12299 path_suffixes: vec!["rs".to_string()],
12300 ..Default::default()
12301 },
12302 ..Default::default()
12303 },
12304 Some(tree_sitter_rust::LANGUAGE.into()),
12305 )));
12306 let mut fake_servers = language_registry.register_fake_lsp(
12307 "Rust",
12308 FakeLspAdapter {
12309 name: language_server_name,
12310 initialization_options: Some(json!({
12311 "testOptionValue": true
12312 })),
12313 initializer: Some(Box::new(move |fake_server| {
12314 let task_restarts = Arc::clone(&closure_restarts);
12315 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12316 task_restarts.fetch_add(1, atomic::Ordering::Release);
12317 futures::future::ready(Ok(()))
12318 });
12319 })),
12320 ..Default::default()
12321 },
12322 );
12323
12324 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12325 let _buffer = project
12326 .update(cx, |project, cx| {
12327 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12328 })
12329 .await
12330 .unwrap();
12331 let _fake_server = fake_servers.next().await.unwrap();
12332 update_test_language_settings(cx, |language_settings| {
12333 language_settings.languages.insert(
12334 language_name.clone(),
12335 LanguageSettingsContent {
12336 tab_size: NonZeroU32::new(8),
12337 ..Default::default()
12338 },
12339 );
12340 });
12341 cx.executor().run_until_parked();
12342 assert_eq!(
12343 server_restarts.load(atomic::Ordering::Acquire),
12344 0,
12345 "Should not restart LSP server on an unrelated change"
12346 );
12347
12348 update_test_project_settings(cx, |project_settings| {
12349 project_settings.lsp.insert(
12350 "Some other server name".into(),
12351 LspSettings {
12352 binary: None,
12353 settings: None,
12354 initialization_options: Some(json!({
12355 "some other init value": false
12356 })),
12357 },
12358 );
12359 });
12360 cx.executor().run_until_parked();
12361 assert_eq!(
12362 server_restarts.load(atomic::Ordering::Acquire),
12363 0,
12364 "Should not restart LSP server on an unrelated LSP settings change"
12365 );
12366
12367 update_test_project_settings(cx, |project_settings| {
12368 project_settings.lsp.insert(
12369 language_server_name.into(),
12370 LspSettings {
12371 binary: None,
12372 settings: None,
12373 initialization_options: Some(json!({
12374 "anotherInitValue": false
12375 })),
12376 },
12377 );
12378 });
12379 cx.executor().run_until_parked();
12380 assert_eq!(
12381 server_restarts.load(atomic::Ordering::Acquire),
12382 1,
12383 "Should restart LSP server on a related LSP settings change"
12384 );
12385
12386 update_test_project_settings(cx, |project_settings| {
12387 project_settings.lsp.insert(
12388 language_server_name.into(),
12389 LspSettings {
12390 binary: None,
12391 settings: None,
12392 initialization_options: Some(json!({
12393 "anotherInitValue": false
12394 })),
12395 },
12396 );
12397 });
12398 cx.executor().run_until_parked();
12399 assert_eq!(
12400 server_restarts.load(atomic::Ordering::Acquire),
12401 1,
12402 "Should not restart LSP server on a related LSP settings change that is the same"
12403 );
12404
12405 update_test_project_settings(cx, |project_settings| {
12406 project_settings.lsp.insert(
12407 language_server_name.into(),
12408 LspSettings {
12409 binary: None,
12410 settings: None,
12411 initialization_options: None,
12412 },
12413 );
12414 });
12415 cx.executor().run_until_parked();
12416 assert_eq!(
12417 server_restarts.load(atomic::Ordering::Acquire),
12418 2,
12419 "Should restart LSP server on another related LSP settings change"
12420 );
12421}
12422
12423#[gpui::test]
12424async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12425 init_test(cx, |_| {});
12426
12427 let mut cx = EditorLspTestContext::new_rust(
12428 lsp::ServerCapabilities {
12429 completion_provider: Some(lsp::CompletionOptions {
12430 trigger_characters: Some(vec![".".to_string()]),
12431 resolve_provider: Some(true),
12432 ..Default::default()
12433 }),
12434 ..Default::default()
12435 },
12436 cx,
12437 )
12438 .await;
12439
12440 cx.set_state("fn main() { let a = 2ˇ; }");
12441 cx.simulate_keystroke(".");
12442 let completion_item = lsp::CompletionItem {
12443 label: "some".into(),
12444 kind: Some(lsp::CompletionItemKind::SNIPPET),
12445 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12446 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12447 kind: lsp::MarkupKind::Markdown,
12448 value: "```rust\nSome(2)\n```".to_string(),
12449 })),
12450 deprecated: Some(false),
12451 sort_text: Some("fffffff2".to_string()),
12452 filter_text: Some("some".to_string()),
12453 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12454 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12455 range: lsp::Range {
12456 start: lsp::Position {
12457 line: 0,
12458 character: 22,
12459 },
12460 end: lsp::Position {
12461 line: 0,
12462 character: 22,
12463 },
12464 },
12465 new_text: "Some(2)".to_string(),
12466 })),
12467 additional_text_edits: Some(vec![lsp::TextEdit {
12468 range: lsp::Range {
12469 start: lsp::Position {
12470 line: 0,
12471 character: 20,
12472 },
12473 end: lsp::Position {
12474 line: 0,
12475 character: 22,
12476 },
12477 },
12478 new_text: "".to_string(),
12479 }]),
12480 ..Default::default()
12481 };
12482
12483 let closure_completion_item = completion_item.clone();
12484 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12485 let task_completion_item = closure_completion_item.clone();
12486 async move {
12487 Ok(Some(lsp::CompletionResponse::Array(vec![
12488 task_completion_item,
12489 ])))
12490 }
12491 });
12492
12493 request.next().await;
12494
12495 cx.condition(|editor, _| editor.context_menu_visible())
12496 .await;
12497 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12498 editor
12499 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12500 .unwrap()
12501 });
12502 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12503
12504 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12505 let task_completion_item = completion_item.clone();
12506 async move { Ok(task_completion_item) }
12507 })
12508 .next()
12509 .await
12510 .unwrap();
12511 apply_additional_edits.await.unwrap();
12512 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12513}
12514
12515#[gpui::test]
12516async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12517 init_test(cx, |_| {});
12518
12519 let mut cx = EditorLspTestContext::new_rust(
12520 lsp::ServerCapabilities {
12521 completion_provider: Some(lsp::CompletionOptions {
12522 trigger_characters: Some(vec![".".to_string()]),
12523 resolve_provider: Some(true),
12524 ..Default::default()
12525 }),
12526 ..Default::default()
12527 },
12528 cx,
12529 )
12530 .await;
12531
12532 cx.set_state("fn main() { let a = 2ˇ; }");
12533 cx.simulate_keystroke(".");
12534
12535 let item1 = lsp::CompletionItem {
12536 label: "method id()".to_string(),
12537 filter_text: Some("id".to_string()),
12538 detail: None,
12539 documentation: None,
12540 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12541 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12542 new_text: ".id".to_string(),
12543 })),
12544 ..lsp::CompletionItem::default()
12545 };
12546
12547 let item2 = lsp::CompletionItem {
12548 label: "other".to_string(),
12549 filter_text: Some("other".to_string()),
12550 detail: None,
12551 documentation: None,
12552 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12553 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12554 new_text: ".other".to_string(),
12555 })),
12556 ..lsp::CompletionItem::default()
12557 };
12558
12559 let item1 = item1.clone();
12560 cx.set_request_handler::<lsp::request::Completion, _, _>({
12561 let item1 = item1.clone();
12562 move |_, _, _| {
12563 let item1 = item1.clone();
12564 let item2 = item2.clone();
12565 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12566 }
12567 })
12568 .next()
12569 .await;
12570
12571 cx.condition(|editor, _| editor.context_menu_visible())
12572 .await;
12573 cx.update_editor(|editor, _, _| {
12574 let context_menu = editor.context_menu.borrow_mut();
12575 let context_menu = context_menu
12576 .as_ref()
12577 .expect("Should have the context menu deployed");
12578 match context_menu {
12579 CodeContextMenu::Completions(completions_menu) => {
12580 let completions = completions_menu.completions.borrow_mut();
12581 assert_eq!(
12582 completions
12583 .iter()
12584 .map(|completion| &completion.label.text)
12585 .collect::<Vec<_>>(),
12586 vec!["method id()", "other"]
12587 )
12588 }
12589 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12590 }
12591 });
12592
12593 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12594 let item1 = item1.clone();
12595 move |_, item_to_resolve, _| {
12596 let item1 = item1.clone();
12597 async move {
12598 if item1 == item_to_resolve {
12599 Ok(lsp::CompletionItem {
12600 label: "method id()".to_string(),
12601 filter_text: Some("id".to_string()),
12602 detail: Some("Now resolved!".to_string()),
12603 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12604 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12605 range: lsp::Range::new(
12606 lsp::Position::new(0, 22),
12607 lsp::Position::new(0, 22),
12608 ),
12609 new_text: ".id".to_string(),
12610 })),
12611 ..lsp::CompletionItem::default()
12612 })
12613 } else {
12614 Ok(item_to_resolve)
12615 }
12616 }
12617 }
12618 })
12619 .next()
12620 .await
12621 .unwrap();
12622 cx.run_until_parked();
12623
12624 cx.update_editor(|editor, window, cx| {
12625 editor.context_menu_next(&Default::default(), window, cx);
12626 });
12627
12628 cx.update_editor(|editor, _, _| {
12629 let context_menu = editor.context_menu.borrow_mut();
12630 let context_menu = context_menu
12631 .as_ref()
12632 .expect("Should have the context menu deployed");
12633 match context_menu {
12634 CodeContextMenu::Completions(completions_menu) => {
12635 let completions = completions_menu.completions.borrow_mut();
12636 assert_eq!(
12637 completions
12638 .iter()
12639 .map(|completion| &completion.label.text)
12640 .collect::<Vec<_>>(),
12641 vec!["method id() Now resolved!", "other"],
12642 "Should update first completion label, but not second as the filter text did not match."
12643 );
12644 }
12645 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12646 }
12647 });
12648}
12649
12650#[gpui::test]
12651async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12652 init_test(cx, |_| {});
12653
12654 let mut cx = EditorLspTestContext::new_rust(
12655 lsp::ServerCapabilities {
12656 completion_provider: Some(lsp::CompletionOptions {
12657 trigger_characters: Some(vec![".".to_string()]),
12658 resolve_provider: Some(true),
12659 ..Default::default()
12660 }),
12661 ..Default::default()
12662 },
12663 cx,
12664 )
12665 .await;
12666
12667 cx.set_state("fn main() { let a = 2ˇ; }");
12668 cx.simulate_keystroke(".");
12669
12670 let unresolved_item_1 = lsp::CompletionItem {
12671 label: "id".to_string(),
12672 filter_text: Some("id".to_string()),
12673 detail: None,
12674 documentation: None,
12675 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12676 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12677 new_text: ".id".to_string(),
12678 })),
12679 ..lsp::CompletionItem::default()
12680 };
12681 let resolved_item_1 = lsp::CompletionItem {
12682 additional_text_edits: Some(vec![lsp::TextEdit {
12683 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12684 new_text: "!!".to_string(),
12685 }]),
12686 ..unresolved_item_1.clone()
12687 };
12688 let unresolved_item_2 = lsp::CompletionItem {
12689 label: "other".to_string(),
12690 filter_text: Some("other".to_string()),
12691 detail: None,
12692 documentation: None,
12693 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12694 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12695 new_text: ".other".to_string(),
12696 })),
12697 ..lsp::CompletionItem::default()
12698 };
12699 let resolved_item_2 = lsp::CompletionItem {
12700 additional_text_edits: Some(vec![lsp::TextEdit {
12701 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12702 new_text: "??".to_string(),
12703 }]),
12704 ..unresolved_item_2.clone()
12705 };
12706
12707 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12708 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12709 cx.lsp
12710 .server
12711 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12712 let unresolved_item_1 = unresolved_item_1.clone();
12713 let resolved_item_1 = resolved_item_1.clone();
12714 let unresolved_item_2 = unresolved_item_2.clone();
12715 let resolved_item_2 = resolved_item_2.clone();
12716 let resolve_requests_1 = resolve_requests_1.clone();
12717 let resolve_requests_2 = resolve_requests_2.clone();
12718 move |unresolved_request, _| {
12719 let unresolved_item_1 = unresolved_item_1.clone();
12720 let resolved_item_1 = resolved_item_1.clone();
12721 let unresolved_item_2 = unresolved_item_2.clone();
12722 let resolved_item_2 = resolved_item_2.clone();
12723 let resolve_requests_1 = resolve_requests_1.clone();
12724 let resolve_requests_2 = resolve_requests_2.clone();
12725 async move {
12726 if unresolved_request == unresolved_item_1 {
12727 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12728 Ok(resolved_item_1.clone())
12729 } else if unresolved_request == unresolved_item_2 {
12730 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12731 Ok(resolved_item_2.clone())
12732 } else {
12733 panic!("Unexpected completion item {unresolved_request:?}")
12734 }
12735 }
12736 }
12737 })
12738 .detach();
12739
12740 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12741 let unresolved_item_1 = unresolved_item_1.clone();
12742 let unresolved_item_2 = unresolved_item_2.clone();
12743 async move {
12744 Ok(Some(lsp::CompletionResponse::Array(vec![
12745 unresolved_item_1,
12746 unresolved_item_2,
12747 ])))
12748 }
12749 })
12750 .next()
12751 .await;
12752
12753 cx.condition(|editor, _| editor.context_menu_visible())
12754 .await;
12755 cx.update_editor(|editor, _, _| {
12756 let context_menu = editor.context_menu.borrow_mut();
12757 let context_menu = context_menu
12758 .as_ref()
12759 .expect("Should have the context menu deployed");
12760 match context_menu {
12761 CodeContextMenu::Completions(completions_menu) => {
12762 let completions = completions_menu.completions.borrow_mut();
12763 assert_eq!(
12764 completions
12765 .iter()
12766 .map(|completion| &completion.label.text)
12767 .collect::<Vec<_>>(),
12768 vec!["id", "other"]
12769 )
12770 }
12771 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12772 }
12773 });
12774 cx.run_until_parked();
12775
12776 cx.update_editor(|editor, window, cx| {
12777 editor.context_menu_next(&ContextMenuNext, window, cx);
12778 });
12779 cx.run_until_parked();
12780 cx.update_editor(|editor, window, cx| {
12781 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12782 });
12783 cx.run_until_parked();
12784 cx.update_editor(|editor, window, cx| {
12785 editor.context_menu_next(&ContextMenuNext, window, cx);
12786 });
12787 cx.run_until_parked();
12788 cx.update_editor(|editor, window, cx| {
12789 editor
12790 .compose_completion(&ComposeCompletion::default(), window, cx)
12791 .expect("No task returned")
12792 })
12793 .await
12794 .expect("Completion failed");
12795 cx.run_until_parked();
12796
12797 cx.update_editor(|editor, _, cx| {
12798 assert_eq!(
12799 resolve_requests_1.load(atomic::Ordering::Acquire),
12800 1,
12801 "Should always resolve once despite multiple selections"
12802 );
12803 assert_eq!(
12804 resolve_requests_2.load(atomic::Ordering::Acquire),
12805 1,
12806 "Should always resolve once after multiple selections and applying the completion"
12807 );
12808 assert_eq!(
12809 editor.text(cx),
12810 "fn main() { let a = ??.other; }",
12811 "Should use resolved data when applying the completion"
12812 );
12813 });
12814}
12815
12816#[gpui::test]
12817async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12818 init_test(cx, |_| {});
12819
12820 let item_0 = lsp::CompletionItem {
12821 label: "abs".into(),
12822 insert_text: Some("abs".into()),
12823 data: Some(json!({ "very": "special"})),
12824 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12825 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12826 lsp::InsertReplaceEdit {
12827 new_text: "abs".to_string(),
12828 insert: lsp::Range::default(),
12829 replace: lsp::Range::default(),
12830 },
12831 )),
12832 ..lsp::CompletionItem::default()
12833 };
12834 let items = iter::once(item_0.clone())
12835 .chain((11..51).map(|i| lsp::CompletionItem {
12836 label: format!("item_{}", i),
12837 insert_text: Some(format!("item_{}", i)),
12838 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12839 ..lsp::CompletionItem::default()
12840 }))
12841 .collect::<Vec<_>>();
12842
12843 let default_commit_characters = vec!["?".to_string()];
12844 let default_data = json!({ "default": "data"});
12845 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12846 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12847 let default_edit_range = lsp::Range {
12848 start: lsp::Position {
12849 line: 0,
12850 character: 5,
12851 },
12852 end: lsp::Position {
12853 line: 0,
12854 character: 5,
12855 },
12856 };
12857
12858 let mut cx = EditorLspTestContext::new_rust(
12859 lsp::ServerCapabilities {
12860 completion_provider: Some(lsp::CompletionOptions {
12861 trigger_characters: Some(vec![".".to_string()]),
12862 resolve_provider: Some(true),
12863 ..Default::default()
12864 }),
12865 ..Default::default()
12866 },
12867 cx,
12868 )
12869 .await;
12870
12871 cx.set_state("fn main() { let a = 2ˇ; }");
12872 cx.simulate_keystroke(".");
12873
12874 let completion_data = default_data.clone();
12875 let completion_characters = default_commit_characters.clone();
12876 let completion_items = items.clone();
12877 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12878 let default_data = completion_data.clone();
12879 let default_commit_characters = completion_characters.clone();
12880 let items = completion_items.clone();
12881 async move {
12882 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12883 items,
12884 item_defaults: Some(lsp::CompletionListItemDefaults {
12885 data: Some(default_data.clone()),
12886 commit_characters: Some(default_commit_characters.clone()),
12887 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12888 default_edit_range,
12889 )),
12890 insert_text_format: Some(default_insert_text_format),
12891 insert_text_mode: Some(default_insert_text_mode),
12892 }),
12893 ..lsp::CompletionList::default()
12894 })))
12895 }
12896 })
12897 .next()
12898 .await;
12899
12900 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12901 cx.lsp
12902 .server
12903 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12904 let closure_resolved_items = resolved_items.clone();
12905 move |item_to_resolve, _| {
12906 let closure_resolved_items = closure_resolved_items.clone();
12907 async move {
12908 closure_resolved_items.lock().push(item_to_resolve.clone());
12909 Ok(item_to_resolve)
12910 }
12911 }
12912 })
12913 .detach();
12914
12915 cx.condition(|editor, _| editor.context_menu_visible())
12916 .await;
12917 cx.run_until_parked();
12918 cx.update_editor(|editor, _, _| {
12919 let menu = editor.context_menu.borrow_mut();
12920 match menu.as_ref().expect("should have the completions menu") {
12921 CodeContextMenu::Completions(completions_menu) => {
12922 assert_eq!(
12923 completions_menu
12924 .entries
12925 .borrow()
12926 .iter()
12927 .map(|mat| mat.string.clone())
12928 .collect::<Vec<String>>(),
12929 items
12930 .iter()
12931 .map(|completion| completion.label.clone())
12932 .collect::<Vec<String>>()
12933 );
12934 }
12935 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12936 }
12937 });
12938 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12939 // with 4 from the end.
12940 assert_eq!(
12941 *resolved_items.lock(),
12942 [&items[0..16], &items[items.len() - 4..items.len()]]
12943 .concat()
12944 .iter()
12945 .cloned()
12946 .map(|mut item| {
12947 if item.data.is_none() {
12948 item.data = Some(default_data.clone());
12949 }
12950 item
12951 })
12952 .collect::<Vec<lsp::CompletionItem>>(),
12953 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
12954 );
12955 resolved_items.lock().clear();
12956
12957 cx.update_editor(|editor, window, cx| {
12958 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12959 });
12960 cx.run_until_parked();
12961 // Completions that have already been resolved are skipped.
12962 assert_eq!(
12963 *resolved_items.lock(),
12964 items[items.len() - 16..items.len() - 4]
12965 .iter()
12966 .cloned()
12967 .map(|mut item| {
12968 if item.data.is_none() {
12969 item.data = Some(default_data.clone());
12970 }
12971 item
12972 })
12973 .collect::<Vec<lsp::CompletionItem>>()
12974 );
12975 resolved_items.lock().clear();
12976}
12977
12978#[gpui::test]
12979async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
12980 init_test(cx, |_| {});
12981
12982 let mut cx = EditorLspTestContext::new(
12983 Language::new(
12984 LanguageConfig {
12985 matcher: LanguageMatcher {
12986 path_suffixes: vec!["jsx".into()],
12987 ..Default::default()
12988 },
12989 overrides: [(
12990 "element".into(),
12991 LanguageConfigOverride {
12992 completion_query_characters: Override::Set(['-'].into_iter().collect()),
12993 ..Default::default()
12994 },
12995 )]
12996 .into_iter()
12997 .collect(),
12998 ..Default::default()
12999 },
13000 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13001 )
13002 .with_override_query("(jsx_self_closing_element) @element")
13003 .unwrap(),
13004 lsp::ServerCapabilities {
13005 completion_provider: Some(lsp::CompletionOptions {
13006 trigger_characters: Some(vec![":".to_string()]),
13007 ..Default::default()
13008 }),
13009 ..Default::default()
13010 },
13011 cx,
13012 )
13013 .await;
13014
13015 cx.lsp
13016 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13017 Ok(Some(lsp::CompletionResponse::Array(vec![
13018 lsp::CompletionItem {
13019 label: "bg-blue".into(),
13020 ..Default::default()
13021 },
13022 lsp::CompletionItem {
13023 label: "bg-red".into(),
13024 ..Default::default()
13025 },
13026 lsp::CompletionItem {
13027 label: "bg-yellow".into(),
13028 ..Default::default()
13029 },
13030 ])))
13031 });
13032
13033 cx.set_state(r#"<p class="bgˇ" />"#);
13034
13035 // Trigger completion when typing a dash, because the dash is an extra
13036 // word character in the 'element' scope, which contains the cursor.
13037 cx.simulate_keystroke("-");
13038 cx.executor().run_until_parked();
13039 cx.update_editor(|editor, _, _| {
13040 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13041 {
13042 assert_eq!(
13043 completion_menu_entries(&menu),
13044 &["bg-red", "bg-blue", "bg-yellow"]
13045 );
13046 } else {
13047 panic!("expected completion menu to be open");
13048 }
13049 });
13050
13051 cx.simulate_keystroke("l");
13052 cx.executor().run_until_parked();
13053 cx.update_editor(|editor, _, _| {
13054 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13055 {
13056 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13057 } else {
13058 panic!("expected completion menu to be open");
13059 }
13060 });
13061
13062 // When filtering completions, consider the character after the '-' to
13063 // be the start of a subword.
13064 cx.set_state(r#"<p class="yelˇ" />"#);
13065 cx.simulate_keystroke("l");
13066 cx.executor().run_until_parked();
13067 cx.update_editor(|editor, _, _| {
13068 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13069 {
13070 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13071 } else {
13072 panic!("expected completion menu to be open");
13073 }
13074 });
13075}
13076
13077fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13078 let entries = menu.entries.borrow();
13079 entries.iter().map(|mat| mat.string.clone()).collect()
13080}
13081
13082#[gpui::test]
13083async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13084 init_test(cx, |settings| {
13085 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13086 FormatterList(vec![Formatter::Prettier].into()),
13087 ))
13088 });
13089
13090 let fs = FakeFs::new(cx.executor());
13091 fs.insert_file(path!("/file.ts"), Default::default()).await;
13092
13093 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13094 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13095
13096 language_registry.add(Arc::new(Language::new(
13097 LanguageConfig {
13098 name: "TypeScript".into(),
13099 matcher: LanguageMatcher {
13100 path_suffixes: vec!["ts".to_string()],
13101 ..Default::default()
13102 },
13103 ..Default::default()
13104 },
13105 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13106 )));
13107 update_test_language_settings(cx, |settings| {
13108 settings.defaults.prettier = Some(PrettierSettings {
13109 allowed: true,
13110 ..PrettierSettings::default()
13111 });
13112 });
13113
13114 let test_plugin = "test_plugin";
13115 let _ = language_registry.register_fake_lsp(
13116 "TypeScript",
13117 FakeLspAdapter {
13118 prettier_plugins: vec![test_plugin],
13119 ..Default::default()
13120 },
13121 );
13122
13123 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13124 let buffer = project
13125 .update(cx, |project, cx| {
13126 project.open_local_buffer(path!("/file.ts"), cx)
13127 })
13128 .await
13129 .unwrap();
13130
13131 let buffer_text = "one\ntwo\nthree\n";
13132 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13133 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13134 editor.update_in(cx, |editor, window, cx| {
13135 editor.set_text(buffer_text, window, cx)
13136 });
13137
13138 editor
13139 .update_in(cx, |editor, window, cx| {
13140 editor.perform_format(
13141 project.clone(),
13142 FormatTrigger::Manual,
13143 FormatTarget::Buffers,
13144 window,
13145 cx,
13146 )
13147 })
13148 .unwrap()
13149 .await;
13150 assert_eq!(
13151 editor.update(cx, |editor, cx| editor.text(cx)),
13152 buffer_text.to_string() + prettier_format_suffix,
13153 "Test prettier formatting was not applied to the original buffer text",
13154 );
13155
13156 update_test_language_settings(cx, |settings| {
13157 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13158 });
13159 let format = editor.update_in(cx, |editor, window, cx| {
13160 editor.perform_format(
13161 project.clone(),
13162 FormatTrigger::Manual,
13163 FormatTarget::Buffers,
13164 window,
13165 cx,
13166 )
13167 });
13168 format.await.unwrap();
13169 assert_eq!(
13170 editor.update(cx, |editor, cx| editor.text(cx)),
13171 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13172 "Autoformatting (via test prettier) was not applied to the original buffer text",
13173 );
13174}
13175
13176#[gpui::test]
13177async fn test_addition_reverts(cx: &mut TestAppContext) {
13178 init_test(cx, |_| {});
13179 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13180 let base_text = indoc! {r#"
13181 struct Row;
13182 struct Row1;
13183 struct Row2;
13184
13185 struct Row4;
13186 struct Row5;
13187 struct Row6;
13188
13189 struct Row8;
13190 struct Row9;
13191 struct Row10;"#};
13192
13193 // When addition hunks are not adjacent to carets, no hunk revert is performed
13194 assert_hunk_revert(
13195 indoc! {r#"struct Row;
13196 struct Row1;
13197 struct Row1.1;
13198 struct Row1.2;
13199 struct Row2;ˇ
13200
13201 struct Row4;
13202 struct Row5;
13203 struct Row6;
13204
13205 struct Row8;
13206 ˇstruct Row9;
13207 struct Row9.1;
13208 struct Row9.2;
13209 struct Row9.3;
13210 struct Row10;"#},
13211 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13212 indoc! {r#"struct Row;
13213 struct Row1;
13214 struct Row1.1;
13215 struct Row1.2;
13216 struct Row2;ˇ
13217
13218 struct Row4;
13219 struct Row5;
13220 struct Row6;
13221
13222 struct Row8;
13223 ˇstruct Row9;
13224 struct Row9.1;
13225 struct Row9.2;
13226 struct Row9.3;
13227 struct Row10;"#},
13228 base_text,
13229 &mut cx,
13230 );
13231 // Same for selections
13232 assert_hunk_revert(
13233 indoc! {r#"struct Row;
13234 struct Row1;
13235 struct Row2;
13236 struct Row2.1;
13237 struct Row2.2;
13238 «ˇ
13239 struct Row4;
13240 struct» Row5;
13241 «struct Row6;
13242 ˇ»
13243 struct Row9.1;
13244 struct Row9.2;
13245 struct Row9.3;
13246 struct Row8;
13247 struct Row9;
13248 struct Row10;"#},
13249 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13250 indoc! {r#"struct Row;
13251 struct Row1;
13252 struct Row2;
13253 struct Row2.1;
13254 struct Row2.2;
13255 «ˇ
13256 struct Row4;
13257 struct» Row5;
13258 «struct Row6;
13259 ˇ»
13260 struct Row9.1;
13261 struct Row9.2;
13262 struct Row9.3;
13263 struct Row8;
13264 struct Row9;
13265 struct Row10;"#},
13266 base_text,
13267 &mut cx,
13268 );
13269
13270 // When carets and selections intersect the addition hunks, those are reverted.
13271 // Adjacent carets got merged.
13272 assert_hunk_revert(
13273 indoc! {r#"struct Row;
13274 ˇ// something on the top
13275 struct Row1;
13276 struct Row2;
13277 struct Roˇw3.1;
13278 struct Row2.2;
13279 struct Row2.3;ˇ
13280
13281 struct Row4;
13282 struct ˇRow5.1;
13283 struct Row5.2;
13284 struct «Rowˇ»5.3;
13285 struct Row5;
13286 struct Row6;
13287 ˇ
13288 struct Row9.1;
13289 struct «Rowˇ»9.2;
13290 struct «ˇRow»9.3;
13291 struct Row8;
13292 struct Row9;
13293 «ˇ// something on bottom»
13294 struct Row10;"#},
13295 vec![
13296 DiffHunkStatusKind::Added,
13297 DiffHunkStatusKind::Added,
13298 DiffHunkStatusKind::Added,
13299 DiffHunkStatusKind::Added,
13300 DiffHunkStatusKind::Added,
13301 ],
13302 indoc! {r#"struct Row;
13303 ˇstruct Row1;
13304 struct Row2;
13305 ˇ
13306 struct Row4;
13307 ˇstruct Row5;
13308 struct Row6;
13309 ˇ
13310 ˇstruct Row8;
13311 struct Row9;
13312 ˇstruct Row10;"#},
13313 base_text,
13314 &mut cx,
13315 );
13316}
13317
13318#[gpui::test]
13319async fn test_modification_reverts(cx: &mut TestAppContext) {
13320 init_test(cx, |_| {});
13321 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13322 let base_text = indoc! {r#"
13323 struct Row;
13324 struct Row1;
13325 struct Row2;
13326
13327 struct Row4;
13328 struct Row5;
13329 struct Row6;
13330
13331 struct Row8;
13332 struct Row9;
13333 struct Row10;"#};
13334
13335 // Modification hunks behave the same as the addition ones.
13336 assert_hunk_revert(
13337 indoc! {r#"struct Row;
13338 struct Row1;
13339 struct Row33;
13340 ˇ
13341 struct Row4;
13342 struct Row5;
13343 struct Row6;
13344 ˇ
13345 struct Row99;
13346 struct Row9;
13347 struct Row10;"#},
13348 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13349 indoc! {r#"struct Row;
13350 struct Row1;
13351 struct Row33;
13352 ˇ
13353 struct Row4;
13354 struct Row5;
13355 struct Row6;
13356 ˇ
13357 struct Row99;
13358 struct Row9;
13359 struct Row10;"#},
13360 base_text,
13361 &mut cx,
13362 );
13363 assert_hunk_revert(
13364 indoc! {r#"struct Row;
13365 struct Row1;
13366 struct Row33;
13367 «ˇ
13368 struct Row4;
13369 struct» Row5;
13370 «struct Row6;
13371 ˇ»
13372 struct Row99;
13373 struct Row9;
13374 struct Row10;"#},
13375 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13376 indoc! {r#"struct Row;
13377 struct Row1;
13378 struct Row33;
13379 «ˇ
13380 struct Row4;
13381 struct» Row5;
13382 «struct Row6;
13383 ˇ»
13384 struct Row99;
13385 struct Row9;
13386 struct Row10;"#},
13387 base_text,
13388 &mut cx,
13389 );
13390
13391 assert_hunk_revert(
13392 indoc! {r#"ˇstruct Row1.1;
13393 struct Row1;
13394 «ˇstr»uct Row22;
13395
13396 struct ˇRow44;
13397 struct Row5;
13398 struct «Rˇ»ow66;ˇ
13399
13400 «struˇ»ct Row88;
13401 struct Row9;
13402 struct Row1011;ˇ"#},
13403 vec![
13404 DiffHunkStatusKind::Modified,
13405 DiffHunkStatusKind::Modified,
13406 DiffHunkStatusKind::Modified,
13407 DiffHunkStatusKind::Modified,
13408 DiffHunkStatusKind::Modified,
13409 DiffHunkStatusKind::Modified,
13410 ],
13411 indoc! {r#"struct Row;
13412 ˇstruct Row1;
13413 struct Row2;
13414 ˇ
13415 struct Row4;
13416 ˇstruct Row5;
13417 struct Row6;
13418 ˇ
13419 struct Row8;
13420 ˇstruct Row9;
13421 struct Row10;ˇ"#},
13422 base_text,
13423 &mut cx,
13424 );
13425}
13426
13427#[gpui::test]
13428async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13429 init_test(cx, |_| {});
13430 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13431 let base_text = indoc! {r#"
13432 one
13433
13434 two
13435 three
13436 "#};
13437
13438 cx.set_head_text(base_text);
13439 cx.set_state("\nˇ\n");
13440 cx.executor().run_until_parked();
13441 cx.update_editor(|editor, _window, cx| {
13442 editor.expand_selected_diff_hunks(cx);
13443 });
13444 cx.executor().run_until_parked();
13445 cx.update_editor(|editor, window, cx| {
13446 editor.backspace(&Default::default(), window, cx);
13447 });
13448 cx.run_until_parked();
13449 cx.assert_state_with_diff(
13450 indoc! {r#"
13451
13452 - two
13453 - threeˇ
13454 +
13455 "#}
13456 .to_string(),
13457 );
13458}
13459
13460#[gpui::test]
13461async fn test_deletion_reverts(cx: &mut TestAppContext) {
13462 init_test(cx, |_| {});
13463 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13464 let base_text = indoc! {r#"struct Row;
13465struct Row1;
13466struct Row2;
13467
13468struct Row4;
13469struct Row5;
13470struct Row6;
13471
13472struct Row8;
13473struct Row9;
13474struct Row10;"#};
13475
13476 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13477 assert_hunk_revert(
13478 indoc! {r#"struct Row;
13479 struct Row2;
13480
13481 ˇstruct Row4;
13482 struct Row5;
13483 struct Row6;
13484 ˇ
13485 struct Row8;
13486 struct Row10;"#},
13487 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13488 indoc! {r#"struct Row;
13489 struct Row2;
13490
13491 ˇstruct Row4;
13492 struct Row5;
13493 struct Row6;
13494 ˇ
13495 struct Row8;
13496 struct Row10;"#},
13497 base_text,
13498 &mut cx,
13499 );
13500 assert_hunk_revert(
13501 indoc! {r#"struct Row;
13502 struct Row2;
13503
13504 «ˇstruct Row4;
13505 struct» Row5;
13506 «struct Row6;
13507 ˇ»
13508 struct Row8;
13509 struct Row10;"#},
13510 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13511 indoc! {r#"struct Row;
13512 struct Row2;
13513
13514 «ˇstruct Row4;
13515 struct» Row5;
13516 «struct Row6;
13517 ˇ»
13518 struct Row8;
13519 struct Row10;"#},
13520 base_text,
13521 &mut cx,
13522 );
13523
13524 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13525 assert_hunk_revert(
13526 indoc! {r#"struct Row;
13527 ˇstruct Row2;
13528
13529 struct Row4;
13530 struct Row5;
13531 struct Row6;
13532
13533 struct Row8;ˇ
13534 struct Row10;"#},
13535 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13536 indoc! {r#"struct Row;
13537 struct Row1;
13538 ˇstruct Row2;
13539
13540 struct Row4;
13541 struct Row5;
13542 struct Row6;
13543
13544 struct Row8;ˇ
13545 struct Row9;
13546 struct Row10;"#},
13547 base_text,
13548 &mut cx,
13549 );
13550 assert_hunk_revert(
13551 indoc! {r#"struct Row;
13552 struct Row2«ˇ;
13553 struct Row4;
13554 struct» Row5;
13555 «struct Row6;
13556
13557 struct Row8;ˇ»
13558 struct Row10;"#},
13559 vec![
13560 DiffHunkStatusKind::Deleted,
13561 DiffHunkStatusKind::Deleted,
13562 DiffHunkStatusKind::Deleted,
13563 ],
13564 indoc! {r#"struct Row;
13565 struct Row1;
13566 struct Row2«ˇ;
13567
13568 struct Row4;
13569 struct» Row5;
13570 «struct Row6;
13571
13572 struct Row8;ˇ»
13573 struct Row9;
13574 struct Row10;"#},
13575 base_text,
13576 &mut cx,
13577 );
13578}
13579
13580#[gpui::test]
13581async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13582 init_test(cx, |_| {});
13583
13584 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13585 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13586 let base_text_3 =
13587 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13588
13589 let text_1 = edit_first_char_of_every_line(base_text_1);
13590 let text_2 = edit_first_char_of_every_line(base_text_2);
13591 let text_3 = edit_first_char_of_every_line(base_text_3);
13592
13593 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13594 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13595 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13596
13597 let multibuffer = cx.new(|cx| {
13598 let mut multibuffer = MultiBuffer::new(ReadWrite);
13599 multibuffer.push_excerpts(
13600 buffer_1.clone(),
13601 [
13602 ExcerptRange {
13603 context: Point::new(0, 0)..Point::new(3, 0),
13604 primary: None,
13605 },
13606 ExcerptRange {
13607 context: Point::new(5, 0)..Point::new(7, 0),
13608 primary: None,
13609 },
13610 ExcerptRange {
13611 context: Point::new(9, 0)..Point::new(10, 4),
13612 primary: None,
13613 },
13614 ],
13615 cx,
13616 );
13617 multibuffer.push_excerpts(
13618 buffer_2.clone(),
13619 [
13620 ExcerptRange {
13621 context: Point::new(0, 0)..Point::new(3, 0),
13622 primary: None,
13623 },
13624 ExcerptRange {
13625 context: Point::new(5, 0)..Point::new(7, 0),
13626 primary: None,
13627 },
13628 ExcerptRange {
13629 context: Point::new(9, 0)..Point::new(10, 4),
13630 primary: None,
13631 },
13632 ],
13633 cx,
13634 );
13635 multibuffer.push_excerpts(
13636 buffer_3.clone(),
13637 [
13638 ExcerptRange {
13639 context: Point::new(0, 0)..Point::new(3, 0),
13640 primary: None,
13641 },
13642 ExcerptRange {
13643 context: Point::new(5, 0)..Point::new(7, 0),
13644 primary: None,
13645 },
13646 ExcerptRange {
13647 context: Point::new(9, 0)..Point::new(10, 4),
13648 primary: None,
13649 },
13650 ],
13651 cx,
13652 );
13653 multibuffer
13654 });
13655
13656 let fs = FakeFs::new(cx.executor());
13657 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13658 let (editor, cx) = cx
13659 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13660 editor.update_in(cx, |editor, _window, cx| {
13661 for (buffer, diff_base) in [
13662 (buffer_1.clone(), base_text_1),
13663 (buffer_2.clone(), base_text_2),
13664 (buffer_3.clone(), base_text_3),
13665 ] {
13666 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13667 editor
13668 .buffer
13669 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13670 }
13671 });
13672 cx.executor().run_until_parked();
13673
13674 editor.update_in(cx, |editor, window, cx| {
13675 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}");
13676 editor.select_all(&SelectAll, window, cx);
13677 editor.git_restore(&Default::default(), window, cx);
13678 });
13679 cx.executor().run_until_parked();
13680
13681 // When all ranges are selected, all buffer hunks are reverted.
13682 editor.update(cx, |editor, cx| {
13683 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");
13684 });
13685 buffer_1.update(cx, |buffer, _| {
13686 assert_eq!(buffer.text(), base_text_1);
13687 });
13688 buffer_2.update(cx, |buffer, _| {
13689 assert_eq!(buffer.text(), base_text_2);
13690 });
13691 buffer_3.update(cx, |buffer, _| {
13692 assert_eq!(buffer.text(), base_text_3);
13693 });
13694
13695 editor.update_in(cx, |editor, window, cx| {
13696 editor.undo(&Default::default(), window, cx);
13697 });
13698
13699 editor.update_in(cx, |editor, window, cx| {
13700 editor.change_selections(None, window, cx, |s| {
13701 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13702 });
13703 editor.git_restore(&Default::default(), window, cx);
13704 });
13705
13706 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13707 // but not affect buffer_2 and its related excerpts.
13708 editor.update(cx, |editor, cx| {
13709 assert_eq!(
13710 editor.text(cx),
13711 "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}"
13712 );
13713 });
13714 buffer_1.update(cx, |buffer, _| {
13715 assert_eq!(buffer.text(), base_text_1);
13716 });
13717 buffer_2.update(cx, |buffer, _| {
13718 assert_eq!(
13719 buffer.text(),
13720 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13721 );
13722 });
13723 buffer_3.update(cx, |buffer, _| {
13724 assert_eq!(
13725 buffer.text(),
13726 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13727 );
13728 });
13729
13730 fn edit_first_char_of_every_line(text: &str) -> String {
13731 text.split('\n')
13732 .map(|line| format!("X{}", &line[1..]))
13733 .collect::<Vec<_>>()
13734 .join("\n")
13735 }
13736}
13737
13738#[gpui::test]
13739async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13740 init_test(cx, |_| {});
13741
13742 let cols = 4;
13743 let rows = 10;
13744 let sample_text_1 = sample_text(rows, cols, 'a');
13745 assert_eq!(
13746 sample_text_1,
13747 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13748 );
13749 let sample_text_2 = sample_text(rows, cols, 'l');
13750 assert_eq!(
13751 sample_text_2,
13752 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13753 );
13754 let sample_text_3 = sample_text(rows, cols, 'v');
13755 assert_eq!(
13756 sample_text_3,
13757 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13758 );
13759
13760 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13761 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13762 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13763
13764 let multi_buffer = cx.new(|cx| {
13765 let mut multibuffer = MultiBuffer::new(ReadWrite);
13766 multibuffer.push_excerpts(
13767 buffer_1.clone(),
13768 [
13769 ExcerptRange {
13770 context: Point::new(0, 0)..Point::new(3, 0),
13771 primary: None,
13772 },
13773 ExcerptRange {
13774 context: Point::new(5, 0)..Point::new(7, 0),
13775 primary: None,
13776 },
13777 ExcerptRange {
13778 context: Point::new(9, 0)..Point::new(10, 4),
13779 primary: None,
13780 },
13781 ],
13782 cx,
13783 );
13784 multibuffer.push_excerpts(
13785 buffer_2.clone(),
13786 [
13787 ExcerptRange {
13788 context: Point::new(0, 0)..Point::new(3, 0),
13789 primary: None,
13790 },
13791 ExcerptRange {
13792 context: Point::new(5, 0)..Point::new(7, 0),
13793 primary: None,
13794 },
13795 ExcerptRange {
13796 context: Point::new(9, 0)..Point::new(10, 4),
13797 primary: None,
13798 },
13799 ],
13800 cx,
13801 );
13802 multibuffer.push_excerpts(
13803 buffer_3.clone(),
13804 [
13805 ExcerptRange {
13806 context: Point::new(0, 0)..Point::new(3, 0),
13807 primary: None,
13808 },
13809 ExcerptRange {
13810 context: Point::new(5, 0)..Point::new(7, 0),
13811 primary: None,
13812 },
13813 ExcerptRange {
13814 context: Point::new(9, 0)..Point::new(10, 4),
13815 primary: None,
13816 },
13817 ],
13818 cx,
13819 );
13820 multibuffer
13821 });
13822
13823 let fs = FakeFs::new(cx.executor());
13824 fs.insert_tree(
13825 "/a",
13826 json!({
13827 "main.rs": sample_text_1,
13828 "other.rs": sample_text_2,
13829 "lib.rs": sample_text_3,
13830 }),
13831 )
13832 .await;
13833 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13835 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13836 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13837 Editor::new(
13838 EditorMode::Full,
13839 multi_buffer,
13840 Some(project.clone()),
13841 window,
13842 cx,
13843 )
13844 });
13845 let multibuffer_item_id = workspace
13846 .update(cx, |workspace, window, cx| {
13847 assert!(
13848 workspace.active_item(cx).is_none(),
13849 "active item should be None before the first item is added"
13850 );
13851 workspace.add_item_to_active_pane(
13852 Box::new(multi_buffer_editor.clone()),
13853 None,
13854 true,
13855 window,
13856 cx,
13857 );
13858 let active_item = workspace
13859 .active_item(cx)
13860 .expect("should have an active item after adding the multi buffer");
13861 assert!(
13862 !active_item.is_singleton(cx),
13863 "A multi buffer was expected to active after adding"
13864 );
13865 active_item.item_id()
13866 })
13867 .unwrap();
13868 cx.executor().run_until_parked();
13869
13870 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13871 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13872 s.select_ranges(Some(1..2))
13873 });
13874 editor.open_excerpts(&OpenExcerpts, window, cx);
13875 });
13876 cx.executor().run_until_parked();
13877 let first_item_id = workspace
13878 .update(cx, |workspace, window, cx| {
13879 let active_item = workspace
13880 .active_item(cx)
13881 .expect("should have an active item after navigating into the 1st buffer");
13882 let first_item_id = active_item.item_id();
13883 assert_ne!(
13884 first_item_id, multibuffer_item_id,
13885 "Should navigate into the 1st buffer and activate it"
13886 );
13887 assert!(
13888 active_item.is_singleton(cx),
13889 "New active item should be a singleton buffer"
13890 );
13891 assert_eq!(
13892 active_item
13893 .act_as::<Editor>(cx)
13894 .expect("should have navigated into an editor for the 1st buffer")
13895 .read(cx)
13896 .text(cx),
13897 sample_text_1
13898 );
13899
13900 workspace
13901 .go_back(workspace.active_pane().downgrade(), window, cx)
13902 .detach_and_log_err(cx);
13903
13904 first_item_id
13905 })
13906 .unwrap();
13907 cx.executor().run_until_parked();
13908 workspace
13909 .update(cx, |workspace, _, cx| {
13910 let active_item = workspace
13911 .active_item(cx)
13912 .expect("should have an active item after navigating back");
13913 assert_eq!(
13914 active_item.item_id(),
13915 multibuffer_item_id,
13916 "Should navigate back to the multi buffer"
13917 );
13918 assert!(!active_item.is_singleton(cx));
13919 })
13920 .unwrap();
13921
13922 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13923 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13924 s.select_ranges(Some(39..40))
13925 });
13926 editor.open_excerpts(&OpenExcerpts, window, cx);
13927 });
13928 cx.executor().run_until_parked();
13929 let second_item_id = workspace
13930 .update(cx, |workspace, window, cx| {
13931 let active_item = workspace
13932 .active_item(cx)
13933 .expect("should have an active item after navigating into the 2nd buffer");
13934 let second_item_id = active_item.item_id();
13935 assert_ne!(
13936 second_item_id, multibuffer_item_id,
13937 "Should navigate away from the multibuffer"
13938 );
13939 assert_ne!(
13940 second_item_id, first_item_id,
13941 "Should navigate into the 2nd buffer and activate it"
13942 );
13943 assert!(
13944 active_item.is_singleton(cx),
13945 "New active item should be a singleton buffer"
13946 );
13947 assert_eq!(
13948 active_item
13949 .act_as::<Editor>(cx)
13950 .expect("should have navigated into an editor")
13951 .read(cx)
13952 .text(cx),
13953 sample_text_2
13954 );
13955
13956 workspace
13957 .go_back(workspace.active_pane().downgrade(), window, cx)
13958 .detach_and_log_err(cx);
13959
13960 second_item_id
13961 })
13962 .unwrap();
13963 cx.executor().run_until_parked();
13964 workspace
13965 .update(cx, |workspace, _, cx| {
13966 let active_item = workspace
13967 .active_item(cx)
13968 .expect("should have an active item after navigating back from the 2nd buffer");
13969 assert_eq!(
13970 active_item.item_id(),
13971 multibuffer_item_id,
13972 "Should navigate back from the 2nd buffer to the multi buffer"
13973 );
13974 assert!(!active_item.is_singleton(cx));
13975 })
13976 .unwrap();
13977
13978 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13979 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13980 s.select_ranges(Some(70..70))
13981 });
13982 editor.open_excerpts(&OpenExcerpts, window, cx);
13983 });
13984 cx.executor().run_until_parked();
13985 workspace
13986 .update(cx, |workspace, window, cx| {
13987 let active_item = workspace
13988 .active_item(cx)
13989 .expect("should have an active item after navigating into the 3rd buffer");
13990 let third_item_id = active_item.item_id();
13991 assert_ne!(
13992 third_item_id, multibuffer_item_id,
13993 "Should navigate into the 3rd buffer and activate it"
13994 );
13995 assert_ne!(third_item_id, first_item_id);
13996 assert_ne!(third_item_id, second_item_id);
13997 assert!(
13998 active_item.is_singleton(cx),
13999 "New active item should be a singleton buffer"
14000 );
14001 assert_eq!(
14002 active_item
14003 .act_as::<Editor>(cx)
14004 .expect("should have navigated into an editor")
14005 .read(cx)
14006 .text(cx),
14007 sample_text_3
14008 );
14009
14010 workspace
14011 .go_back(workspace.active_pane().downgrade(), window, cx)
14012 .detach_and_log_err(cx);
14013 })
14014 .unwrap();
14015 cx.executor().run_until_parked();
14016 workspace
14017 .update(cx, |workspace, _, cx| {
14018 let active_item = workspace
14019 .active_item(cx)
14020 .expect("should have an active item after navigating back from the 3rd buffer");
14021 assert_eq!(
14022 active_item.item_id(),
14023 multibuffer_item_id,
14024 "Should navigate back from the 3rd buffer to the multi buffer"
14025 );
14026 assert!(!active_item.is_singleton(cx));
14027 })
14028 .unwrap();
14029}
14030
14031#[gpui::test]
14032async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14033 init_test(cx, |_| {});
14034
14035 let mut cx = EditorTestContext::new(cx).await;
14036
14037 let diff_base = r#"
14038 use some::mod;
14039
14040 const A: u32 = 42;
14041
14042 fn main() {
14043 println!("hello");
14044
14045 println!("world");
14046 }
14047 "#
14048 .unindent();
14049
14050 cx.set_state(
14051 &r#"
14052 use some::modified;
14053
14054 ˇ
14055 fn main() {
14056 println!("hello there");
14057
14058 println!("around the");
14059 println!("world");
14060 }
14061 "#
14062 .unindent(),
14063 );
14064
14065 cx.set_head_text(&diff_base);
14066 executor.run_until_parked();
14067
14068 cx.update_editor(|editor, window, cx| {
14069 editor.go_to_next_hunk(&GoToHunk, window, cx);
14070 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14071 });
14072 executor.run_until_parked();
14073 cx.assert_state_with_diff(
14074 r#"
14075 use some::modified;
14076
14077
14078 fn main() {
14079 - println!("hello");
14080 + ˇ println!("hello there");
14081
14082 println!("around the");
14083 println!("world");
14084 }
14085 "#
14086 .unindent(),
14087 );
14088
14089 cx.update_editor(|editor, window, cx| {
14090 for _ in 0..2 {
14091 editor.go_to_next_hunk(&GoToHunk, window, cx);
14092 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14093 }
14094 });
14095 executor.run_until_parked();
14096 cx.assert_state_with_diff(
14097 r#"
14098 - use some::mod;
14099 + ˇuse some::modified;
14100
14101
14102 fn main() {
14103 - println!("hello");
14104 + println!("hello there");
14105
14106 + println!("around the");
14107 println!("world");
14108 }
14109 "#
14110 .unindent(),
14111 );
14112
14113 cx.update_editor(|editor, window, cx| {
14114 editor.go_to_next_hunk(&GoToHunk, window, cx);
14115 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14116 });
14117 executor.run_until_parked();
14118 cx.assert_state_with_diff(
14119 r#"
14120 - use some::mod;
14121 + use some::modified;
14122
14123 - const A: u32 = 42;
14124 ˇ
14125 fn main() {
14126 - println!("hello");
14127 + println!("hello there");
14128
14129 + println!("around the");
14130 println!("world");
14131 }
14132 "#
14133 .unindent(),
14134 );
14135
14136 cx.update_editor(|editor, window, cx| {
14137 editor.cancel(&Cancel, window, cx);
14138 });
14139
14140 cx.assert_state_with_diff(
14141 r#"
14142 use some::modified;
14143
14144 ˇ
14145 fn main() {
14146 println!("hello there");
14147
14148 println!("around the");
14149 println!("world");
14150 }
14151 "#
14152 .unindent(),
14153 );
14154}
14155
14156#[gpui::test]
14157async fn test_diff_base_change_with_expanded_diff_hunks(
14158 executor: BackgroundExecutor,
14159 cx: &mut TestAppContext,
14160) {
14161 init_test(cx, |_| {});
14162
14163 let mut cx = EditorTestContext::new(cx).await;
14164
14165 let diff_base = r#"
14166 use some::mod1;
14167 use some::mod2;
14168
14169 const A: u32 = 42;
14170 const B: u32 = 42;
14171 const C: u32 = 42;
14172
14173 fn main() {
14174 println!("hello");
14175
14176 println!("world");
14177 }
14178 "#
14179 .unindent();
14180
14181 cx.set_state(
14182 &r#"
14183 use some::mod2;
14184
14185 const A: u32 = 42;
14186 const C: u32 = 42;
14187
14188 fn main(ˇ) {
14189 //println!("hello");
14190
14191 println!("world");
14192 //
14193 //
14194 }
14195 "#
14196 .unindent(),
14197 );
14198
14199 cx.set_head_text(&diff_base);
14200 executor.run_until_parked();
14201
14202 cx.update_editor(|editor, window, cx| {
14203 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14204 });
14205 executor.run_until_parked();
14206 cx.assert_state_with_diff(
14207 r#"
14208 - use some::mod1;
14209 use some::mod2;
14210
14211 const A: u32 = 42;
14212 - const B: u32 = 42;
14213 const C: u32 = 42;
14214
14215 fn main(ˇ) {
14216 - println!("hello");
14217 + //println!("hello");
14218
14219 println!("world");
14220 + //
14221 + //
14222 }
14223 "#
14224 .unindent(),
14225 );
14226
14227 cx.set_head_text("new diff base!");
14228 executor.run_until_parked();
14229 cx.assert_state_with_diff(
14230 r#"
14231 - new diff base!
14232 + use some::mod2;
14233 +
14234 + const A: u32 = 42;
14235 + const C: u32 = 42;
14236 +
14237 + fn main(ˇ) {
14238 + //println!("hello");
14239 +
14240 + println!("world");
14241 + //
14242 + //
14243 + }
14244 "#
14245 .unindent(),
14246 );
14247}
14248
14249#[gpui::test]
14250async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14251 init_test(cx, |_| {});
14252
14253 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14254 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14255 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14256 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14257 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14258 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14259
14260 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14261 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14262 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14263
14264 let multi_buffer = cx.new(|cx| {
14265 let mut multibuffer = MultiBuffer::new(ReadWrite);
14266 multibuffer.push_excerpts(
14267 buffer_1.clone(),
14268 [
14269 ExcerptRange {
14270 context: Point::new(0, 0)..Point::new(3, 0),
14271 primary: None,
14272 },
14273 ExcerptRange {
14274 context: Point::new(5, 0)..Point::new(7, 0),
14275 primary: None,
14276 },
14277 ExcerptRange {
14278 context: Point::new(9, 0)..Point::new(10, 3),
14279 primary: None,
14280 },
14281 ],
14282 cx,
14283 );
14284 multibuffer.push_excerpts(
14285 buffer_2.clone(),
14286 [
14287 ExcerptRange {
14288 context: Point::new(0, 0)..Point::new(3, 0),
14289 primary: None,
14290 },
14291 ExcerptRange {
14292 context: Point::new(5, 0)..Point::new(7, 0),
14293 primary: None,
14294 },
14295 ExcerptRange {
14296 context: Point::new(9, 0)..Point::new(10, 3),
14297 primary: None,
14298 },
14299 ],
14300 cx,
14301 );
14302 multibuffer.push_excerpts(
14303 buffer_3.clone(),
14304 [
14305 ExcerptRange {
14306 context: Point::new(0, 0)..Point::new(3, 0),
14307 primary: None,
14308 },
14309 ExcerptRange {
14310 context: Point::new(5, 0)..Point::new(7, 0),
14311 primary: None,
14312 },
14313 ExcerptRange {
14314 context: Point::new(9, 0)..Point::new(10, 3),
14315 primary: None,
14316 },
14317 ],
14318 cx,
14319 );
14320 multibuffer
14321 });
14322
14323 let editor =
14324 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14325 editor
14326 .update(cx, |editor, _window, cx| {
14327 for (buffer, diff_base) in [
14328 (buffer_1.clone(), file_1_old),
14329 (buffer_2.clone(), file_2_old),
14330 (buffer_3.clone(), file_3_old),
14331 ] {
14332 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14333 editor
14334 .buffer
14335 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14336 }
14337 })
14338 .unwrap();
14339
14340 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14341 cx.run_until_parked();
14342
14343 cx.assert_editor_state(
14344 &"
14345 ˇaaa
14346 ccc
14347 ddd
14348
14349 ggg
14350 hhh
14351
14352
14353 lll
14354 mmm
14355 NNN
14356
14357 qqq
14358 rrr
14359
14360 uuu
14361 111
14362 222
14363 333
14364
14365 666
14366 777
14367
14368 000
14369 !!!"
14370 .unindent(),
14371 );
14372
14373 cx.update_editor(|editor, window, cx| {
14374 editor.select_all(&SelectAll, window, cx);
14375 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14376 });
14377 cx.executor().run_until_parked();
14378
14379 cx.assert_state_with_diff(
14380 "
14381 «aaa
14382 - bbb
14383 ccc
14384 ddd
14385
14386 ggg
14387 hhh
14388
14389
14390 lll
14391 mmm
14392 - nnn
14393 + NNN
14394
14395 qqq
14396 rrr
14397
14398 uuu
14399 111
14400 222
14401 333
14402
14403 + 666
14404 777
14405
14406 000
14407 !!!ˇ»"
14408 .unindent(),
14409 );
14410}
14411
14412#[gpui::test]
14413async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14414 init_test(cx, |_| {});
14415
14416 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14417 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14418
14419 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14420 let multi_buffer = cx.new(|cx| {
14421 let mut multibuffer = MultiBuffer::new(ReadWrite);
14422 multibuffer.push_excerpts(
14423 buffer.clone(),
14424 [
14425 ExcerptRange {
14426 context: Point::new(0, 0)..Point::new(2, 0),
14427 primary: None,
14428 },
14429 ExcerptRange {
14430 context: Point::new(4, 0)..Point::new(7, 0),
14431 primary: None,
14432 },
14433 ExcerptRange {
14434 context: Point::new(9, 0)..Point::new(10, 0),
14435 primary: None,
14436 },
14437 ],
14438 cx,
14439 );
14440 multibuffer
14441 });
14442
14443 let editor =
14444 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14445 editor
14446 .update(cx, |editor, _window, cx| {
14447 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14448 editor
14449 .buffer
14450 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14451 })
14452 .unwrap();
14453
14454 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14455 cx.run_until_parked();
14456
14457 cx.update_editor(|editor, window, cx| {
14458 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14459 });
14460 cx.executor().run_until_parked();
14461
14462 // When the start of a hunk coincides with the start of its excerpt,
14463 // the hunk is expanded. When the start of a a hunk is earlier than
14464 // the start of its excerpt, the hunk is not expanded.
14465 cx.assert_state_with_diff(
14466 "
14467 ˇaaa
14468 - bbb
14469 + BBB
14470
14471 - ddd
14472 - eee
14473 + DDD
14474 + EEE
14475 fff
14476
14477 iii
14478 "
14479 .unindent(),
14480 );
14481}
14482
14483#[gpui::test]
14484async fn test_edits_around_expanded_insertion_hunks(
14485 executor: BackgroundExecutor,
14486 cx: &mut TestAppContext,
14487) {
14488 init_test(cx, |_| {});
14489
14490 let mut cx = EditorTestContext::new(cx).await;
14491
14492 let diff_base = r#"
14493 use some::mod1;
14494 use some::mod2;
14495
14496 const A: u32 = 42;
14497
14498 fn main() {
14499 println!("hello");
14500
14501 println!("world");
14502 }
14503 "#
14504 .unindent();
14505 executor.run_until_parked();
14506 cx.set_state(
14507 &r#"
14508 use some::mod1;
14509 use some::mod2;
14510
14511 const A: u32 = 42;
14512 const B: u32 = 42;
14513 const C: u32 = 42;
14514 ˇ
14515
14516 fn main() {
14517 println!("hello");
14518
14519 println!("world");
14520 }
14521 "#
14522 .unindent(),
14523 );
14524
14525 cx.set_head_text(&diff_base);
14526 executor.run_until_parked();
14527
14528 cx.update_editor(|editor, window, cx| {
14529 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14530 });
14531 executor.run_until_parked();
14532
14533 cx.assert_state_with_diff(
14534 r#"
14535 use some::mod1;
14536 use some::mod2;
14537
14538 const A: u32 = 42;
14539 + const B: u32 = 42;
14540 + const C: u32 = 42;
14541 + ˇ
14542
14543 fn main() {
14544 println!("hello");
14545
14546 println!("world");
14547 }
14548 "#
14549 .unindent(),
14550 );
14551
14552 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14553 executor.run_until_parked();
14554
14555 cx.assert_state_with_diff(
14556 r#"
14557 use some::mod1;
14558 use some::mod2;
14559
14560 const A: u32 = 42;
14561 + const B: u32 = 42;
14562 + const C: u32 = 42;
14563 + const D: u32 = 42;
14564 + ˇ
14565
14566 fn main() {
14567 println!("hello");
14568
14569 println!("world");
14570 }
14571 "#
14572 .unindent(),
14573 );
14574
14575 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14576 executor.run_until_parked();
14577
14578 cx.assert_state_with_diff(
14579 r#"
14580 use some::mod1;
14581 use some::mod2;
14582
14583 const A: u32 = 42;
14584 + const B: u32 = 42;
14585 + const C: u32 = 42;
14586 + const D: u32 = 42;
14587 + const E: u32 = 42;
14588 + ˇ
14589
14590 fn main() {
14591 println!("hello");
14592
14593 println!("world");
14594 }
14595 "#
14596 .unindent(),
14597 );
14598
14599 cx.update_editor(|editor, window, cx| {
14600 editor.delete_line(&DeleteLine, window, cx);
14601 });
14602 executor.run_until_parked();
14603
14604 cx.assert_state_with_diff(
14605 r#"
14606 use some::mod1;
14607 use some::mod2;
14608
14609 const A: u32 = 42;
14610 + const B: u32 = 42;
14611 + const C: u32 = 42;
14612 + const D: u32 = 42;
14613 + const E: u32 = 42;
14614 ˇ
14615 fn main() {
14616 println!("hello");
14617
14618 println!("world");
14619 }
14620 "#
14621 .unindent(),
14622 );
14623
14624 cx.update_editor(|editor, window, cx| {
14625 editor.move_up(&MoveUp, window, cx);
14626 editor.delete_line(&DeleteLine, window, cx);
14627 editor.move_up(&MoveUp, window, cx);
14628 editor.delete_line(&DeleteLine, window, cx);
14629 editor.move_up(&MoveUp, window, cx);
14630 editor.delete_line(&DeleteLine, window, cx);
14631 });
14632 executor.run_until_parked();
14633 cx.assert_state_with_diff(
14634 r#"
14635 use some::mod1;
14636 use some::mod2;
14637
14638 const A: u32 = 42;
14639 + const B: u32 = 42;
14640 ˇ
14641 fn main() {
14642 println!("hello");
14643
14644 println!("world");
14645 }
14646 "#
14647 .unindent(),
14648 );
14649
14650 cx.update_editor(|editor, window, cx| {
14651 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14652 editor.delete_line(&DeleteLine, window, cx);
14653 });
14654 executor.run_until_parked();
14655 cx.assert_state_with_diff(
14656 r#"
14657 ˇ
14658 fn main() {
14659 println!("hello");
14660
14661 println!("world");
14662 }
14663 "#
14664 .unindent(),
14665 );
14666}
14667
14668#[gpui::test]
14669async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14670 init_test(cx, |_| {});
14671
14672 let mut cx = EditorTestContext::new(cx).await;
14673 cx.set_head_text(indoc! { "
14674 one
14675 two
14676 three
14677 four
14678 five
14679 "
14680 });
14681 cx.set_state(indoc! { "
14682 one
14683 ˇthree
14684 five
14685 "});
14686 cx.run_until_parked();
14687 cx.update_editor(|editor, window, cx| {
14688 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14689 });
14690 cx.assert_state_with_diff(
14691 indoc! { "
14692 one
14693 - two
14694 ˇthree
14695 - four
14696 five
14697 "}
14698 .to_string(),
14699 );
14700 cx.update_editor(|editor, window, cx| {
14701 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14702 });
14703
14704 cx.assert_state_with_diff(
14705 indoc! { "
14706 one
14707 ˇthree
14708 five
14709 "}
14710 .to_string(),
14711 );
14712
14713 cx.set_state(indoc! { "
14714 one
14715 ˇTWO
14716 three
14717 four
14718 five
14719 "});
14720 cx.run_until_parked();
14721 cx.update_editor(|editor, window, cx| {
14722 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14723 });
14724
14725 cx.assert_state_with_diff(
14726 indoc! { "
14727 one
14728 - two
14729 + ˇTWO
14730 three
14731 four
14732 five
14733 "}
14734 .to_string(),
14735 );
14736 cx.update_editor(|editor, window, cx| {
14737 editor.move_up(&Default::default(), window, cx);
14738 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14739 });
14740 cx.assert_state_with_diff(
14741 indoc! { "
14742 one
14743 ˇTWO
14744 three
14745 four
14746 five
14747 "}
14748 .to_string(),
14749 );
14750}
14751
14752#[gpui::test]
14753async fn test_edits_around_expanded_deletion_hunks(
14754 executor: BackgroundExecutor,
14755 cx: &mut TestAppContext,
14756) {
14757 init_test(cx, |_| {});
14758
14759 let mut cx = EditorTestContext::new(cx).await;
14760
14761 let diff_base = r#"
14762 use some::mod1;
14763 use some::mod2;
14764
14765 const A: u32 = 42;
14766 const B: u32 = 42;
14767 const C: u32 = 42;
14768
14769
14770 fn main() {
14771 println!("hello");
14772
14773 println!("world");
14774 }
14775 "#
14776 .unindent();
14777 executor.run_until_parked();
14778 cx.set_state(
14779 &r#"
14780 use some::mod1;
14781 use some::mod2;
14782
14783 ˇconst B: u32 = 42;
14784 const C: u32 = 42;
14785
14786
14787 fn main() {
14788 println!("hello");
14789
14790 println!("world");
14791 }
14792 "#
14793 .unindent(),
14794 );
14795
14796 cx.set_head_text(&diff_base);
14797 executor.run_until_parked();
14798
14799 cx.update_editor(|editor, window, cx| {
14800 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14801 });
14802 executor.run_until_parked();
14803
14804 cx.assert_state_with_diff(
14805 r#"
14806 use some::mod1;
14807 use some::mod2;
14808
14809 - const A: u32 = 42;
14810 ˇconst B: u32 = 42;
14811 const C: u32 = 42;
14812
14813
14814 fn main() {
14815 println!("hello");
14816
14817 println!("world");
14818 }
14819 "#
14820 .unindent(),
14821 );
14822
14823 cx.update_editor(|editor, window, cx| {
14824 editor.delete_line(&DeleteLine, window, cx);
14825 });
14826 executor.run_until_parked();
14827 cx.assert_state_with_diff(
14828 r#"
14829 use some::mod1;
14830 use some::mod2;
14831
14832 - const A: u32 = 42;
14833 - const B: u32 = 42;
14834 ˇconst C: u32 = 42;
14835
14836
14837 fn main() {
14838 println!("hello");
14839
14840 println!("world");
14841 }
14842 "#
14843 .unindent(),
14844 );
14845
14846 cx.update_editor(|editor, window, cx| {
14847 editor.delete_line(&DeleteLine, window, cx);
14848 });
14849 executor.run_until_parked();
14850 cx.assert_state_with_diff(
14851 r#"
14852 use some::mod1;
14853 use some::mod2;
14854
14855 - const A: u32 = 42;
14856 - const B: u32 = 42;
14857 - const C: u32 = 42;
14858 ˇ
14859
14860 fn main() {
14861 println!("hello");
14862
14863 println!("world");
14864 }
14865 "#
14866 .unindent(),
14867 );
14868
14869 cx.update_editor(|editor, window, cx| {
14870 editor.handle_input("replacement", window, cx);
14871 });
14872 executor.run_until_parked();
14873 cx.assert_state_with_diff(
14874 r#"
14875 use some::mod1;
14876 use some::mod2;
14877
14878 - const A: u32 = 42;
14879 - const B: u32 = 42;
14880 - const C: u32 = 42;
14881 -
14882 + replacementˇ
14883
14884 fn main() {
14885 println!("hello");
14886
14887 println!("world");
14888 }
14889 "#
14890 .unindent(),
14891 );
14892}
14893
14894#[gpui::test]
14895async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14896 init_test(cx, |_| {});
14897
14898 let mut cx = EditorTestContext::new(cx).await;
14899
14900 let base_text = r#"
14901 one
14902 two
14903 three
14904 four
14905 five
14906 "#
14907 .unindent();
14908 executor.run_until_parked();
14909 cx.set_state(
14910 &r#"
14911 one
14912 two
14913 fˇour
14914 five
14915 "#
14916 .unindent(),
14917 );
14918
14919 cx.set_head_text(&base_text);
14920 executor.run_until_parked();
14921
14922 cx.update_editor(|editor, window, cx| {
14923 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14924 });
14925 executor.run_until_parked();
14926
14927 cx.assert_state_with_diff(
14928 r#"
14929 one
14930 two
14931 - three
14932 fˇour
14933 five
14934 "#
14935 .unindent(),
14936 );
14937
14938 cx.update_editor(|editor, window, cx| {
14939 editor.backspace(&Backspace, window, cx);
14940 editor.backspace(&Backspace, window, cx);
14941 });
14942 executor.run_until_parked();
14943 cx.assert_state_with_diff(
14944 r#"
14945 one
14946 two
14947 - threeˇ
14948 - four
14949 + our
14950 five
14951 "#
14952 .unindent(),
14953 );
14954}
14955
14956#[gpui::test]
14957async fn test_edit_after_expanded_modification_hunk(
14958 executor: BackgroundExecutor,
14959 cx: &mut TestAppContext,
14960) {
14961 init_test(cx, |_| {});
14962
14963 let mut cx = EditorTestContext::new(cx).await;
14964
14965 let diff_base = r#"
14966 use some::mod1;
14967 use some::mod2;
14968
14969 const A: u32 = 42;
14970 const B: u32 = 42;
14971 const C: u32 = 42;
14972 const D: u32 = 42;
14973
14974
14975 fn main() {
14976 println!("hello");
14977
14978 println!("world");
14979 }"#
14980 .unindent();
14981
14982 cx.set_state(
14983 &r#"
14984 use some::mod1;
14985 use some::mod2;
14986
14987 const A: u32 = 42;
14988 const B: u32 = 42;
14989 const C: u32 = 43ˇ
14990 const D: u32 = 42;
14991
14992
14993 fn main() {
14994 println!("hello");
14995
14996 println!("world");
14997 }"#
14998 .unindent(),
14999 );
15000
15001 cx.set_head_text(&diff_base);
15002 executor.run_until_parked();
15003 cx.update_editor(|editor, window, cx| {
15004 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15005 });
15006 executor.run_until_parked();
15007
15008 cx.assert_state_with_diff(
15009 r#"
15010 use some::mod1;
15011 use some::mod2;
15012
15013 const A: u32 = 42;
15014 const B: u32 = 42;
15015 - const C: u32 = 42;
15016 + const C: u32 = 43ˇ
15017 const D: u32 = 42;
15018
15019
15020 fn main() {
15021 println!("hello");
15022
15023 println!("world");
15024 }"#
15025 .unindent(),
15026 );
15027
15028 cx.update_editor(|editor, window, cx| {
15029 editor.handle_input("\nnew_line\n", window, cx);
15030 });
15031 executor.run_until_parked();
15032
15033 cx.assert_state_with_diff(
15034 r#"
15035 use some::mod1;
15036 use some::mod2;
15037
15038 const A: u32 = 42;
15039 const B: u32 = 42;
15040 - const C: u32 = 42;
15041 + const C: u32 = 43
15042 + new_line
15043 + ˇ
15044 const D: u32 = 42;
15045
15046
15047 fn main() {
15048 println!("hello");
15049
15050 println!("world");
15051 }"#
15052 .unindent(),
15053 );
15054}
15055
15056#[gpui::test]
15057async fn test_stage_and_unstage_added_file_hunk(
15058 executor: BackgroundExecutor,
15059 cx: &mut TestAppContext,
15060) {
15061 init_test(cx, |_| {});
15062
15063 let mut cx = EditorTestContext::new(cx).await;
15064 cx.update_editor(|editor, _, cx| {
15065 editor.set_expand_all_diff_hunks(cx);
15066 });
15067
15068 let working_copy = r#"
15069 ˇfn main() {
15070 println!("hello, world!");
15071 }
15072 "#
15073 .unindent();
15074
15075 cx.set_state(&working_copy);
15076 executor.run_until_parked();
15077
15078 cx.assert_state_with_diff(
15079 r#"
15080 + ˇfn main() {
15081 + println!("hello, world!");
15082 + }
15083 "#
15084 .unindent(),
15085 );
15086 cx.assert_index_text(None);
15087
15088 cx.update_editor(|editor, window, cx| {
15089 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15090 });
15091 executor.run_until_parked();
15092 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15093 cx.assert_state_with_diff(
15094 r#"
15095 + ˇfn main() {
15096 + println!("hello, world!");
15097 + }
15098 "#
15099 .unindent(),
15100 );
15101
15102 cx.update_editor(|editor, window, cx| {
15103 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15104 });
15105 executor.run_until_parked();
15106 cx.assert_index_text(None);
15107}
15108
15109async fn setup_indent_guides_editor(
15110 text: &str,
15111 cx: &mut TestAppContext,
15112) -> (BufferId, EditorTestContext) {
15113 init_test(cx, |_| {});
15114
15115 let mut cx = EditorTestContext::new(cx).await;
15116
15117 let buffer_id = cx.update_editor(|editor, window, cx| {
15118 editor.set_text(text, window, cx);
15119 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15120
15121 buffer_ids[0]
15122 });
15123
15124 (buffer_id, cx)
15125}
15126
15127fn assert_indent_guides(
15128 range: Range<u32>,
15129 expected: Vec<IndentGuide>,
15130 active_indices: Option<Vec<usize>>,
15131 cx: &mut EditorTestContext,
15132) {
15133 let indent_guides = cx.update_editor(|editor, window, cx| {
15134 let snapshot = editor.snapshot(window, cx).display_snapshot;
15135 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15136 editor,
15137 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15138 true,
15139 &snapshot,
15140 cx,
15141 );
15142
15143 indent_guides.sort_by(|a, b| {
15144 a.depth.cmp(&b.depth).then(
15145 a.start_row
15146 .cmp(&b.start_row)
15147 .then(a.end_row.cmp(&b.end_row)),
15148 )
15149 });
15150 indent_guides
15151 });
15152
15153 if let Some(expected) = active_indices {
15154 let active_indices = cx.update_editor(|editor, window, cx| {
15155 let snapshot = editor.snapshot(window, cx).display_snapshot;
15156 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15157 });
15158
15159 assert_eq!(
15160 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15161 expected,
15162 "Active indent guide indices do not match"
15163 );
15164 }
15165
15166 assert_eq!(indent_guides, expected, "Indent guides do not match");
15167}
15168
15169fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15170 IndentGuide {
15171 buffer_id,
15172 start_row: MultiBufferRow(start_row),
15173 end_row: MultiBufferRow(end_row),
15174 depth,
15175 tab_size: 4,
15176 settings: IndentGuideSettings {
15177 enabled: true,
15178 line_width: 1,
15179 active_line_width: 1,
15180 ..Default::default()
15181 },
15182 }
15183}
15184
15185#[gpui::test]
15186async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15187 let (buffer_id, mut cx) = setup_indent_guides_editor(
15188 &"
15189 fn main() {
15190 let a = 1;
15191 }"
15192 .unindent(),
15193 cx,
15194 )
15195 .await;
15196
15197 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15198}
15199
15200#[gpui::test]
15201async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15202 let (buffer_id, mut cx) = setup_indent_guides_editor(
15203 &"
15204 fn main() {
15205 let a = 1;
15206 let b = 2;
15207 }"
15208 .unindent(),
15209 cx,
15210 )
15211 .await;
15212
15213 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15214}
15215
15216#[gpui::test]
15217async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15218 let (buffer_id, mut cx) = setup_indent_guides_editor(
15219 &"
15220 fn main() {
15221 let a = 1;
15222 if a == 3 {
15223 let b = 2;
15224 } else {
15225 let c = 3;
15226 }
15227 }"
15228 .unindent(),
15229 cx,
15230 )
15231 .await;
15232
15233 assert_indent_guides(
15234 0..8,
15235 vec![
15236 indent_guide(buffer_id, 1, 6, 0),
15237 indent_guide(buffer_id, 3, 3, 1),
15238 indent_guide(buffer_id, 5, 5, 1),
15239 ],
15240 None,
15241 &mut cx,
15242 );
15243}
15244
15245#[gpui::test]
15246async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15247 let (buffer_id, mut cx) = setup_indent_guides_editor(
15248 &"
15249 fn main() {
15250 let a = 1;
15251 let b = 2;
15252 let c = 3;
15253 }"
15254 .unindent(),
15255 cx,
15256 )
15257 .await;
15258
15259 assert_indent_guides(
15260 0..5,
15261 vec![
15262 indent_guide(buffer_id, 1, 3, 0),
15263 indent_guide(buffer_id, 2, 2, 1),
15264 ],
15265 None,
15266 &mut cx,
15267 );
15268}
15269
15270#[gpui::test]
15271async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15272 let (buffer_id, mut cx) = setup_indent_guides_editor(
15273 &"
15274 fn main() {
15275 let a = 1;
15276
15277 let c = 3;
15278 }"
15279 .unindent(),
15280 cx,
15281 )
15282 .await;
15283
15284 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15285}
15286
15287#[gpui::test]
15288async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15289 let (buffer_id, mut cx) = setup_indent_guides_editor(
15290 &"
15291 fn main() {
15292 let a = 1;
15293
15294 let c = 3;
15295
15296 if a == 3 {
15297 let b = 2;
15298 } else {
15299 let c = 3;
15300 }
15301 }"
15302 .unindent(),
15303 cx,
15304 )
15305 .await;
15306
15307 assert_indent_guides(
15308 0..11,
15309 vec![
15310 indent_guide(buffer_id, 1, 9, 0),
15311 indent_guide(buffer_id, 6, 6, 1),
15312 indent_guide(buffer_id, 8, 8, 1),
15313 ],
15314 None,
15315 &mut cx,
15316 );
15317}
15318
15319#[gpui::test]
15320async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15321 let (buffer_id, mut cx) = setup_indent_guides_editor(
15322 &"
15323 fn main() {
15324 let a = 1;
15325
15326 let c = 3;
15327
15328 if a == 3 {
15329 let b = 2;
15330 } else {
15331 let c = 3;
15332 }
15333 }"
15334 .unindent(),
15335 cx,
15336 )
15337 .await;
15338
15339 assert_indent_guides(
15340 1..11,
15341 vec![
15342 indent_guide(buffer_id, 1, 9, 0),
15343 indent_guide(buffer_id, 6, 6, 1),
15344 indent_guide(buffer_id, 8, 8, 1),
15345 ],
15346 None,
15347 &mut cx,
15348 );
15349}
15350
15351#[gpui::test]
15352async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15353 let (buffer_id, mut cx) = setup_indent_guides_editor(
15354 &"
15355 fn main() {
15356 let a = 1;
15357
15358 let c = 3;
15359
15360 if a == 3 {
15361 let b = 2;
15362 } else {
15363 let c = 3;
15364 }
15365 }"
15366 .unindent(),
15367 cx,
15368 )
15369 .await;
15370
15371 assert_indent_guides(
15372 1..10,
15373 vec![
15374 indent_guide(buffer_id, 1, 9, 0),
15375 indent_guide(buffer_id, 6, 6, 1),
15376 indent_guide(buffer_id, 8, 8, 1),
15377 ],
15378 None,
15379 &mut cx,
15380 );
15381}
15382
15383#[gpui::test]
15384async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15385 let (buffer_id, mut cx) = setup_indent_guides_editor(
15386 &"
15387 block1
15388 block2
15389 block3
15390 block4
15391 block2
15392 block1
15393 block1"
15394 .unindent(),
15395 cx,
15396 )
15397 .await;
15398
15399 assert_indent_guides(
15400 1..10,
15401 vec![
15402 indent_guide(buffer_id, 1, 4, 0),
15403 indent_guide(buffer_id, 2, 3, 1),
15404 indent_guide(buffer_id, 3, 3, 2),
15405 ],
15406 None,
15407 &mut cx,
15408 );
15409}
15410
15411#[gpui::test]
15412async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15413 let (buffer_id, mut cx) = setup_indent_guides_editor(
15414 &"
15415 block1
15416 block2
15417 block3
15418
15419 block1
15420 block1"
15421 .unindent(),
15422 cx,
15423 )
15424 .await;
15425
15426 assert_indent_guides(
15427 0..6,
15428 vec![
15429 indent_guide(buffer_id, 1, 2, 0),
15430 indent_guide(buffer_id, 2, 2, 1),
15431 ],
15432 None,
15433 &mut cx,
15434 );
15435}
15436
15437#[gpui::test]
15438async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15439 let (buffer_id, mut cx) = setup_indent_guides_editor(
15440 &"
15441 block1
15442
15443
15444
15445 block2
15446 "
15447 .unindent(),
15448 cx,
15449 )
15450 .await;
15451
15452 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15453}
15454
15455#[gpui::test]
15456async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15457 let (buffer_id, mut cx) = setup_indent_guides_editor(
15458 &"
15459 def a:
15460 \tb = 3
15461 \tif True:
15462 \t\tc = 4
15463 \t\td = 5
15464 \tprint(b)
15465 "
15466 .unindent(),
15467 cx,
15468 )
15469 .await;
15470
15471 assert_indent_guides(
15472 0..6,
15473 vec![
15474 indent_guide(buffer_id, 1, 6, 0),
15475 indent_guide(buffer_id, 3, 4, 1),
15476 ],
15477 None,
15478 &mut cx,
15479 );
15480}
15481
15482#[gpui::test]
15483async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15484 let (buffer_id, mut cx) = setup_indent_guides_editor(
15485 &"
15486 fn main() {
15487 let a = 1;
15488 }"
15489 .unindent(),
15490 cx,
15491 )
15492 .await;
15493
15494 cx.update_editor(|editor, window, cx| {
15495 editor.change_selections(None, window, cx, |s| {
15496 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15497 });
15498 });
15499
15500 assert_indent_guides(
15501 0..3,
15502 vec![indent_guide(buffer_id, 1, 1, 0)],
15503 Some(vec![0]),
15504 &mut cx,
15505 );
15506}
15507
15508#[gpui::test]
15509async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15510 let (buffer_id, mut cx) = setup_indent_guides_editor(
15511 &"
15512 fn main() {
15513 if 1 == 2 {
15514 let a = 1;
15515 }
15516 }"
15517 .unindent(),
15518 cx,
15519 )
15520 .await;
15521
15522 cx.update_editor(|editor, window, cx| {
15523 editor.change_selections(None, window, cx, |s| {
15524 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15525 });
15526 });
15527
15528 assert_indent_guides(
15529 0..4,
15530 vec![
15531 indent_guide(buffer_id, 1, 3, 0),
15532 indent_guide(buffer_id, 2, 2, 1),
15533 ],
15534 Some(vec![1]),
15535 &mut cx,
15536 );
15537
15538 cx.update_editor(|editor, window, cx| {
15539 editor.change_selections(None, window, cx, |s| {
15540 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15541 });
15542 });
15543
15544 assert_indent_guides(
15545 0..4,
15546 vec![
15547 indent_guide(buffer_id, 1, 3, 0),
15548 indent_guide(buffer_id, 2, 2, 1),
15549 ],
15550 Some(vec![1]),
15551 &mut cx,
15552 );
15553
15554 cx.update_editor(|editor, window, cx| {
15555 editor.change_selections(None, window, cx, |s| {
15556 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15557 });
15558 });
15559
15560 assert_indent_guides(
15561 0..4,
15562 vec![
15563 indent_guide(buffer_id, 1, 3, 0),
15564 indent_guide(buffer_id, 2, 2, 1),
15565 ],
15566 Some(vec![0]),
15567 &mut cx,
15568 );
15569}
15570
15571#[gpui::test]
15572async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15573 let (buffer_id, mut cx) = setup_indent_guides_editor(
15574 &"
15575 fn main() {
15576 let a = 1;
15577
15578 let b = 2;
15579 }"
15580 .unindent(),
15581 cx,
15582 )
15583 .await;
15584
15585 cx.update_editor(|editor, window, cx| {
15586 editor.change_selections(None, window, cx, |s| {
15587 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15588 });
15589 });
15590
15591 assert_indent_guides(
15592 0..5,
15593 vec![indent_guide(buffer_id, 1, 3, 0)],
15594 Some(vec![0]),
15595 &mut cx,
15596 );
15597}
15598
15599#[gpui::test]
15600async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15601 let (buffer_id, mut cx) = setup_indent_guides_editor(
15602 &"
15603 def m:
15604 a = 1
15605 pass"
15606 .unindent(),
15607 cx,
15608 )
15609 .await;
15610
15611 cx.update_editor(|editor, window, cx| {
15612 editor.change_selections(None, window, cx, |s| {
15613 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15614 });
15615 });
15616
15617 assert_indent_guides(
15618 0..3,
15619 vec![indent_guide(buffer_id, 1, 2, 0)],
15620 Some(vec![0]),
15621 &mut cx,
15622 );
15623}
15624
15625#[gpui::test]
15626async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15627 init_test(cx, |_| {});
15628 let mut cx = EditorTestContext::new(cx).await;
15629 let text = indoc! {
15630 "
15631 impl A {
15632 fn b() {
15633 0;
15634 3;
15635 5;
15636 6;
15637 7;
15638 }
15639 }
15640 "
15641 };
15642 let base_text = indoc! {
15643 "
15644 impl A {
15645 fn b() {
15646 0;
15647 1;
15648 2;
15649 3;
15650 4;
15651 }
15652 fn c() {
15653 5;
15654 6;
15655 7;
15656 }
15657 }
15658 "
15659 };
15660
15661 cx.update_editor(|editor, window, cx| {
15662 editor.set_text(text, window, cx);
15663
15664 editor.buffer().update(cx, |multibuffer, cx| {
15665 let buffer = multibuffer.as_singleton().unwrap();
15666 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15667
15668 multibuffer.set_all_diff_hunks_expanded(cx);
15669 multibuffer.add_diff(diff, cx);
15670
15671 buffer.read(cx).remote_id()
15672 })
15673 });
15674 cx.run_until_parked();
15675
15676 cx.assert_state_with_diff(
15677 indoc! { "
15678 impl A {
15679 fn b() {
15680 0;
15681 - 1;
15682 - 2;
15683 3;
15684 - 4;
15685 - }
15686 - fn c() {
15687 5;
15688 6;
15689 7;
15690 }
15691 }
15692 ˇ"
15693 }
15694 .to_string(),
15695 );
15696
15697 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15698 editor
15699 .snapshot(window, cx)
15700 .buffer_snapshot
15701 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15702 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15703 .collect::<Vec<_>>()
15704 });
15705 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15706 assert_eq!(
15707 actual_guides,
15708 vec![
15709 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15710 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15711 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15712 ]
15713 );
15714}
15715
15716#[gpui::test]
15717async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15718 init_test(cx, |_| {});
15719 let mut cx = EditorTestContext::new(cx).await;
15720
15721 let diff_base = r#"
15722 a
15723 b
15724 c
15725 "#
15726 .unindent();
15727
15728 cx.set_state(
15729 &r#"
15730 ˇA
15731 b
15732 C
15733 "#
15734 .unindent(),
15735 );
15736 cx.set_head_text(&diff_base);
15737 cx.update_editor(|editor, window, cx| {
15738 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15739 });
15740 executor.run_until_parked();
15741
15742 let both_hunks_expanded = r#"
15743 - a
15744 + ˇA
15745 b
15746 - c
15747 + C
15748 "#
15749 .unindent();
15750
15751 cx.assert_state_with_diff(both_hunks_expanded.clone());
15752
15753 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15754 let snapshot = editor.snapshot(window, cx);
15755 let hunks = editor
15756 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15757 .collect::<Vec<_>>();
15758 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15759 let buffer_id = hunks[0].buffer_id;
15760 hunks
15761 .into_iter()
15762 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15763 .collect::<Vec<_>>()
15764 });
15765 assert_eq!(hunk_ranges.len(), 2);
15766
15767 cx.update_editor(|editor, _, cx| {
15768 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15769 });
15770 executor.run_until_parked();
15771
15772 let second_hunk_expanded = r#"
15773 ˇA
15774 b
15775 - c
15776 + C
15777 "#
15778 .unindent();
15779
15780 cx.assert_state_with_diff(second_hunk_expanded);
15781
15782 cx.update_editor(|editor, _, cx| {
15783 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15784 });
15785 executor.run_until_parked();
15786
15787 cx.assert_state_with_diff(both_hunks_expanded.clone());
15788
15789 cx.update_editor(|editor, _, cx| {
15790 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15791 });
15792 executor.run_until_parked();
15793
15794 let first_hunk_expanded = r#"
15795 - a
15796 + ˇA
15797 b
15798 C
15799 "#
15800 .unindent();
15801
15802 cx.assert_state_with_diff(first_hunk_expanded);
15803
15804 cx.update_editor(|editor, _, cx| {
15805 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15806 });
15807 executor.run_until_parked();
15808
15809 cx.assert_state_with_diff(both_hunks_expanded);
15810
15811 cx.set_state(
15812 &r#"
15813 ˇA
15814 b
15815 "#
15816 .unindent(),
15817 );
15818 cx.run_until_parked();
15819
15820 // TODO this cursor position seems bad
15821 cx.assert_state_with_diff(
15822 r#"
15823 - ˇa
15824 + A
15825 b
15826 "#
15827 .unindent(),
15828 );
15829
15830 cx.update_editor(|editor, window, cx| {
15831 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15832 });
15833
15834 cx.assert_state_with_diff(
15835 r#"
15836 - ˇa
15837 + A
15838 b
15839 - c
15840 "#
15841 .unindent(),
15842 );
15843
15844 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15845 let snapshot = editor.snapshot(window, cx);
15846 let hunks = editor
15847 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15848 .collect::<Vec<_>>();
15849 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15850 let buffer_id = hunks[0].buffer_id;
15851 hunks
15852 .into_iter()
15853 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15854 .collect::<Vec<_>>()
15855 });
15856 assert_eq!(hunk_ranges.len(), 2);
15857
15858 cx.update_editor(|editor, _, cx| {
15859 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15860 });
15861 executor.run_until_parked();
15862
15863 cx.assert_state_with_diff(
15864 r#"
15865 - ˇa
15866 + A
15867 b
15868 "#
15869 .unindent(),
15870 );
15871}
15872
15873#[gpui::test]
15874async fn test_toggle_deletion_hunk_at_start_of_file(
15875 executor: BackgroundExecutor,
15876 cx: &mut TestAppContext,
15877) {
15878 init_test(cx, |_| {});
15879 let mut cx = EditorTestContext::new(cx).await;
15880
15881 let diff_base = r#"
15882 a
15883 b
15884 c
15885 "#
15886 .unindent();
15887
15888 cx.set_state(
15889 &r#"
15890 ˇb
15891 c
15892 "#
15893 .unindent(),
15894 );
15895 cx.set_head_text(&diff_base);
15896 cx.update_editor(|editor, window, cx| {
15897 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15898 });
15899 executor.run_until_parked();
15900
15901 let hunk_expanded = r#"
15902 - a
15903 ˇb
15904 c
15905 "#
15906 .unindent();
15907
15908 cx.assert_state_with_diff(hunk_expanded.clone());
15909
15910 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15911 let snapshot = editor.snapshot(window, cx);
15912 let hunks = editor
15913 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15914 .collect::<Vec<_>>();
15915 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15916 let buffer_id = hunks[0].buffer_id;
15917 hunks
15918 .into_iter()
15919 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15920 .collect::<Vec<_>>()
15921 });
15922 assert_eq!(hunk_ranges.len(), 1);
15923
15924 cx.update_editor(|editor, _, cx| {
15925 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15926 });
15927 executor.run_until_parked();
15928
15929 let hunk_collapsed = r#"
15930 ˇb
15931 c
15932 "#
15933 .unindent();
15934
15935 cx.assert_state_with_diff(hunk_collapsed);
15936
15937 cx.update_editor(|editor, _, cx| {
15938 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15939 });
15940 executor.run_until_parked();
15941
15942 cx.assert_state_with_diff(hunk_expanded.clone());
15943}
15944
15945#[gpui::test]
15946async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15947 init_test(cx, |_| {});
15948
15949 let fs = FakeFs::new(cx.executor());
15950 fs.insert_tree(
15951 path!("/test"),
15952 json!({
15953 ".git": {},
15954 "file-1": "ONE\n",
15955 "file-2": "TWO\n",
15956 "file-3": "THREE\n",
15957 }),
15958 )
15959 .await;
15960
15961 fs.set_head_for_repo(
15962 path!("/test/.git").as_ref(),
15963 &[
15964 ("file-1".into(), "one\n".into()),
15965 ("file-2".into(), "two\n".into()),
15966 ("file-3".into(), "three\n".into()),
15967 ],
15968 );
15969
15970 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15971 let mut buffers = vec![];
15972 for i in 1..=3 {
15973 let buffer = project
15974 .update(cx, |project, cx| {
15975 let path = format!(path!("/test/file-{}"), i);
15976 project.open_local_buffer(path, cx)
15977 })
15978 .await
15979 .unwrap();
15980 buffers.push(buffer);
15981 }
15982
15983 let multibuffer = cx.new(|cx| {
15984 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
15985 multibuffer.set_all_diff_hunks_expanded(cx);
15986 for buffer in &buffers {
15987 let snapshot = buffer.read(cx).snapshot();
15988 multibuffer.set_excerpts_for_path(
15989 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
15990 buffer.clone(),
15991 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
15992 DEFAULT_MULTIBUFFER_CONTEXT,
15993 cx,
15994 );
15995 }
15996 multibuffer
15997 });
15998
15999 let editor = cx.add_window(|window, cx| {
16000 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16001 });
16002 cx.run_until_parked();
16003
16004 let snapshot = editor
16005 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16006 .unwrap();
16007 let hunks = snapshot
16008 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16009 .map(|hunk| match hunk {
16010 DisplayDiffHunk::Unfolded {
16011 display_row_range, ..
16012 } => display_row_range,
16013 DisplayDiffHunk::Folded { .. } => unreachable!(),
16014 })
16015 .collect::<Vec<_>>();
16016 assert_eq!(
16017 hunks,
16018 [
16019 DisplayRow(2)..DisplayRow(4),
16020 DisplayRow(7)..DisplayRow(9),
16021 DisplayRow(12)..DisplayRow(14),
16022 ]
16023 );
16024}
16025
16026#[gpui::test]
16027async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16028 init_test(cx, |_| {});
16029
16030 let mut cx = EditorTestContext::new(cx).await;
16031 cx.set_head_text(indoc! { "
16032 one
16033 two
16034 three
16035 four
16036 five
16037 "
16038 });
16039 cx.set_index_text(indoc! { "
16040 one
16041 two
16042 three
16043 four
16044 five
16045 "
16046 });
16047 cx.set_state(indoc! {"
16048 one
16049 TWO
16050 ˇTHREE
16051 FOUR
16052 five
16053 "});
16054 cx.run_until_parked();
16055 cx.update_editor(|editor, window, cx| {
16056 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16057 });
16058 cx.run_until_parked();
16059 cx.assert_index_text(Some(indoc! {"
16060 one
16061 TWO
16062 THREE
16063 FOUR
16064 five
16065 "}));
16066 cx.set_state(indoc! { "
16067 one
16068 TWO
16069 ˇTHREE-HUNDRED
16070 FOUR
16071 five
16072 "});
16073 cx.run_until_parked();
16074 cx.update_editor(|editor, window, cx| {
16075 let snapshot = editor.snapshot(window, cx);
16076 let hunks = editor
16077 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16078 .collect::<Vec<_>>();
16079 assert_eq!(hunks.len(), 1);
16080 assert_eq!(
16081 hunks[0].status(),
16082 DiffHunkStatus {
16083 kind: DiffHunkStatusKind::Modified,
16084 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16085 }
16086 );
16087
16088 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16089 });
16090 cx.run_until_parked();
16091 cx.assert_index_text(Some(indoc! {"
16092 one
16093 TWO
16094 THREE-HUNDRED
16095 FOUR
16096 five
16097 "}));
16098}
16099
16100#[gpui::test]
16101fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16102 init_test(cx, |_| {});
16103
16104 let editor = cx.add_window(|window, cx| {
16105 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16106 build_editor(buffer, window, cx)
16107 });
16108
16109 let render_args = Arc::new(Mutex::new(None));
16110 let snapshot = editor
16111 .update(cx, |editor, window, cx| {
16112 let snapshot = editor.buffer().read(cx).snapshot(cx);
16113 let range =
16114 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16115
16116 struct RenderArgs {
16117 row: MultiBufferRow,
16118 folded: bool,
16119 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16120 }
16121
16122 let crease = Crease::inline(
16123 range,
16124 FoldPlaceholder::test(),
16125 {
16126 let toggle_callback = render_args.clone();
16127 move |row, folded, callback, _window, _cx| {
16128 *toggle_callback.lock() = Some(RenderArgs {
16129 row,
16130 folded,
16131 callback,
16132 });
16133 div()
16134 }
16135 },
16136 |_row, _folded, _window, _cx| div(),
16137 );
16138
16139 editor.insert_creases(Some(crease), cx);
16140 let snapshot = editor.snapshot(window, cx);
16141 let _div = snapshot.render_crease_toggle(
16142 MultiBufferRow(1),
16143 false,
16144 cx.entity().clone(),
16145 window,
16146 cx,
16147 );
16148 snapshot
16149 })
16150 .unwrap();
16151
16152 let render_args = render_args.lock().take().unwrap();
16153 assert_eq!(render_args.row, MultiBufferRow(1));
16154 assert!(!render_args.folded);
16155 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16156
16157 cx.update_window(*editor, |_, window, cx| {
16158 (render_args.callback)(true, window, cx)
16159 })
16160 .unwrap();
16161 let snapshot = editor
16162 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16163 .unwrap();
16164 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16165
16166 cx.update_window(*editor, |_, window, cx| {
16167 (render_args.callback)(false, window, cx)
16168 })
16169 .unwrap();
16170 let snapshot = editor
16171 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16172 .unwrap();
16173 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16174}
16175
16176#[gpui::test]
16177async fn test_input_text(cx: &mut TestAppContext) {
16178 init_test(cx, |_| {});
16179 let mut cx = EditorTestContext::new(cx).await;
16180
16181 cx.set_state(
16182 &r#"ˇone
16183 two
16184
16185 three
16186 fourˇ
16187 five
16188
16189 siˇx"#
16190 .unindent(),
16191 );
16192
16193 cx.dispatch_action(HandleInput(String::new()));
16194 cx.assert_editor_state(
16195 &r#"ˇone
16196 two
16197
16198 three
16199 fourˇ
16200 five
16201
16202 siˇx"#
16203 .unindent(),
16204 );
16205
16206 cx.dispatch_action(HandleInput("AAAA".to_string()));
16207 cx.assert_editor_state(
16208 &r#"AAAAˇone
16209 two
16210
16211 three
16212 fourAAAAˇ
16213 five
16214
16215 siAAAAˇx"#
16216 .unindent(),
16217 );
16218}
16219
16220#[gpui::test]
16221async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16222 init_test(cx, |_| {});
16223
16224 let mut cx = EditorTestContext::new(cx).await;
16225 cx.set_state(
16226 r#"let foo = 1;
16227let foo = 2;
16228let foo = 3;
16229let fooˇ = 4;
16230let foo = 5;
16231let foo = 6;
16232let foo = 7;
16233let foo = 8;
16234let foo = 9;
16235let foo = 10;
16236let foo = 11;
16237let foo = 12;
16238let foo = 13;
16239let foo = 14;
16240let foo = 15;"#,
16241 );
16242
16243 cx.update_editor(|e, window, cx| {
16244 assert_eq!(
16245 e.next_scroll_position,
16246 NextScrollCursorCenterTopBottom::Center,
16247 "Default next scroll direction is center",
16248 );
16249
16250 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16251 assert_eq!(
16252 e.next_scroll_position,
16253 NextScrollCursorCenterTopBottom::Top,
16254 "After center, next scroll direction should be top",
16255 );
16256
16257 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16258 assert_eq!(
16259 e.next_scroll_position,
16260 NextScrollCursorCenterTopBottom::Bottom,
16261 "After top, next scroll direction should be bottom",
16262 );
16263
16264 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16265 assert_eq!(
16266 e.next_scroll_position,
16267 NextScrollCursorCenterTopBottom::Center,
16268 "After bottom, scrolling should start over",
16269 );
16270
16271 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16272 assert_eq!(
16273 e.next_scroll_position,
16274 NextScrollCursorCenterTopBottom::Top,
16275 "Scrolling continues if retriggered fast enough"
16276 );
16277 });
16278
16279 cx.executor()
16280 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16281 cx.executor().run_until_parked();
16282 cx.update_editor(|e, _, _| {
16283 assert_eq!(
16284 e.next_scroll_position,
16285 NextScrollCursorCenterTopBottom::Center,
16286 "If scrolling is not triggered fast enough, it should reset"
16287 );
16288 });
16289}
16290
16291#[gpui::test]
16292async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16293 init_test(cx, |_| {});
16294 let mut cx = EditorLspTestContext::new_rust(
16295 lsp::ServerCapabilities {
16296 definition_provider: Some(lsp::OneOf::Left(true)),
16297 references_provider: Some(lsp::OneOf::Left(true)),
16298 ..lsp::ServerCapabilities::default()
16299 },
16300 cx,
16301 )
16302 .await;
16303
16304 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16305 let go_to_definition = cx
16306 .lsp
16307 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16308 move |params, _| async move {
16309 if empty_go_to_definition {
16310 Ok(None)
16311 } else {
16312 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16313 uri: params.text_document_position_params.text_document.uri,
16314 range: lsp::Range::new(
16315 lsp::Position::new(4, 3),
16316 lsp::Position::new(4, 6),
16317 ),
16318 })))
16319 }
16320 },
16321 );
16322 let references = cx
16323 .lsp
16324 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16325 Ok(Some(vec![lsp::Location {
16326 uri: params.text_document_position.text_document.uri,
16327 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16328 }]))
16329 });
16330 (go_to_definition, references)
16331 };
16332
16333 cx.set_state(
16334 &r#"fn one() {
16335 let mut a = ˇtwo();
16336 }
16337
16338 fn two() {}"#
16339 .unindent(),
16340 );
16341 set_up_lsp_handlers(false, &mut cx);
16342 let navigated = cx
16343 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16344 .await
16345 .expect("Failed to navigate to definition");
16346 assert_eq!(
16347 navigated,
16348 Navigated::Yes,
16349 "Should have navigated to definition from the GetDefinition response"
16350 );
16351 cx.assert_editor_state(
16352 &r#"fn one() {
16353 let mut a = two();
16354 }
16355
16356 fn «twoˇ»() {}"#
16357 .unindent(),
16358 );
16359
16360 let editors = cx.update_workspace(|workspace, _, cx| {
16361 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16362 });
16363 cx.update_editor(|_, _, test_editor_cx| {
16364 assert_eq!(
16365 editors.len(),
16366 1,
16367 "Initially, only one, test, editor should be open in the workspace"
16368 );
16369 assert_eq!(
16370 test_editor_cx.entity(),
16371 editors.last().expect("Asserted len is 1").clone()
16372 );
16373 });
16374
16375 set_up_lsp_handlers(true, &mut cx);
16376 let navigated = cx
16377 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16378 .await
16379 .expect("Failed to navigate to lookup references");
16380 assert_eq!(
16381 navigated,
16382 Navigated::Yes,
16383 "Should have navigated to references as a fallback after empty GoToDefinition response"
16384 );
16385 // We should not change the selections in the existing file,
16386 // if opening another milti buffer with the references
16387 cx.assert_editor_state(
16388 &r#"fn one() {
16389 let mut a = two();
16390 }
16391
16392 fn «twoˇ»() {}"#
16393 .unindent(),
16394 );
16395 let editors = cx.update_workspace(|workspace, _, cx| {
16396 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16397 });
16398 cx.update_editor(|_, _, test_editor_cx| {
16399 assert_eq!(
16400 editors.len(),
16401 2,
16402 "After falling back to references search, we open a new editor with the results"
16403 );
16404 let references_fallback_text = editors
16405 .into_iter()
16406 .find(|new_editor| *new_editor != test_editor_cx.entity())
16407 .expect("Should have one non-test editor now")
16408 .read(test_editor_cx)
16409 .text(test_editor_cx);
16410 assert_eq!(
16411 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16412 "Should use the range from the references response and not the GoToDefinition one"
16413 );
16414 });
16415}
16416
16417#[gpui::test]
16418async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16419 init_test(cx, |_| {});
16420 cx.update(|cx| {
16421 let mut editor_settings = EditorSettings::get_global(cx).clone();
16422 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16423 EditorSettings::override_global(editor_settings, cx);
16424 });
16425 let mut cx = EditorLspTestContext::new_rust(
16426 lsp::ServerCapabilities {
16427 definition_provider: Some(lsp::OneOf::Left(true)),
16428 references_provider: Some(lsp::OneOf::Left(true)),
16429 ..lsp::ServerCapabilities::default()
16430 },
16431 cx,
16432 )
16433 .await;
16434 let original_state = r#"fn one() {
16435 let mut a = ˇtwo();
16436 }
16437
16438 fn two() {}"#
16439 .unindent();
16440 cx.set_state(&original_state);
16441
16442 let mut go_to_definition = cx
16443 .lsp
16444 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16445 move |_, _| async move { Ok(None) },
16446 );
16447 let _references = cx
16448 .lsp
16449 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16450 panic!("Should not call for references with no go to definition fallback")
16451 });
16452
16453 let navigated = cx
16454 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16455 .await
16456 .expect("Failed to navigate to lookup references");
16457 go_to_definition
16458 .next()
16459 .await
16460 .expect("Should have called the go_to_definition handler");
16461
16462 assert_eq!(
16463 navigated,
16464 Navigated::No,
16465 "Should have navigated to references as a fallback after empty GoToDefinition response"
16466 );
16467 cx.assert_editor_state(&original_state);
16468 let editors = cx.update_workspace(|workspace, _, cx| {
16469 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16470 });
16471 cx.update_editor(|_, _, _| {
16472 assert_eq!(
16473 editors.len(),
16474 1,
16475 "After unsuccessful fallback, no other editor should have been opened"
16476 );
16477 });
16478}
16479
16480#[gpui::test]
16481async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16482 init_test(cx, |_| {});
16483
16484 let language = Arc::new(Language::new(
16485 LanguageConfig::default(),
16486 Some(tree_sitter_rust::LANGUAGE.into()),
16487 ));
16488
16489 let text = r#"
16490 #[cfg(test)]
16491 mod tests() {
16492 #[test]
16493 fn runnable_1() {
16494 let a = 1;
16495 }
16496
16497 #[test]
16498 fn runnable_2() {
16499 let a = 1;
16500 let b = 2;
16501 }
16502 }
16503 "#
16504 .unindent();
16505
16506 let fs = FakeFs::new(cx.executor());
16507 fs.insert_file("/file.rs", Default::default()).await;
16508
16509 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16510 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16511 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16512 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16513 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16514
16515 let editor = cx.new_window_entity(|window, cx| {
16516 Editor::new(
16517 EditorMode::Full,
16518 multi_buffer,
16519 Some(project.clone()),
16520 window,
16521 cx,
16522 )
16523 });
16524
16525 editor.update_in(cx, |editor, window, cx| {
16526 let snapshot = editor.buffer().read(cx).snapshot(cx);
16527 editor.tasks.insert(
16528 (buffer.read(cx).remote_id(), 3),
16529 RunnableTasks {
16530 templates: vec![],
16531 offset: snapshot.anchor_before(43),
16532 column: 0,
16533 extra_variables: HashMap::default(),
16534 context_range: BufferOffset(43)..BufferOffset(85),
16535 },
16536 );
16537 editor.tasks.insert(
16538 (buffer.read(cx).remote_id(), 8),
16539 RunnableTasks {
16540 templates: vec![],
16541 offset: snapshot.anchor_before(86),
16542 column: 0,
16543 extra_variables: HashMap::default(),
16544 context_range: BufferOffset(86)..BufferOffset(191),
16545 },
16546 );
16547
16548 // Test finding task when cursor is inside function body
16549 editor.change_selections(None, window, cx, |s| {
16550 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16551 });
16552 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16553 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16554
16555 // Test finding task when cursor is on function name
16556 editor.change_selections(None, window, cx, |s| {
16557 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16558 });
16559 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16560 assert_eq!(row, 8, "Should find task when cursor is on function name");
16561 });
16562}
16563
16564#[gpui::test]
16565async fn test_folding_buffers(cx: &mut TestAppContext) {
16566 init_test(cx, |_| {});
16567
16568 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16569 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16570 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16571
16572 let fs = FakeFs::new(cx.executor());
16573 fs.insert_tree(
16574 path!("/a"),
16575 json!({
16576 "first.rs": sample_text_1,
16577 "second.rs": sample_text_2,
16578 "third.rs": sample_text_3,
16579 }),
16580 )
16581 .await;
16582 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16583 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16584 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16585 let worktree = project.update(cx, |project, cx| {
16586 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16587 assert_eq!(worktrees.len(), 1);
16588 worktrees.pop().unwrap()
16589 });
16590 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16591
16592 let buffer_1 = project
16593 .update(cx, |project, cx| {
16594 project.open_buffer((worktree_id, "first.rs"), cx)
16595 })
16596 .await
16597 .unwrap();
16598 let buffer_2 = project
16599 .update(cx, |project, cx| {
16600 project.open_buffer((worktree_id, "second.rs"), cx)
16601 })
16602 .await
16603 .unwrap();
16604 let buffer_3 = project
16605 .update(cx, |project, cx| {
16606 project.open_buffer((worktree_id, "third.rs"), cx)
16607 })
16608 .await
16609 .unwrap();
16610
16611 let multi_buffer = cx.new(|cx| {
16612 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16613 multi_buffer.push_excerpts(
16614 buffer_1.clone(),
16615 [
16616 ExcerptRange {
16617 context: Point::new(0, 0)..Point::new(3, 0),
16618 primary: None,
16619 },
16620 ExcerptRange {
16621 context: Point::new(5, 0)..Point::new(7, 0),
16622 primary: None,
16623 },
16624 ExcerptRange {
16625 context: Point::new(9, 0)..Point::new(10, 4),
16626 primary: None,
16627 },
16628 ],
16629 cx,
16630 );
16631 multi_buffer.push_excerpts(
16632 buffer_2.clone(),
16633 [
16634 ExcerptRange {
16635 context: Point::new(0, 0)..Point::new(3, 0),
16636 primary: None,
16637 },
16638 ExcerptRange {
16639 context: Point::new(5, 0)..Point::new(7, 0),
16640 primary: None,
16641 },
16642 ExcerptRange {
16643 context: Point::new(9, 0)..Point::new(10, 4),
16644 primary: None,
16645 },
16646 ],
16647 cx,
16648 );
16649 multi_buffer.push_excerpts(
16650 buffer_3.clone(),
16651 [
16652 ExcerptRange {
16653 context: Point::new(0, 0)..Point::new(3, 0),
16654 primary: None,
16655 },
16656 ExcerptRange {
16657 context: Point::new(5, 0)..Point::new(7, 0),
16658 primary: None,
16659 },
16660 ExcerptRange {
16661 context: Point::new(9, 0)..Point::new(10, 4),
16662 primary: None,
16663 },
16664 ],
16665 cx,
16666 );
16667 multi_buffer
16668 });
16669 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16670 Editor::new(
16671 EditorMode::Full,
16672 multi_buffer.clone(),
16673 Some(project.clone()),
16674 window,
16675 cx,
16676 )
16677 });
16678
16679 assert_eq!(
16680 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16681 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16682 );
16683
16684 multi_buffer_editor.update(cx, |editor, cx| {
16685 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16686 });
16687 assert_eq!(
16688 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16689 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16690 "After folding the first buffer, its text should not be displayed"
16691 );
16692
16693 multi_buffer_editor.update(cx, |editor, cx| {
16694 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16695 });
16696 assert_eq!(
16697 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16698 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16699 "After folding the second buffer, its text should not be displayed"
16700 );
16701
16702 multi_buffer_editor.update(cx, |editor, cx| {
16703 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16704 });
16705 assert_eq!(
16706 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16707 "\n\n\n\n\n",
16708 "After folding the third buffer, its text should not be displayed"
16709 );
16710
16711 // Emulate selection inside the fold logic, that should work
16712 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16713 editor
16714 .snapshot(window, cx)
16715 .next_line_boundary(Point::new(0, 4));
16716 });
16717
16718 multi_buffer_editor.update(cx, |editor, cx| {
16719 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16720 });
16721 assert_eq!(
16722 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16723 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16724 "After unfolding the second buffer, its text should be displayed"
16725 );
16726
16727 // Typing inside of buffer 1 causes that buffer to be unfolded.
16728 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16729 assert_eq!(
16730 multi_buffer
16731 .read(cx)
16732 .snapshot(cx)
16733 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16734 .collect::<String>(),
16735 "bbbb"
16736 );
16737 editor.change_selections(None, window, cx, |selections| {
16738 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16739 });
16740 editor.handle_input("B", window, cx);
16741 });
16742
16743 assert_eq!(
16744 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16745 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16746 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16747 );
16748
16749 multi_buffer_editor.update(cx, |editor, cx| {
16750 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16751 });
16752 assert_eq!(
16753 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16754 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16755 "After unfolding the all buffers, all original text should be displayed"
16756 );
16757}
16758
16759#[gpui::test]
16760async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16761 init_test(cx, |_| {});
16762
16763 let sample_text_1 = "1111\n2222\n3333".to_string();
16764 let sample_text_2 = "4444\n5555\n6666".to_string();
16765 let sample_text_3 = "7777\n8888\n9999".to_string();
16766
16767 let fs = FakeFs::new(cx.executor());
16768 fs.insert_tree(
16769 path!("/a"),
16770 json!({
16771 "first.rs": sample_text_1,
16772 "second.rs": sample_text_2,
16773 "third.rs": sample_text_3,
16774 }),
16775 )
16776 .await;
16777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16778 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16779 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16780 let worktree = project.update(cx, |project, cx| {
16781 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16782 assert_eq!(worktrees.len(), 1);
16783 worktrees.pop().unwrap()
16784 });
16785 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16786
16787 let buffer_1 = project
16788 .update(cx, |project, cx| {
16789 project.open_buffer((worktree_id, "first.rs"), cx)
16790 })
16791 .await
16792 .unwrap();
16793 let buffer_2 = project
16794 .update(cx, |project, cx| {
16795 project.open_buffer((worktree_id, "second.rs"), cx)
16796 })
16797 .await
16798 .unwrap();
16799 let buffer_3 = project
16800 .update(cx, |project, cx| {
16801 project.open_buffer((worktree_id, "third.rs"), cx)
16802 })
16803 .await
16804 .unwrap();
16805
16806 let multi_buffer = cx.new(|cx| {
16807 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16808 multi_buffer.push_excerpts(
16809 buffer_1.clone(),
16810 [ExcerptRange {
16811 context: Point::new(0, 0)..Point::new(3, 0),
16812 primary: None,
16813 }],
16814 cx,
16815 );
16816 multi_buffer.push_excerpts(
16817 buffer_2.clone(),
16818 [ExcerptRange {
16819 context: Point::new(0, 0)..Point::new(3, 0),
16820 primary: None,
16821 }],
16822 cx,
16823 );
16824 multi_buffer.push_excerpts(
16825 buffer_3.clone(),
16826 [ExcerptRange {
16827 context: Point::new(0, 0)..Point::new(3, 0),
16828 primary: None,
16829 }],
16830 cx,
16831 );
16832 multi_buffer
16833 });
16834
16835 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16836 Editor::new(
16837 EditorMode::Full,
16838 multi_buffer,
16839 Some(project.clone()),
16840 window,
16841 cx,
16842 )
16843 });
16844
16845 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16846 assert_eq!(
16847 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16848 full_text,
16849 );
16850
16851 multi_buffer_editor.update(cx, |editor, cx| {
16852 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16853 });
16854 assert_eq!(
16855 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16856 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16857 "After folding the first buffer, its text should not be displayed"
16858 );
16859
16860 multi_buffer_editor.update(cx, |editor, cx| {
16861 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16862 });
16863
16864 assert_eq!(
16865 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16866 "\n\n\n\n\n\n7777\n8888\n9999",
16867 "After folding the second buffer, its text should not be displayed"
16868 );
16869
16870 multi_buffer_editor.update(cx, |editor, cx| {
16871 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16872 });
16873 assert_eq!(
16874 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16875 "\n\n\n\n\n",
16876 "After folding the third buffer, its text should not be displayed"
16877 );
16878
16879 multi_buffer_editor.update(cx, |editor, cx| {
16880 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16881 });
16882 assert_eq!(
16883 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16884 "\n\n\n\n4444\n5555\n6666\n\n",
16885 "After unfolding the second buffer, its text should be displayed"
16886 );
16887
16888 multi_buffer_editor.update(cx, |editor, cx| {
16889 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16890 });
16891 assert_eq!(
16892 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16893 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16894 "After unfolding the first buffer, its text should be displayed"
16895 );
16896
16897 multi_buffer_editor.update(cx, |editor, cx| {
16898 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16899 });
16900 assert_eq!(
16901 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16902 full_text,
16903 "After unfolding all buffers, all original text should be displayed"
16904 );
16905}
16906
16907#[gpui::test]
16908async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16909 init_test(cx, |_| {});
16910
16911 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16912
16913 let fs = FakeFs::new(cx.executor());
16914 fs.insert_tree(
16915 path!("/a"),
16916 json!({
16917 "main.rs": sample_text,
16918 }),
16919 )
16920 .await;
16921 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16922 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16923 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16924 let worktree = project.update(cx, |project, cx| {
16925 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16926 assert_eq!(worktrees.len(), 1);
16927 worktrees.pop().unwrap()
16928 });
16929 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16930
16931 let buffer_1 = project
16932 .update(cx, |project, cx| {
16933 project.open_buffer((worktree_id, "main.rs"), cx)
16934 })
16935 .await
16936 .unwrap();
16937
16938 let multi_buffer = cx.new(|cx| {
16939 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16940 multi_buffer.push_excerpts(
16941 buffer_1.clone(),
16942 [ExcerptRange {
16943 context: Point::new(0, 0)
16944 ..Point::new(
16945 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16946 0,
16947 ),
16948 primary: None,
16949 }],
16950 cx,
16951 );
16952 multi_buffer
16953 });
16954 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16955 Editor::new(
16956 EditorMode::Full,
16957 multi_buffer,
16958 Some(project.clone()),
16959 window,
16960 cx,
16961 )
16962 });
16963
16964 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16965 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16966 enum TestHighlight {}
16967 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16968 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16969 editor.highlight_text::<TestHighlight>(
16970 vec![highlight_range.clone()],
16971 HighlightStyle::color(Hsla::green()),
16972 cx,
16973 );
16974 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16975 });
16976
16977 let full_text = format!("\n\n{sample_text}");
16978 assert_eq!(
16979 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16980 full_text,
16981 );
16982}
16983
16984#[gpui::test]
16985async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
16986 init_test(cx, |_| {});
16987 cx.update(|cx| {
16988 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
16989 "keymaps/default-linux.json",
16990 cx,
16991 )
16992 .unwrap();
16993 cx.bind_keys(default_key_bindings);
16994 });
16995
16996 let (editor, cx) = cx.add_window_view(|window, cx| {
16997 let multi_buffer = MultiBuffer::build_multi(
16998 [
16999 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17000 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17001 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17002 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17003 ],
17004 cx,
17005 );
17006 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17007
17008 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17009 // fold all but the second buffer, so that we test navigating between two
17010 // adjacent folded buffers, as well as folded buffers at the start and
17011 // end the multibuffer
17012 editor.fold_buffer(buffer_ids[0], cx);
17013 editor.fold_buffer(buffer_ids[2], cx);
17014 editor.fold_buffer(buffer_ids[3], cx);
17015
17016 editor
17017 });
17018 cx.simulate_resize(size(px(1000.), px(1000.)));
17019
17020 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17021 cx.assert_excerpts_with_selections(indoc! {"
17022 [EXCERPT]
17023 ˇ[FOLDED]
17024 [EXCERPT]
17025 a1
17026 b1
17027 [EXCERPT]
17028 [FOLDED]
17029 [EXCERPT]
17030 [FOLDED]
17031 "
17032 });
17033 cx.simulate_keystroke("down");
17034 cx.assert_excerpts_with_selections(indoc! {"
17035 [EXCERPT]
17036 [FOLDED]
17037 [EXCERPT]
17038 ˇa1
17039 b1
17040 [EXCERPT]
17041 [FOLDED]
17042 [EXCERPT]
17043 [FOLDED]
17044 "
17045 });
17046 cx.simulate_keystroke("down");
17047 cx.assert_excerpts_with_selections(indoc! {"
17048 [EXCERPT]
17049 [FOLDED]
17050 [EXCERPT]
17051 a1
17052 ˇb1
17053 [EXCERPT]
17054 [FOLDED]
17055 [EXCERPT]
17056 [FOLDED]
17057 "
17058 });
17059 cx.simulate_keystroke("down");
17060 cx.assert_excerpts_with_selections(indoc! {"
17061 [EXCERPT]
17062 [FOLDED]
17063 [EXCERPT]
17064 a1
17065 b1
17066 ˇ[EXCERPT]
17067 [FOLDED]
17068 [EXCERPT]
17069 [FOLDED]
17070 "
17071 });
17072 cx.simulate_keystroke("down");
17073 cx.assert_excerpts_with_selections(indoc! {"
17074 [EXCERPT]
17075 [FOLDED]
17076 [EXCERPT]
17077 a1
17078 b1
17079 [EXCERPT]
17080 ˇ[FOLDED]
17081 [EXCERPT]
17082 [FOLDED]
17083 "
17084 });
17085 for _ in 0..5 {
17086 cx.simulate_keystroke("down");
17087 cx.assert_excerpts_with_selections(indoc! {"
17088 [EXCERPT]
17089 [FOLDED]
17090 [EXCERPT]
17091 a1
17092 b1
17093 [EXCERPT]
17094 [FOLDED]
17095 [EXCERPT]
17096 ˇ[FOLDED]
17097 "
17098 });
17099 }
17100
17101 cx.simulate_keystroke("up");
17102 cx.assert_excerpts_with_selections(indoc! {"
17103 [EXCERPT]
17104 [FOLDED]
17105 [EXCERPT]
17106 a1
17107 b1
17108 [EXCERPT]
17109 ˇ[FOLDED]
17110 [EXCERPT]
17111 [FOLDED]
17112 "
17113 });
17114 cx.simulate_keystroke("up");
17115 cx.assert_excerpts_with_selections(indoc! {"
17116 [EXCERPT]
17117 [FOLDED]
17118 [EXCERPT]
17119 a1
17120 b1
17121 ˇ[EXCERPT]
17122 [FOLDED]
17123 [EXCERPT]
17124 [FOLDED]
17125 "
17126 });
17127 cx.simulate_keystroke("up");
17128 cx.assert_excerpts_with_selections(indoc! {"
17129 [EXCERPT]
17130 [FOLDED]
17131 [EXCERPT]
17132 a1
17133 ˇb1
17134 [EXCERPT]
17135 [FOLDED]
17136 [EXCERPT]
17137 [FOLDED]
17138 "
17139 });
17140 cx.simulate_keystroke("up");
17141 cx.assert_excerpts_with_selections(indoc! {"
17142 [EXCERPT]
17143 [FOLDED]
17144 [EXCERPT]
17145 ˇa1
17146 b1
17147 [EXCERPT]
17148 [FOLDED]
17149 [EXCERPT]
17150 [FOLDED]
17151 "
17152 });
17153 for _ in 0..5 {
17154 cx.simulate_keystroke("up");
17155 cx.assert_excerpts_with_selections(indoc! {"
17156 [EXCERPT]
17157 ˇ[FOLDED]
17158 [EXCERPT]
17159 a1
17160 b1
17161 [EXCERPT]
17162 [FOLDED]
17163 [EXCERPT]
17164 [FOLDED]
17165 "
17166 });
17167 }
17168}
17169
17170#[gpui::test]
17171async fn test_inline_completion_text(cx: &mut TestAppContext) {
17172 init_test(cx, |_| {});
17173
17174 // Simple insertion
17175 assert_highlighted_edits(
17176 "Hello, world!",
17177 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17178 true,
17179 cx,
17180 |highlighted_edits, cx| {
17181 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17182 assert_eq!(highlighted_edits.highlights.len(), 1);
17183 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17184 assert_eq!(
17185 highlighted_edits.highlights[0].1.background_color,
17186 Some(cx.theme().status().created_background)
17187 );
17188 },
17189 )
17190 .await;
17191
17192 // Replacement
17193 assert_highlighted_edits(
17194 "This is a test.",
17195 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17196 false,
17197 cx,
17198 |highlighted_edits, cx| {
17199 assert_eq!(highlighted_edits.text, "That is a test.");
17200 assert_eq!(highlighted_edits.highlights.len(), 1);
17201 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17202 assert_eq!(
17203 highlighted_edits.highlights[0].1.background_color,
17204 Some(cx.theme().status().created_background)
17205 );
17206 },
17207 )
17208 .await;
17209
17210 // Multiple edits
17211 assert_highlighted_edits(
17212 "Hello, world!",
17213 vec![
17214 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17215 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17216 ],
17217 false,
17218 cx,
17219 |highlighted_edits, cx| {
17220 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17221 assert_eq!(highlighted_edits.highlights.len(), 2);
17222 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17223 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17224 assert_eq!(
17225 highlighted_edits.highlights[0].1.background_color,
17226 Some(cx.theme().status().created_background)
17227 );
17228 assert_eq!(
17229 highlighted_edits.highlights[1].1.background_color,
17230 Some(cx.theme().status().created_background)
17231 );
17232 },
17233 )
17234 .await;
17235
17236 // Multiple lines with edits
17237 assert_highlighted_edits(
17238 "First line\nSecond line\nThird line\nFourth line",
17239 vec![
17240 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17241 (
17242 Point::new(2, 0)..Point::new(2, 10),
17243 "New third line".to_string(),
17244 ),
17245 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17246 ],
17247 false,
17248 cx,
17249 |highlighted_edits, cx| {
17250 assert_eq!(
17251 highlighted_edits.text,
17252 "Second modified\nNew third line\nFourth updated line"
17253 );
17254 assert_eq!(highlighted_edits.highlights.len(), 3);
17255 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17256 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17257 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17258 for highlight in &highlighted_edits.highlights {
17259 assert_eq!(
17260 highlight.1.background_color,
17261 Some(cx.theme().status().created_background)
17262 );
17263 }
17264 },
17265 )
17266 .await;
17267}
17268
17269#[gpui::test]
17270async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17271 init_test(cx, |_| {});
17272
17273 // Deletion
17274 assert_highlighted_edits(
17275 "Hello, world!",
17276 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17277 true,
17278 cx,
17279 |highlighted_edits, cx| {
17280 assert_eq!(highlighted_edits.text, "Hello, world!");
17281 assert_eq!(highlighted_edits.highlights.len(), 1);
17282 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17283 assert_eq!(
17284 highlighted_edits.highlights[0].1.background_color,
17285 Some(cx.theme().status().deleted_background)
17286 );
17287 },
17288 )
17289 .await;
17290
17291 // Insertion
17292 assert_highlighted_edits(
17293 "Hello, world!",
17294 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17295 true,
17296 cx,
17297 |highlighted_edits, cx| {
17298 assert_eq!(highlighted_edits.highlights.len(), 1);
17299 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17300 assert_eq!(
17301 highlighted_edits.highlights[0].1.background_color,
17302 Some(cx.theme().status().created_background)
17303 );
17304 },
17305 )
17306 .await;
17307}
17308
17309async fn assert_highlighted_edits(
17310 text: &str,
17311 edits: Vec<(Range<Point>, String)>,
17312 include_deletions: bool,
17313 cx: &mut TestAppContext,
17314 assertion_fn: impl Fn(HighlightedText, &App),
17315) {
17316 let window = cx.add_window(|window, cx| {
17317 let buffer = MultiBuffer::build_simple(text, cx);
17318 Editor::new(EditorMode::Full, buffer, None, window, cx)
17319 });
17320 let cx = &mut VisualTestContext::from_window(*window, cx);
17321
17322 let (buffer, snapshot) = window
17323 .update(cx, |editor, _window, cx| {
17324 (
17325 editor.buffer().clone(),
17326 editor.buffer().read(cx).snapshot(cx),
17327 )
17328 })
17329 .unwrap();
17330
17331 let edits = edits
17332 .into_iter()
17333 .map(|(range, edit)| {
17334 (
17335 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17336 edit,
17337 )
17338 })
17339 .collect::<Vec<_>>();
17340
17341 let text_anchor_edits = edits
17342 .clone()
17343 .into_iter()
17344 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17345 .collect::<Vec<_>>();
17346
17347 let edit_preview = window
17348 .update(cx, |_, _window, cx| {
17349 buffer
17350 .read(cx)
17351 .as_singleton()
17352 .unwrap()
17353 .read(cx)
17354 .preview_edits(text_anchor_edits.into(), cx)
17355 })
17356 .unwrap()
17357 .await;
17358
17359 cx.update(|_window, cx| {
17360 let highlighted_edits = inline_completion_edit_text(
17361 &snapshot.as_singleton().unwrap().2,
17362 &edits,
17363 &edit_preview,
17364 include_deletions,
17365 cx,
17366 );
17367 assertion_fn(highlighted_edits, cx)
17368 });
17369}
17370
17371#[track_caller]
17372fn assert_breakpoint(
17373 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17374 path: &Arc<Path>,
17375 expected: Vec<(u32, Breakpoint)>,
17376) {
17377 if expected.len() == 0usize {
17378 assert!(!breakpoints.contains_key(path), "{}", path.display());
17379 } else {
17380 let mut breakpoint = breakpoints
17381 .get(path)
17382 .unwrap()
17383 .into_iter()
17384 .map(|breakpoint| {
17385 (
17386 breakpoint.row,
17387 Breakpoint {
17388 message: breakpoint.message.clone(),
17389 state: breakpoint.state,
17390 },
17391 )
17392 })
17393 .collect::<Vec<_>>();
17394
17395 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17396
17397 assert_eq!(expected, breakpoint);
17398 }
17399}
17400
17401fn add_log_breakpoint_at_cursor(
17402 editor: &mut Editor,
17403 log_message: &str,
17404 window: &mut Window,
17405 cx: &mut Context<Editor>,
17406) {
17407 let (anchor, bp) = editor
17408 .breakpoint_at_cursor_head(window, cx)
17409 .unwrap_or_else(|| {
17410 let cursor_position: Point = editor.selections.newest(cx).head();
17411
17412 let breakpoint_position = editor
17413 .snapshot(window, cx)
17414 .display_snapshot
17415 .buffer_snapshot
17416 .anchor_before(Point::new(cursor_position.row, 0));
17417
17418 (
17419 breakpoint_position,
17420 Breakpoint {
17421 message: Some(Arc::from(log_message)),
17422 state: BreakpointState::Enabled,
17423 },
17424 )
17425 });
17426
17427 editor.edit_breakpoint_at_anchor(
17428 anchor,
17429 bp,
17430 BreakpointEditAction::EditLogMessage(log_message.into()),
17431 cx,
17432 );
17433}
17434
17435#[gpui::test]
17436async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17437 init_test(cx, |_| {});
17438
17439 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17440 let fs = FakeFs::new(cx.executor());
17441 fs.insert_tree(
17442 path!("/a"),
17443 json!({
17444 "main.rs": sample_text,
17445 }),
17446 )
17447 .await;
17448 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17449 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17450 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17451
17452 let fs = FakeFs::new(cx.executor());
17453 fs.insert_tree(
17454 path!("/a"),
17455 json!({
17456 "main.rs": sample_text,
17457 }),
17458 )
17459 .await;
17460 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17461 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17462 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17463 let worktree_id = workspace
17464 .update(cx, |workspace, _window, cx| {
17465 workspace.project().update(cx, |project, cx| {
17466 project.worktrees(cx).next().unwrap().read(cx).id()
17467 })
17468 })
17469 .unwrap();
17470
17471 let buffer = project
17472 .update(cx, |project, cx| {
17473 project.open_buffer((worktree_id, "main.rs"), cx)
17474 })
17475 .await
17476 .unwrap();
17477
17478 let (editor, cx) = cx.add_window_view(|window, cx| {
17479 Editor::new(
17480 EditorMode::Full,
17481 MultiBuffer::build_from_buffer(buffer, cx),
17482 Some(project.clone()),
17483 window,
17484 cx,
17485 )
17486 });
17487
17488 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17489 let abs_path = project.read_with(cx, |project, cx| {
17490 project
17491 .absolute_path(&project_path, cx)
17492 .map(|path_buf| Arc::from(path_buf.to_owned()))
17493 .unwrap()
17494 });
17495
17496 // assert we can add breakpoint on the first line
17497 editor.update_in(cx, |editor, window, cx| {
17498 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17499 editor.move_to_end(&MoveToEnd, window, cx);
17500 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17501 });
17502
17503 let breakpoints = editor.update(cx, |editor, cx| {
17504 editor
17505 .breakpoint_store()
17506 .as_ref()
17507 .unwrap()
17508 .read(cx)
17509 .all_breakpoints(cx)
17510 .clone()
17511 });
17512
17513 assert_eq!(1, breakpoints.len());
17514 assert_breakpoint(
17515 &breakpoints,
17516 &abs_path,
17517 vec![
17518 (0, Breakpoint::new_standard()),
17519 (3, Breakpoint::new_standard()),
17520 ],
17521 );
17522
17523 editor.update_in(cx, |editor, window, cx| {
17524 editor.move_to_beginning(&MoveToBeginning, window, cx);
17525 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17526 });
17527
17528 let breakpoints = editor.update(cx, |editor, cx| {
17529 editor
17530 .breakpoint_store()
17531 .as_ref()
17532 .unwrap()
17533 .read(cx)
17534 .all_breakpoints(cx)
17535 .clone()
17536 });
17537
17538 assert_eq!(1, breakpoints.len());
17539 assert_breakpoint(
17540 &breakpoints,
17541 &abs_path,
17542 vec![(3, Breakpoint::new_standard())],
17543 );
17544
17545 editor.update_in(cx, |editor, window, cx| {
17546 editor.move_to_end(&MoveToEnd, window, cx);
17547 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17548 });
17549
17550 let breakpoints = editor.update(cx, |editor, cx| {
17551 editor
17552 .breakpoint_store()
17553 .as_ref()
17554 .unwrap()
17555 .read(cx)
17556 .all_breakpoints(cx)
17557 .clone()
17558 });
17559
17560 assert_eq!(0, breakpoints.len());
17561 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17562}
17563
17564#[gpui::test]
17565async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17566 init_test(cx, |_| {});
17567
17568 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17569
17570 let fs = FakeFs::new(cx.executor());
17571 fs.insert_tree(
17572 path!("/a"),
17573 json!({
17574 "main.rs": sample_text,
17575 }),
17576 )
17577 .await;
17578 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17579 let (workspace, cx) =
17580 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17581
17582 let worktree_id = workspace.update(cx, |workspace, cx| {
17583 workspace.project().update(cx, |project, cx| {
17584 project.worktrees(cx).next().unwrap().read(cx).id()
17585 })
17586 });
17587
17588 let buffer = project
17589 .update(cx, |project, cx| {
17590 project.open_buffer((worktree_id, "main.rs"), cx)
17591 })
17592 .await
17593 .unwrap();
17594
17595 let (editor, cx) = cx.add_window_view(|window, cx| {
17596 Editor::new(
17597 EditorMode::Full,
17598 MultiBuffer::build_from_buffer(buffer, cx),
17599 Some(project.clone()),
17600 window,
17601 cx,
17602 )
17603 });
17604
17605 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17606 let abs_path = project.read_with(cx, |project, cx| {
17607 project
17608 .absolute_path(&project_path, cx)
17609 .map(|path_buf| Arc::from(path_buf.to_owned()))
17610 .unwrap()
17611 });
17612
17613 editor.update_in(cx, |editor, window, cx| {
17614 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17615 });
17616
17617 let breakpoints = editor.update(cx, |editor, cx| {
17618 editor
17619 .breakpoint_store()
17620 .as_ref()
17621 .unwrap()
17622 .read(cx)
17623 .all_breakpoints(cx)
17624 .clone()
17625 });
17626
17627 assert_breakpoint(
17628 &breakpoints,
17629 &abs_path,
17630 vec![(0, Breakpoint::new_log("hello world"))],
17631 );
17632
17633 // Removing a log message from a log breakpoint should remove it
17634 editor.update_in(cx, |editor, window, cx| {
17635 add_log_breakpoint_at_cursor(editor, "", window, cx);
17636 });
17637
17638 let breakpoints = editor.update(cx, |editor, cx| {
17639 editor
17640 .breakpoint_store()
17641 .as_ref()
17642 .unwrap()
17643 .read(cx)
17644 .all_breakpoints(cx)
17645 .clone()
17646 });
17647
17648 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17649
17650 editor.update_in(cx, |editor, window, cx| {
17651 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17652 editor.move_to_end(&MoveToEnd, window, cx);
17653 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17654 // Not adding a log message to a standard breakpoint shouldn't remove it
17655 add_log_breakpoint_at_cursor(editor, "", window, cx);
17656 });
17657
17658 let breakpoints = editor.update(cx, |editor, cx| {
17659 editor
17660 .breakpoint_store()
17661 .as_ref()
17662 .unwrap()
17663 .read(cx)
17664 .all_breakpoints(cx)
17665 .clone()
17666 });
17667
17668 assert_breakpoint(
17669 &breakpoints,
17670 &abs_path,
17671 vec![
17672 (0, Breakpoint::new_standard()),
17673 (3, Breakpoint::new_standard()),
17674 ],
17675 );
17676
17677 editor.update_in(cx, |editor, window, cx| {
17678 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17679 });
17680
17681 let breakpoints = editor.update(cx, |editor, cx| {
17682 editor
17683 .breakpoint_store()
17684 .as_ref()
17685 .unwrap()
17686 .read(cx)
17687 .all_breakpoints(cx)
17688 .clone()
17689 });
17690
17691 assert_breakpoint(
17692 &breakpoints,
17693 &abs_path,
17694 vec![
17695 (0, Breakpoint::new_standard()),
17696 (3, Breakpoint::new_log("hello world")),
17697 ],
17698 );
17699
17700 editor.update_in(cx, |editor, window, cx| {
17701 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17702 });
17703
17704 let breakpoints = editor.update(cx, |editor, cx| {
17705 editor
17706 .breakpoint_store()
17707 .as_ref()
17708 .unwrap()
17709 .read(cx)
17710 .all_breakpoints(cx)
17711 .clone()
17712 });
17713
17714 assert_breakpoint(
17715 &breakpoints,
17716 &abs_path,
17717 vec![
17718 (0, Breakpoint::new_standard()),
17719 (3, Breakpoint::new_log("hello Earth!!")),
17720 ],
17721 );
17722}
17723
17724/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17725/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17726/// or when breakpoints were placed out of order. This tests for a regression too
17727#[gpui::test]
17728async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17729 init_test(cx, |_| {});
17730
17731 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17732 let fs = FakeFs::new(cx.executor());
17733 fs.insert_tree(
17734 path!("/a"),
17735 json!({
17736 "main.rs": sample_text,
17737 }),
17738 )
17739 .await;
17740 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17741 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17742 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17743
17744 let fs = FakeFs::new(cx.executor());
17745 fs.insert_tree(
17746 path!("/a"),
17747 json!({
17748 "main.rs": sample_text,
17749 }),
17750 )
17751 .await;
17752 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17753 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17754 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17755 let worktree_id = workspace
17756 .update(cx, |workspace, _window, cx| {
17757 workspace.project().update(cx, |project, cx| {
17758 project.worktrees(cx).next().unwrap().read(cx).id()
17759 })
17760 })
17761 .unwrap();
17762
17763 let buffer = project
17764 .update(cx, |project, cx| {
17765 project.open_buffer((worktree_id, "main.rs"), cx)
17766 })
17767 .await
17768 .unwrap();
17769
17770 let (editor, cx) = cx.add_window_view(|window, cx| {
17771 Editor::new(
17772 EditorMode::Full,
17773 MultiBuffer::build_from_buffer(buffer, cx),
17774 Some(project.clone()),
17775 window,
17776 cx,
17777 )
17778 });
17779
17780 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17781 let abs_path = project.read_with(cx, |project, cx| {
17782 project
17783 .absolute_path(&project_path, cx)
17784 .map(|path_buf| Arc::from(path_buf.to_owned()))
17785 .unwrap()
17786 });
17787
17788 // assert we can add breakpoint on the first line
17789 editor.update_in(cx, |editor, window, cx| {
17790 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17791 editor.move_to_end(&MoveToEnd, window, cx);
17792 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17793 editor.move_up(&MoveUp, window, cx);
17794 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17795 });
17796
17797 let breakpoints = editor.update(cx, |editor, cx| {
17798 editor
17799 .breakpoint_store()
17800 .as_ref()
17801 .unwrap()
17802 .read(cx)
17803 .all_breakpoints(cx)
17804 .clone()
17805 });
17806
17807 assert_eq!(1, breakpoints.len());
17808 assert_breakpoint(
17809 &breakpoints,
17810 &abs_path,
17811 vec![
17812 (0, Breakpoint::new_standard()),
17813 (2, Breakpoint::new_standard()),
17814 (3, Breakpoint::new_standard()),
17815 ],
17816 );
17817
17818 editor.update_in(cx, |editor, window, cx| {
17819 editor.move_to_beginning(&MoveToBeginning, window, cx);
17820 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17821 editor.move_to_end(&MoveToEnd, window, cx);
17822 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17823 // Disabling a breakpoint that doesn't exist should do nothing
17824 editor.move_up(&MoveUp, window, cx);
17825 editor.move_up(&MoveUp, window, cx);
17826 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17827 });
17828
17829 let breakpoints = editor.update(cx, |editor, cx| {
17830 editor
17831 .breakpoint_store()
17832 .as_ref()
17833 .unwrap()
17834 .read(cx)
17835 .all_breakpoints(cx)
17836 .clone()
17837 });
17838
17839 let disable_breakpoint = {
17840 let mut bp = Breakpoint::new_standard();
17841 bp.state = BreakpointState::Disabled;
17842 bp
17843 };
17844
17845 assert_eq!(1, breakpoints.len());
17846 assert_breakpoint(
17847 &breakpoints,
17848 &abs_path,
17849 vec![
17850 (0, disable_breakpoint.clone()),
17851 (2, Breakpoint::new_standard()),
17852 (3, disable_breakpoint.clone()),
17853 ],
17854 );
17855
17856 editor.update_in(cx, |editor, window, cx| {
17857 editor.move_to_beginning(&MoveToBeginning, window, cx);
17858 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17859 editor.move_to_end(&MoveToEnd, window, cx);
17860 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17861 editor.move_up(&MoveUp, window, cx);
17862 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17863 });
17864
17865 let breakpoints = editor.update(cx, |editor, cx| {
17866 editor
17867 .breakpoint_store()
17868 .as_ref()
17869 .unwrap()
17870 .read(cx)
17871 .all_breakpoints(cx)
17872 .clone()
17873 });
17874
17875 assert_eq!(1, breakpoints.len());
17876 assert_breakpoint(
17877 &breakpoints,
17878 &abs_path,
17879 vec![
17880 (0, Breakpoint::new_standard()),
17881 (2, disable_breakpoint),
17882 (3, Breakpoint::new_standard()),
17883 ],
17884 );
17885}
17886
17887#[gpui::test]
17888async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17889 init_test(cx, |_| {});
17890 let capabilities = lsp::ServerCapabilities {
17891 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17892 prepare_provider: Some(true),
17893 work_done_progress_options: Default::default(),
17894 })),
17895 ..Default::default()
17896 };
17897 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17898
17899 cx.set_state(indoc! {"
17900 struct Fˇoo {}
17901 "});
17902
17903 cx.update_editor(|editor, _, cx| {
17904 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17905 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17906 editor.highlight_background::<DocumentHighlightRead>(
17907 &[highlight_range],
17908 |c| c.editor_document_highlight_read_background,
17909 cx,
17910 );
17911 });
17912
17913 let mut prepare_rename_handler = cx
17914 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17915 move |_, _, _| async move {
17916 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17917 start: lsp::Position {
17918 line: 0,
17919 character: 7,
17920 },
17921 end: lsp::Position {
17922 line: 0,
17923 character: 10,
17924 },
17925 })))
17926 },
17927 );
17928 let prepare_rename_task = cx
17929 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17930 .expect("Prepare rename was not started");
17931 prepare_rename_handler.next().await.unwrap();
17932 prepare_rename_task.await.expect("Prepare rename failed");
17933
17934 let mut rename_handler =
17935 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17936 let edit = lsp::TextEdit {
17937 range: lsp::Range {
17938 start: lsp::Position {
17939 line: 0,
17940 character: 7,
17941 },
17942 end: lsp::Position {
17943 line: 0,
17944 character: 10,
17945 },
17946 },
17947 new_text: "FooRenamed".to_string(),
17948 };
17949 Ok(Some(lsp::WorkspaceEdit::new(
17950 // Specify the same edit twice
17951 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17952 )))
17953 });
17954 let rename_task = cx
17955 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17956 .expect("Confirm rename was not started");
17957 rename_handler.next().await.unwrap();
17958 rename_task.await.expect("Confirm rename failed");
17959 cx.run_until_parked();
17960
17961 // Despite two edits, only one is actually applied as those are identical
17962 cx.assert_editor_state(indoc! {"
17963 struct FooRenamedˇ {}
17964 "});
17965}
17966
17967#[gpui::test]
17968async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17969 init_test(cx, |_| {});
17970 // These capabilities indicate that the server does not support prepare rename.
17971 let capabilities = lsp::ServerCapabilities {
17972 rename_provider: Some(lsp::OneOf::Left(true)),
17973 ..Default::default()
17974 };
17975 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17976
17977 cx.set_state(indoc! {"
17978 struct Fˇoo {}
17979 "});
17980
17981 cx.update_editor(|editor, _window, cx| {
17982 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17983 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17984 editor.highlight_background::<DocumentHighlightRead>(
17985 &[highlight_range],
17986 |c| c.editor_document_highlight_read_background,
17987 cx,
17988 );
17989 });
17990
17991 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17992 .expect("Prepare rename was not started")
17993 .await
17994 .expect("Prepare rename failed");
17995
17996 let mut rename_handler =
17997 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17998 let edit = lsp::TextEdit {
17999 range: lsp::Range {
18000 start: lsp::Position {
18001 line: 0,
18002 character: 7,
18003 },
18004 end: lsp::Position {
18005 line: 0,
18006 character: 10,
18007 },
18008 },
18009 new_text: "FooRenamed".to_string(),
18010 };
18011 Ok(Some(lsp::WorkspaceEdit::new(
18012 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18013 )))
18014 });
18015 let rename_task = cx
18016 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18017 .expect("Confirm rename was not started");
18018 rename_handler.next().await.unwrap();
18019 rename_task.await.expect("Confirm rename failed");
18020 cx.run_until_parked();
18021
18022 // Correct range is renamed, as `surrounding_word` is used to find it.
18023 cx.assert_editor_state(indoc! {"
18024 struct FooRenamedˇ {}
18025 "});
18026}
18027
18028#[gpui::test]
18029async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18030 init_test(cx, |_| {});
18031 let mut cx = EditorTestContext::new(cx).await;
18032
18033 let language = Arc::new(
18034 Language::new(
18035 LanguageConfig::default(),
18036 Some(tree_sitter_html::LANGUAGE.into()),
18037 )
18038 .with_brackets_query(
18039 r#"
18040 ("<" @open "/>" @close)
18041 ("</" @open ">" @close)
18042 ("<" @open ">" @close)
18043 ("\"" @open "\"" @close)
18044 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18045 "#,
18046 )
18047 .unwrap(),
18048 );
18049 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18050
18051 cx.set_state(indoc! {"
18052 <span>ˇ</span>
18053 "});
18054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18055 cx.assert_editor_state(indoc! {"
18056 <span>
18057 ˇ
18058 </span>
18059 "});
18060
18061 cx.set_state(indoc! {"
18062 <span><span></span>ˇ</span>
18063 "});
18064 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18065 cx.assert_editor_state(indoc! {"
18066 <span><span></span>
18067 ˇ</span>
18068 "});
18069
18070 cx.set_state(indoc! {"
18071 <span>ˇ
18072 </span>
18073 "});
18074 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18075 cx.assert_editor_state(indoc! {"
18076 <span>
18077 ˇ
18078 </span>
18079 "});
18080}
18081
18082#[gpui::test(iterations = 10)]
18083async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18084 init_test(cx, |_| {});
18085
18086 let fs = FakeFs::new(cx.executor());
18087 fs.insert_tree(
18088 path!("/dir"),
18089 json!({
18090 "a.ts": "a",
18091 }),
18092 )
18093 .await;
18094
18095 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18096 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18098
18099 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18100 language_registry.add(Arc::new(Language::new(
18101 LanguageConfig {
18102 name: "TypeScript".into(),
18103 matcher: LanguageMatcher {
18104 path_suffixes: vec!["ts".to_string()],
18105 ..Default::default()
18106 },
18107 ..Default::default()
18108 },
18109 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18110 )));
18111 let mut fake_language_servers = language_registry.register_fake_lsp(
18112 "TypeScript",
18113 FakeLspAdapter {
18114 capabilities: lsp::ServerCapabilities {
18115 code_lens_provider: Some(lsp::CodeLensOptions {
18116 resolve_provider: Some(true),
18117 }),
18118 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18119 commands: vec!["_the/command".to_string()],
18120 ..lsp::ExecuteCommandOptions::default()
18121 }),
18122 ..lsp::ServerCapabilities::default()
18123 },
18124 ..FakeLspAdapter::default()
18125 },
18126 );
18127
18128 let (buffer, _handle) = project
18129 .update(cx, |p, cx| {
18130 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18131 })
18132 .await
18133 .unwrap();
18134 cx.executor().run_until_parked();
18135
18136 let fake_server = fake_language_servers.next().await.unwrap();
18137
18138 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18139 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18140 drop(buffer_snapshot);
18141 let actions = cx
18142 .update_window(*workspace, |_, window, cx| {
18143 project.code_actions(&buffer, anchor..anchor, window, cx)
18144 })
18145 .unwrap();
18146
18147 fake_server
18148 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18149 Ok(Some(vec![
18150 lsp::CodeLens {
18151 range: lsp::Range::default(),
18152 command: Some(lsp::Command {
18153 title: "Code lens command".to_owned(),
18154 command: "_the/command".to_owned(),
18155 arguments: None,
18156 }),
18157 data: None,
18158 },
18159 lsp::CodeLens {
18160 range: lsp::Range::default(),
18161 command: Some(lsp::Command {
18162 title: "Command not in capabilities".to_owned(),
18163 command: "not in capabilities".to_owned(),
18164 arguments: None,
18165 }),
18166 data: None,
18167 },
18168 lsp::CodeLens {
18169 range: lsp::Range {
18170 start: lsp::Position {
18171 line: 1,
18172 character: 1,
18173 },
18174 end: lsp::Position {
18175 line: 1,
18176 character: 1,
18177 },
18178 },
18179 command: Some(lsp::Command {
18180 title: "Command not in range".to_owned(),
18181 command: "_the/command".to_owned(),
18182 arguments: None,
18183 }),
18184 data: None,
18185 },
18186 ]))
18187 })
18188 .next()
18189 .await;
18190
18191 let actions = actions.await.unwrap();
18192 assert_eq!(
18193 actions.len(),
18194 1,
18195 "Should have only one valid action for the 0..0 range"
18196 );
18197 let action = actions[0].clone();
18198 let apply = project.update(cx, |project, cx| {
18199 project.apply_code_action(buffer.clone(), action, true, cx)
18200 });
18201
18202 // Resolving the code action does not populate its edits. In absence of
18203 // edits, we must execute the given command.
18204 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18205 |mut lens, _| async move {
18206 let lens_command = lens.command.as_mut().expect("should have a command");
18207 assert_eq!(lens_command.title, "Code lens command");
18208 lens_command.arguments = Some(vec![json!("the-argument")]);
18209 Ok(lens)
18210 },
18211 );
18212
18213 // While executing the command, the language server sends the editor
18214 // a `workspaceEdit` request.
18215 fake_server
18216 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18217 let fake = fake_server.clone();
18218 move |params, _| {
18219 assert_eq!(params.command, "_the/command");
18220 let fake = fake.clone();
18221 async move {
18222 fake.server
18223 .request::<lsp::request::ApplyWorkspaceEdit>(
18224 lsp::ApplyWorkspaceEditParams {
18225 label: None,
18226 edit: lsp::WorkspaceEdit {
18227 changes: Some(
18228 [(
18229 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18230 vec![lsp::TextEdit {
18231 range: lsp::Range::new(
18232 lsp::Position::new(0, 0),
18233 lsp::Position::new(0, 0),
18234 ),
18235 new_text: "X".into(),
18236 }],
18237 )]
18238 .into_iter()
18239 .collect(),
18240 ),
18241 ..Default::default()
18242 },
18243 },
18244 )
18245 .await
18246 .unwrap();
18247 Ok(Some(json!(null)))
18248 }
18249 }
18250 })
18251 .next()
18252 .await;
18253
18254 // Applying the code lens command returns a project transaction containing the edits
18255 // sent by the language server in its `workspaceEdit` request.
18256 let transaction = apply.await.unwrap();
18257 assert!(transaction.0.contains_key(&buffer));
18258 buffer.update(cx, |buffer, cx| {
18259 assert_eq!(buffer.text(), "Xa");
18260 buffer.undo(cx);
18261 assert_eq!(buffer.text(), "a");
18262 });
18263}
18264
18265#[gpui::test]
18266async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18267 init_test(cx, |_| {});
18268
18269 let fs = FakeFs::new(cx.executor());
18270 let main_text = r#"fn main() {
18271println!("1");
18272println!("2");
18273println!("3");
18274println!("4");
18275println!("5");
18276}"#;
18277 let lib_text = "mod foo {}";
18278 fs.insert_tree(
18279 path!("/a"),
18280 json!({
18281 "lib.rs": lib_text,
18282 "main.rs": main_text,
18283 }),
18284 )
18285 .await;
18286
18287 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18288 let (workspace, cx) =
18289 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18290 let worktree_id = workspace.update(cx, |workspace, cx| {
18291 workspace.project().update(cx, |project, cx| {
18292 project.worktrees(cx).next().unwrap().read(cx).id()
18293 })
18294 });
18295
18296 let expected_ranges = vec![
18297 Point::new(0, 0)..Point::new(0, 0),
18298 Point::new(1, 0)..Point::new(1, 1),
18299 Point::new(2, 0)..Point::new(2, 2),
18300 Point::new(3, 0)..Point::new(3, 3),
18301 ];
18302
18303 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18304 let editor_1 = workspace
18305 .update_in(cx, |workspace, window, cx| {
18306 workspace.open_path(
18307 (worktree_id, "main.rs"),
18308 Some(pane_1.downgrade()),
18309 true,
18310 window,
18311 cx,
18312 )
18313 })
18314 .unwrap()
18315 .await
18316 .downcast::<Editor>()
18317 .unwrap();
18318 pane_1.update(cx, |pane, cx| {
18319 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18320 open_editor.update(cx, |editor, cx| {
18321 assert_eq!(
18322 editor.display_text(cx),
18323 main_text,
18324 "Original main.rs text on initial open",
18325 );
18326 assert_eq!(
18327 editor
18328 .selections
18329 .all::<Point>(cx)
18330 .into_iter()
18331 .map(|s| s.range())
18332 .collect::<Vec<_>>(),
18333 vec![Point::zero()..Point::zero()],
18334 "Default selections on initial open",
18335 );
18336 })
18337 });
18338 editor_1.update_in(cx, |editor, window, cx| {
18339 editor.change_selections(None, window, cx, |s| {
18340 s.select_ranges(expected_ranges.clone());
18341 });
18342 });
18343
18344 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18345 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18346 });
18347 let editor_2 = workspace
18348 .update_in(cx, |workspace, window, cx| {
18349 workspace.open_path(
18350 (worktree_id, "main.rs"),
18351 Some(pane_2.downgrade()),
18352 true,
18353 window,
18354 cx,
18355 )
18356 })
18357 .unwrap()
18358 .await
18359 .downcast::<Editor>()
18360 .unwrap();
18361 pane_2.update(cx, |pane, cx| {
18362 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18363 open_editor.update(cx, |editor, cx| {
18364 assert_eq!(
18365 editor.display_text(cx),
18366 main_text,
18367 "Original main.rs text on initial open in another panel",
18368 );
18369 assert_eq!(
18370 editor
18371 .selections
18372 .all::<Point>(cx)
18373 .into_iter()
18374 .map(|s| s.range())
18375 .collect::<Vec<_>>(),
18376 vec![Point::zero()..Point::zero()],
18377 "Default selections on initial open in another panel",
18378 );
18379 })
18380 });
18381
18382 editor_2.update_in(cx, |editor, window, cx| {
18383 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18384 });
18385
18386 let _other_editor_1 = workspace
18387 .update_in(cx, |workspace, window, cx| {
18388 workspace.open_path(
18389 (worktree_id, "lib.rs"),
18390 Some(pane_1.downgrade()),
18391 true,
18392 window,
18393 cx,
18394 )
18395 })
18396 .unwrap()
18397 .await
18398 .downcast::<Editor>()
18399 .unwrap();
18400 pane_1
18401 .update_in(cx, |pane, window, cx| {
18402 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18403 .unwrap()
18404 })
18405 .await
18406 .unwrap();
18407 drop(editor_1);
18408 pane_1.update(cx, |pane, cx| {
18409 pane.active_item()
18410 .unwrap()
18411 .downcast::<Editor>()
18412 .unwrap()
18413 .update(cx, |editor, cx| {
18414 assert_eq!(
18415 editor.display_text(cx),
18416 lib_text,
18417 "Other file should be open and active",
18418 );
18419 });
18420 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18421 });
18422
18423 let _other_editor_2 = workspace
18424 .update_in(cx, |workspace, window, cx| {
18425 workspace.open_path(
18426 (worktree_id, "lib.rs"),
18427 Some(pane_2.downgrade()),
18428 true,
18429 window,
18430 cx,
18431 )
18432 })
18433 .unwrap()
18434 .await
18435 .downcast::<Editor>()
18436 .unwrap();
18437 pane_2
18438 .update_in(cx, |pane, window, cx| {
18439 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18440 .unwrap()
18441 })
18442 .await
18443 .unwrap();
18444 drop(editor_2);
18445 pane_2.update(cx, |pane, cx| {
18446 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18447 open_editor.update(cx, |editor, cx| {
18448 assert_eq!(
18449 editor.display_text(cx),
18450 lib_text,
18451 "Other file should be open and active in another panel too",
18452 );
18453 });
18454 assert_eq!(
18455 pane.items().count(),
18456 1,
18457 "No other editors should be open in another pane",
18458 );
18459 });
18460
18461 let _editor_1_reopened = workspace
18462 .update_in(cx, |workspace, window, cx| {
18463 workspace.open_path(
18464 (worktree_id, "main.rs"),
18465 Some(pane_1.downgrade()),
18466 true,
18467 window,
18468 cx,
18469 )
18470 })
18471 .unwrap()
18472 .await
18473 .downcast::<Editor>()
18474 .unwrap();
18475 let _editor_2_reopened = workspace
18476 .update_in(cx, |workspace, window, cx| {
18477 workspace.open_path(
18478 (worktree_id, "main.rs"),
18479 Some(pane_2.downgrade()),
18480 true,
18481 window,
18482 cx,
18483 )
18484 })
18485 .unwrap()
18486 .await
18487 .downcast::<Editor>()
18488 .unwrap();
18489 pane_1.update(cx, |pane, cx| {
18490 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18491 open_editor.update(cx, |editor, cx| {
18492 assert_eq!(
18493 editor.display_text(cx),
18494 main_text,
18495 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18496 );
18497 assert_eq!(
18498 editor
18499 .selections
18500 .all::<Point>(cx)
18501 .into_iter()
18502 .map(|s| s.range())
18503 .collect::<Vec<_>>(),
18504 expected_ranges,
18505 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18506 );
18507 })
18508 });
18509 pane_2.update(cx, |pane, cx| {
18510 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18511 open_editor.update(cx, |editor, cx| {
18512 assert_eq!(
18513 editor.display_text(cx),
18514 r#"fn main() {
18515⋯rintln!("1");
18516⋯intln!("2");
18517⋯ntln!("3");
18518println!("4");
18519println!("5");
18520}"#,
18521 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18522 );
18523 assert_eq!(
18524 editor
18525 .selections
18526 .all::<Point>(cx)
18527 .into_iter()
18528 .map(|s| s.range())
18529 .collect::<Vec<_>>(),
18530 vec![Point::zero()..Point::zero()],
18531 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18532 );
18533 })
18534 });
18535}
18536
18537#[gpui::test]
18538async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18539 init_test(cx, |_| {});
18540
18541 let fs = FakeFs::new(cx.executor());
18542 let main_text = r#"fn main() {
18543println!("1");
18544println!("2");
18545println!("3");
18546println!("4");
18547println!("5");
18548}"#;
18549 let lib_text = "mod foo {}";
18550 fs.insert_tree(
18551 path!("/a"),
18552 json!({
18553 "lib.rs": lib_text,
18554 "main.rs": main_text,
18555 }),
18556 )
18557 .await;
18558
18559 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18560 let (workspace, cx) =
18561 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18562 let worktree_id = workspace.update(cx, |workspace, cx| {
18563 workspace.project().update(cx, |project, cx| {
18564 project.worktrees(cx).next().unwrap().read(cx).id()
18565 })
18566 });
18567
18568 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18569 let editor = workspace
18570 .update_in(cx, |workspace, window, cx| {
18571 workspace.open_path(
18572 (worktree_id, "main.rs"),
18573 Some(pane.downgrade()),
18574 true,
18575 window,
18576 cx,
18577 )
18578 })
18579 .unwrap()
18580 .await
18581 .downcast::<Editor>()
18582 .unwrap();
18583 pane.update(cx, |pane, cx| {
18584 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18585 open_editor.update(cx, |editor, cx| {
18586 assert_eq!(
18587 editor.display_text(cx),
18588 main_text,
18589 "Original main.rs text on initial open",
18590 );
18591 })
18592 });
18593 editor.update_in(cx, |editor, window, cx| {
18594 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18595 });
18596
18597 cx.update_global(|store: &mut SettingsStore, cx| {
18598 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18599 s.restore_on_file_reopen = Some(false);
18600 });
18601 });
18602 editor.update_in(cx, |editor, window, cx| {
18603 editor.fold_ranges(
18604 vec![
18605 Point::new(1, 0)..Point::new(1, 1),
18606 Point::new(2, 0)..Point::new(2, 2),
18607 Point::new(3, 0)..Point::new(3, 3),
18608 ],
18609 false,
18610 window,
18611 cx,
18612 );
18613 });
18614 pane.update_in(cx, |pane, window, cx| {
18615 pane.close_all_items(&CloseAllItems::default(), window, cx)
18616 .unwrap()
18617 })
18618 .await
18619 .unwrap();
18620 pane.update(cx, |pane, _| {
18621 assert!(pane.active_item().is_none());
18622 });
18623 cx.update_global(|store: &mut SettingsStore, cx| {
18624 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18625 s.restore_on_file_reopen = Some(true);
18626 });
18627 });
18628
18629 let _editor_reopened = workspace
18630 .update_in(cx, |workspace, window, cx| {
18631 workspace.open_path(
18632 (worktree_id, "main.rs"),
18633 Some(pane.downgrade()),
18634 true,
18635 window,
18636 cx,
18637 )
18638 })
18639 .unwrap()
18640 .await
18641 .downcast::<Editor>()
18642 .unwrap();
18643 pane.update(cx, |pane, cx| {
18644 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18645 open_editor.update(cx, |editor, cx| {
18646 assert_eq!(
18647 editor.display_text(cx),
18648 main_text,
18649 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18650 );
18651 })
18652 });
18653}
18654
18655fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18656 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18657 point..point
18658}
18659
18660fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18661 let (text, ranges) = marked_text_ranges(marked_text, true);
18662 assert_eq!(editor.text(cx), text);
18663 assert_eq!(
18664 editor.selections.ranges(cx),
18665 ranges,
18666 "Assert selections are {}",
18667 marked_text
18668 );
18669}
18670
18671pub fn handle_signature_help_request(
18672 cx: &mut EditorLspTestContext,
18673 mocked_response: lsp::SignatureHelp,
18674) -> impl Future<Output = ()> + use<> {
18675 let mut request =
18676 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18677 let mocked_response = mocked_response.clone();
18678 async move { Ok(Some(mocked_response)) }
18679 });
18680
18681 async move {
18682 request.next().await;
18683 }
18684}
18685
18686/// Handle completion request passing a marked string specifying where the completion
18687/// should be triggered from using '|' character, what range should be replaced, and what completions
18688/// should be returned using '<' and '>' to delimit the range
18689pub fn handle_completion_request(
18690 cx: &mut EditorLspTestContext,
18691 marked_string: &str,
18692 completions: Vec<&'static str>,
18693 counter: Arc<AtomicUsize>,
18694) -> impl Future<Output = ()> {
18695 let complete_from_marker: TextRangeMarker = '|'.into();
18696 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18697 let (_, mut marked_ranges) = marked_text_ranges_by(
18698 marked_string,
18699 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18700 );
18701
18702 let complete_from_position =
18703 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18704 let replace_range =
18705 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18706
18707 let mut request =
18708 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18709 let completions = completions.clone();
18710 counter.fetch_add(1, atomic::Ordering::Release);
18711 async move {
18712 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18713 assert_eq!(
18714 params.text_document_position.position,
18715 complete_from_position
18716 );
18717 Ok(Some(lsp::CompletionResponse::Array(
18718 completions
18719 .iter()
18720 .map(|completion_text| lsp::CompletionItem {
18721 label: completion_text.to_string(),
18722 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18723 range: replace_range,
18724 new_text: completion_text.to_string(),
18725 })),
18726 ..Default::default()
18727 })
18728 .collect(),
18729 )))
18730 }
18731 });
18732
18733 async move {
18734 request.next().await;
18735 }
18736}
18737
18738fn handle_resolve_completion_request(
18739 cx: &mut EditorLspTestContext,
18740 edits: Option<Vec<(&'static str, &'static str)>>,
18741) -> impl Future<Output = ()> {
18742 let edits = edits.map(|edits| {
18743 edits
18744 .iter()
18745 .map(|(marked_string, new_text)| {
18746 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18747 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18748 lsp::TextEdit::new(replace_range, new_text.to_string())
18749 })
18750 .collect::<Vec<_>>()
18751 });
18752
18753 let mut request =
18754 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18755 let edits = edits.clone();
18756 async move {
18757 Ok(lsp::CompletionItem {
18758 additional_text_edits: edits,
18759 ..Default::default()
18760 })
18761 }
18762 });
18763
18764 async move {
18765 request.next().await;
18766 }
18767}
18768
18769pub(crate) fn update_test_language_settings(
18770 cx: &mut TestAppContext,
18771 f: impl Fn(&mut AllLanguageSettingsContent),
18772) {
18773 cx.update(|cx| {
18774 SettingsStore::update_global(cx, |store, cx| {
18775 store.update_user_settings::<AllLanguageSettings>(cx, f);
18776 });
18777 });
18778}
18779
18780pub(crate) fn update_test_project_settings(
18781 cx: &mut TestAppContext,
18782 f: impl Fn(&mut ProjectSettings),
18783) {
18784 cx.update(|cx| {
18785 SettingsStore::update_global(cx, |store, cx| {
18786 store.update_user_settings::<ProjectSettings>(cx, f);
18787 });
18788 });
18789}
18790
18791pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18792 cx.update(|cx| {
18793 assets::Assets.load_test_fonts(cx);
18794 let store = SettingsStore::test(cx);
18795 cx.set_global(store);
18796 theme::init(theme::LoadThemes::JustBase, cx);
18797 release_channel::init(SemanticVersion::default(), cx);
18798 client::init_settings(cx);
18799 language::init(cx);
18800 Project::init_settings(cx);
18801 workspace::init_settings(cx);
18802 crate::init(cx);
18803 });
18804
18805 update_test_language_settings(cx, f);
18806}
18807
18808#[track_caller]
18809fn assert_hunk_revert(
18810 not_reverted_text_with_selections: &str,
18811 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18812 expected_reverted_text_with_selections: &str,
18813 base_text: &str,
18814 cx: &mut EditorLspTestContext,
18815) {
18816 cx.set_state(not_reverted_text_with_selections);
18817 cx.set_head_text(base_text);
18818 cx.executor().run_until_parked();
18819
18820 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18821 let snapshot = editor.snapshot(window, cx);
18822 let reverted_hunk_statuses = snapshot
18823 .buffer_snapshot
18824 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18825 .map(|hunk| hunk.status().kind)
18826 .collect::<Vec<_>>();
18827
18828 editor.git_restore(&Default::default(), window, cx);
18829 reverted_hunk_statuses
18830 });
18831 cx.executor().run_until_parked();
18832 cx.assert_editor_state(expected_reverted_text_with_selections);
18833 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18834}