1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
26 Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
280 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
283 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])
1909 });
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
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\n");
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\nthree");
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\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
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(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\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(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
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 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 ˇ )
3298 ˇ)
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
3518 ˇ»four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_toggle_case(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // If all lower case -> upper case
4320 cx.set_state(indoc! {"
4321 «hello worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «HELLO WORLDˇ»
4326 "});
4327
4328 // If all upper case -> lower case
4329 cx.set_state(indoc! {"
4330 «HELLO WORLDˇ»
4331 "});
4332 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4333 cx.assert_editor_state(indoc! {"
4334 «hello worldˇ»
4335 "});
4336
4337 // If any upper case characters are identified -> lower case
4338 // This matches JetBrains IDEs
4339 cx.set_state(indoc! {"
4340 «hEllo worldˇ»
4341 "});
4342 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4343 cx.assert_editor_state(indoc! {"
4344 «hello worldˇ»
4345 "});
4346}
4347
4348#[gpui::test]
4349async fn test_manipulate_text(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 let mut cx = EditorTestContext::new(cx).await;
4353
4354 // Test convert_to_upper_case()
4355 cx.set_state(indoc! {"
4356 «hello worldˇ»
4357 "});
4358 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 «HELLO WORLDˇ»
4361 "});
4362
4363 // Test convert_to_lower_case()
4364 cx.set_state(indoc! {"
4365 «HELLO WORLDˇ»
4366 "});
4367 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «hello worldˇ»
4370 "});
4371
4372 // Test multiple line, single selection case
4373 cx.set_state(indoc! {"
4374 «The quick brown
4375 fox jumps over
4376 the lazy dogˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 «The Quick Brown
4381 Fox Jumps Over
4382 The Lazy Dogˇ»
4383 "});
4384
4385 // Test multiple line, single selection case
4386 cx.set_state(indoc! {"
4387 «The quick brown
4388 fox jumps over
4389 the lazy dogˇ»
4390 "});
4391 cx.update_editor(|e, window, cx| {
4392 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4393 });
4394 cx.assert_editor_state(indoc! {"
4395 «TheQuickBrown
4396 FoxJumpsOver
4397 TheLazyDogˇ»
4398 "});
4399
4400 // From here on out, test more complex cases of manipulate_text()
4401
4402 // Test no selection case - should affect words cursors are in
4403 // Cursor at beginning, middle, and end of word
4404 cx.set_state(indoc! {"
4405 ˇhello big beauˇtiful worldˇ
4406 "});
4407 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4410 "});
4411
4412 // Test multiple selections on a single line and across multiple lines
4413 cx.set_state(indoc! {"
4414 «Theˇ» quick «brown
4415 foxˇ» jumps «overˇ»
4416 the «lazyˇ» dog
4417 "});
4418 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4419 cx.assert_editor_state(indoc! {"
4420 «THEˇ» quick «BROWN
4421 FOXˇ» jumps «OVERˇ»
4422 the «LAZYˇ» dog
4423 "});
4424
4425 // Test case where text length grows
4426 cx.set_state(indoc! {"
4427 «tschüߡ»
4428 "});
4429 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4430 cx.assert_editor_state(indoc! {"
4431 «TSCHÜSSˇ»
4432 "});
4433
4434 // Test to make sure we don't crash when text shrinks
4435 cx.set_state(indoc! {"
4436 aaa_bbbˇ
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «aaaBbbˇ»
4443 "});
4444
4445 // Test to make sure we all aware of the fact that each word can grow and shrink
4446 // Final selections should be aware of this fact
4447 cx.set_state(indoc! {"
4448 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4449 "});
4450 cx.update_editor(|e, window, cx| {
4451 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4452 });
4453 cx.assert_editor_state(indoc! {"
4454 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4455 "});
4456
4457 cx.set_state(indoc! {"
4458 «hElLo, WoRld!ˇ»
4459 "});
4460 cx.update_editor(|e, window, cx| {
4461 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4462 });
4463 cx.assert_editor_state(indoc! {"
4464 «HeLlO, wOrLD!ˇ»
4465 "});
4466}
4467
4468#[gpui::test]
4469fn test_duplicate_line(cx: &mut TestAppContext) {
4470 init_test(cx, |_| {});
4471
4472 let editor = cx.add_window(|window, cx| {
4473 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4474 build_editor(buffer, window, cx)
4475 });
4476 _ = editor.update(cx, |editor, window, cx| {
4477 editor.change_selections(None, window, cx, |s| {
4478 s.select_display_ranges([
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4480 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4481 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4482 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4483 ])
4484 });
4485 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4486 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4487 assert_eq!(
4488 editor.selections.display_ranges(cx),
4489 vec![
4490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4491 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4492 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4493 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(None, window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4507 ])
4508 });
4509 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4510 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4511 assert_eq!(
4512 editor.selections.display_ranges(cx),
4513 vec![
4514 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4515 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4516 ]
4517 );
4518 });
4519
4520 // With `move_upwards` the selections stay in place, except for
4521 // the lines inserted above them
4522 let editor = cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4524 build_editor(buffer, window, cx)
4525 });
4526 _ = editor.update(cx, |editor, window, cx| {
4527 editor.change_selections(None, window, cx, |s| {
4528 s.select_display_ranges([
4529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4531 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4532 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4533 ])
4534 });
4535 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4536 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4537 assert_eq!(
4538 editor.selections.display_ranges(cx),
4539 vec![
4540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4541 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4542 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4543 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4544 ]
4545 );
4546 });
4547
4548 let editor = cx.add_window(|window, cx| {
4549 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4550 build_editor(buffer, window, cx)
4551 });
4552 _ = editor.update(cx, |editor, window, cx| {
4553 editor.change_selections(None, window, cx, |s| {
4554 s.select_display_ranges([
4555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4556 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4557 ])
4558 });
4559 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4560 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4561 assert_eq!(
4562 editor.selections.display_ranges(cx),
4563 vec![
4564 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4566 ]
4567 );
4568 });
4569
4570 let editor = cx.add_window(|window, cx| {
4571 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4572 build_editor(buffer, window, cx)
4573 });
4574 _ = editor.update(cx, |editor, window, cx| {
4575 editor.change_selections(None, window, cx, |s| {
4576 s.select_display_ranges([
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4578 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4579 ])
4580 });
4581 editor.duplicate_selection(&DuplicateSelection, window, cx);
4582 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4587 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4588 ]
4589 );
4590 });
4591}
4592
4593#[gpui::test]
4594fn test_move_line_up_down(cx: &mut TestAppContext) {
4595 init_test(cx, |_| {});
4596
4597 let editor = cx.add_window(|window, cx| {
4598 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4599 build_editor(buffer, window, cx)
4600 });
4601 _ = editor.update(cx, |editor, window, cx| {
4602 editor.fold_creases(
4603 vec![
4604 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4606 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4607 ],
4608 true,
4609 window,
4610 cx,
4611 );
4612 editor.change_selections(None, window, cx, |s| {
4613 s.select_display_ranges([
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4616 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4617 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4618 ])
4619 });
4620 assert_eq!(
4621 editor.display_text(cx),
4622 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4623 );
4624
4625 editor.move_line_up(&MoveLineUp, window, cx);
4626 assert_eq!(
4627 editor.display_text(cx),
4628 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4629 );
4630 assert_eq!(
4631 editor.selections.display_ranges(cx),
4632 vec![
4633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4634 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4635 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4637 ]
4638 );
4639 });
4640
4641 _ = editor.update(cx, |editor, window, cx| {
4642 editor.move_line_down(&MoveLineDown, window, cx);
4643 assert_eq!(
4644 editor.display_text(cx),
4645 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4646 );
4647 assert_eq!(
4648 editor.selections.display_ranges(cx),
4649 vec![
4650 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4651 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4652 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4653 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4654 ]
4655 );
4656 });
4657
4658 _ = editor.update(cx, |editor, window, cx| {
4659 editor.move_line_down(&MoveLineDown, window, cx);
4660 assert_eq!(
4661 editor.display_text(cx),
4662 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4663 );
4664 assert_eq!(
4665 editor.selections.display_ranges(cx),
4666 vec![
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4669 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4670 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4671 ]
4672 );
4673 });
4674
4675 _ = editor.update(cx, |editor, window, cx| {
4676 editor.move_line_up(&MoveLineUp, window, cx);
4677 assert_eq!(
4678 editor.display_text(cx),
4679 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4680 );
4681 assert_eq!(
4682 editor.selections.display_ranges(cx),
4683 vec![
4684 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4685 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4686 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4687 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4688 ]
4689 );
4690 });
4691}
4692
4693#[gpui::test]
4694fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4695 init_test(cx, |_| {});
4696
4697 let editor = cx.add_window(|window, cx| {
4698 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4699 build_editor(buffer, window, cx)
4700 });
4701 _ = editor.update(cx, |editor, window, cx| {
4702 let snapshot = editor.buffer.read(cx).snapshot(cx);
4703 editor.insert_blocks(
4704 [BlockProperties {
4705 style: BlockStyle::Fixed,
4706 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4707 height: Some(1),
4708 render: Arc::new(|_| div().into_any()),
4709 priority: 0,
4710 render_in_minimap: true,
4711 }],
4712 Some(Autoscroll::fit()),
4713 cx,
4714 );
4715 editor.change_selections(None, window, cx, |s| {
4716 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4717 });
4718 editor.move_line_down(&MoveLineDown, window, cx);
4719 });
4720}
4721
4722#[gpui::test]
4723async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727 cx.set_state(
4728 &"
4729 ˇzero
4730 one
4731 two
4732 three
4733 four
4734 five
4735 "
4736 .unindent(),
4737 );
4738
4739 // Create a four-line block that replaces three lines of text.
4740 cx.update_editor(|editor, window, cx| {
4741 let snapshot = editor.snapshot(window, cx);
4742 let snapshot = &snapshot.buffer_snapshot;
4743 let placement = BlockPlacement::Replace(
4744 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4745 );
4746 editor.insert_blocks(
4747 [BlockProperties {
4748 placement,
4749 height: Some(4),
4750 style: BlockStyle::Sticky,
4751 render: Arc::new(|_| gpui::div().into_any_element()),
4752 priority: 0,
4753 render_in_minimap: true,
4754 }],
4755 None,
4756 cx,
4757 );
4758 });
4759
4760 // Move down so that the cursor touches the block.
4761 cx.update_editor(|editor, window, cx| {
4762 editor.move_down(&Default::default(), window, cx);
4763 });
4764 cx.assert_editor_state(
4765 &"
4766 zero
4767 «one
4768 two
4769 threeˇ»
4770 four
4771 five
4772 "
4773 .unindent(),
4774 );
4775
4776 // Move down past the block.
4777 cx.update_editor(|editor, window, cx| {
4778 editor.move_down(&Default::default(), window, cx);
4779 });
4780 cx.assert_editor_state(
4781 &"
4782 zero
4783 one
4784 two
4785 three
4786 ˇfour
4787 five
4788 "
4789 .unindent(),
4790 );
4791}
4792
4793#[gpui::test]
4794fn test_transpose(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "bac");
4803 assert_eq!(editor.selections.ranges(cx), [2..2]);
4804
4805 editor.transpose(&Default::default(), window, cx);
4806 assert_eq!(editor.text(cx), "bca");
4807 assert_eq!(editor.selections.ranges(cx), [3..3]);
4808
4809 editor.transpose(&Default::default(), window, cx);
4810 assert_eq!(editor.text(cx), "bac");
4811 assert_eq!(editor.selections.ranges(cx), [3..3]);
4812
4813 editor
4814 });
4815
4816 _ = cx.add_window(|window, cx| {
4817 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4818 editor.set_style(EditorStyle::default(), window, cx);
4819 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4820 editor.transpose(&Default::default(), window, cx);
4821 assert_eq!(editor.text(cx), "acb\nde");
4822 assert_eq!(editor.selections.ranges(cx), [3..3]);
4823
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "acbd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "acbde\n");
4831 assert_eq!(editor.selections.ranges(cx), [6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "acbd\ne");
4835 assert_eq!(editor.selections.ranges(cx), [6..6]);
4836
4837 editor
4838 });
4839
4840 _ = cx.add_window(|window, cx| {
4841 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4842 editor.set_style(EditorStyle::default(), window, cx);
4843 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4844 editor.transpose(&Default::default(), window, cx);
4845 assert_eq!(editor.text(cx), "bacd\ne");
4846 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4847
4848 editor.transpose(&Default::default(), window, cx);
4849 assert_eq!(editor.text(cx), "bcade\n");
4850 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4851
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "bcda\ne");
4854 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "bcade\n");
4858 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "bcaed\n");
4862 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4863
4864 editor
4865 });
4866
4867 _ = cx.add_window(|window, cx| {
4868 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4869 editor.set_style(EditorStyle::default(), window, cx);
4870 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4871 editor.transpose(&Default::default(), window, cx);
4872 assert_eq!(editor.text(cx), "🏀🍐✋");
4873 assert_eq!(editor.selections.ranges(cx), [8..8]);
4874
4875 editor.transpose(&Default::default(), window, cx);
4876 assert_eq!(editor.text(cx), "🏀✋🍐");
4877 assert_eq!(editor.selections.ranges(cx), [11..11]);
4878
4879 editor.transpose(&Default::default(), window, cx);
4880 assert_eq!(editor.text(cx), "🏀🍐✋");
4881 assert_eq!(editor.selections.ranges(cx), [11..11]);
4882
4883 editor
4884 });
4885}
4886
4887#[gpui::test]
4888async fn test_rewrap(cx: &mut TestAppContext) {
4889 init_test(cx, |settings| {
4890 settings.languages.extend([
4891 (
4892 "Markdown".into(),
4893 LanguageSettingsContent {
4894 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4895 ..Default::default()
4896 },
4897 ),
4898 (
4899 "Plain Text".into(),
4900 LanguageSettingsContent {
4901 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4902 ..Default::default()
4903 },
4904 ),
4905 ])
4906 });
4907
4908 let mut cx = EditorTestContext::new(cx).await;
4909
4910 let language_with_c_comments = Arc::new(Language::new(
4911 LanguageConfig {
4912 line_comments: vec!["// ".into()],
4913 ..LanguageConfig::default()
4914 },
4915 None,
4916 ));
4917 let language_with_pound_comments = Arc::new(Language::new(
4918 LanguageConfig {
4919 line_comments: vec!["# ".into()],
4920 ..LanguageConfig::default()
4921 },
4922 None,
4923 ));
4924 let markdown_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Markdown".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931 let language_with_doc_comments = Arc::new(Language::new(
4932 LanguageConfig {
4933 line_comments: vec!["// ".into(), "/// ".into()],
4934 ..LanguageConfig::default()
4935 },
4936 Some(tree_sitter_rust::LANGUAGE.into()),
4937 ));
4938
4939 let plaintext_language = Arc::new(Language::new(
4940 LanguageConfig {
4941 name: "Plain Text".into(),
4942 ..LanguageConfig::default()
4943 },
4944 None,
4945 ));
4946
4947 assert_rewrap(
4948 indoc! {"
4949 // ˇ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.
4950 "},
4951 indoc! {"
4952 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4953 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4954 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4955 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4956 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4957 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4958 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4959 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4960 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4961 // porttitor id. Aliquam id accumsan eros.
4962 "},
4963 language_with_c_comments.clone(),
4964 &mut cx,
4965 );
4966
4967 // Test that rewrapping works inside of a selection
4968 assert_rewrap(
4969 indoc! {"
4970 «// 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.ˇ»
4971 "},
4972 indoc! {"
4973 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4974 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4975 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4976 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4977 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4978 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4979 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4980 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4981 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4982 // porttitor id. Aliquam id accumsan eros.ˇ»
4983 "},
4984 language_with_c_comments.clone(),
4985 &mut cx,
4986 );
4987
4988 // Test that cursors that expand to the same region are collapsed.
4989 assert_rewrap(
4990 indoc! {"
4991 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4992 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4993 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4994 // ˇ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.
4995 "},
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4998 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4999 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5000 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5001 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5002 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5003 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5004 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5005 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5006 // porttitor id. Aliquam id accumsan eros.
5007 "},
5008 language_with_c_comments.clone(),
5009 &mut cx,
5010 );
5011
5012 // Test that non-contiguous selections are treated separately.
5013 assert_rewrap(
5014 indoc! {"
5015 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5016 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5017 //
5018 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5019 // ˇ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.
5020 "},
5021 indoc! {"
5022 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5023 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5024 // auctor, eu lacinia sapien scelerisque.
5025 //
5026 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5027 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5028 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5029 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5030 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5031 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5032 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5033 "},
5034 language_with_c_comments.clone(),
5035 &mut cx,
5036 );
5037
5038 // Test that different comment prefixes are supported.
5039 assert_rewrap(
5040 indoc! {"
5041 # ˇ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.
5042 "},
5043 indoc! {"
5044 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5045 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5046 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5047 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5048 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5049 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5050 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5051 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5052 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5053 # accumsan eros.
5054 "},
5055 language_with_pound_comments.clone(),
5056 &mut cx,
5057 );
5058
5059 // Test that rewrapping is ignored outside of comments in most languages.
5060 assert_rewrap(
5061 indoc! {"
5062 /// Adds two numbers.
5063 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5064 fn add(a: u32, b: u32) -> u32 {
5065 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ˇ
5066 }
5067 "},
5068 indoc! {"
5069 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5070 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5071 fn add(a: u32, b: u32) -> u32 {
5072 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ˇ
5073 }
5074 "},
5075 language_with_doc_comments.clone(),
5076 &mut cx,
5077 );
5078
5079 // Test that rewrapping works in Markdown and Plain Text languages.
5080 assert_rewrap(
5081 indoc! {"
5082 # Hello
5083
5084 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.
5085 "},
5086 indoc! {"
5087 # Hello
5088
5089 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5090 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5091 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5092 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5093 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5094 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5095 Integer sit amet scelerisque nisi.
5096 "},
5097 markdown_language,
5098 &mut cx,
5099 );
5100
5101 assert_rewrap(
5102 indoc! {"
5103 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.
5104 "},
5105 indoc! {"
5106 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5107 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5108 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5109 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5110 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5111 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5112 Integer sit amet scelerisque nisi.
5113 "},
5114 plaintext_language,
5115 &mut cx,
5116 );
5117
5118 // Test rewrapping unaligned comments in a selection.
5119 assert_rewrap(
5120 indoc! {"
5121 fn foo() {
5122 if true {
5123 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5124 // Praesent semper egestas tellus id dignissim.ˇ»
5125 do_something();
5126 } else {
5127 //
5128 }
5129 }
5130 "},
5131 indoc! {"
5132 fn foo() {
5133 if true {
5134 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5135 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5136 // egestas tellus id dignissim.ˇ»
5137 do_something();
5138 } else {
5139 //
5140 }
5141 }
5142 "},
5143 language_with_doc_comments.clone(),
5144 &mut cx,
5145 );
5146
5147 assert_rewrap(
5148 indoc! {"
5149 fn foo() {
5150 if true {
5151 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5152 // Praesent semper egestas tellus id dignissim.»
5153 do_something();
5154 } else {
5155 //
5156 }
5157
5158 }
5159 "},
5160 indoc! {"
5161 fn foo() {
5162 if true {
5163 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5164 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5165 // egestas tellus id dignissim.»
5166 do_something();
5167 } else {
5168 //
5169 }
5170
5171 }
5172 "},
5173 language_with_doc_comments.clone(),
5174 &mut cx,
5175 );
5176
5177 #[track_caller]
5178 fn assert_rewrap(
5179 unwrapped_text: &str,
5180 wrapped_text: &str,
5181 language: Arc<Language>,
5182 cx: &mut EditorTestContext,
5183 ) {
5184 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5185 cx.set_state(unwrapped_text);
5186 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5187 cx.assert_editor_state(wrapped_text);
5188 }
5189}
5190
5191#[gpui::test]
5192async fn test_hard_wrap(cx: &mut TestAppContext) {
5193 init_test(cx, |_| {});
5194 let mut cx = EditorTestContext::new(cx).await;
5195
5196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5197 cx.update_editor(|editor, _, cx| {
5198 editor.set_hard_wrap(Some(14), cx);
5199 });
5200
5201 cx.set_state(indoc!(
5202 "
5203 one two three ˇ
5204 "
5205 ));
5206 cx.simulate_input("four");
5207 cx.run_until_parked();
5208
5209 cx.assert_editor_state(indoc!(
5210 "
5211 one two three
5212 fourˇ
5213 "
5214 ));
5215
5216 cx.update_editor(|editor, window, cx| {
5217 editor.newline(&Default::default(), window, cx);
5218 });
5219 cx.run_until_parked();
5220 cx.assert_editor_state(indoc!(
5221 "
5222 one two three
5223 four
5224 ˇ
5225 "
5226 ));
5227
5228 cx.simulate_input("five");
5229 cx.run_until_parked();
5230 cx.assert_editor_state(indoc!(
5231 "
5232 one two three
5233 four
5234 fiveˇ
5235 "
5236 ));
5237
5238 cx.update_editor(|editor, window, cx| {
5239 editor.newline(&Default::default(), window, cx);
5240 });
5241 cx.run_until_parked();
5242 cx.simulate_input("# ");
5243 cx.run_until_parked();
5244 cx.assert_editor_state(indoc!(
5245 "
5246 one two three
5247 four
5248 five
5249 # ˇ
5250 "
5251 ));
5252
5253 cx.update_editor(|editor, window, cx| {
5254 editor.newline(&Default::default(), window, cx);
5255 });
5256 cx.run_until_parked();
5257 cx.assert_editor_state(indoc!(
5258 "
5259 one two three
5260 four
5261 five
5262 #\x20
5263 #ˇ
5264 "
5265 ));
5266
5267 cx.simulate_input(" 6");
5268 cx.run_until_parked();
5269 cx.assert_editor_state(indoc!(
5270 "
5271 one two three
5272 four
5273 five
5274 #
5275 # 6ˇ
5276 "
5277 ));
5278}
5279
5280#[gpui::test]
5281async fn test_clipboard(cx: &mut TestAppContext) {
5282 init_test(cx, |_| {});
5283
5284 let mut cx = EditorTestContext::new(cx).await;
5285
5286 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5287 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5288 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5289
5290 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5291 cx.set_state("two ˇfour ˇsix ˇ");
5292 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5293 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5294
5295 // Paste again but with only two cursors. Since the number of cursors doesn't
5296 // match the number of slices in the clipboard, the entire clipboard text
5297 // is pasted at each cursor.
5298 cx.set_state("ˇtwo one✅ four three six five ˇ");
5299 cx.update_editor(|e, window, cx| {
5300 e.handle_input("( ", window, cx);
5301 e.paste(&Paste, window, cx);
5302 e.handle_input(") ", window, cx);
5303 });
5304 cx.assert_editor_state(
5305 &([
5306 "( one✅ ",
5307 "three ",
5308 "five ) ˇtwo one✅ four three six five ( one✅ ",
5309 "three ",
5310 "five ) ˇ",
5311 ]
5312 .join("\n")),
5313 );
5314
5315 // Cut with three selections, one of which is full-line.
5316 cx.set_state(indoc! {"
5317 1«2ˇ»3
5318 4ˇ567
5319 «8ˇ»9"});
5320 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5321 cx.assert_editor_state(indoc! {"
5322 1ˇ3
5323 ˇ9"});
5324
5325 // Paste with three selections, noticing how the copied selection that was full-line
5326 // gets inserted before the second cursor.
5327 cx.set_state(indoc! {"
5328 1ˇ3
5329 9ˇ
5330 «oˇ»ne"});
5331 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5332 cx.assert_editor_state(indoc! {"
5333 12ˇ3
5334 4567
5335 9ˇ
5336 8ˇne"});
5337
5338 // Copy with a single cursor only, which writes the whole line into the clipboard.
5339 cx.set_state(indoc! {"
5340 The quick brown
5341 fox juˇmps over
5342 the lazy dog"});
5343 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5344 assert_eq!(
5345 cx.read_from_clipboard()
5346 .and_then(|item| item.text().as_deref().map(str::to_string)),
5347 Some("fox jumps over\n".to_string())
5348 );
5349
5350 // Paste with three selections, noticing how the copied full-line selection is inserted
5351 // before the empty selections but replaces the selection that is non-empty.
5352 cx.set_state(indoc! {"
5353 Tˇhe quick brown
5354 «foˇ»x jumps over
5355 tˇhe lazy dog"});
5356 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5357 cx.assert_editor_state(indoc! {"
5358 fox jumps over
5359 Tˇhe quick brown
5360 fox jumps over
5361 ˇx jumps over
5362 fox jumps over
5363 tˇhe lazy dog"});
5364}
5365
5366#[gpui::test]
5367async fn test_copy_trim(cx: &mut TestAppContext) {
5368 init_test(cx, |_| {});
5369
5370 let mut cx = EditorTestContext::new(cx).await;
5371 cx.set_state(
5372 r#" «for selection in selections.iter() {
5373 let mut start = selection.start;
5374 let mut end = selection.end;
5375 let is_entire_line = selection.is_empty();
5376 if is_entire_line {
5377 start = Point::new(start.row, 0);ˇ»
5378 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5379 }
5380 "#,
5381 );
5382 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5383 assert_eq!(
5384 cx.read_from_clipboard()
5385 .and_then(|item| item.text().as_deref().map(str::to_string)),
5386 Some(
5387 "for selection in selections.iter() {
5388 let mut start = selection.start;
5389 let mut end = selection.end;
5390 let is_entire_line = selection.is_empty();
5391 if is_entire_line {
5392 start = Point::new(start.row, 0);"
5393 .to_string()
5394 ),
5395 "Regular copying preserves all indentation selected",
5396 );
5397 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5398 assert_eq!(
5399 cx.read_from_clipboard()
5400 .and_then(|item| item.text().as_deref().map(str::to_string)),
5401 Some(
5402 "for selection in selections.iter() {
5403let mut start = selection.start;
5404let mut end = selection.end;
5405let is_entire_line = selection.is_empty();
5406if is_entire_line {
5407 start = Point::new(start.row, 0);"
5408 .to_string()
5409 ),
5410 "Copying with stripping should strip all leading whitespaces"
5411 );
5412
5413 cx.set_state(
5414 r#" « for selection in selections.iter() {
5415 let mut start = selection.start;
5416 let mut end = selection.end;
5417 let is_entire_line = selection.is_empty();
5418 if is_entire_line {
5419 start = Point::new(start.row, 0);ˇ»
5420 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5421 }
5422 "#,
5423 );
5424 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5425 assert_eq!(
5426 cx.read_from_clipboard()
5427 .and_then(|item| item.text().as_deref().map(str::to_string)),
5428 Some(
5429 " for selection in selections.iter() {
5430 let mut start = selection.start;
5431 let mut end = selection.end;
5432 let is_entire_line = selection.is_empty();
5433 if is_entire_line {
5434 start = Point::new(start.row, 0);"
5435 .to_string()
5436 ),
5437 "Regular copying preserves all indentation selected",
5438 );
5439 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5440 assert_eq!(
5441 cx.read_from_clipboard()
5442 .and_then(|item| item.text().as_deref().map(str::to_string)),
5443 Some(
5444 "for selection in selections.iter() {
5445let mut start = selection.start;
5446let mut end = selection.end;
5447let is_entire_line = selection.is_empty();
5448if is_entire_line {
5449 start = Point::new(start.row, 0);"
5450 .to_string()
5451 ),
5452 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5453 );
5454
5455 cx.set_state(
5456 r#" «ˇ for selection in selections.iter() {
5457 let mut start = selection.start;
5458 let mut end = selection.end;
5459 let is_entire_line = selection.is_empty();
5460 if is_entire_line {
5461 start = Point::new(start.row, 0);»
5462 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5463 }
5464 "#,
5465 );
5466 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5467 assert_eq!(
5468 cx.read_from_clipboard()
5469 .and_then(|item| item.text().as_deref().map(str::to_string)),
5470 Some(
5471 " for selection in selections.iter() {
5472 let mut start = selection.start;
5473 let mut end = selection.end;
5474 let is_entire_line = selection.is_empty();
5475 if is_entire_line {
5476 start = Point::new(start.row, 0);"
5477 .to_string()
5478 ),
5479 "Regular copying for reverse selection works the same",
5480 );
5481 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5482 assert_eq!(
5483 cx.read_from_clipboard()
5484 .and_then(|item| item.text().as_deref().map(str::to_string)),
5485 Some(
5486 "for selection in selections.iter() {
5487let mut start = selection.start;
5488let mut end = selection.end;
5489let is_entire_line = selection.is_empty();
5490if is_entire_line {
5491 start = Point::new(start.row, 0);"
5492 .to_string()
5493 ),
5494 "Copying with stripping for reverse selection works the same"
5495 );
5496
5497 cx.set_state(
5498 r#" for selection «in selections.iter() {
5499 let mut start = selection.start;
5500 let mut end = selection.end;
5501 let is_entire_line = selection.is_empty();
5502 if is_entire_line {
5503 start = Point::new(start.row, 0);ˇ»
5504 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5505 }
5506 "#,
5507 );
5508 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5509 assert_eq!(
5510 cx.read_from_clipboard()
5511 .and_then(|item| item.text().as_deref().map(str::to_string)),
5512 Some(
5513 "in selections.iter() {
5514 let mut start = selection.start;
5515 let mut end = selection.end;
5516 let is_entire_line = selection.is_empty();
5517 if is_entire_line {
5518 start = Point::new(start.row, 0);"
5519 .to_string()
5520 ),
5521 "When selecting past the indent, the copying works as usual",
5522 );
5523 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5524 assert_eq!(
5525 cx.read_from_clipboard()
5526 .and_then(|item| item.text().as_deref().map(str::to_string)),
5527 Some(
5528 "in selections.iter() {
5529 let mut start = selection.start;
5530 let mut end = selection.end;
5531 let is_entire_line = selection.is_empty();
5532 if is_entire_line {
5533 start = Point::new(start.row, 0);"
5534 .to_string()
5535 ),
5536 "When selecting past the indent, nothing is trimmed"
5537 );
5538
5539 cx.set_state(
5540 r#" «for selection in selections.iter() {
5541 let mut start = selection.start;
5542
5543 let mut end = selection.end;
5544 let is_entire_line = selection.is_empty();
5545 if is_entire_line {
5546 start = Point::new(start.row, 0);
5547ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5548 }
5549 "#,
5550 );
5551 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5552 assert_eq!(
5553 cx.read_from_clipboard()
5554 .and_then(|item| item.text().as_deref().map(str::to_string)),
5555 Some(
5556 "for selection in selections.iter() {
5557let mut start = selection.start;
5558
5559let mut end = selection.end;
5560let is_entire_line = selection.is_empty();
5561if is_entire_line {
5562 start = Point::new(start.row, 0);
5563"
5564 .to_string()
5565 ),
5566 "Copying with stripping should ignore empty lines"
5567 );
5568}
5569
5570#[gpui::test]
5571async fn test_paste_multiline(cx: &mut TestAppContext) {
5572 init_test(cx, |_| {});
5573
5574 let mut cx = EditorTestContext::new(cx).await;
5575 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5576
5577 // Cut an indented block, without the leading whitespace.
5578 cx.set_state(indoc! {"
5579 const a: B = (
5580 c(),
5581 «d(
5582 e,
5583 f
5584 )ˇ»
5585 );
5586 "});
5587 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5588 cx.assert_editor_state(indoc! {"
5589 const a: B = (
5590 c(),
5591 ˇ
5592 );
5593 "});
5594
5595 // Paste it at the same position.
5596 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5597 cx.assert_editor_state(indoc! {"
5598 const a: B = (
5599 c(),
5600 d(
5601 e,
5602 f
5603 )ˇ
5604 );
5605 "});
5606
5607 // Paste it at a line with a lower indent level.
5608 cx.set_state(indoc! {"
5609 ˇ
5610 const a: B = (
5611 c(),
5612 );
5613 "});
5614 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5615 cx.assert_editor_state(indoc! {"
5616 d(
5617 e,
5618 f
5619 )ˇ
5620 const a: B = (
5621 c(),
5622 );
5623 "});
5624
5625 // Cut an indented block, with the leading whitespace.
5626 cx.set_state(indoc! {"
5627 const a: B = (
5628 c(),
5629 « d(
5630 e,
5631 f
5632 )
5633 ˇ»);
5634 "});
5635 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5636 cx.assert_editor_state(indoc! {"
5637 const a: B = (
5638 c(),
5639 ˇ);
5640 "});
5641
5642 // Paste it at the same position.
5643 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5644 cx.assert_editor_state(indoc! {"
5645 const a: B = (
5646 c(),
5647 d(
5648 e,
5649 f
5650 )
5651 ˇ);
5652 "});
5653
5654 // Paste it at a line with a higher indent level.
5655 cx.set_state(indoc! {"
5656 const a: B = (
5657 c(),
5658 d(
5659 e,
5660 fˇ
5661 )
5662 );
5663 "});
5664 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5665 cx.assert_editor_state(indoc! {"
5666 const a: B = (
5667 c(),
5668 d(
5669 e,
5670 f d(
5671 e,
5672 f
5673 )
5674 ˇ
5675 )
5676 );
5677 "});
5678
5679 // Copy an indented block, starting mid-line
5680 cx.set_state(indoc! {"
5681 const a: B = (
5682 c(),
5683 somethin«g(
5684 e,
5685 f
5686 )ˇ»
5687 );
5688 "});
5689 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5690
5691 // Paste it on a line with a lower indent level
5692 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5693 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5694 cx.assert_editor_state(indoc! {"
5695 const a: B = (
5696 c(),
5697 something(
5698 e,
5699 f
5700 )
5701 );
5702 g(
5703 e,
5704 f
5705 )ˇ"});
5706}
5707
5708#[gpui::test]
5709async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5710 init_test(cx, |_| {});
5711
5712 cx.write_to_clipboard(ClipboardItem::new_string(
5713 " d(\n e\n );\n".into(),
5714 ));
5715
5716 let mut cx = EditorTestContext::new(cx).await;
5717 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5718
5719 cx.set_state(indoc! {"
5720 fn a() {
5721 b();
5722 if c() {
5723 ˇ
5724 }
5725 }
5726 "});
5727
5728 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5729 cx.assert_editor_state(indoc! {"
5730 fn a() {
5731 b();
5732 if c() {
5733 d(
5734 e
5735 );
5736 ˇ
5737 }
5738 }
5739 "});
5740
5741 cx.set_state(indoc! {"
5742 fn a() {
5743 b();
5744 ˇ
5745 }
5746 "});
5747
5748 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5749 cx.assert_editor_state(indoc! {"
5750 fn a() {
5751 b();
5752 d(
5753 e
5754 );
5755 ˇ
5756 }
5757 "});
5758}
5759
5760#[gpui::test]
5761fn test_select_all(cx: &mut TestAppContext) {
5762 init_test(cx, |_| {});
5763
5764 let editor = cx.add_window(|window, cx| {
5765 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5766 build_editor(buffer, window, cx)
5767 });
5768 _ = editor.update(cx, |editor, window, cx| {
5769 editor.select_all(&SelectAll, window, cx);
5770 assert_eq!(
5771 editor.selections.display_ranges(cx),
5772 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5773 );
5774 });
5775}
5776
5777#[gpui::test]
5778fn test_select_line(cx: &mut TestAppContext) {
5779 init_test(cx, |_| {});
5780
5781 let editor = cx.add_window(|window, cx| {
5782 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5783 build_editor(buffer, window, cx)
5784 });
5785 _ = editor.update(cx, |editor, window, cx| {
5786 editor.change_selections(None, window, cx, |s| {
5787 s.select_display_ranges([
5788 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5790 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5791 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5792 ])
5793 });
5794 editor.select_line(&SelectLine, window, cx);
5795 assert_eq!(
5796 editor.selections.display_ranges(cx),
5797 vec![
5798 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5799 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5800 ]
5801 );
5802 });
5803
5804 _ = editor.update(cx, |editor, window, cx| {
5805 editor.select_line(&SelectLine, window, cx);
5806 assert_eq!(
5807 editor.selections.display_ranges(cx),
5808 vec![
5809 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5810 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5811 ]
5812 );
5813 });
5814
5815 _ = editor.update(cx, |editor, window, cx| {
5816 editor.select_line(&SelectLine, window, cx);
5817 assert_eq!(
5818 editor.selections.display_ranges(cx),
5819 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5820 );
5821 });
5822}
5823
5824#[gpui::test]
5825async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5826 init_test(cx, |_| {});
5827 let mut cx = EditorTestContext::new(cx).await;
5828
5829 #[track_caller]
5830 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5831 cx.set_state(initial_state);
5832 cx.update_editor(|e, window, cx| {
5833 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5834 });
5835 cx.assert_editor_state(expected_state);
5836 }
5837
5838 // Selection starts and ends at the middle of lines, left-to-right
5839 test(
5840 &mut cx,
5841 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5842 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5843 );
5844 // Same thing, right-to-left
5845 test(
5846 &mut cx,
5847 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5848 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5849 );
5850
5851 // Whole buffer, left-to-right, last line *doesn't* end with newline
5852 test(
5853 &mut cx,
5854 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5855 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5856 );
5857 // Same thing, right-to-left
5858 test(
5859 &mut cx,
5860 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5861 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5862 );
5863
5864 // Whole buffer, left-to-right, last line ends with newline
5865 test(
5866 &mut cx,
5867 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5868 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5869 );
5870 // Same thing, right-to-left
5871 test(
5872 &mut cx,
5873 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5874 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5875 );
5876
5877 // Starts at the end of a line, ends at the start of another
5878 test(
5879 &mut cx,
5880 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5881 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5882 );
5883}
5884
5885#[gpui::test]
5886async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5887 init_test(cx, |_| {});
5888
5889 let editor = cx.add_window(|window, cx| {
5890 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5891 build_editor(buffer, window, cx)
5892 });
5893
5894 // setup
5895 _ = editor.update(cx, |editor, window, cx| {
5896 editor.fold_creases(
5897 vec![
5898 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5899 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5900 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5901 ],
5902 true,
5903 window,
5904 cx,
5905 );
5906 assert_eq!(
5907 editor.display_text(cx),
5908 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5909 );
5910 });
5911
5912 _ = editor.update(cx, |editor, window, cx| {
5913 editor.change_selections(None, window, cx, |s| {
5914 s.select_display_ranges([
5915 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5916 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5917 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5918 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5919 ])
5920 });
5921 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5922 assert_eq!(
5923 editor.display_text(cx),
5924 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5925 );
5926 });
5927 EditorTestContext::for_editor(editor, cx)
5928 .await
5929 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5930
5931 _ = editor.update(cx, |editor, window, cx| {
5932 editor.change_selections(None, window, cx, |s| {
5933 s.select_display_ranges([
5934 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5935 ])
5936 });
5937 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5938 assert_eq!(
5939 editor.display_text(cx),
5940 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5941 );
5942 assert_eq!(
5943 editor.selections.display_ranges(cx),
5944 [
5945 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5946 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5947 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5948 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5949 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5950 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5951 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5952 ]
5953 );
5954 });
5955 EditorTestContext::for_editor(editor, cx)
5956 .await
5957 .assert_editor_state(
5958 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5959 );
5960}
5961
5962#[gpui::test]
5963async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5964 init_test(cx, |_| {});
5965
5966 let mut cx = EditorTestContext::new(cx).await;
5967
5968 cx.set_state(indoc!(
5969 r#"abc
5970 defˇghi
5971
5972 jk
5973 nlmo
5974 "#
5975 ));
5976
5977 cx.update_editor(|editor, window, cx| {
5978 editor.add_selection_above(&Default::default(), window, cx);
5979 });
5980
5981 cx.assert_editor_state(indoc!(
5982 r#"abcˇ
5983 defˇghi
5984
5985 jk
5986 nlmo
5987 "#
5988 ));
5989
5990 cx.update_editor(|editor, window, cx| {
5991 editor.add_selection_above(&Default::default(), window, cx);
5992 });
5993
5994 cx.assert_editor_state(indoc!(
5995 r#"abcˇ
5996 defˇghi
5997
5998 jk
5999 nlmo
6000 "#
6001 ));
6002
6003 cx.update_editor(|editor, window, cx| {
6004 editor.add_selection_below(&Default::default(), window, cx);
6005 });
6006
6007 cx.assert_editor_state(indoc!(
6008 r#"abc
6009 defˇghi
6010
6011 jk
6012 nlmo
6013 "#
6014 ));
6015
6016 cx.update_editor(|editor, window, cx| {
6017 editor.undo_selection(&Default::default(), window, cx);
6018 });
6019
6020 cx.assert_editor_state(indoc!(
6021 r#"abcˇ
6022 defˇghi
6023
6024 jk
6025 nlmo
6026 "#
6027 ));
6028
6029 cx.update_editor(|editor, window, cx| {
6030 editor.redo_selection(&Default::default(), window, cx);
6031 });
6032
6033 cx.assert_editor_state(indoc!(
6034 r#"abc
6035 defˇghi
6036
6037 jk
6038 nlmo
6039 "#
6040 ));
6041
6042 cx.update_editor(|editor, window, cx| {
6043 editor.add_selection_below(&Default::default(), window, cx);
6044 });
6045
6046 cx.assert_editor_state(indoc!(
6047 r#"abc
6048 defˇghi
6049 ˇ
6050 jk
6051 nlmo
6052 "#
6053 ));
6054
6055 cx.update_editor(|editor, window, cx| {
6056 editor.add_selection_below(&Default::default(), window, cx);
6057 });
6058
6059 cx.assert_editor_state(indoc!(
6060 r#"abc
6061 defˇghi
6062 ˇ
6063 jkˇ
6064 nlmo
6065 "#
6066 ));
6067
6068 cx.update_editor(|editor, window, cx| {
6069 editor.add_selection_below(&Default::default(), window, cx);
6070 });
6071
6072 cx.assert_editor_state(indoc!(
6073 r#"abc
6074 defˇghi
6075 ˇ
6076 jkˇ
6077 nlmˇo
6078 "#
6079 ));
6080
6081 cx.update_editor(|editor, window, cx| {
6082 editor.add_selection_below(&Default::default(), window, cx);
6083 });
6084
6085 cx.assert_editor_state(indoc!(
6086 r#"abc
6087 defˇghi
6088 ˇ
6089 jkˇ
6090 nlmˇo
6091 ˇ"#
6092 ));
6093
6094 // change selections
6095 cx.set_state(indoc!(
6096 r#"abc
6097 def«ˇg»hi
6098
6099 jk
6100 nlmo
6101 "#
6102 ));
6103
6104 cx.update_editor(|editor, window, cx| {
6105 editor.add_selection_below(&Default::default(), window, cx);
6106 });
6107
6108 cx.assert_editor_state(indoc!(
6109 r#"abc
6110 def«ˇg»hi
6111
6112 jk
6113 nlm«ˇo»
6114 "#
6115 ));
6116
6117 cx.update_editor(|editor, window, cx| {
6118 editor.add_selection_below(&Default::default(), window, cx);
6119 });
6120
6121 cx.assert_editor_state(indoc!(
6122 r#"abc
6123 def«ˇg»hi
6124
6125 jk
6126 nlm«ˇo»
6127 "#
6128 ));
6129
6130 cx.update_editor(|editor, window, cx| {
6131 editor.add_selection_above(&Default::default(), window, cx);
6132 });
6133
6134 cx.assert_editor_state(indoc!(
6135 r#"abc
6136 def«ˇg»hi
6137
6138 jk
6139 nlmo
6140 "#
6141 ));
6142
6143 cx.update_editor(|editor, window, cx| {
6144 editor.add_selection_above(&Default::default(), window, cx);
6145 });
6146
6147 cx.assert_editor_state(indoc!(
6148 r#"abc
6149 def«ˇg»hi
6150
6151 jk
6152 nlmo
6153 "#
6154 ));
6155
6156 // Change selections again
6157 cx.set_state(indoc!(
6158 r#"a«bc
6159 defgˇ»hi
6160
6161 jk
6162 nlmo
6163 "#
6164 ));
6165
6166 cx.update_editor(|editor, window, cx| {
6167 editor.add_selection_below(&Default::default(), window, cx);
6168 });
6169
6170 cx.assert_editor_state(indoc!(
6171 r#"a«bcˇ»
6172 d«efgˇ»hi
6173
6174 j«kˇ»
6175 nlmo
6176 "#
6177 ));
6178
6179 cx.update_editor(|editor, window, cx| {
6180 editor.add_selection_below(&Default::default(), window, cx);
6181 });
6182 cx.assert_editor_state(indoc!(
6183 r#"a«bcˇ»
6184 d«efgˇ»hi
6185
6186 j«kˇ»
6187 n«lmoˇ»
6188 "#
6189 ));
6190 cx.update_editor(|editor, window, cx| {
6191 editor.add_selection_above(&Default::default(), window, cx);
6192 });
6193
6194 cx.assert_editor_state(indoc!(
6195 r#"a«bcˇ»
6196 d«efgˇ»hi
6197
6198 j«kˇ»
6199 nlmo
6200 "#
6201 ));
6202
6203 // Change selections again
6204 cx.set_state(indoc!(
6205 r#"abc
6206 d«ˇefghi
6207
6208 jk
6209 nlm»o
6210 "#
6211 ));
6212
6213 cx.update_editor(|editor, window, cx| {
6214 editor.add_selection_above(&Default::default(), window, cx);
6215 });
6216
6217 cx.assert_editor_state(indoc!(
6218 r#"a«ˇbc»
6219 d«ˇef»ghi
6220
6221 j«ˇk»
6222 n«ˇlm»o
6223 "#
6224 ));
6225
6226 cx.update_editor(|editor, window, cx| {
6227 editor.add_selection_below(&Default::default(), window, cx);
6228 });
6229
6230 cx.assert_editor_state(indoc!(
6231 r#"abc
6232 d«ˇef»ghi
6233
6234 j«ˇk»
6235 n«ˇlm»o
6236 "#
6237 ));
6238}
6239
6240#[gpui::test]
6241async fn test_select_next(cx: &mut TestAppContext) {
6242 init_test(cx, |_| {});
6243
6244 let mut cx = EditorTestContext::new(cx).await;
6245 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6246
6247 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6248 .unwrap();
6249 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6250
6251 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6252 .unwrap();
6253 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6254
6255 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6256 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6257
6258 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6259 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6260
6261 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6262 .unwrap();
6263 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6264
6265 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6266 .unwrap();
6267 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6268
6269 // Test selection direction should be preserved
6270 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6271
6272 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6273 .unwrap();
6274 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6275}
6276
6277#[gpui::test]
6278async fn test_select_all_matches(cx: &mut TestAppContext) {
6279 init_test(cx, |_| {});
6280
6281 let mut cx = EditorTestContext::new(cx).await;
6282
6283 // Test caret-only selections
6284 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6285 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6286 .unwrap();
6287 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6288
6289 // Test left-to-right selections
6290 cx.set_state("abc\n«abcˇ»\nabc");
6291 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6292 .unwrap();
6293 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6294
6295 // Test right-to-left selections
6296 cx.set_state("abc\n«ˇabc»\nabc");
6297 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6298 .unwrap();
6299 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6300
6301 // Test selecting whitespace with caret selection
6302 cx.set_state("abc\nˇ abc\nabc");
6303 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6304 .unwrap();
6305 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6306
6307 // Test selecting whitespace with left-to-right selection
6308 cx.set_state("abc\n«ˇ »abc\nabc");
6309 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6310 .unwrap();
6311 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6312
6313 // Test no matches with right-to-left selection
6314 cx.set_state("abc\n« ˇ»abc\nabc");
6315 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6316 .unwrap();
6317 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6318}
6319
6320#[gpui::test]
6321async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 let mut cx = EditorTestContext::new(cx).await;
6325
6326 let large_body_1 = "\nd".repeat(200);
6327 let large_body_2 = "\ne".repeat(200);
6328
6329 cx.set_state(&format!(
6330 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6331 ));
6332 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6333 let scroll_position = editor.scroll_position(cx);
6334 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6335 scroll_position
6336 });
6337
6338 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6339 .unwrap();
6340 cx.assert_editor_state(&format!(
6341 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6342 ));
6343 let scroll_position_after_selection =
6344 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6345 assert_eq!(
6346 initial_scroll_position, scroll_position_after_selection,
6347 "Scroll position should not change after selecting all matches"
6348 );
6349}
6350
6351#[gpui::test]
6352async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6353 init_test(cx, |_| {});
6354
6355 let mut cx = EditorLspTestContext::new_rust(
6356 lsp::ServerCapabilities {
6357 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6358 ..Default::default()
6359 },
6360 cx,
6361 )
6362 .await;
6363
6364 cx.set_state(indoc! {"
6365 line 1
6366 line 2
6367 linˇe 3
6368 line 4
6369 line 5
6370 "});
6371
6372 // Make an edit
6373 cx.update_editor(|editor, window, cx| {
6374 editor.handle_input("X", window, cx);
6375 });
6376
6377 // Move cursor to a different position
6378 cx.update_editor(|editor, window, cx| {
6379 editor.change_selections(None, window, cx, |s| {
6380 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6381 });
6382 });
6383
6384 cx.assert_editor_state(indoc! {"
6385 line 1
6386 line 2
6387 linXe 3
6388 line 4
6389 liˇne 5
6390 "});
6391
6392 cx.lsp
6393 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6394 Ok(Some(vec![lsp::TextEdit::new(
6395 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6396 "PREFIX ".to_string(),
6397 )]))
6398 });
6399
6400 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6401 .unwrap()
6402 .await
6403 .unwrap();
6404
6405 cx.assert_editor_state(indoc! {"
6406 PREFIX line 1
6407 line 2
6408 linXe 3
6409 line 4
6410 liˇne 5
6411 "});
6412
6413 // Undo formatting
6414 cx.update_editor(|editor, window, cx| {
6415 editor.undo(&Default::default(), window, cx);
6416 });
6417
6418 // Verify cursor moved back to position after edit
6419 cx.assert_editor_state(indoc! {"
6420 line 1
6421 line 2
6422 linXˇe 3
6423 line 4
6424 line 5
6425 "});
6426}
6427
6428#[gpui::test]
6429async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6430 init_test(cx, |_| {});
6431
6432 let mut cx = EditorTestContext::new(cx).await;
6433
6434 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6435 cx.update_editor(|editor, window, cx| {
6436 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6437 });
6438
6439 cx.set_state(indoc! {"
6440 line 1
6441 line 2
6442 linˇe 3
6443 line 4
6444 line 5
6445 line 6
6446 line 7
6447 line 8
6448 line 9
6449 line 10
6450 "});
6451
6452 let snapshot = cx.buffer_snapshot();
6453 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6454
6455 cx.update(|_, cx| {
6456 provider.update(cx, |provider, _| {
6457 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6458 id: None,
6459 edits: vec![(edit_position..edit_position, "X".into())],
6460 edit_preview: None,
6461 }))
6462 })
6463 });
6464
6465 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6466 cx.update_editor(|editor, window, cx| {
6467 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6468 });
6469
6470 cx.assert_editor_state(indoc! {"
6471 line 1
6472 line 2
6473 lineXˇ 3
6474 line 4
6475 line 5
6476 line 6
6477 line 7
6478 line 8
6479 line 9
6480 line 10
6481 "});
6482
6483 cx.update_editor(|editor, window, cx| {
6484 editor.change_selections(None, window, cx, |s| {
6485 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6486 });
6487 });
6488
6489 cx.assert_editor_state(indoc! {"
6490 line 1
6491 line 2
6492 lineX 3
6493 line 4
6494 line 5
6495 line 6
6496 line 7
6497 line 8
6498 line 9
6499 liˇne 10
6500 "});
6501
6502 cx.update_editor(|editor, window, cx| {
6503 editor.undo(&Default::default(), window, cx);
6504 });
6505
6506 cx.assert_editor_state(indoc! {"
6507 line 1
6508 line 2
6509 lineˇ 3
6510 line 4
6511 line 5
6512 line 6
6513 line 7
6514 line 8
6515 line 9
6516 line 10
6517 "});
6518}
6519
6520#[gpui::test]
6521async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6522 init_test(cx, |_| {});
6523
6524 let mut cx = EditorTestContext::new(cx).await;
6525 cx.set_state(
6526 r#"let foo = 2;
6527lˇet foo = 2;
6528let fooˇ = 2;
6529let foo = 2;
6530let foo = ˇ2;"#,
6531 );
6532
6533 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6534 .unwrap();
6535 cx.assert_editor_state(
6536 r#"let foo = 2;
6537«letˇ» foo = 2;
6538let «fooˇ» = 2;
6539let foo = 2;
6540let foo = «2ˇ»;"#,
6541 );
6542
6543 // noop for multiple selections with different contents
6544 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6545 .unwrap();
6546 cx.assert_editor_state(
6547 r#"let foo = 2;
6548«letˇ» foo = 2;
6549let «fooˇ» = 2;
6550let foo = 2;
6551let foo = «2ˇ»;"#,
6552 );
6553
6554 // Test last selection direction should be preserved
6555 cx.set_state(
6556 r#"let foo = 2;
6557let foo = 2;
6558let «fooˇ» = 2;
6559let «ˇfoo» = 2;
6560let foo = 2;"#,
6561 );
6562
6563 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6564 .unwrap();
6565 cx.assert_editor_state(
6566 r#"let foo = 2;
6567let foo = 2;
6568let «fooˇ» = 2;
6569let «ˇfoo» = 2;
6570let «ˇfoo» = 2;"#,
6571 );
6572}
6573
6574#[gpui::test]
6575async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6576 init_test(cx, |_| {});
6577
6578 let mut cx =
6579 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6580
6581 cx.assert_editor_state(indoc! {"
6582 ˇbbb
6583 ccc
6584
6585 bbb
6586 ccc
6587 "});
6588 cx.dispatch_action(SelectPrevious::default());
6589 cx.assert_editor_state(indoc! {"
6590 «bbbˇ»
6591 ccc
6592
6593 bbb
6594 ccc
6595 "});
6596 cx.dispatch_action(SelectPrevious::default());
6597 cx.assert_editor_state(indoc! {"
6598 «bbbˇ»
6599 ccc
6600
6601 «bbbˇ»
6602 ccc
6603 "});
6604}
6605
6606#[gpui::test]
6607async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6608 init_test(cx, |_| {});
6609
6610 let mut cx = EditorTestContext::new(cx).await;
6611 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6612
6613 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6614 .unwrap();
6615 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6616
6617 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6618 .unwrap();
6619 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6620
6621 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6622 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6623
6624 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6625 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6626
6627 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6628 .unwrap();
6629 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6630
6631 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6632 .unwrap();
6633 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6634}
6635
6636#[gpui::test]
6637async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6638 init_test(cx, |_| {});
6639
6640 let mut cx = EditorTestContext::new(cx).await;
6641 cx.set_state("aˇ");
6642
6643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6644 .unwrap();
6645 cx.assert_editor_state("«aˇ»");
6646 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6647 .unwrap();
6648 cx.assert_editor_state("«aˇ»");
6649}
6650
6651#[gpui::test]
6652async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6653 init_test(cx, |_| {});
6654
6655 let mut cx = EditorTestContext::new(cx).await;
6656 cx.set_state(
6657 r#"let foo = 2;
6658lˇet foo = 2;
6659let fooˇ = 2;
6660let foo = 2;
6661let foo = ˇ2;"#,
6662 );
6663
6664 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6665 .unwrap();
6666 cx.assert_editor_state(
6667 r#"let foo = 2;
6668«letˇ» foo = 2;
6669let «fooˇ» = 2;
6670let foo = 2;
6671let foo = «2ˇ»;"#,
6672 );
6673
6674 // noop for multiple selections with different contents
6675 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6676 .unwrap();
6677 cx.assert_editor_state(
6678 r#"let foo = 2;
6679«letˇ» foo = 2;
6680let «fooˇ» = 2;
6681let foo = 2;
6682let foo = «2ˇ»;"#,
6683 );
6684}
6685
6686#[gpui::test]
6687async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6688 init_test(cx, |_| {});
6689
6690 let mut cx = EditorTestContext::new(cx).await;
6691 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6692
6693 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6694 .unwrap();
6695 // selection direction is preserved
6696 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6697
6698 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6699 .unwrap();
6700 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6701
6702 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6703 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6704
6705 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6706 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6707
6708 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6709 .unwrap();
6710 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6711
6712 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6713 .unwrap();
6714 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6715}
6716
6717#[gpui::test]
6718async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6719 init_test(cx, |_| {});
6720
6721 let language = Arc::new(Language::new(
6722 LanguageConfig::default(),
6723 Some(tree_sitter_rust::LANGUAGE.into()),
6724 ));
6725
6726 let text = r#"
6727 use mod1::mod2::{mod3, mod4};
6728
6729 fn fn_1(param1: bool, param2: &str) {
6730 let var1 = "text";
6731 }
6732 "#
6733 .unindent();
6734
6735 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6736 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6737 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6738
6739 editor
6740 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6741 .await;
6742
6743 editor.update_in(cx, |editor, window, cx| {
6744 editor.change_selections(None, window, cx, |s| {
6745 s.select_display_ranges([
6746 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6747 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6748 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6749 ]);
6750 });
6751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6752 });
6753 editor.update(cx, |editor, cx| {
6754 assert_text_with_selections(
6755 editor,
6756 indoc! {r#"
6757 use mod1::mod2::{mod3, «mod4ˇ»};
6758
6759 fn fn_1«ˇ(param1: bool, param2: &str)» {
6760 let var1 = "«ˇtext»";
6761 }
6762 "#},
6763 cx,
6764 );
6765 });
6766
6767 editor.update_in(cx, |editor, window, cx| {
6768 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6769 });
6770 editor.update(cx, |editor, cx| {
6771 assert_text_with_selections(
6772 editor,
6773 indoc! {r#"
6774 use mod1::mod2::«{mod3, mod4}ˇ»;
6775
6776 «ˇfn fn_1(param1: bool, param2: &str) {
6777 let var1 = "text";
6778 }»
6779 "#},
6780 cx,
6781 );
6782 });
6783
6784 editor.update_in(cx, |editor, window, cx| {
6785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6786 });
6787 assert_eq!(
6788 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6789 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6790 );
6791
6792 // Trying to expand the selected syntax node one more time has no effect.
6793 editor.update_in(cx, |editor, window, cx| {
6794 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6795 });
6796 assert_eq!(
6797 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6798 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6799 );
6800
6801 editor.update_in(cx, |editor, window, cx| {
6802 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6803 });
6804 editor.update(cx, |editor, cx| {
6805 assert_text_with_selections(
6806 editor,
6807 indoc! {r#"
6808 use mod1::mod2::«{mod3, mod4}ˇ»;
6809
6810 «ˇfn fn_1(param1: bool, param2: &str) {
6811 let var1 = "text";
6812 }»
6813 "#},
6814 cx,
6815 );
6816 });
6817
6818 editor.update_in(cx, |editor, window, cx| {
6819 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6820 });
6821 editor.update(cx, |editor, cx| {
6822 assert_text_with_selections(
6823 editor,
6824 indoc! {r#"
6825 use mod1::mod2::{mod3, «mod4ˇ»};
6826
6827 fn fn_1«ˇ(param1: bool, param2: &str)» {
6828 let var1 = "«ˇtext»";
6829 }
6830 "#},
6831 cx,
6832 );
6833 });
6834
6835 editor.update_in(cx, |editor, window, cx| {
6836 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6837 });
6838 editor.update(cx, |editor, cx| {
6839 assert_text_with_selections(
6840 editor,
6841 indoc! {r#"
6842 use mod1::mod2::{mod3, mo«ˇ»d4};
6843
6844 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6845 let var1 = "te«ˇ»xt";
6846 }
6847 "#},
6848 cx,
6849 );
6850 });
6851
6852 // Trying to shrink the selected syntax node one more time has no effect.
6853 editor.update_in(cx, |editor, window, cx| {
6854 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6855 });
6856 editor.update_in(cx, |editor, _, cx| {
6857 assert_text_with_selections(
6858 editor,
6859 indoc! {r#"
6860 use mod1::mod2::{mod3, mo«ˇ»d4};
6861
6862 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6863 let var1 = "te«ˇ»xt";
6864 }
6865 "#},
6866 cx,
6867 );
6868 });
6869
6870 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6871 // a fold.
6872 editor.update_in(cx, |editor, window, cx| {
6873 editor.fold_creases(
6874 vec![
6875 Crease::simple(
6876 Point::new(0, 21)..Point::new(0, 24),
6877 FoldPlaceholder::test(),
6878 ),
6879 Crease::simple(
6880 Point::new(3, 20)..Point::new(3, 22),
6881 FoldPlaceholder::test(),
6882 ),
6883 ],
6884 true,
6885 window,
6886 cx,
6887 );
6888 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6889 });
6890 editor.update(cx, |editor, cx| {
6891 assert_text_with_selections(
6892 editor,
6893 indoc! {r#"
6894 use mod1::mod2::«{mod3, mod4}ˇ»;
6895
6896 fn fn_1«ˇ(param1: bool, param2: &str)» {
6897 let var1 = "«ˇtext»";
6898 }
6899 "#},
6900 cx,
6901 );
6902 });
6903}
6904
6905#[gpui::test]
6906async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6907 init_test(cx, |_| {});
6908
6909 let language = Arc::new(Language::new(
6910 LanguageConfig::default(),
6911 Some(tree_sitter_rust::LANGUAGE.into()),
6912 ));
6913
6914 let text = "let a = 2;";
6915
6916 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6917 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6918 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6919
6920 editor
6921 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6922 .await;
6923
6924 // Test case 1: Cursor at end of word
6925 editor.update_in(cx, |editor, window, cx| {
6926 editor.change_selections(None, window, cx, |s| {
6927 s.select_display_ranges([
6928 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6929 ]);
6930 });
6931 });
6932 editor.update(cx, |editor, cx| {
6933 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6934 });
6935 editor.update_in(cx, |editor, window, cx| {
6936 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6937 });
6938 editor.update(cx, |editor, cx| {
6939 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6940 });
6941 editor.update_in(cx, |editor, window, cx| {
6942 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6943 });
6944 editor.update(cx, |editor, cx| {
6945 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6946 });
6947
6948 // Test case 2: Cursor at end of statement
6949 editor.update_in(cx, |editor, window, cx| {
6950 editor.change_selections(None, window, cx, |s| {
6951 s.select_display_ranges([
6952 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6953 ]);
6954 });
6955 });
6956 editor.update(cx, |editor, cx| {
6957 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6958 });
6959 editor.update_in(cx, |editor, window, cx| {
6960 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6961 });
6962 editor.update(cx, |editor, cx| {
6963 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6964 });
6965}
6966
6967#[gpui::test]
6968async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6969 init_test(cx, |_| {});
6970
6971 let language = Arc::new(Language::new(
6972 LanguageConfig::default(),
6973 Some(tree_sitter_rust::LANGUAGE.into()),
6974 ));
6975
6976 let text = r#"
6977 use mod1::mod2::{mod3, mod4};
6978
6979 fn fn_1(param1: bool, param2: &str) {
6980 let var1 = "hello world";
6981 }
6982 "#
6983 .unindent();
6984
6985 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6986 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6987 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6988
6989 editor
6990 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6991 .await;
6992
6993 // Test 1: Cursor on a letter of a string word
6994 editor.update_in(cx, |editor, window, cx| {
6995 editor.change_selections(None, window, cx, |s| {
6996 s.select_display_ranges([
6997 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6998 ]);
6999 });
7000 });
7001 editor.update_in(cx, |editor, window, cx| {
7002 assert_text_with_selections(
7003 editor,
7004 indoc! {r#"
7005 use mod1::mod2::{mod3, mod4};
7006
7007 fn fn_1(param1: bool, param2: &str) {
7008 let var1 = "hˇello world";
7009 }
7010 "#},
7011 cx,
7012 );
7013 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7014 assert_text_with_selections(
7015 editor,
7016 indoc! {r#"
7017 use mod1::mod2::{mod3, mod4};
7018
7019 fn fn_1(param1: bool, param2: &str) {
7020 let var1 = "«ˇhello» world";
7021 }
7022 "#},
7023 cx,
7024 );
7025 });
7026
7027 // Test 2: Partial selection within a word
7028 editor.update_in(cx, |editor, window, cx| {
7029 editor.change_selections(None, window, cx, |s| {
7030 s.select_display_ranges([
7031 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7032 ]);
7033 });
7034 });
7035 editor.update_in(cx, |editor, window, cx| {
7036 assert_text_with_selections(
7037 editor,
7038 indoc! {r#"
7039 use mod1::mod2::{mod3, mod4};
7040
7041 fn fn_1(param1: bool, param2: &str) {
7042 let var1 = "h«elˇ»lo world";
7043 }
7044 "#},
7045 cx,
7046 );
7047 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7048 assert_text_with_selections(
7049 editor,
7050 indoc! {r#"
7051 use mod1::mod2::{mod3, mod4};
7052
7053 fn fn_1(param1: bool, param2: &str) {
7054 let var1 = "«ˇhello» world";
7055 }
7056 "#},
7057 cx,
7058 );
7059 });
7060
7061 // Test 3: Complete word already selected
7062 editor.update_in(cx, |editor, window, cx| {
7063 editor.change_selections(None, window, cx, |s| {
7064 s.select_display_ranges([
7065 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7066 ]);
7067 });
7068 });
7069 editor.update_in(cx, |editor, window, cx| {
7070 assert_text_with_selections(
7071 editor,
7072 indoc! {r#"
7073 use mod1::mod2::{mod3, mod4};
7074
7075 fn fn_1(param1: bool, param2: &str) {
7076 let var1 = "«helloˇ» world";
7077 }
7078 "#},
7079 cx,
7080 );
7081 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7082 assert_text_with_selections(
7083 editor,
7084 indoc! {r#"
7085 use mod1::mod2::{mod3, mod4};
7086
7087 fn fn_1(param1: bool, param2: &str) {
7088 let var1 = "«hello worldˇ»";
7089 }
7090 "#},
7091 cx,
7092 );
7093 });
7094
7095 // Test 4: Selection spanning across words
7096 editor.update_in(cx, |editor, window, cx| {
7097 editor.change_selections(None, window, cx, |s| {
7098 s.select_display_ranges([
7099 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7100 ]);
7101 });
7102 });
7103 editor.update_in(cx, |editor, window, cx| {
7104 assert_text_with_selections(
7105 editor,
7106 indoc! {r#"
7107 use mod1::mod2::{mod3, mod4};
7108
7109 fn fn_1(param1: bool, param2: &str) {
7110 let var1 = "hel«lo woˇ»rld";
7111 }
7112 "#},
7113 cx,
7114 );
7115 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7116 assert_text_with_selections(
7117 editor,
7118 indoc! {r#"
7119 use mod1::mod2::{mod3, mod4};
7120
7121 fn fn_1(param1: bool, param2: &str) {
7122 let var1 = "«ˇhello world»";
7123 }
7124 "#},
7125 cx,
7126 );
7127 });
7128
7129 // Test 5: Expansion beyond string
7130 editor.update_in(cx, |editor, window, cx| {
7131 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7132 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7133 assert_text_with_selections(
7134 editor,
7135 indoc! {r#"
7136 use mod1::mod2::{mod3, mod4};
7137
7138 fn fn_1(param1: bool, param2: &str) {
7139 «ˇlet var1 = "hello world";»
7140 }
7141 "#},
7142 cx,
7143 );
7144 });
7145}
7146
7147#[gpui::test]
7148async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7149 init_test(cx, |_| {});
7150
7151 let base_text = r#"
7152 impl A {
7153 // this is an uncommitted comment
7154
7155 fn b() {
7156 c();
7157 }
7158
7159 // this is another uncommitted comment
7160
7161 fn d() {
7162 // e
7163 // f
7164 }
7165 }
7166
7167 fn g() {
7168 // h
7169 }
7170 "#
7171 .unindent();
7172
7173 let text = r#"
7174 ˇimpl A {
7175
7176 fn b() {
7177 c();
7178 }
7179
7180 fn d() {
7181 // e
7182 // f
7183 }
7184 }
7185
7186 fn g() {
7187 // h
7188 }
7189 "#
7190 .unindent();
7191
7192 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7193 cx.set_state(&text);
7194 cx.set_head_text(&base_text);
7195 cx.update_editor(|editor, window, cx| {
7196 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7197 });
7198
7199 cx.assert_state_with_diff(
7200 "
7201 ˇimpl A {
7202 - // this is an uncommitted comment
7203
7204 fn b() {
7205 c();
7206 }
7207
7208 - // this is another uncommitted comment
7209 -
7210 fn d() {
7211 // e
7212 // f
7213 }
7214 }
7215
7216 fn g() {
7217 // h
7218 }
7219 "
7220 .unindent(),
7221 );
7222
7223 let expected_display_text = "
7224 impl A {
7225 // this is an uncommitted comment
7226
7227 fn b() {
7228 ⋯
7229 }
7230
7231 // this is another uncommitted comment
7232
7233 fn d() {
7234 ⋯
7235 }
7236 }
7237
7238 fn g() {
7239 ⋯
7240 }
7241 "
7242 .unindent();
7243
7244 cx.update_editor(|editor, window, cx| {
7245 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7246 assert_eq!(editor.display_text(cx), expected_display_text);
7247 });
7248}
7249
7250#[gpui::test]
7251async fn test_autoindent(cx: &mut TestAppContext) {
7252 init_test(cx, |_| {});
7253
7254 let language = Arc::new(
7255 Language::new(
7256 LanguageConfig {
7257 brackets: BracketPairConfig {
7258 pairs: vec![
7259 BracketPair {
7260 start: "{".to_string(),
7261 end: "}".to_string(),
7262 close: false,
7263 surround: false,
7264 newline: true,
7265 },
7266 BracketPair {
7267 start: "(".to_string(),
7268 end: ")".to_string(),
7269 close: false,
7270 surround: false,
7271 newline: true,
7272 },
7273 ],
7274 ..Default::default()
7275 },
7276 ..Default::default()
7277 },
7278 Some(tree_sitter_rust::LANGUAGE.into()),
7279 )
7280 .with_indents_query(
7281 r#"
7282 (_ "(" ")" @end) @indent
7283 (_ "{" "}" @end) @indent
7284 "#,
7285 )
7286 .unwrap(),
7287 );
7288
7289 let text = "fn a() {}";
7290
7291 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7293 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7294 editor
7295 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7296 .await;
7297
7298 editor.update_in(cx, |editor, window, cx| {
7299 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7300 editor.newline(&Newline, window, cx);
7301 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7302 assert_eq!(
7303 editor.selections.ranges(cx),
7304 &[
7305 Point::new(1, 4)..Point::new(1, 4),
7306 Point::new(3, 4)..Point::new(3, 4),
7307 Point::new(5, 0)..Point::new(5, 0)
7308 ]
7309 );
7310 });
7311}
7312
7313#[gpui::test]
7314async fn test_autoindent_selections(cx: &mut TestAppContext) {
7315 init_test(cx, |_| {});
7316
7317 {
7318 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7319 cx.set_state(indoc! {"
7320 impl A {
7321
7322 fn b() {}
7323
7324 «fn c() {
7325
7326 }ˇ»
7327 }
7328 "});
7329
7330 cx.update_editor(|editor, window, cx| {
7331 editor.autoindent(&Default::default(), window, cx);
7332 });
7333
7334 cx.assert_editor_state(indoc! {"
7335 impl A {
7336
7337 fn b() {}
7338
7339 «fn c() {
7340
7341 }ˇ»
7342 }
7343 "});
7344 }
7345
7346 {
7347 let mut cx = EditorTestContext::new_multibuffer(
7348 cx,
7349 [indoc! { "
7350 impl A {
7351 «
7352 // a
7353 fn b(){}
7354 »
7355 «
7356 }
7357 fn c(){}
7358 »
7359 "}],
7360 );
7361
7362 let buffer = cx.update_editor(|editor, _, cx| {
7363 let buffer = editor.buffer().update(cx, |buffer, _| {
7364 buffer.all_buffers().iter().next().unwrap().clone()
7365 });
7366 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7367 buffer
7368 });
7369
7370 cx.run_until_parked();
7371 cx.update_editor(|editor, window, cx| {
7372 editor.select_all(&Default::default(), window, cx);
7373 editor.autoindent(&Default::default(), window, cx)
7374 });
7375 cx.run_until_parked();
7376
7377 cx.update(|_, cx| {
7378 assert_eq!(
7379 buffer.read(cx).text(),
7380 indoc! { "
7381 impl A {
7382
7383 // a
7384 fn b(){}
7385
7386
7387 }
7388 fn c(){}
7389
7390 " }
7391 )
7392 });
7393 }
7394}
7395
7396#[gpui::test]
7397async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7398 init_test(cx, |_| {});
7399
7400 let mut cx = EditorTestContext::new(cx).await;
7401
7402 let language = Arc::new(Language::new(
7403 LanguageConfig {
7404 brackets: BracketPairConfig {
7405 pairs: vec![
7406 BracketPair {
7407 start: "{".to_string(),
7408 end: "}".to_string(),
7409 close: true,
7410 surround: true,
7411 newline: true,
7412 },
7413 BracketPair {
7414 start: "(".to_string(),
7415 end: ")".to_string(),
7416 close: true,
7417 surround: true,
7418 newline: true,
7419 },
7420 BracketPair {
7421 start: "/*".to_string(),
7422 end: " */".to_string(),
7423 close: true,
7424 surround: true,
7425 newline: true,
7426 },
7427 BracketPair {
7428 start: "[".to_string(),
7429 end: "]".to_string(),
7430 close: false,
7431 surround: false,
7432 newline: true,
7433 },
7434 BracketPair {
7435 start: "\"".to_string(),
7436 end: "\"".to_string(),
7437 close: true,
7438 surround: true,
7439 newline: false,
7440 },
7441 BracketPair {
7442 start: "<".to_string(),
7443 end: ">".to_string(),
7444 close: false,
7445 surround: true,
7446 newline: true,
7447 },
7448 ],
7449 ..Default::default()
7450 },
7451 autoclose_before: "})]".to_string(),
7452 ..Default::default()
7453 },
7454 Some(tree_sitter_rust::LANGUAGE.into()),
7455 ));
7456
7457 cx.language_registry().add(language.clone());
7458 cx.update_buffer(|buffer, cx| {
7459 buffer.set_language(Some(language), cx);
7460 });
7461
7462 cx.set_state(
7463 &r#"
7464 🏀ˇ
7465 εˇ
7466 ❤️ˇ
7467 "#
7468 .unindent(),
7469 );
7470
7471 // autoclose multiple nested brackets at multiple cursors
7472 cx.update_editor(|editor, window, cx| {
7473 editor.handle_input("{", window, cx);
7474 editor.handle_input("{", window, cx);
7475 editor.handle_input("{", window, cx);
7476 });
7477 cx.assert_editor_state(
7478 &"
7479 🏀{{{ˇ}}}
7480 ε{{{ˇ}}}
7481 ❤️{{{ˇ}}}
7482 "
7483 .unindent(),
7484 );
7485
7486 // insert a different closing bracket
7487 cx.update_editor(|editor, window, cx| {
7488 editor.handle_input(")", window, cx);
7489 });
7490 cx.assert_editor_state(
7491 &"
7492 🏀{{{)ˇ}}}
7493 ε{{{)ˇ}}}
7494 ❤️{{{)ˇ}}}
7495 "
7496 .unindent(),
7497 );
7498
7499 // skip over the auto-closed brackets when typing a closing bracket
7500 cx.update_editor(|editor, window, cx| {
7501 editor.move_right(&MoveRight, window, cx);
7502 editor.handle_input("}", window, cx);
7503 editor.handle_input("}", window, cx);
7504 editor.handle_input("}", window, cx);
7505 });
7506 cx.assert_editor_state(
7507 &"
7508 🏀{{{)}}}}ˇ
7509 ε{{{)}}}}ˇ
7510 ❤️{{{)}}}}ˇ
7511 "
7512 .unindent(),
7513 );
7514
7515 // autoclose multi-character pairs
7516 cx.set_state(
7517 &"
7518 ˇ
7519 ˇ
7520 "
7521 .unindent(),
7522 );
7523 cx.update_editor(|editor, window, cx| {
7524 editor.handle_input("/", window, cx);
7525 editor.handle_input("*", window, cx);
7526 });
7527 cx.assert_editor_state(
7528 &"
7529 /*ˇ */
7530 /*ˇ */
7531 "
7532 .unindent(),
7533 );
7534
7535 // one cursor autocloses a multi-character pair, one cursor
7536 // does not autoclose.
7537 cx.set_state(
7538 &"
7539 /ˇ
7540 ˇ
7541 "
7542 .unindent(),
7543 );
7544 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7545 cx.assert_editor_state(
7546 &"
7547 /*ˇ */
7548 *ˇ
7549 "
7550 .unindent(),
7551 );
7552
7553 // Don't autoclose if the next character isn't whitespace and isn't
7554 // listed in the language's "autoclose_before" section.
7555 cx.set_state("ˇa b");
7556 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7557 cx.assert_editor_state("{ˇa b");
7558
7559 // Don't autoclose if `close` is false for the bracket pair
7560 cx.set_state("ˇ");
7561 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7562 cx.assert_editor_state("[ˇ");
7563
7564 // Surround with brackets if text is selected
7565 cx.set_state("«aˇ» b");
7566 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7567 cx.assert_editor_state("{«aˇ»} b");
7568
7569 // Autoclose when not immediately after a word character
7570 cx.set_state("a ˇ");
7571 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7572 cx.assert_editor_state("a \"ˇ\"");
7573
7574 // Autoclose pair where the start and end characters are the same
7575 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7576 cx.assert_editor_state("a \"\"ˇ");
7577
7578 // Don't autoclose when immediately after a word character
7579 cx.set_state("aˇ");
7580 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7581 cx.assert_editor_state("a\"ˇ");
7582
7583 // Do autoclose when after a non-word character
7584 cx.set_state("{ˇ");
7585 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7586 cx.assert_editor_state("{\"ˇ\"");
7587
7588 // Non identical pairs autoclose regardless of preceding character
7589 cx.set_state("aˇ");
7590 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7591 cx.assert_editor_state("a{ˇ}");
7592
7593 // Don't autoclose pair if autoclose is disabled
7594 cx.set_state("ˇ");
7595 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7596 cx.assert_editor_state("<ˇ");
7597
7598 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7599 cx.set_state("«aˇ» b");
7600 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7601 cx.assert_editor_state("<«aˇ»> b");
7602}
7603
7604#[gpui::test]
7605async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7606 init_test(cx, |settings| {
7607 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7608 });
7609
7610 let mut cx = EditorTestContext::new(cx).await;
7611
7612 let language = Arc::new(Language::new(
7613 LanguageConfig {
7614 brackets: BracketPairConfig {
7615 pairs: vec![
7616 BracketPair {
7617 start: "{".to_string(),
7618 end: "}".to_string(),
7619 close: true,
7620 surround: true,
7621 newline: true,
7622 },
7623 BracketPair {
7624 start: "(".to_string(),
7625 end: ")".to_string(),
7626 close: true,
7627 surround: true,
7628 newline: true,
7629 },
7630 BracketPair {
7631 start: "[".to_string(),
7632 end: "]".to_string(),
7633 close: false,
7634 surround: false,
7635 newline: true,
7636 },
7637 ],
7638 ..Default::default()
7639 },
7640 autoclose_before: "})]".to_string(),
7641 ..Default::default()
7642 },
7643 Some(tree_sitter_rust::LANGUAGE.into()),
7644 ));
7645
7646 cx.language_registry().add(language.clone());
7647 cx.update_buffer(|buffer, cx| {
7648 buffer.set_language(Some(language), cx);
7649 });
7650
7651 cx.set_state(
7652 &"
7653 ˇ
7654 ˇ
7655 ˇ
7656 "
7657 .unindent(),
7658 );
7659
7660 // ensure only matching closing brackets are skipped over
7661 cx.update_editor(|editor, window, cx| {
7662 editor.handle_input("}", window, cx);
7663 editor.move_left(&MoveLeft, window, cx);
7664 editor.handle_input(")", window, cx);
7665 editor.move_left(&MoveLeft, window, cx);
7666 });
7667 cx.assert_editor_state(
7668 &"
7669 ˇ)}
7670 ˇ)}
7671 ˇ)}
7672 "
7673 .unindent(),
7674 );
7675
7676 // skip-over closing brackets at multiple cursors
7677 cx.update_editor(|editor, window, cx| {
7678 editor.handle_input(")", window, cx);
7679 editor.handle_input("}", window, cx);
7680 });
7681 cx.assert_editor_state(
7682 &"
7683 )}ˇ
7684 )}ˇ
7685 )}ˇ
7686 "
7687 .unindent(),
7688 );
7689
7690 // ignore non-close brackets
7691 cx.update_editor(|editor, window, cx| {
7692 editor.handle_input("]", window, cx);
7693 editor.move_left(&MoveLeft, window, cx);
7694 editor.handle_input("]", window, cx);
7695 });
7696 cx.assert_editor_state(
7697 &"
7698 )}]ˇ]
7699 )}]ˇ]
7700 )}]ˇ]
7701 "
7702 .unindent(),
7703 );
7704}
7705
7706#[gpui::test]
7707async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7708 init_test(cx, |_| {});
7709
7710 let mut cx = EditorTestContext::new(cx).await;
7711
7712 let html_language = Arc::new(
7713 Language::new(
7714 LanguageConfig {
7715 name: "HTML".into(),
7716 brackets: BracketPairConfig {
7717 pairs: vec![
7718 BracketPair {
7719 start: "<".into(),
7720 end: ">".into(),
7721 close: true,
7722 ..Default::default()
7723 },
7724 BracketPair {
7725 start: "{".into(),
7726 end: "}".into(),
7727 close: true,
7728 ..Default::default()
7729 },
7730 BracketPair {
7731 start: "(".into(),
7732 end: ")".into(),
7733 close: true,
7734 ..Default::default()
7735 },
7736 ],
7737 ..Default::default()
7738 },
7739 autoclose_before: "})]>".into(),
7740 ..Default::default()
7741 },
7742 Some(tree_sitter_html::LANGUAGE.into()),
7743 )
7744 .with_injection_query(
7745 r#"
7746 (script_element
7747 (raw_text) @injection.content
7748 (#set! injection.language "javascript"))
7749 "#,
7750 )
7751 .unwrap(),
7752 );
7753
7754 let javascript_language = Arc::new(Language::new(
7755 LanguageConfig {
7756 name: "JavaScript".into(),
7757 brackets: BracketPairConfig {
7758 pairs: vec![
7759 BracketPair {
7760 start: "/*".into(),
7761 end: " */".into(),
7762 close: true,
7763 ..Default::default()
7764 },
7765 BracketPair {
7766 start: "{".into(),
7767 end: "}".into(),
7768 close: true,
7769 ..Default::default()
7770 },
7771 BracketPair {
7772 start: "(".into(),
7773 end: ")".into(),
7774 close: true,
7775 ..Default::default()
7776 },
7777 ],
7778 ..Default::default()
7779 },
7780 autoclose_before: "})]>".into(),
7781 ..Default::default()
7782 },
7783 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7784 ));
7785
7786 cx.language_registry().add(html_language.clone());
7787 cx.language_registry().add(javascript_language.clone());
7788
7789 cx.update_buffer(|buffer, cx| {
7790 buffer.set_language(Some(html_language), cx);
7791 });
7792
7793 cx.set_state(
7794 &r#"
7795 <body>ˇ
7796 <script>
7797 var x = 1;ˇ
7798 </script>
7799 </body>ˇ
7800 "#
7801 .unindent(),
7802 );
7803
7804 // Precondition: different languages are active at different locations.
7805 cx.update_editor(|editor, window, cx| {
7806 let snapshot = editor.snapshot(window, cx);
7807 let cursors = editor.selections.ranges::<usize>(cx);
7808 let languages = cursors
7809 .iter()
7810 .map(|c| snapshot.language_at(c.start).unwrap().name())
7811 .collect::<Vec<_>>();
7812 assert_eq!(
7813 languages,
7814 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7815 );
7816 });
7817
7818 // Angle brackets autoclose in HTML, but not JavaScript.
7819 cx.update_editor(|editor, window, cx| {
7820 editor.handle_input("<", window, cx);
7821 editor.handle_input("a", window, cx);
7822 });
7823 cx.assert_editor_state(
7824 &r#"
7825 <body><aˇ>
7826 <script>
7827 var x = 1;<aˇ
7828 </script>
7829 </body><aˇ>
7830 "#
7831 .unindent(),
7832 );
7833
7834 // Curly braces and parens autoclose in both HTML and JavaScript.
7835 cx.update_editor(|editor, window, cx| {
7836 editor.handle_input(" b=", window, cx);
7837 editor.handle_input("{", window, cx);
7838 editor.handle_input("c", window, cx);
7839 editor.handle_input("(", window, cx);
7840 });
7841 cx.assert_editor_state(
7842 &r#"
7843 <body><a b={c(ˇ)}>
7844 <script>
7845 var x = 1;<a b={c(ˇ)}
7846 </script>
7847 </body><a b={c(ˇ)}>
7848 "#
7849 .unindent(),
7850 );
7851
7852 // Brackets that were already autoclosed are skipped.
7853 cx.update_editor(|editor, window, cx| {
7854 editor.handle_input(")", window, cx);
7855 editor.handle_input("d", window, cx);
7856 editor.handle_input("}", window, cx);
7857 });
7858 cx.assert_editor_state(
7859 &r#"
7860 <body><a b={c()d}ˇ>
7861 <script>
7862 var x = 1;<a b={c()d}ˇ
7863 </script>
7864 </body><a b={c()d}ˇ>
7865 "#
7866 .unindent(),
7867 );
7868 cx.update_editor(|editor, window, cx| {
7869 editor.handle_input(">", window, cx);
7870 });
7871 cx.assert_editor_state(
7872 &r#"
7873 <body><a b={c()d}>ˇ
7874 <script>
7875 var x = 1;<a b={c()d}>ˇ
7876 </script>
7877 </body><a b={c()d}>ˇ
7878 "#
7879 .unindent(),
7880 );
7881
7882 // Reset
7883 cx.set_state(
7884 &r#"
7885 <body>ˇ
7886 <script>
7887 var x = 1;ˇ
7888 </script>
7889 </body>ˇ
7890 "#
7891 .unindent(),
7892 );
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.handle_input("<", window, cx);
7896 });
7897 cx.assert_editor_state(
7898 &r#"
7899 <body><ˇ>
7900 <script>
7901 var x = 1;<ˇ
7902 </script>
7903 </body><ˇ>
7904 "#
7905 .unindent(),
7906 );
7907
7908 // When backspacing, the closing angle brackets are removed.
7909 cx.update_editor(|editor, window, cx| {
7910 editor.backspace(&Backspace, window, cx);
7911 });
7912 cx.assert_editor_state(
7913 &r#"
7914 <body>ˇ
7915 <script>
7916 var x = 1;ˇ
7917 </script>
7918 </body>ˇ
7919 "#
7920 .unindent(),
7921 );
7922
7923 // Block comments autoclose in JavaScript, but not HTML.
7924 cx.update_editor(|editor, window, cx| {
7925 editor.handle_input("/", window, cx);
7926 editor.handle_input("*", window, cx);
7927 });
7928 cx.assert_editor_state(
7929 &r#"
7930 <body>/*ˇ
7931 <script>
7932 var x = 1;/*ˇ */
7933 </script>
7934 </body>/*ˇ
7935 "#
7936 .unindent(),
7937 );
7938}
7939
7940#[gpui::test]
7941async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7942 init_test(cx, |_| {});
7943
7944 let mut cx = EditorTestContext::new(cx).await;
7945
7946 let rust_language = Arc::new(
7947 Language::new(
7948 LanguageConfig {
7949 name: "Rust".into(),
7950 brackets: serde_json::from_value(json!([
7951 { "start": "{", "end": "}", "close": true, "newline": true },
7952 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7953 ]))
7954 .unwrap(),
7955 autoclose_before: "})]>".into(),
7956 ..Default::default()
7957 },
7958 Some(tree_sitter_rust::LANGUAGE.into()),
7959 )
7960 .with_override_query("(string_literal) @string")
7961 .unwrap(),
7962 );
7963
7964 cx.language_registry().add(rust_language.clone());
7965 cx.update_buffer(|buffer, cx| {
7966 buffer.set_language(Some(rust_language), cx);
7967 });
7968
7969 cx.set_state(
7970 &r#"
7971 let x = ˇ
7972 "#
7973 .unindent(),
7974 );
7975
7976 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7977 cx.update_editor(|editor, window, cx| {
7978 editor.handle_input("\"", window, cx);
7979 });
7980 cx.assert_editor_state(
7981 &r#"
7982 let x = "ˇ"
7983 "#
7984 .unindent(),
7985 );
7986
7987 // Inserting another quotation mark. The cursor moves across the existing
7988 // automatically-inserted quotation mark.
7989 cx.update_editor(|editor, window, cx| {
7990 editor.handle_input("\"", window, cx);
7991 });
7992 cx.assert_editor_state(
7993 &r#"
7994 let x = ""ˇ
7995 "#
7996 .unindent(),
7997 );
7998
7999 // Reset
8000 cx.set_state(
8001 &r#"
8002 let x = ˇ
8003 "#
8004 .unindent(),
8005 );
8006
8007 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8008 cx.update_editor(|editor, window, cx| {
8009 editor.handle_input("\"", window, cx);
8010 editor.handle_input(" ", window, cx);
8011 editor.move_left(&Default::default(), window, cx);
8012 editor.handle_input("\\", window, cx);
8013 editor.handle_input("\"", window, cx);
8014 });
8015 cx.assert_editor_state(
8016 &r#"
8017 let x = "\"ˇ "
8018 "#
8019 .unindent(),
8020 );
8021
8022 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8023 // mark. Nothing is inserted.
8024 cx.update_editor(|editor, window, cx| {
8025 editor.move_right(&Default::default(), window, cx);
8026 editor.handle_input("\"", window, cx);
8027 });
8028 cx.assert_editor_state(
8029 &r#"
8030 let x = "\" "ˇ
8031 "#
8032 .unindent(),
8033 );
8034}
8035
8036#[gpui::test]
8037async fn test_surround_with_pair(cx: &mut TestAppContext) {
8038 init_test(cx, |_| {});
8039
8040 let language = Arc::new(Language::new(
8041 LanguageConfig {
8042 brackets: BracketPairConfig {
8043 pairs: vec![
8044 BracketPair {
8045 start: "{".to_string(),
8046 end: "}".to_string(),
8047 close: true,
8048 surround: true,
8049 newline: true,
8050 },
8051 BracketPair {
8052 start: "/* ".to_string(),
8053 end: "*/".to_string(),
8054 close: true,
8055 surround: true,
8056 ..Default::default()
8057 },
8058 ],
8059 ..Default::default()
8060 },
8061 ..Default::default()
8062 },
8063 Some(tree_sitter_rust::LANGUAGE.into()),
8064 ));
8065
8066 let text = r#"
8067 a
8068 b
8069 c
8070 "#
8071 .unindent();
8072
8073 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8075 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8076 editor
8077 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8078 .await;
8079
8080 editor.update_in(cx, |editor, window, cx| {
8081 editor.change_selections(None, window, cx, |s| {
8082 s.select_display_ranges([
8083 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8084 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8085 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8086 ])
8087 });
8088
8089 editor.handle_input("{", window, cx);
8090 editor.handle_input("{", window, cx);
8091 editor.handle_input("{", window, cx);
8092 assert_eq!(
8093 editor.text(cx),
8094 "
8095 {{{a}}}
8096 {{{b}}}
8097 {{{c}}}
8098 "
8099 .unindent()
8100 );
8101 assert_eq!(
8102 editor.selections.display_ranges(cx),
8103 [
8104 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8105 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8106 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8107 ]
8108 );
8109
8110 editor.undo(&Undo, window, cx);
8111 editor.undo(&Undo, window, cx);
8112 editor.undo(&Undo, window, cx);
8113 assert_eq!(
8114 editor.text(cx),
8115 "
8116 a
8117 b
8118 c
8119 "
8120 .unindent()
8121 );
8122 assert_eq!(
8123 editor.selections.display_ranges(cx),
8124 [
8125 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8126 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8127 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8128 ]
8129 );
8130
8131 // Ensure inserting the first character of a multi-byte bracket pair
8132 // doesn't surround the selections with the bracket.
8133 editor.handle_input("/", window, cx);
8134 assert_eq!(
8135 editor.text(cx),
8136 "
8137 /
8138 /
8139 /
8140 "
8141 .unindent()
8142 );
8143 assert_eq!(
8144 editor.selections.display_ranges(cx),
8145 [
8146 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8147 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8148 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8149 ]
8150 );
8151
8152 editor.undo(&Undo, window, cx);
8153 assert_eq!(
8154 editor.text(cx),
8155 "
8156 a
8157 b
8158 c
8159 "
8160 .unindent()
8161 );
8162 assert_eq!(
8163 editor.selections.display_ranges(cx),
8164 [
8165 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8166 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8167 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8168 ]
8169 );
8170
8171 // Ensure inserting the last character of a multi-byte bracket pair
8172 // doesn't surround the selections with the bracket.
8173 editor.handle_input("*", window, cx);
8174 assert_eq!(
8175 editor.text(cx),
8176 "
8177 *
8178 *
8179 *
8180 "
8181 .unindent()
8182 );
8183 assert_eq!(
8184 editor.selections.display_ranges(cx),
8185 [
8186 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8187 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8188 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8189 ]
8190 );
8191 });
8192}
8193
8194#[gpui::test]
8195async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8196 init_test(cx, |_| {});
8197
8198 let language = Arc::new(Language::new(
8199 LanguageConfig {
8200 brackets: BracketPairConfig {
8201 pairs: vec![BracketPair {
8202 start: "{".to_string(),
8203 end: "}".to_string(),
8204 close: true,
8205 surround: true,
8206 newline: true,
8207 }],
8208 ..Default::default()
8209 },
8210 autoclose_before: "}".to_string(),
8211 ..Default::default()
8212 },
8213 Some(tree_sitter_rust::LANGUAGE.into()),
8214 ));
8215
8216 let text = r#"
8217 a
8218 b
8219 c
8220 "#
8221 .unindent();
8222
8223 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8224 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8225 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8226 editor
8227 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8228 .await;
8229
8230 editor.update_in(cx, |editor, window, cx| {
8231 editor.change_selections(None, window, cx, |s| {
8232 s.select_ranges([
8233 Point::new(0, 1)..Point::new(0, 1),
8234 Point::new(1, 1)..Point::new(1, 1),
8235 Point::new(2, 1)..Point::new(2, 1),
8236 ])
8237 });
8238
8239 editor.handle_input("{", window, cx);
8240 editor.handle_input("{", window, cx);
8241 editor.handle_input("_", window, cx);
8242 assert_eq!(
8243 editor.text(cx),
8244 "
8245 a{{_}}
8246 b{{_}}
8247 c{{_}}
8248 "
8249 .unindent()
8250 );
8251 assert_eq!(
8252 editor.selections.ranges::<Point>(cx),
8253 [
8254 Point::new(0, 4)..Point::new(0, 4),
8255 Point::new(1, 4)..Point::new(1, 4),
8256 Point::new(2, 4)..Point::new(2, 4)
8257 ]
8258 );
8259
8260 editor.backspace(&Default::default(), window, cx);
8261 editor.backspace(&Default::default(), window, cx);
8262 assert_eq!(
8263 editor.text(cx),
8264 "
8265 a{}
8266 b{}
8267 c{}
8268 "
8269 .unindent()
8270 );
8271 assert_eq!(
8272 editor.selections.ranges::<Point>(cx),
8273 [
8274 Point::new(0, 2)..Point::new(0, 2),
8275 Point::new(1, 2)..Point::new(1, 2),
8276 Point::new(2, 2)..Point::new(2, 2)
8277 ]
8278 );
8279
8280 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8281 assert_eq!(
8282 editor.text(cx),
8283 "
8284 a
8285 b
8286 c
8287 "
8288 .unindent()
8289 );
8290 assert_eq!(
8291 editor.selections.ranges::<Point>(cx),
8292 [
8293 Point::new(0, 1)..Point::new(0, 1),
8294 Point::new(1, 1)..Point::new(1, 1),
8295 Point::new(2, 1)..Point::new(2, 1)
8296 ]
8297 );
8298 });
8299}
8300
8301#[gpui::test]
8302async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8303 init_test(cx, |settings| {
8304 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8305 });
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308
8309 let language = Arc::new(Language::new(
8310 LanguageConfig {
8311 brackets: BracketPairConfig {
8312 pairs: vec![
8313 BracketPair {
8314 start: "{".to_string(),
8315 end: "}".to_string(),
8316 close: true,
8317 surround: true,
8318 newline: true,
8319 },
8320 BracketPair {
8321 start: "(".to_string(),
8322 end: ")".to_string(),
8323 close: true,
8324 surround: true,
8325 newline: true,
8326 },
8327 BracketPair {
8328 start: "[".to_string(),
8329 end: "]".to_string(),
8330 close: false,
8331 surround: true,
8332 newline: true,
8333 },
8334 ],
8335 ..Default::default()
8336 },
8337 autoclose_before: "})]".to_string(),
8338 ..Default::default()
8339 },
8340 Some(tree_sitter_rust::LANGUAGE.into()),
8341 ));
8342
8343 cx.language_registry().add(language.clone());
8344 cx.update_buffer(|buffer, cx| {
8345 buffer.set_language(Some(language), cx);
8346 });
8347
8348 cx.set_state(
8349 &"
8350 {(ˇ)}
8351 [[ˇ]]
8352 {(ˇ)}
8353 "
8354 .unindent(),
8355 );
8356
8357 cx.update_editor(|editor, window, cx| {
8358 editor.backspace(&Default::default(), window, cx);
8359 editor.backspace(&Default::default(), window, cx);
8360 });
8361
8362 cx.assert_editor_state(
8363 &"
8364 ˇ
8365 ˇ]]
8366 ˇ
8367 "
8368 .unindent(),
8369 );
8370
8371 cx.update_editor(|editor, window, cx| {
8372 editor.handle_input("{", window, cx);
8373 editor.handle_input("{", window, cx);
8374 editor.move_right(&MoveRight, window, cx);
8375 editor.move_right(&MoveRight, window, cx);
8376 editor.move_left(&MoveLeft, window, cx);
8377 editor.move_left(&MoveLeft, window, cx);
8378 editor.backspace(&Default::default(), window, cx);
8379 });
8380
8381 cx.assert_editor_state(
8382 &"
8383 {ˇ}
8384 {ˇ}]]
8385 {ˇ}
8386 "
8387 .unindent(),
8388 );
8389
8390 cx.update_editor(|editor, window, cx| {
8391 editor.backspace(&Default::default(), window, cx);
8392 });
8393
8394 cx.assert_editor_state(
8395 &"
8396 ˇ
8397 ˇ]]
8398 ˇ
8399 "
8400 .unindent(),
8401 );
8402}
8403
8404#[gpui::test]
8405async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8406 init_test(cx, |_| {});
8407
8408 let language = Arc::new(Language::new(
8409 LanguageConfig::default(),
8410 Some(tree_sitter_rust::LANGUAGE.into()),
8411 ));
8412
8413 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8414 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8415 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8416 editor
8417 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8418 .await;
8419
8420 editor.update_in(cx, |editor, window, cx| {
8421 editor.set_auto_replace_emoji_shortcode(true);
8422
8423 editor.handle_input("Hello ", window, cx);
8424 editor.handle_input(":wave", window, cx);
8425 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8426
8427 editor.handle_input(":", window, cx);
8428 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8429
8430 editor.handle_input(" :smile", window, cx);
8431 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8432
8433 editor.handle_input(":", window, cx);
8434 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8435
8436 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8437 editor.handle_input(":wave", window, cx);
8438 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8439
8440 editor.handle_input(":", window, cx);
8441 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8442
8443 editor.handle_input(":1", window, cx);
8444 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8445
8446 editor.handle_input(":", window, cx);
8447 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8448
8449 // Ensure shortcode does not get replaced when it is part of a word
8450 editor.handle_input(" Test:wave", window, cx);
8451 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8452
8453 editor.handle_input(":", window, cx);
8454 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8455
8456 editor.set_auto_replace_emoji_shortcode(false);
8457
8458 // Ensure shortcode does not get replaced when auto replace is off
8459 editor.handle_input(" :wave", window, cx);
8460 assert_eq!(
8461 editor.text(cx),
8462 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8463 );
8464
8465 editor.handle_input(":", window, cx);
8466 assert_eq!(
8467 editor.text(cx),
8468 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8469 );
8470 });
8471}
8472
8473#[gpui::test]
8474async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8475 init_test(cx, |_| {});
8476
8477 let (text, insertion_ranges) = marked_text_ranges(
8478 indoc! {"
8479 ˇ
8480 "},
8481 false,
8482 );
8483
8484 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8485 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8486
8487 _ = editor.update_in(cx, |editor, window, cx| {
8488 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8489
8490 editor
8491 .insert_snippet(&insertion_ranges, snippet, window, cx)
8492 .unwrap();
8493
8494 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8495 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8496 assert_eq!(editor.text(cx), expected_text);
8497 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8498 }
8499
8500 assert(
8501 editor,
8502 cx,
8503 indoc! {"
8504 type «» =•
8505 "},
8506 );
8507
8508 assert!(editor.context_menu_visible(), "There should be a matches");
8509 });
8510}
8511
8512#[gpui::test]
8513async fn test_snippets(cx: &mut TestAppContext) {
8514 init_test(cx, |_| {});
8515
8516 let mut cx = EditorTestContext::new(cx).await;
8517
8518 cx.set_state(indoc! {"
8519 a.ˇ b
8520 a.ˇ b
8521 a.ˇ b
8522 "});
8523
8524 cx.update_editor(|editor, window, cx| {
8525 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8526 let insertion_ranges = editor
8527 .selections
8528 .all(cx)
8529 .iter()
8530 .map(|s| s.range().clone())
8531 .collect::<Vec<_>>();
8532 editor
8533 .insert_snippet(&insertion_ranges, snippet, window, cx)
8534 .unwrap();
8535 });
8536
8537 cx.assert_editor_state(indoc! {"
8538 a.f(«oneˇ», two, «threeˇ») b
8539 a.f(«oneˇ», two, «threeˇ») b
8540 a.f(«oneˇ», two, «threeˇ») b
8541 "});
8542
8543 // Can't move earlier than the first tab stop
8544 cx.update_editor(|editor, window, cx| {
8545 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8546 });
8547 cx.assert_editor_state(indoc! {"
8548 a.f(«oneˇ», two, «threeˇ») b
8549 a.f(«oneˇ», two, «threeˇ») b
8550 a.f(«oneˇ», two, «threeˇ») b
8551 "});
8552
8553 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8554 cx.assert_editor_state(indoc! {"
8555 a.f(one, «twoˇ», three) b
8556 a.f(one, «twoˇ», three) b
8557 a.f(one, «twoˇ», three) b
8558 "});
8559
8560 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8561 cx.assert_editor_state(indoc! {"
8562 a.f(«oneˇ», two, «threeˇ») b
8563 a.f(«oneˇ», two, «threeˇ») b
8564 a.f(«oneˇ», two, «threeˇ») b
8565 "});
8566
8567 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8568 cx.assert_editor_state(indoc! {"
8569 a.f(one, «twoˇ», three) b
8570 a.f(one, «twoˇ», three) b
8571 a.f(one, «twoˇ», three) b
8572 "});
8573 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8574 cx.assert_editor_state(indoc! {"
8575 a.f(one, two, three)ˇ b
8576 a.f(one, two, three)ˇ b
8577 a.f(one, two, three)ˇ b
8578 "});
8579
8580 // As soon as the last tab stop is reached, snippet state is gone
8581 cx.update_editor(|editor, window, cx| {
8582 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8583 });
8584 cx.assert_editor_state(indoc! {"
8585 a.f(one, two, three)ˇ b
8586 a.f(one, two, three)ˇ b
8587 a.f(one, two, three)ˇ b
8588 "});
8589}
8590
8591#[gpui::test]
8592async fn test_snippet_indentation(cx: &mut TestAppContext) {
8593 init_test(cx, |_| {});
8594
8595 let mut cx = EditorTestContext::new(cx).await;
8596
8597 cx.update_editor(|editor, window, cx| {
8598 let snippet = Snippet::parse(indoc! {"
8599 /*
8600 * Multiline comment with leading indentation
8601 *
8602 * $1
8603 */
8604 $0"})
8605 .unwrap();
8606 let insertion_ranges = editor
8607 .selections
8608 .all(cx)
8609 .iter()
8610 .map(|s| s.range().clone())
8611 .collect::<Vec<_>>();
8612 editor
8613 .insert_snippet(&insertion_ranges, snippet, window, cx)
8614 .unwrap();
8615 });
8616
8617 cx.assert_editor_state(indoc! {"
8618 /*
8619 * Multiline comment with leading indentation
8620 *
8621 * ˇ
8622 */
8623 "});
8624
8625 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8626 cx.assert_editor_state(indoc! {"
8627 /*
8628 * Multiline comment with leading indentation
8629 *
8630 *•
8631 */
8632 ˇ"});
8633}
8634
8635#[gpui::test]
8636async fn test_document_format_during_save(cx: &mut TestAppContext) {
8637 init_test(cx, |_| {});
8638
8639 let fs = FakeFs::new(cx.executor());
8640 fs.insert_file(path!("/file.rs"), Default::default()).await;
8641
8642 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8643
8644 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8645 language_registry.add(rust_lang());
8646 let mut fake_servers = language_registry.register_fake_lsp(
8647 "Rust",
8648 FakeLspAdapter {
8649 capabilities: lsp::ServerCapabilities {
8650 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8651 ..Default::default()
8652 },
8653 ..Default::default()
8654 },
8655 );
8656
8657 let buffer = project
8658 .update(cx, |project, cx| {
8659 project.open_local_buffer(path!("/file.rs"), cx)
8660 })
8661 .await
8662 .unwrap();
8663
8664 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8665 let (editor, cx) = cx.add_window_view(|window, cx| {
8666 build_editor_with_project(project.clone(), buffer, window, cx)
8667 });
8668 editor.update_in(cx, |editor, window, cx| {
8669 editor.set_text("one\ntwo\nthree\n", window, cx)
8670 });
8671 assert!(cx.read(|cx| editor.is_dirty(cx)));
8672
8673 cx.executor().start_waiting();
8674 let fake_server = fake_servers.next().await.unwrap();
8675
8676 {
8677 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8678 move |params, _| async move {
8679 assert_eq!(
8680 params.text_document.uri,
8681 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8682 );
8683 assert_eq!(params.options.tab_size, 4);
8684 Ok(Some(vec![lsp::TextEdit::new(
8685 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8686 ", ".to_string(),
8687 )]))
8688 },
8689 );
8690 let save = editor
8691 .update_in(cx, |editor, window, cx| {
8692 editor.save(true, project.clone(), window, cx)
8693 })
8694 .unwrap();
8695 cx.executor().start_waiting();
8696 save.await;
8697
8698 assert_eq!(
8699 editor.update(cx, |editor, cx| editor.text(cx)),
8700 "one, two\nthree\n"
8701 );
8702 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8703 }
8704
8705 {
8706 editor.update_in(cx, |editor, window, cx| {
8707 editor.set_text("one\ntwo\nthree\n", window, cx)
8708 });
8709 assert!(cx.read(|cx| editor.is_dirty(cx)));
8710
8711 // Ensure we can still save even if formatting hangs.
8712 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8713 move |params, _| async move {
8714 assert_eq!(
8715 params.text_document.uri,
8716 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8717 );
8718 futures::future::pending::<()>().await;
8719 unreachable!()
8720 },
8721 );
8722 let save = editor
8723 .update_in(cx, |editor, window, cx| {
8724 editor.save(true, project.clone(), window, cx)
8725 })
8726 .unwrap();
8727 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8728 cx.executor().start_waiting();
8729 save.await;
8730 assert_eq!(
8731 editor.update(cx, |editor, cx| editor.text(cx)),
8732 "one\ntwo\nthree\n"
8733 );
8734 }
8735
8736 // For non-dirty buffer, no formatting request should be sent
8737 {
8738 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8739
8740 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8741 panic!("Should not be invoked on non-dirty buffer");
8742 });
8743 let save = editor
8744 .update_in(cx, |editor, window, cx| {
8745 editor.save(true, project.clone(), window, cx)
8746 })
8747 .unwrap();
8748 cx.executor().start_waiting();
8749 save.await;
8750 }
8751
8752 // Set rust language override and assert overridden tabsize is sent to language server
8753 update_test_language_settings(cx, |settings| {
8754 settings.languages.insert(
8755 "Rust".into(),
8756 LanguageSettingsContent {
8757 tab_size: NonZeroU32::new(8),
8758 ..Default::default()
8759 },
8760 );
8761 });
8762
8763 {
8764 editor.update_in(cx, |editor, window, cx| {
8765 editor.set_text("somehting_new\n", window, cx)
8766 });
8767 assert!(cx.read(|cx| editor.is_dirty(cx)));
8768 let _formatting_request_signal = fake_server
8769 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8770 assert_eq!(
8771 params.text_document.uri,
8772 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8773 );
8774 assert_eq!(params.options.tab_size, 8);
8775 Ok(Some(vec![]))
8776 });
8777 let save = editor
8778 .update_in(cx, |editor, window, cx| {
8779 editor.save(true, project.clone(), window, cx)
8780 })
8781 .unwrap();
8782 cx.executor().start_waiting();
8783 save.await;
8784 }
8785}
8786
8787#[gpui::test]
8788async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8789 init_test(cx, |_| {});
8790
8791 let cols = 4;
8792 let rows = 10;
8793 let sample_text_1 = sample_text(rows, cols, 'a');
8794 assert_eq!(
8795 sample_text_1,
8796 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8797 );
8798 let sample_text_2 = sample_text(rows, cols, 'l');
8799 assert_eq!(
8800 sample_text_2,
8801 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8802 );
8803 let sample_text_3 = sample_text(rows, cols, 'v');
8804 assert_eq!(
8805 sample_text_3,
8806 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8807 );
8808
8809 let fs = FakeFs::new(cx.executor());
8810 fs.insert_tree(
8811 path!("/a"),
8812 json!({
8813 "main.rs": sample_text_1,
8814 "other.rs": sample_text_2,
8815 "lib.rs": sample_text_3,
8816 }),
8817 )
8818 .await;
8819
8820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8821 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8822 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8823
8824 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8825 language_registry.add(rust_lang());
8826 let mut fake_servers = language_registry.register_fake_lsp(
8827 "Rust",
8828 FakeLspAdapter {
8829 capabilities: lsp::ServerCapabilities {
8830 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8831 ..Default::default()
8832 },
8833 ..Default::default()
8834 },
8835 );
8836
8837 let worktree = project.update(cx, |project, cx| {
8838 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8839 assert_eq!(worktrees.len(), 1);
8840 worktrees.pop().unwrap()
8841 });
8842 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8843
8844 let buffer_1 = project
8845 .update(cx, |project, cx| {
8846 project.open_buffer((worktree_id, "main.rs"), cx)
8847 })
8848 .await
8849 .unwrap();
8850 let buffer_2 = project
8851 .update(cx, |project, cx| {
8852 project.open_buffer((worktree_id, "other.rs"), cx)
8853 })
8854 .await
8855 .unwrap();
8856 let buffer_3 = project
8857 .update(cx, |project, cx| {
8858 project.open_buffer((worktree_id, "lib.rs"), cx)
8859 })
8860 .await
8861 .unwrap();
8862
8863 let multi_buffer = cx.new(|cx| {
8864 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8865 multi_buffer.push_excerpts(
8866 buffer_1.clone(),
8867 [
8868 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8869 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8870 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8871 ],
8872 cx,
8873 );
8874 multi_buffer.push_excerpts(
8875 buffer_2.clone(),
8876 [
8877 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8878 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8879 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8880 ],
8881 cx,
8882 );
8883 multi_buffer.push_excerpts(
8884 buffer_3.clone(),
8885 [
8886 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8887 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8888 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8889 ],
8890 cx,
8891 );
8892 multi_buffer
8893 });
8894 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8895 Editor::new(
8896 EditorMode::full(),
8897 multi_buffer,
8898 Some(project.clone()),
8899 window,
8900 cx,
8901 )
8902 });
8903
8904 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8905 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8906 s.select_ranges(Some(1..2))
8907 });
8908 editor.insert("|one|two|three|", window, cx);
8909 });
8910 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8911 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8912 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8913 s.select_ranges(Some(60..70))
8914 });
8915 editor.insert("|four|five|six|", window, cx);
8916 });
8917 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8918
8919 // First two buffers should be edited, but not the third one.
8920 assert_eq!(
8921 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8922 "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}",
8923 );
8924 buffer_1.update(cx, |buffer, _| {
8925 assert!(buffer.is_dirty());
8926 assert_eq!(
8927 buffer.text(),
8928 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8929 )
8930 });
8931 buffer_2.update(cx, |buffer, _| {
8932 assert!(buffer.is_dirty());
8933 assert_eq!(
8934 buffer.text(),
8935 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8936 )
8937 });
8938 buffer_3.update(cx, |buffer, _| {
8939 assert!(!buffer.is_dirty());
8940 assert_eq!(buffer.text(), sample_text_3,)
8941 });
8942 cx.executor().run_until_parked();
8943
8944 cx.executor().start_waiting();
8945 let save = multi_buffer_editor
8946 .update_in(cx, |editor, window, cx| {
8947 editor.save(true, project.clone(), window, cx)
8948 })
8949 .unwrap();
8950
8951 let fake_server = fake_servers.next().await.unwrap();
8952 fake_server
8953 .server
8954 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8955 Ok(Some(vec![lsp::TextEdit::new(
8956 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8957 format!("[{} formatted]", params.text_document.uri),
8958 )]))
8959 })
8960 .detach();
8961 save.await;
8962
8963 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8964 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8965 assert_eq!(
8966 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8967 uri!(
8968 "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}"
8969 ),
8970 );
8971 buffer_1.update(cx, |buffer, _| {
8972 assert!(!buffer.is_dirty());
8973 assert_eq!(
8974 buffer.text(),
8975 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8976 )
8977 });
8978 buffer_2.update(cx, |buffer, _| {
8979 assert!(!buffer.is_dirty());
8980 assert_eq!(
8981 buffer.text(),
8982 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8983 )
8984 });
8985 buffer_3.update(cx, |buffer, _| {
8986 assert!(!buffer.is_dirty());
8987 assert_eq!(buffer.text(), sample_text_3,)
8988 });
8989}
8990
8991#[gpui::test]
8992async fn test_range_format_during_save(cx: &mut TestAppContext) {
8993 init_test(cx, |_| {});
8994
8995 let fs = FakeFs::new(cx.executor());
8996 fs.insert_file(path!("/file.rs"), Default::default()).await;
8997
8998 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8999
9000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9001 language_registry.add(rust_lang());
9002 let mut fake_servers = language_registry.register_fake_lsp(
9003 "Rust",
9004 FakeLspAdapter {
9005 capabilities: lsp::ServerCapabilities {
9006 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9007 ..Default::default()
9008 },
9009 ..Default::default()
9010 },
9011 );
9012
9013 let buffer = project
9014 .update(cx, |project, cx| {
9015 project.open_local_buffer(path!("/file.rs"), cx)
9016 })
9017 .await
9018 .unwrap();
9019
9020 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9021 let (editor, cx) = cx.add_window_view(|window, cx| {
9022 build_editor_with_project(project.clone(), buffer, window, cx)
9023 });
9024 editor.update_in(cx, |editor, window, cx| {
9025 editor.set_text("one\ntwo\nthree\n", window, cx)
9026 });
9027 assert!(cx.read(|cx| editor.is_dirty(cx)));
9028
9029 cx.executor().start_waiting();
9030 let fake_server = fake_servers.next().await.unwrap();
9031
9032 let save = editor
9033 .update_in(cx, |editor, window, cx| {
9034 editor.save(true, project.clone(), window, cx)
9035 })
9036 .unwrap();
9037 fake_server
9038 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9039 assert_eq!(
9040 params.text_document.uri,
9041 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9042 );
9043 assert_eq!(params.options.tab_size, 4);
9044 Ok(Some(vec![lsp::TextEdit::new(
9045 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9046 ", ".to_string(),
9047 )]))
9048 })
9049 .next()
9050 .await;
9051 cx.executor().start_waiting();
9052 save.await;
9053 assert_eq!(
9054 editor.update(cx, |editor, cx| editor.text(cx)),
9055 "one, two\nthree\n"
9056 );
9057 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9058
9059 editor.update_in(cx, |editor, window, cx| {
9060 editor.set_text("one\ntwo\nthree\n", window, cx)
9061 });
9062 assert!(cx.read(|cx| editor.is_dirty(cx)));
9063
9064 // Ensure we can still save even if formatting hangs.
9065 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9066 move |params, _| async move {
9067 assert_eq!(
9068 params.text_document.uri,
9069 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9070 );
9071 futures::future::pending::<()>().await;
9072 unreachable!()
9073 },
9074 );
9075 let save = editor
9076 .update_in(cx, |editor, window, cx| {
9077 editor.save(true, project.clone(), window, cx)
9078 })
9079 .unwrap();
9080 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9081 cx.executor().start_waiting();
9082 save.await;
9083 assert_eq!(
9084 editor.update(cx, |editor, cx| editor.text(cx)),
9085 "one\ntwo\nthree\n"
9086 );
9087 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9088
9089 // For non-dirty buffer, no formatting request should be sent
9090 let save = editor
9091 .update_in(cx, |editor, window, cx| {
9092 editor.save(true, project.clone(), window, cx)
9093 })
9094 .unwrap();
9095 let _pending_format_request = fake_server
9096 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9097 panic!("Should not be invoked on non-dirty buffer");
9098 })
9099 .next();
9100 cx.executor().start_waiting();
9101 save.await;
9102
9103 // Set Rust language override and assert overridden tabsize is sent to language server
9104 update_test_language_settings(cx, |settings| {
9105 settings.languages.insert(
9106 "Rust".into(),
9107 LanguageSettingsContent {
9108 tab_size: NonZeroU32::new(8),
9109 ..Default::default()
9110 },
9111 );
9112 });
9113
9114 editor.update_in(cx, |editor, window, cx| {
9115 editor.set_text("somehting_new\n", window, cx)
9116 });
9117 assert!(cx.read(|cx| editor.is_dirty(cx)));
9118 let save = editor
9119 .update_in(cx, |editor, window, cx| {
9120 editor.save(true, project.clone(), window, cx)
9121 })
9122 .unwrap();
9123 fake_server
9124 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9125 assert_eq!(
9126 params.text_document.uri,
9127 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9128 );
9129 assert_eq!(params.options.tab_size, 8);
9130 Ok(Some(Vec::new()))
9131 })
9132 .next()
9133 .await;
9134 save.await;
9135}
9136
9137#[gpui::test]
9138async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9139 init_test(cx, |settings| {
9140 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9141 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9142 ))
9143 });
9144
9145 let fs = FakeFs::new(cx.executor());
9146 fs.insert_file(path!("/file.rs"), Default::default()).await;
9147
9148 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9149
9150 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9151 language_registry.add(Arc::new(Language::new(
9152 LanguageConfig {
9153 name: "Rust".into(),
9154 matcher: LanguageMatcher {
9155 path_suffixes: vec!["rs".to_string()],
9156 ..Default::default()
9157 },
9158 ..LanguageConfig::default()
9159 },
9160 Some(tree_sitter_rust::LANGUAGE.into()),
9161 )));
9162 update_test_language_settings(cx, |settings| {
9163 // Enable Prettier formatting for the same buffer, and ensure
9164 // LSP is called instead of Prettier.
9165 settings.defaults.prettier = Some(PrettierSettings {
9166 allowed: true,
9167 ..PrettierSettings::default()
9168 });
9169 });
9170 let mut fake_servers = language_registry.register_fake_lsp(
9171 "Rust",
9172 FakeLspAdapter {
9173 capabilities: lsp::ServerCapabilities {
9174 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9175 ..Default::default()
9176 },
9177 ..Default::default()
9178 },
9179 );
9180
9181 let buffer = project
9182 .update(cx, |project, cx| {
9183 project.open_local_buffer(path!("/file.rs"), cx)
9184 })
9185 .await
9186 .unwrap();
9187
9188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9189 let (editor, cx) = cx.add_window_view(|window, cx| {
9190 build_editor_with_project(project.clone(), buffer, window, cx)
9191 });
9192 editor.update_in(cx, |editor, window, cx| {
9193 editor.set_text("one\ntwo\nthree\n", window, cx)
9194 });
9195
9196 cx.executor().start_waiting();
9197 let fake_server = fake_servers.next().await.unwrap();
9198
9199 let format = editor
9200 .update_in(cx, |editor, window, cx| {
9201 editor.perform_format(
9202 project.clone(),
9203 FormatTrigger::Manual,
9204 FormatTarget::Buffers,
9205 window,
9206 cx,
9207 )
9208 })
9209 .unwrap();
9210 fake_server
9211 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9212 assert_eq!(
9213 params.text_document.uri,
9214 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9215 );
9216 assert_eq!(params.options.tab_size, 4);
9217 Ok(Some(vec![lsp::TextEdit::new(
9218 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9219 ", ".to_string(),
9220 )]))
9221 })
9222 .next()
9223 .await;
9224 cx.executor().start_waiting();
9225 format.await;
9226 assert_eq!(
9227 editor.update(cx, |editor, cx| editor.text(cx)),
9228 "one, two\nthree\n"
9229 );
9230
9231 editor.update_in(cx, |editor, window, cx| {
9232 editor.set_text("one\ntwo\nthree\n", window, cx)
9233 });
9234 // Ensure we don't lock if formatting hangs.
9235 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9236 move |params, _| async move {
9237 assert_eq!(
9238 params.text_document.uri,
9239 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9240 );
9241 futures::future::pending::<()>().await;
9242 unreachable!()
9243 },
9244 );
9245 let format = editor
9246 .update_in(cx, |editor, window, cx| {
9247 editor.perform_format(
9248 project,
9249 FormatTrigger::Manual,
9250 FormatTarget::Buffers,
9251 window,
9252 cx,
9253 )
9254 })
9255 .unwrap();
9256 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9257 cx.executor().start_waiting();
9258 format.await;
9259 assert_eq!(
9260 editor.update(cx, |editor, cx| editor.text(cx)),
9261 "one\ntwo\nthree\n"
9262 );
9263}
9264
9265#[gpui::test]
9266async fn test_multiple_formatters(cx: &mut TestAppContext) {
9267 init_test(cx, |settings| {
9268 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9269 settings.defaults.formatter =
9270 Some(language_settings::SelectedFormatter::List(FormatterList(
9271 vec![
9272 Formatter::LanguageServer { name: None },
9273 Formatter::CodeActions(
9274 [
9275 ("code-action-1".into(), true),
9276 ("code-action-2".into(), true),
9277 ]
9278 .into_iter()
9279 .collect(),
9280 ),
9281 ]
9282 .into(),
9283 )))
9284 });
9285
9286 let fs = FakeFs::new(cx.executor());
9287 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9288 .await;
9289
9290 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9291 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9292 language_registry.add(rust_lang());
9293
9294 let mut fake_servers = language_registry.register_fake_lsp(
9295 "Rust",
9296 FakeLspAdapter {
9297 capabilities: lsp::ServerCapabilities {
9298 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9299 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9300 commands: vec!["the-command-for-code-action-1".into()],
9301 ..Default::default()
9302 }),
9303 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9304 ..Default::default()
9305 },
9306 ..Default::default()
9307 },
9308 );
9309
9310 let buffer = project
9311 .update(cx, |project, cx| {
9312 project.open_local_buffer(path!("/file.rs"), cx)
9313 })
9314 .await
9315 .unwrap();
9316
9317 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9318 let (editor, cx) = cx.add_window_view(|window, cx| {
9319 build_editor_with_project(project.clone(), buffer, window, cx)
9320 });
9321
9322 cx.executor().start_waiting();
9323
9324 let fake_server = fake_servers.next().await.unwrap();
9325 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9326 move |_params, _| async move {
9327 Ok(Some(vec![lsp::TextEdit::new(
9328 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9329 "applied-formatting\n".to_string(),
9330 )]))
9331 },
9332 );
9333 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9334 move |params, _| async move {
9335 assert_eq!(
9336 params.context.only,
9337 Some(vec!["code-action-1".into(), "code-action-2".into()])
9338 );
9339 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9340 Ok(Some(vec![
9341 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9342 kind: Some("code-action-1".into()),
9343 edit: Some(lsp::WorkspaceEdit::new(
9344 [(
9345 uri.clone(),
9346 vec![lsp::TextEdit::new(
9347 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9348 "applied-code-action-1-edit\n".to_string(),
9349 )],
9350 )]
9351 .into_iter()
9352 .collect(),
9353 )),
9354 command: Some(lsp::Command {
9355 command: "the-command-for-code-action-1".into(),
9356 ..Default::default()
9357 }),
9358 ..Default::default()
9359 }),
9360 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9361 kind: Some("code-action-2".into()),
9362 edit: Some(lsp::WorkspaceEdit::new(
9363 [(
9364 uri.clone(),
9365 vec![lsp::TextEdit::new(
9366 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9367 "applied-code-action-2-edit\n".to_string(),
9368 )],
9369 )]
9370 .into_iter()
9371 .collect(),
9372 )),
9373 ..Default::default()
9374 }),
9375 ]))
9376 },
9377 );
9378
9379 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9380 move |params, _| async move { Ok(params) }
9381 });
9382
9383 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9384 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9385 let fake = fake_server.clone();
9386 let lock = command_lock.clone();
9387 move |params, _| {
9388 assert_eq!(params.command, "the-command-for-code-action-1");
9389 let fake = fake.clone();
9390 let lock = lock.clone();
9391 async move {
9392 lock.lock().await;
9393 fake.server
9394 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9395 label: None,
9396 edit: lsp::WorkspaceEdit {
9397 changes: Some(
9398 [(
9399 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9400 vec![lsp::TextEdit {
9401 range: lsp::Range::new(
9402 lsp::Position::new(0, 0),
9403 lsp::Position::new(0, 0),
9404 ),
9405 new_text: "applied-code-action-1-command\n".into(),
9406 }],
9407 )]
9408 .into_iter()
9409 .collect(),
9410 ),
9411 ..Default::default()
9412 },
9413 })
9414 .await
9415 .into_response()
9416 .unwrap();
9417 Ok(Some(json!(null)))
9418 }
9419 }
9420 });
9421
9422 cx.executor().start_waiting();
9423 editor
9424 .update_in(cx, |editor, window, cx| {
9425 editor.perform_format(
9426 project.clone(),
9427 FormatTrigger::Manual,
9428 FormatTarget::Buffers,
9429 window,
9430 cx,
9431 )
9432 })
9433 .unwrap()
9434 .await;
9435 editor.update(cx, |editor, cx| {
9436 assert_eq!(
9437 editor.text(cx),
9438 r#"
9439 applied-code-action-2-edit
9440 applied-code-action-1-command
9441 applied-code-action-1-edit
9442 applied-formatting
9443 one
9444 two
9445 three
9446 "#
9447 .unindent()
9448 );
9449 });
9450
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.undo(&Default::default(), window, cx);
9453 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9454 });
9455
9456 // Perform a manual edit while waiting for an LSP command
9457 // that's being run as part of a formatting code action.
9458 let lock_guard = command_lock.lock().await;
9459 let format = editor
9460 .update_in(cx, |editor, window, cx| {
9461 editor.perform_format(
9462 project.clone(),
9463 FormatTrigger::Manual,
9464 FormatTarget::Buffers,
9465 window,
9466 cx,
9467 )
9468 })
9469 .unwrap();
9470 cx.run_until_parked();
9471 editor.update(cx, |editor, cx| {
9472 assert_eq!(
9473 editor.text(cx),
9474 r#"
9475 applied-code-action-1-edit
9476 applied-formatting
9477 one
9478 two
9479 three
9480 "#
9481 .unindent()
9482 );
9483
9484 editor.buffer.update(cx, |buffer, cx| {
9485 let ix = buffer.len(cx);
9486 buffer.edit([(ix..ix, "edited\n")], None, cx);
9487 });
9488 });
9489
9490 // Allow the LSP command to proceed. Because the buffer was edited,
9491 // the second code action will not be run.
9492 drop(lock_guard);
9493 format.await;
9494 editor.update_in(cx, |editor, window, cx| {
9495 assert_eq!(
9496 editor.text(cx),
9497 r#"
9498 applied-code-action-1-command
9499 applied-code-action-1-edit
9500 applied-formatting
9501 one
9502 two
9503 three
9504 edited
9505 "#
9506 .unindent()
9507 );
9508
9509 // The manual edit is undone first, because it is the last thing the user did
9510 // (even though the command completed afterwards).
9511 editor.undo(&Default::default(), window, cx);
9512 assert_eq!(
9513 editor.text(cx),
9514 r#"
9515 applied-code-action-1-command
9516 applied-code-action-1-edit
9517 applied-formatting
9518 one
9519 two
9520 three
9521 "#
9522 .unindent()
9523 );
9524
9525 // All the formatting (including the command, which completed after the manual edit)
9526 // is undone together.
9527 editor.undo(&Default::default(), window, cx);
9528 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9529 });
9530}
9531
9532#[gpui::test]
9533async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9534 init_test(cx, |settings| {
9535 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9536 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9537 ))
9538 });
9539
9540 let fs = FakeFs::new(cx.executor());
9541 fs.insert_file(path!("/file.ts"), Default::default()).await;
9542
9543 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9544
9545 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9546 language_registry.add(Arc::new(Language::new(
9547 LanguageConfig {
9548 name: "TypeScript".into(),
9549 matcher: LanguageMatcher {
9550 path_suffixes: vec!["ts".to_string()],
9551 ..Default::default()
9552 },
9553 ..LanguageConfig::default()
9554 },
9555 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9556 )));
9557 update_test_language_settings(cx, |settings| {
9558 settings.defaults.prettier = Some(PrettierSettings {
9559 allowed: true,
9560 ..PrettierSettings::default()
9561 });
9562 });
9563 let mut fake_servers = language_registry.register_fake_lsp(
9564 "TypeScript",
9565 FakeLspAdapter {
9566 capabilities: lsp::ServerCapabilities {
9567 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9568 ..Default::default()
9569 },
9570 ..Default::default()
9571 },
9572 );
9573
9574 let buffer = project
9575 .update(cx, |project, cx| {
9576 project.open_local_buffer(path!("/file.ts"), cx)
9577 })
9578 .await
9579 .unwrap();
9580
9581 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9582 let (editor, cx) = cx.add_window_view(|window, cx| {
9583 build_editor_with_project(project.clone(), buffer, window, cx)
9584 });
9585 editor.update_in(cx, |editor, window, cx| {
9586 editor.set_text(
9587 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9588 window,
9589 cx,
9590 )
9591 });
9592
9593 cx.executor().start_waiting();
9594 let fake_server = fake_servers.next().await.unwrap();
9595
9596 let format = editor
9597 .update_in(cx, |editor, window, cx| {
9598 editor.perform_code_action_kind(
9599 project.clone(),
9600 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9601 window,
9602 cx,
9603 )
9604 })
9605 .unwrap();
9606 fake_server
9607 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9608 assert_eq!(
9609 params.text_document.uri,
9610 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9611 );
9612 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9613 lsp::CodeAction {
9614 title: "Organize Imports".to_string(),
9615 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9616 edit: Some(lsp::WorkspaceEdit {
9617 changes: Some(
9618 [(
9619 params.text_document.uri.clone(),
9620 vec![lsp::TextEdit::new(
9621 lsp::Range::new(
9622 lsp::Position::new(1, 0),
9623 lsp::Position::new(2, 0),
9624 ),
9625 "".to_string(),
9626 )],
9627 )]
9628 .into_iter()
9629 .collect(),
9630 ),
9631 ..Default::default()
9632 }),
9633 ..Default::default()
9634 },
9635 )]))
9636 })
9637 .next()
9638 .await;
9639 cx.executor().start_waiting();
9640 format.await;
9641 assert_eq!(
9642 editor.update(cx, |editor, cx| editor.text(cx)),
9643 "import { a } from 'module';\n\nconst x = a;\n"
9644 );
9645
9646 editor.update_in(cx, |editor, window, cx| {
9647 editor.set_text(
9648 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9649 window,
9650 cx,
9651 )
9652 });
9653 // Ensure we don't lock if code action hangs.
9654 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9655 move |params, _| async move {
9656 assert_eq!(
9657 params.text_document.uri,
9658 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9659 );
9660 futures::future::pending::<()>().await;
9661 unreachable!()
9662 },
9663 );
9664 let format = editor
9665 .update_in(cx, |editor, window, cx| {
9666 editor.perform_code_action_kind(
9667 project,
9668 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9669 window,
9670 cx,
9671 )
9672 })
9673 .unwrap();
9674 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9675 cx.executor().start_waiting();
9676 format.await;
9677 assert_eq!(
9678 editor.update(cx, |editor, cx| editor.text(cx)),
9679 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9680 );
9681}
9682
9683#[gpui::test]
9684async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9685 init_test(cx, |_| {});
9686
9687 let mut cx = EditorLspTestContext::new_rust(
9688 lsp::ServerCapabilities {
9689 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9690 ..Default::default()
9691 },
9692 cx,
9693 )
9694 .await;
9695
9696 cx.set_state(indoc! {"
9697 one.twoˇ
9698 "});
9699
9700 // The format request takes a long time. When it completes, it inserts
9701 // a newline and an indent before the `.`
9702 cx.lsp
9703 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9704 let executor = cx.background_executor().clone();
9705 async move {
9706 executor.timer(Duration::from_millis(100)).await;
9707 Ok(Some(vec![lsp::TextEdit {
9708 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9709 new_text: "\n ".into(),
9710 }]))
9711 }
9712 });
9713
9714 // Submit a format request.
9715 let format_1 = cx
9716 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9717 .unwrap();
9718 cx.executor().run_until_parked();
9719
9720 // Submit a second format request.
9721 let format_2 = cx
9722 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9723 .unwrap();
9724 cx.executor().run_until_parked();
9725
9726 // Wait for both format requests to complete
9727 cx.executor().advance_clock(Duration::from_millis(200));
9728 cx.executor().start_waiting();
9729 format_1.await.unwrap();
9730 cx.executor().start_waiting();
9731 format_2.await.unwrap();
9732
9733 // The formatting edits only happens once.
9734 cx.assert_editor_state(indoc! {"
9735 one
9736 .twoˇ
9737 "});
9738}
9739
9740#[gpui::test]
9741async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9742 init_test(cx, |settings| {
9743 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9744 });
9745
9746 let mut cx = EditorLspTestContext::new_rust(
9747 lsp::ServerCapabilities {
9748 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9749 ..Default::default()
9750 },
9751 cx,
9752 )
9753 .await;
9754
9755 // Set up a buffer white some trailing whitespace and no trailing newline.
9756 cx.set_state(
9757 &[
9758 "one ", //
9759 "twoˇ", //
9760 "three ", //
9761 "four", //
9762 ]
9763 .join("\n"),
9764 );
9765
9766 // Submit a format request.
9767 let format = cx
9768 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9769 .unwrap();
9770
9771 // Record which buffer changes have been sent to the language server
9772 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9773 cx.lsp
9774 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9775 let buffer_changes = buffer_changes.clone();
9776 move |params, _| {
9777 buffer_changes.lock().extend(
9778 params
9779 .content_changes
9780 .into_iter()
9781 .map(|e| (e.range.unwrap(), e.text)),
9782 );
9783 }
9784 });
9785
9786 // Handle formatting requests to the language server.
9787 cx.lsp
9788 .set_request_handler::<lsp::request::Formatting, _, _>({
9789 let buffer_changes = buffer_changes.clone();
9790 move |_, _| {
9791 // When formatting is requested, trailing whitespace has already been stripped,
9792 // and the trailing newline has already been added.
9793 assert_eq!(
9794 &buffer_changes.lock()[1..],
9795 &[
9796 (
9797 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9798 "".into()
9799 ),
9800 (
9801 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9802 "".into()
9803 ),
9804 (
9805 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9806 "\n".into()
9807 ),
9808 ]
9809 );
9810
9811 // Insert blank lines between each line of the buffer.
9812 async move {
9813 Ok(Some(vec![
9814 lsp::TextEdit {
9815 range: lsp::Range::new(
9816 lsp::Position::new(1, 0),
9817 lsp::Position::new(1, 0),
9818 ),
9819 new_text: "\n".into(),
9820 },
9821 lsp::TextEdit {
9822 range: lsp::Range::new(
9823 lsp::Position::new(2, 0),
9824 lsp::Position::new(2, 0),
9825 ),
9826 new_text: "\n".into(),
9827 },
9828 ]))
9829 }
9830 }
9831 });
9832
9833 // After formatting the buffer, the trailing whitespace is stripped,
9834 // a newline is appended, and the edits provided by the language server
9835 // have been applied.
9836 format.await.unwrap();
9837 cx.assert_editor_state(
9838 &[
9839 "one", //
9840 "", //
9841 "twoˇ", //
9842 "", //
9843 "three", //
9844 "four", //
9845 "", //
9846 ]
9847 .join("\n"),
9848 );
9849
9850 // Undoing the formatting undoes the trailing whitespace removal, the
9851 // trailing newline, and the LSP edits.
9852 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9853 cx.assert_editor_state(
9854 &[
9855 "one ", //
9856 "twoˇ", //
9857 "three ", //
9858 "four", //
9859 ]
9860 .join("\n"),
9861 );
9862}
9863
9864#[gpui::test]
9865async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9866 cx: &mut TestAppContext,
9867) {
9868 init_test(cx, |_| {});
9869
9870 cx.update(|cx| {
9871 cx.update_global::<SettingsStore, _>(|settings, cx| {
9872 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9873 settings.auto_signature_help = Some(true);
9874 });
9875 });
9876 });
9877
9878 let mut cx = EditorLspTestContext::new_rust(
9879 lsp::ServerCapabilities {
9880 signature_help_provider: Some(lsp::SignatureHelpOptions {
9881 ..Default::default()
9882 }),
9883 ..Default::default()
9884 },
9885 cx,
9886 )
9887 .await;
9888
9889 let language = Language::new(
9890 LanguageConfig {
9891 name: "Rust".into(),
9892 brackets: BracketPairConfig {
9893 pairs: vec![
9894 BracketPair {
9895 start: "{".to_string(),
9896 end: "}".to_string(),
9897 close: true,
9898 surround: true,
9899 newline: true,
9900 },
9901 BracketPair {
9902 start: "(".to_string(),
9903 end: ")".to_string(),
9904 close: true,
9905 surround: true,
9906 newline: true,
9907 },
9908 BracketPair {
9909 start: "/*".to_string(),
9910 end: " */".to_string(),
9911 close: true,
9912 surround: true,
9913 newline: true,
9914 },
9915 BracketPair {
9916 start: "[".to_string(),
9917 end: "]".to_string(),
9918 close: false,
9919 surround: false,
9920 newline: true,
9921 },
9922 BracketPair {
9923 start: "\"".to_string(),
9924 end: "\"".to_string(),
9925 close: true,
9926 surround: true,
9927 newline: false,
9928 },
9929 BracketPair {
9930 start: "<".to_string(),
9931 end: ">".to_string(),
9932 close: false,
9933 surround: true,
9934 newline: true,
9935 },
9936 ],
9937 ..Default::default()
9938 },
9939 autoclose_before: "})]".to_string(),
9940 ..Default::default()
9941 },
9942 Some(tree_sitter_rust::LANGUAGE.into()),
9943 );
9944 let language = Arc::new(language);
9945
9946 cx.language_registry().add(language.clone());
9947 cx.update_buffer(|buffer, cx| {
9948 buffer.set_language(Some(language), cx);
9949 });
9950
9951 cx.set_state(
9952 &r#"
9953 fn main() {
9954 sampleˇ
9955 }
9956 "#
9957 .unindent(),
9958 );
9959
9960 cx.update_editor(|editor, window, cx| {
9961 editor.handle_input("(", window, cx);
9962 });
9963 cx.assert_editor_state(
9964 &"
9965 fn main() {
9966 sample(ˇ)
9967 }
9968 "
9969 .unindent(),
9970 );
9971
9972 let mocked_response = lsp::SignatureHelp {
9973 signatures: vec![lsp::SignatureInformation {
9974 label: "fn sample(param1: u8, param2: u8)".to_string(),
9975 documentation: None,
9976 parameters: Some(vec![
9977 lsp::ParameterInformation {
9978 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9979 documentation: None,
9980 },
9981 lsp::ParameterInformation {
9982 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9983 documentation: None,
9984 },
9985 ]),
9986 active_parameter: None,
9987 }],
9988 active_signature: Some(0),
9989 active_parameter: Some(0),
9990 };
9991 handle_signature_help_request(&mut cx, mocked_response).await;
9992
9993 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9994 .await;
9995
9996 cx.editor(|editor, _, _| {
9997 let signature_help_state = editor.signature_help_state.popover().cloned();
9998 assert_eq!(
9999 signature_help_state.unwrap().label,
10000 "param1: u8, param2: u8"
10001 );
10002 });
10003}
10004
10005#[gpui::test]
10006async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10007 init_test(cx, |_| {});
10008
10009 cx.update(|cx| {
10010 cx.update_global::<SettingsStore, _>(|settings, cx| {
10011 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10012 settings.auto_signature_help = Some(false);
10013 settings.show_signature_help_after_edits = Some(false);
10014 });
10015 });
10016 });
10017
10018 let mut cx = EditorLspTestContext::new_rust(
10019 lsp::ServerCapabilities {
10020 signature_help_provider: Some(lsp::SignatureHelpOptions {
10021 ..Default::default()
10022 }),
10023 ..Default::default()
10024 },
10025 cx,
10026 )
10027 .await;
10028
10029 let language = Language::new(
10030 LanguageConfig {
10031 name: "Rust".into(),
10032 brackets: BracketPairConfig {
10033 pairs: vec![
10034 BracketPair {
10035 start: "{".to_string(),
10036 end: "}".to_string(),
10037 close: true,
10038 surround: true,
10039 newline: true,
10040 },
10041 BracketPair {
10042 start: "(".to_string(),
10043 end: ")".to_string(),
10044 close: true,
10045 surround: true,
10046 newline: true,
10047 },
10048 BracketPair {
10049 start: "/*".to_string(),
10050 end: " */".to_string(),
10051 close: true,
10052 surround: true,
10053 newline: true,
10054 },
10055 BracketPair {
10056 start: "[".to_string(),
10057 end: "]".to_string(),
10058 close: false,
10059 surround: false,
10060 newline: true,
10061 },
10062 BracketPair {
10063 start: "\"".to_string(),
10064 end: "\"".to_string(),
10065 close: true,
10066 surround: true,
10067 newline: false,
10068 },
10069 BracketPair {
10070 start: "<".to_string(),
10071 end: ">".to_string(),
10072 close: false,
10073 surround: true,
10074 newline: true,
10075 },
10076 ],
10077 ..Default::default()
10078 },
10079 autoclose_before: "})]".to_string(),
10080 ..Default::default()
10081 },
10082 Some(tree_sitter_rust::LANGUAGE.into()),
10083 );
10084 let language = Arc::new(language);
10085
10086 cx.language_registry().add(language.clone());
10087 cx.update_buffer(|buffer, cx| {
10088 buffer.set_language(Some(language), cx);
10089 });
10090
10091 // Ensure that signature_help is not called when no signature help is enabled.
10092 cx.set_state(
10093 &r#"
10094 fn main() {
10095 sampleˇ
10096 }
10097 "#
10098 .unindent(),
10099 );
10100 cx.update_editor(|editor, window, cx| {
10101 editor.handle_input("(", window, cx);
10102 });
10103 cx.assert_editor_state(
10104 &"
10105 fn main() {
10106 sample(ˇ)
10107 }
10108 "
10109 .unindent(),
10110 );
10111 cx.editor(|editor, _, _| {
10112 assert!(editor.signature_help_state.task().is_none());
10113 });
10114
10115 let mocked_response = lsp::SignatureHelp {
10116 signatures: vec![lsp::SignatureInformation {
10117 label: "fn sample(param1: u8, param2: u8)".to_string(),
10118 documentation: None,
10119 parameters: Some(vec![
10120 lsp::ParameterInformation {
10121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10122 documentation: None,
10123 },
10124 lsp::ParameterInformation {
10125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10126 documentation: None,
10127 },
10128 ]),
10129 active_parameter: None,
10130 }],
10131 active_signature: Some(0),
10132 active_parameter: Some(0),
10133 };
10134
10135 // Ensure that signature_help is called when enabled afte edits
10136 cx.update(|_, cx| {
10137 cx.update_global::<SettingsStore, _>(|settings, cx| {
10138 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10139 settings.auto_signature_help = Some(false);
10140 settings.show_signature_help_after_edits = Some(true);
10141 });
10142 });
10143 });
10144 cx.set_state(
10145 &r#"
10146 fn main() {
10147 sampleˇ
10148 }
10149 "#
10150 .unindent(),
10151 );
10152 cx.update_editor(|editor, window, cx| {
10153 editor.handle_input("(", window, cx);
10154 });
10155 cx.assert_editor_state(
10156 &"
10157 fn main() {
10158 sample(ˇ)
10159 }
10160 "
10161 .unindent(),
10162 );
10163 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10164 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10165 .await;
10166 cx.update_editor(|editor, _, _| {
10167 let signature_help_state = editor.signature_help_state.popover().cloned();
10168 assert!(signature_help_state.is_some());
10169 assert_eq!(
10170 signature_help_state.unwrap().label,
10171 "param1: u8, param2: u8"
10172 );
10173 editor.signature_help_state = SignatureHelpState::default();
10174 });
10175
10176 // Ensure that signature_help is called when auto signature help override is enabled
10177 cx.update(|_, cx| {
10178 cx.update_global::<SettingsStore, _>(|settings, cx| {
10179 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10180 settings.auto_signature_help = Some(true);
10181 settings.show_signature_help_after_edits = Some(false);
10182 });
10183 });
10184 });
10185 cx.set_state(
10186 &r#"
10187 fn main() {
10188 sampleˇ
10189 }
10190 "#
10191 .unindent(),
10192 );
10193 cx.update_editor(|editor, window, cx| {
10194 editor.handle_input("(", window, cx);
10195 });
10196 cx.assert_editor_state(
10197 &"
10198 fn main() {
10199 sample(ˇ)
10200 }
10201 "
10202 .unindent(),
10203 );
10204 handle_signature_help_request(&mut cx, mocked_response).await;
10205 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10206 .await;
10207 cx.editor(|editor, _, _| {
10208 let signature_help_state = editor.signature_help_state.popover().cloned();
10209 assert!(signature_help_state.is_some());
10210 assert_eq!(
10211 signature_help_state.unwrap().label,
10212 "param1: u8, param2: u8"
10213 );
10214 });
10215}
10216
10217#[gpui::test]
10218async fn test_signature_help(cx: &mut TestAppContext) {
10219 init_test(cx, |_| {});
10220 cx.update(|cx| {
10221 cx.update_global::<SettingsStore, _>(|settings, cx| {
10222 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10223 settings.auto_signature_help = Some(true);
10224 });
10225 });
10226 });
10227
10228 let mut cx = EditorLspTestContext::new_rust(
10229 lsp::ServerCapabilities {
10230 signature_help_provider: Some(lsp::SignatureHelpOptions {
10231 ..Default::default()
10232 }),
10233 ..Default::default()
10234 },
10235 cx,
10236 )
10237 .await;
10238
10239 // A test that directly calls `show_signature_help`
10240 cx.update_editor(|editor, window, cx| {
10241 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10242 });
10243
10244 let mocked_response = lsp::SignatureHelp {
10245 signatures: vec![lsp::SignatureInformation {
10246 label: "fn sample(param1: u8, param2: u8)".to_string(),
10247 documentation: None,
10248 parameters: Some(vec![
10249 lsp::ParameterInformation {
10250 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10251 documentation: None,
10252 },
10253 lsp::ParameterInformation {
10254 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10255 documentation: None,
10256 },
10257 ]),
10258 active_parameter: None,
10259 }],
10260 active_signature: Some(0),
10261 active_parameter: Some(0),
10262 };
10263 handle_signature_help_request(&mut cx, mocked_response).await;
10264
10265 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10266 .await;
10267
10268 cx.editor(|editor, _, _| {
10269 let signature_help_state = editor.signature_help_state.popover().cloned();
10270 assert!(signature_help_state.is_some());
10271 assert_eq!(
10272 signature_help_state.unwrap().label,
10273 "param1: u8, param2: u8"
10274 );
10275 });
10276
10277 // When exiting outside from inside the brackets, `signature_help` is closed.
10278 cx.set_state(indoc! {"
10279 fn main() {
10280 sample(ˇ);
10281 }
10282
10283 fn sample(param1: u8, param2: u8) {}
10284 "});
10285
10286 cx.update_editor(|editor, window, cx| {
10287 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10288 });
10289
10290 let mocked_response = lsp::SignatureHelp {
10291 signatures: Vec::new(),
10292 active_signature: None,
10293 active_parameter: None,
10294 };
10295 handle_signature_help_request(&mut cx, mocked_response).await;
10296
10297 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10298 .await;
10299
10300 cx.editor(|editor, _, _| {
10301 assert!(!editor.signature_help_state.is_shown());
10302 });
10303
10304 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10305 cx.set_state(indoc! {"
10306 fn main() {
10307 sample(ˇ);
10308 }
10309
10310 fn sample(param1: u8, param2: u8) {}
10311 "});
10312
10313 let mocked_response = lsp::SignatureHelp {
10314 signatures: vec![lsp::SignatureInformation {
10315 label: "fn sample(param1: u8, param2: u8)".to_string(),
10316 documentation: None,
10317 parameters: Some(vec![
10318 lsp::ParameterInformation {
10319 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10320 documentation: None,
10321 },
10322 lsp::ParameterInformation {
10323 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10324 documentation: None,
10325 },
10326 ]),
10327 active_parameter: None,
10328 }],
10329 active_signature: Some(0),
10330 active_parameter: Some(0),
10331 };
10332 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10333 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10334 .await;
10335 cx.editor(|editor, _, _| {
10336 assert!(editor.signature_help_state.is_shown());
10337 });
10338
10339 // Restore the popover with more parameter input
10340 cx.set_state(indoc! {"
10341 fn main() {
10342 sample(param1, param2ˇ);
10343 }
10344
10345 fn sample(param1: u8, param2: u8) {}
10346 "});
10347
10348 let mocked_response = lsp::SignatureHelp {
10349 signatures: vec![lsp::SignatureInformation {
10350 label: "fn sample(param1: u8, param2: u8)".to_string(),
10351 documentation: None,
10352 parameters: Some(vec![
10353 lsp::ParameterInformation {
10354 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10355 documentation: None,
10356 },
10357 lsp::ParameterInformation {
10358 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10359 documentation: None,
10360 },
10361 ]),
10362 active_parameter: None,
10363 }],
10364 active_signature: Some(0),
10365 active_parameter: Some(1),
10366 };
10367 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10368 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10369 .await;
10370
10371 // When selecting a range, the popover is gone.
10372 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10373 cx.update_editor(|editor, window, cx| {
10374 editor.change_selections(None, window, cx, |s| {
10375 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10376 })
10377 });
10378 cx.assert_editor_state(indoc! {"
10379 fn main() {
10380 sample(param1, «ˇparam2»);
10381 }
10382
10383 fn sample(param1: u8, param2: u8) {}
10384 "});
10385 cx.editor(|editor, _, _| {
10386 assert!(!editor.signature_help_state.is_shown());
10387 });
10388
10389 // When unselecting again, the popover is back if within the brackets.
10390 cx.update_editor(|editor, window, cx| {
10391 editor.change_selections(None, window, cx, |s| {
10392 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10393 })
10394 });
10395 cx.assert_editor_state(indoc! {"
10396 fn main() {
10397 sample(param1, ˇparam2);
10398 }
10399
10400 fn sample(param1: u8, param2: u8) {}
10401 "});
10402 handle_signature_help_request(&mut cx, mocked_response).await;
10403 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10404 .await;
10405 cx.editor(|editor, _, _| {
10406 assert!(editor.signature_help_state.is_shown());
10407 });
10408
10409 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10410 cx.update_editor(|editor, window, cx| {
10411 editor.change_selections(None, window, cx, |s| {
10412 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10413 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10414 })
10415 });
10416 cx.assert_editor_state(indoc! {"
10417 fn main() {
10418 sample(param1, ˇparam2);
10419 }
10420
10421 fn sample(param1: u8, param2: u8) {}
10422 "});
10423
10424 let mocked_response = lsp::SignatureHelp {
10425 signatures: vec![lsp::SignatureInformation {
10426 label: "fn sample(param1: u8, param2: u8)".to_string(),
10427 documentation: None,
10428 parameters: Some(vec![
10429 lsp::ParameterInformation {
10430 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10431 documentation: None,
10432 },
10433 lsp::ParameterInformation {
10434 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10435 documentation: None,
10436 },
10437 ]),
10438 active_parameter: None,
10439 }],
10440 active_signature: Some(0),
10441 active_parameter: Some(1),
10442 };
10443 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10444 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10445 .await;
10446 cx.update_editor(|editor, _, cx| {
10447 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10448 });
10449 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10450 .await;
10451 cx.update_editor(|editor, window, cx| {
10452 editor.change_selections(None, window, cx, |s| {
10453 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10454 })
10455 });
10456 cx.assert_editor_state(indoc! {"
10457 fn main() {
10458 sample(param1, «ˇparam2»);
10459 }
10460
10461 fn sample(param1: u8, param2: u8) {}
10462 "});
10463 cx.update_editor(|editor, window, cx| {
10464 editor.change_selections(None, window, cx, |s| {
10465 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10466 })
10467 });
10468 cx.assert_editor_state(indoc! {"
10469 fn main() {
10470 sample(param1, ˇparam2);
10471 }
10472
10473 fn sample(param1: u8, param2: u8) {}
10474 "});
10475 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10476 .await;
10477}
10478
10479#[gpui::test]
10480async fn test_completion_mode(cx: &mut TestAppContext) {
10481 init_test(cx, |_| {});
10482 let mut cx = EditorLspTestContext::new_rust(
10483 lsp::ServerCapabilities {
10484 completion_provider: Some(lsp::CompletionOptions {
10485 resolve_provider: Some(true),
10486 ..Default::default()
10487 }),
10488 ..Default::default()
10489 },
10490 cx,
10491 )
10492 .await;
10493
10494 struct Run {
10495 run_description: &'static str,
10496 initial_state: String,
10497 buffer_marked_text: String,
10498 completion_label: &'static str,
10499 completion_text: &'static str,
10500 expected_with_insert_mode: String,
10501 expected_with_replace_mode: String,
10502 expected_with_replace_subsequence_mode: String,
10503 expected_with_replace_suffix_mode: String,
10504 }
10505
10506 let runs = [
10507 Run {
10508 run_description: "Start of word matches completion text",
10509 initial_state: "before ediˇ after".into(),
10510 buffer_marked_text: "before <edi|> after".into(),
10511 completion_label: "editor",
10512 completion_text: "editor",
10513 expected_with_insert_mode: "before editorˇ after".into(),
10514 expected_with_replace_mode: "before editorˇ after".into(),
10515 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10516 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10517 },
10518 Run {
10519 run_description: "Accept same text at the middle of the word",
10520 initial_state: "before ediˇtor after".into(),
10521 buffer_marked_text: "before <edi|tor> after".into(),
10522 completion_label: "editor",
10523 completion_text: "editor",
10524 expected_with_insert_mode: "before editorˇtor after".into(),
10525 expected_with_replace_mode: "before editorˇ after".into(),
10526 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10527 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10528 },
10529 Run {
10530 run_description: "End of word matches completion text -- cursor at end",
10531 initial_state: "before torˇ after".into(),
10532 buffer_marked_text: "before <tor|> after".into(),
10533 completion_label: "editor",
10534 completion_text: "editor",
10535 expected_with_insert_mode: "before editorˇ after".into(),
10536 expected_with_replace_mode: "before editorˇ after".into(),
10537 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10538 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10539 },
10540 Run {
10541 run_description: "End of word matches completion text -- cursor at start",
10542 initial_state: "before ˇtor after".into(),
10543 buffer_marked_text: "before <|tor> after".into(),
10544 completion_label: "editor",
10545 completion_text: "editor",
10546 expected_with_insert_mode: "before editorˇtor after".into(),
10547 expected_with_replace_mode: "before editorˇ after".into(),
10548 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10549 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10550 },
10551 Run {
10552 run_description: "Prepend text containing whitespace",
10553 initial_state: "pˇfield: bool".into(),
10554 buffer_marked_text: "<p|field>: bool".into(),
10555 completion_label: "pub ",
10556 completion_text: "pub ",
10557 expected_with_insert_mode: "pub ˇfield: bool".into(),
10558 expected_with_replace_mode: "pub ˇ: bool".into(),
10559 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10560 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10561 },
10562 Run {
10563 run_description: "Add element to start of list",
10564 initial_state: "[element_ˇelement_2]".into(),
10565 buffer_marked_text: "[<element_|element_2>]".into(),
10566 completion_label: "element_1",
10567 completion_text: "element_1",
10568 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10569 expected_with_replace_mode: "[element_1ˇ]".into(),
10570 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10571 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10572 },
10573 Run {
10574 run_description: "Add element to start of list -- first and second elements are equal",
10575 initial_state: "[elˇelement]".into(),
10576 buffer_marked_text: "[<el|element>]".into(),
10577 completion_label: "element",
10578 completion_text: "element",
10579 expected_with_insert_mode: "[elementˇelement]".into(),
10580 expected_with_replace_mode: "[elementˇ]".into(),
10581 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10582 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10583 },
10584 Run {
10585 run_description: "Ends with matching suffix",
10586 initial_state: "SubˇError".into(),
10587 buffer_marked_text: "<Sub|Error>".into(),
10588 completion_label: "SubscriptionError",
10589 completion_text: "SubscriptionError",
10590 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10591 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10592 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10593 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10594 },
10595 Run {
10596 run_description: "Suffix is a subsequence -- contiguous",
10597 initial_state: "SubˇErr".into(),
10598 buffer_marked_text: "<Sub|Err>".into(),
10599 completion_label: "SubscriptionError",
10600 completion_text: "SubscriptionError",
10601 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10602 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10603 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10604 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10605 },
10606 Run {
10607 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10608 initial_state: "Suˇscrirr".into(),
10609 buffer_marked_text: "<Su|scrirr>".into(),
10610 completion_label: "SubscriptionError",
10611 completion_text: "SubscriptionError",
10612 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10613 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10614 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10615 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10616 },
10617 Run {
10618 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10619 initial_state: "foo(indˇix)".into(),
10620 buffer_marked_text: "foo(<ind|ix>)".into(),
10621 completion_label: "node_index",
10622 completion_text: "node_index",
10623 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10624 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10625 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10626 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10627 },
10628 Run {
10629 run_description: "Replace range ends before cursor - should extend to cursor",
10630 initial_state: "before editˇo after".into(),
10631 buffer_marked_text: "before <{ed}>it|o after".into(),
10632 completion_label: "editor",
10633 completion_text: "editor",
10634 expected_with_insert_mode: "before editorˇo after".into(),
10635 expected_with_replace_mode: "before editorˇo after".into(),
10636 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10637 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10638 },
10639 Run {
10640 run_description: "Uses label for suffix matching",
10641 initial_state: "before ediˇtor after".into(),
10642 buffer_marked_text: "before <edi|tor> after".into(),
10643 completion_label: "editor",
10644 completion_text: "editor()",
10645 expected_with_insert_mode: "before editor()ˇtor after".into(),
10646 expected_with_replace_mode: "before editor()ˇ after".into(),
10647 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
10648 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
10649 },
10650 Run {
10651 run_description: "Case insensitive subsequence and suffix matching",
10652 initial_state: "before EDiˇtoR after".into(),
10653 buffer_marked_text: "before <EDi|toR> after".into(),
10654 completion_label: "editor",
10655 completion_text: "editor",
10656 expected_with_insert_mode: "before editorˇtoR after".into(),
10657 expected_with_replace_mode: "before editorˇ after".into(),
10658 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10659 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10660 },
10661 ];
10662
10663 for run in runs {
10664 let run_variations = [
10665 (LspInsertMode::Insert, run.expected_with_insert_mode),
10666 (LspInsertMode::Replace, run.expected_with_replace_mode),
10667 (
10668 LspInsertMode::ReplaceSubsequence,
10669 run.expected_with_replace_subsequence_mode,
10670 ),
10671 (
10672 LspInsertMode::ReplaceSuffix,
10673 run.expected_with_replace_suffix_mode,
10674 ),
10675 ];
10676
10677 for (lsp_insert_mode, expected_text) in run_variations {
10678 eprintln!(
10679 "run = {:?}, mode = {lsp_insert_mode:.?}",
10680 run.run_description,
10681 );
10682
10683 update_test_language_settings(&mut cx, |settings| {
10684 settings.defaults.completions = Some(CompletionSettings {
10685 lsp_insert_mode,
10686 words: WordsCompletionMode::Disabled,
10687 lsp: true,
10688 lsp_fetch_timeout_ms: 0,
10689 });
10690 });
10691
10692 cx.set_state(&run.initial_state);
10693 cx.update_editor(|editor, window, cx| {
10694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10695 });
10696
10697 let counter = Arc::new(AtomicUsize::new(0));
10698 handle_completion_request_with_insert_and_replace(
10699 &mut cx,
10700 &run.buffer_marked_text,
10701 vec![(run.completion_label, run.completion_text)],
10702 counter.clone(),
10703 )
10704 .await;
10705 cx.condition(|editor, _| editor.context_menu_visible())
10706 .await;
10707 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10708
10709 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10710 editor
10711 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10712 .unwrap()
10713 });
10714 cx.assert_editor_state(&expected_text);
10715 handle_resolve_completion_request(&mut cx, None).await;
10716 apply_additional_edits.await.unwrap();
10717 }
10718 }
10719}
10720
10721#[gpui::test]
10722async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10723 init_test(cx, |_| {});
10724 let mut cx = EditorLspTestContext::new_rust(
10725 lsp::ServerCapabilities {
10726 completion_provider: Some(lsp::CompletionOptions {
10727 resolve_provider: Some(true),
10728 ..Default::default()
10729 }),
10730 ..Default::default()
10731 },
10732 cx,
10733 )
10734 .await;
10735
10736 let initial_state = "SubˇError";
10737 let buffer_marked_text = "<Sub|Error>";
10738 let completion_text = "SubscriptionError";
10739 let expected_with_insert_mode = "SubscriptionErrorˇError";
10740 let expected_with_replace_mode = "SubscriptionErrorˇ";
10741
10742 update_test_language_settings(&mut cx, |settings| {
10743 settings.defaults.completions = Some(CompletionSettings {
10744 words: WordsCompletionMode::Disabled,
10745 // set the opposite here to ensure that the action is overriding the default behavior
10746 lsp_insert_mode: LspInsertMode::Insert,
10747 lsp: true,
10748 lsp_fetch_timeout_ms: 0,
10749 });
10750 });
10751
10752 cx.set_state(initial_state);
10753 cx.update_editor(|editor, window, cx| {
10754 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10755 });
10756
10757 let counter = Arc::new(AtomicUsize::new(0));
10758 handle_completion_request_with_insert_and_replace(
10759 &mut cx,
10760 &buffer_marked_text,
10761 vec![(completion_text, completion_text)],
10762 counter.clone(),
10763 )
10764 .await;
10765 cx.condition(|editor, _| editor.context_menu_visible())
10766 .await;
10767 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10768
10769 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10770 editor
10771 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10772 .unwrap()
10773 });
10774 cx.assert_editor_state(&expected_with_replace_mode);
10775 handle_resolve_completion_request(&mut cx, None).await;
10776 apply_additional_edits.await.unwrap();
10777
10778 update_test_language_settings(&mut cx, |settings| {
10779 settings.defaults.completions = Some(CompletionSettings {
10780 words: WordsCompletionMode::Disabled,
10781 // set the opposite here to ensure that the action is overriding the default behavior
10782 lsp_insert_mode: LspInsertMode::Replace,
10783 lsp: true,
10784 lsp_fetch_timeout_ms: 0,
10785 });
10786 });
10787
10788 cx.set_state(initial_state);
10789 cx.update_editor(|editor, window, cx| {
10790 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10791 });
10792 handle_completion_request_with_insert_and_replace(
10793 &mut cx,
10794 &buffer_marked_text,
10795 vec![(completion_text, completion_text)],
10796 counter.clone(),
10797 )
10798 .await;
10799 cx.condition(|editor, _| editor.context_menu_visible())
10800 .await;
10801 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10802
10803 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10804 editor
10805 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10806 .unwrap()
10807 });
10808 cx.assert_editor_state(&expected_with_insert_mode);
10809 handle_resolve_completion_request(&mut cx, None).await;
10810 apply_additional_edits.await.unwrap();
10811}
10812
10813#[gpui::test]
10814async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10815 init_test(cx, |_| {});
10816 let mut cx = EditorLspTestContext::new_rust(
10817 lsp::ServerCapabilities {
10818 completion_provider: Some(lsp::CompletionOptions {
10819 resolve_provider: Some(true),
10820 ..Default::default()
10821 }),
10822 ..Default::default()
10823 },
10824 cx,
10825 )
10826 .await;
10827
10828 // scenario: surrounding text matches completion text
10829 let completion_text = "to_offset";
10830 let initial_state = indoc! {"
10831 1. buf.to_offˇsuffix
10832 2. buf.to_offˇsuf
10833 3. buf.to_offˇfix
10834 4. buf.to_offˇ
10835 5. into_offˇensive
10836 6. ˇsuffix
10837 7. let ˇ //
10838 8. aaˇzz
10839 9. buf.to_off«zzzzzˇ»suffix
10840 10. buf.«ˇzzzzz»suffix
10841 11. to_off«ˇzzzzz»
10842
10843 buf.to_offˇsuffix // newest cursor
10844 "};
10845 let completion_marked_buffer = indoc! {"
10846 1. buf.to_offsuffix
10847 2. buf.to_offsuf
10848 3. buf.to_offfix
10849 4. buf.to_off
10850 5. into_offensive
10851 6. suffix
10852 7. let //
10853 8. aazz
10854 9. buf.to_offzzzzzsuffix
10855 10. buf.zzzzzsuffix
10856 11. to_offzzzzz
10857
10858 buf.<to_off|suffix> // newest cursor
10859 "};
10860 let expected = indoc! {"
10861 1. buf.to_offsetˇ
10862 2. buf.to_offsetˇsuf
10863 3. buf.to_offsetˇfix
10864 4. buf.to_offsetˇ
10865 5. into_offsetˇensive
10866 6. to_offsetˇsuffix
10867 7. let to_offsetˇ //
10868 8. aato_offsetˇzz
10869 9. buf.to_offsetˇ
10870 10. buf.to_offsetˇsuffix
10871 11. to_offsetˇ
10872
10873 buf.to_offsetˇ // newest cursor
10874 "};
10875 cx.set_state(initial_state);
10876 cx.update_editor(|editor, window, cx| {
10877 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10878 });
10879 handle_completion_request_with_insert_and_replace(
10880 &mut cx,
10881 completion_marked_buffer,
10882 vec![(completion_text, completion_text)],
10883 Arc::new(AtomicUsize::new(0)),
10884 )
10885 .await;
10886 cx.condition(|editor, _| editor.context_menu_visible())
10887 .await;
10888 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10889 editor
10890 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10891 .unwrap()
10892 });
10893 cx.assert_editor_state(expected);
10894 handle_resolve_completion_request(&mut cx, None).await;
10895 apply_additional_edits.await.unwrap();
10896
10897 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10898 let completion_text = "foo_and_bar";
10899 let initial_state = indoc! {"
10900 1. ooanbˇ
10901 2. zooanbˇ
10902 3. ooanbˇz
10903 4. zooanbˇz
10904 5. ooanˇ
10905 6. oanbˇ
10906
10907 ooanbˇ
10908 "};
10909 let completion_marked_buffer = indoc! {"
10910 1. ooanb
10911 2. zooanb
10912 3. ooanbz
10913 4. zooanbz
10914 5. ooan
10915 6. oanb
10916
10917 <ooanb|>
10918 "};
10919 let expected = indoc! {"
10920 1. foo_and_barˇ
10921 2. zfoo_and_barˇ
10922 3. foo_and_barˇz
10923 4. zfoo_and_barˇz
10924 5. ooanfoo_and_barˇ
10925 6. oanbfoo_and_barˇ
10926
10927 foo_and_barˇ
10928 "};
10929 cx.set_state(initial_state);
10930 cx.update_editor(|editor, window, cx| {
10931 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10932 });
10933 handle_completion_request_with_insert_and_replace(
10934 &mut cx,
10935 completion_marked_buffer,
10936 vec![(completion_text, completion_text)],
10937 Arc::new(AtomicUsize::new(0)),
10938 )
10939 .await;
10940 cx.condition(|editor, _| editor.context_menu_visible())
10941 .await;
10942 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10943 editor
10944 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10945 .unwrap()
10946 });
10947 cx.assert_editor_state(expected);
10948 handle_resolve_completion_request(&mut cx, None).await;
10949 apply_additional_edits.await.unwrap();
10950
10951 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10952 // (expects the same as if it was inserted at the end)
10953 let completion_text = "foo_and_bar";
10954 let initial_state = indoc! {"
10955 1. ooˇanb
10956 2. zooˇanb
10957 3. ooˇanbz
10958 4. zooˇanbz
10959
10960 ooˇanb
10961 "};
10962 let completion_marked_buffer = indoc! {"
10963 1. ooanb
10964 2. zooanb
10965 3. ooanbz
10966 4. zooanbz
10967
10968 <oo|anb>
10969 "};
10970 let expected = indoc! {"
10971 1. foo_and_barˇ
10972 2. zfoo_and_barˇ
10973 3. foo_and_barˇz
10974 4. zfoo_and_barˇz
10975
10976 foo_and_barˇ
10977 "};
10978 cx.set_state(initial_state);
10979 cx.update_editor(|editor, window, cx| {
10980 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10981 });
10982 handle_completion_request_with_insert_and_replace(
10983 &mut cx,
10984 completion_marked_buffer,
10985 vec![(completion_text, completion_text)],
10986 Arc::new(AtomicUsize::new(0)),
10987 )
10988 .await;
10989 cx.condition(|editor, _| editor.context_menu_visible())
10990 .await;
10991 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10992 editor
10993 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10994 .unwrap()
10995 });
10996 cx.assert_editor_state(expected);
10997 handle_resolve_completion_request(&mut cx, None).await;
10998 apply_additional_edits.await.unwrap();
10999}
11000
11001// This used to crash
11002#[gpui::test]
11003async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11004 init_test(cx, |_| {});
11005
11006 let buffer_text = indoc! {"
11007 fn main() {
11008 10.satu;
11009
11010 //
11011 // separate cursors so they open in different excerpts (manually reproducible)
11012 //
11013
11014 10.satu20;
11015 }
11016 "};
11017 let multibuffer_text_with_selections = indoc! {"
11018 fn main() {
11019 10.satuˇ;
11020
11021 //
11022
11023 //
11024
11025 10.satuˇ20;
11026 }
11027 "};
11028 let expected_multibuffer = indoc! {"
11029 fn main() {
11030 10.saturating_sub()ˇ;
11031
11032 //
11033
11034 //
11035
11036 10.saturating_sub()ˇ;
11037 }
11038 "};
11039
11040 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11041 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11042
11043 let fs = FakeFs::new(cx.executor());
11044 fs.insert_tree(
11045 path!("/a"),
11046 json!({
11047 "main.rs": buffer_text,
11048 }),
11049 )
11050 .await;
11051
11052 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11053 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11054 language_registry.add(rust_lang());
11055 let mut fake_servers = language_registry.register_fake_lsp(
11056 "Rust",
11057 FakeLspAdapter {
11058 capabilities: lsp::ServerCapabilities {
11059 completion_provider: Some(lsp::CompletionOptions {
11060 resolve_provider: None,
11061 ..lsp::CompletionOptions::default()
11062 }),
11063 ..lsp::ServerCapabilities::default()
11064 },
11065 ..FakeLspAdapter::default()
11066 },
11067 );
11068 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11069 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11070 let buffer = project
11071 .update(cx, |project, cx| {
11072 project.open_local_buffer(path!("/a/main.rs"), cx)
11073 })
11074 .await
11075 .unwrap();
11076
11077 let multi_buffer = cx.new(|cx| {
11078 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11079 multi_buffer.push_excerpts(
11080 buffer.clone(),
11081 [ExcerptRange::new(0..first_excerpt_end)],
11082 cx,
11083 );
11084 multi_buffer.push_excerpts(
11085 buffer.clone(),
11086 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11087 cx,
11088 );
11089 multi_buffer
11090 });
11091
11092 let editor = workspace
11093 .update(cx, |_, window, cx| {
11094 cx.new(|cx| {
11095 Editor::new(
11096 EditorMode::Full {
11097 scale_ui_elements_with_buffer_font_size: false,
11098 show_active_line_background: false,
11099 sized_by_content: false,
11100 },
11101 multi_buffer.clone(),
11102 Some(project.clone()),
11103 window,
11104 cx,
11105 )
11106 })
11107 })
11108 .unwrap();
11109
11110 let pane = workspace
11111 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11112 .unwrap();
11113 pane.update_in(cx, |pane, window, cx| {
11114 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11115 });
11116
11117 let fake_server = fake_servers.next().await.unwrap();
11118
11119 editor.update_in(cx, |editor, window, cx| {
11120 editor.change_selections(None, window, cx, |s| {
11121 s.select_ranges([
11122 Point::new(1, 11)..Point::new(1, 11),
11123 Point::new(7, 11)..Point::new(7, 11),
11124 ])
11125 });
11126
11127 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11128 });
11129
11130 editor.update_in(cx, |editor, window, cx| {
11131 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11132 });
11133
11134 fake_server
11135 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11136 let completion_item = lsp::CompletionItem {
11137 label: "saturating_sub()".into(),
11138 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11139 lsp::InsertReplaceEdit {
11140 new_text: "saturating_sub()".to_owned(),
11141 insert: lsp::Range::new(
11142 lsp::Position::new(7, 7),
11143 lsp::Position::new(7, 11),
11144 ),
11145 replace: lsp::Range::new(
11146 lsp::Position::new(7, 7),
11147 lsp::Position::new(7, 13),
11148 ),
11149 },
11150 )),
11151 ..lsp::CompletionItem::default()
11152 };
11153
11154 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11155 })
11156 .next()
11157 .await
11158 .unwrap();
11159
11160 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11161 .await;
11162
11163 editor
11164 .update_in(cx, |editor, window, cx| {
11165 editor
11166 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11167 .unwrap()
11168 })
11169 .await
11170 .unwrap();
11171
11172 editor.update(cx, |editor, cx| {
11173 assert_text_with_selections(editor, expected_multibuffer, cx);
11174 })
11175}
11176
11177#[gpui::test]
11178async fn test_completion(cx: &mut TestAppContext) {
11179 init_test(cx, |_| {});
11180
11181 let mut cx = EditorLspTestContext::new_rust(
11182 lsp::ServerCapabilities {
11183 completion_provider: Some(lsp::CompletionOptions {
11184 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11185 resolve_provider: Some(true),
11186 ..Default::default()
11187 }),
11188 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11189 ..Default::default()
11190 },
11191 cx,
11192 )
11193 .await;
11194 let counter = Arc::new(AtomicUsize::new(0));
11195
11196 cx.set_state(indoc! {"
11197 oneˇ
11198 two
11199 three
11200 "});
11201 cx.simulate_keystroke(".");
11202 handle_completion_request(
11203 indoc! {"
11204 one.|<>
11205 two
11206 three
11207 "},
11208 vec!["first_completion", "second_completion"],
11209 true,
11210 counter.clone(),
11211 &mut cx,
11212 )
11213 .await;
11214 cx.condition(|editor, _| editor.context_menu_visible())
11215 .await;
11216 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11217
11218 let _handler = handle_signature_help_request(
11219 &mut cx,
11220 lsp::SignatureHelp {
11221 signatures: vec![lsp::SignatureInformation {
11222 label: "test signature".to_string(),
11223 documentation: None,
11224 parameters: Some(vec![lsp::ParameterInformation {
11225 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11226 documentation: None,
11227 }]),
11228 active_parameter: None,
11229 }],
11230 active_signature: None,
11231 active_parameter: None,
11232 },
11233 );
11234 cx.update_editor(|editor, window, cx| {
11235 assert!(
11236 !editor.signature_help_state.is_shown(),
11237 "No signature help was called for"
11238 );
11239 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11240 });
11241 cx.run_until_parked();
11242 cx.update_editor(|editor, _, _| {
11243 assert!(
11244 !editor.signature_help_state.is_shown(),
11245 "No signature help should be shown when completions menu is open"
11246 );
11247 });
11248
11249 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11250 editor.context_menu_next(&Default::default(), window, cx);
11251 editor
11252 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11253 .unwrap()
11254 });
11255 cx.assert_editor_state(indoc! {"
11256 one.second_completionˇ
11257 two
11258 three
11259 "});
11260
11261 handle_resolve_completion_request(
11262 &mut cx,
11263 Some(vec![
11264 (
11265 //This overlaps with the primary completion edit which is
11266 //misbehavior from the LSP spec, test that we filter it out
11267 indoc! {"
11268 one.second_ˇcompletion
11269 two
11270 threeˇ
11271 "},
11272 "overlapping additional edit",
11273 ),
11274 (
11275 indoc! {"
11276 one.second_completion
11277 two
11278 threeˇ
11279 "},
11280 "\nadditional edit",
11281 ),
11282 ]),
11283 )
11284 .await;
11285 apply_additional_edits.await.unwrap();
11286 cx.assert_editor_state(indoc! {"
11287 one.second_completionˇ
11288 two
11289 three
11290 additional edit
11291 "});
11292
11293 cx.set_state(indoc! {"
11294 one.second_completion
11295 twoˇ
11296 threeˇ
11297 additional edit
11298 "});
11299 cx.simulate_keystroke(" ");
11300 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11301 cx.simulate_keystroke("s");
11302 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11303
11304 cx.assert_editor_state(indoc! {"
11305 one.second_completion
11306 two sˇ
11307 three sˇ
11308 additional edit
11309 "});
11310 handle_completion_request(
11311 indoc! {"
11312 one.second_completion
11313 two s
11314 three <s|>
11315 additional edit
11316 "},
11317 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11318 true,
11319 counter.clone(),
11320 &mut cx,
11321 )
11322 .await;
11323 cx.condition(|editor, _| editor.context_menu_visible())
11324 .await;
11325 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11326
11327 cx.simulate_keystroke("i");
11328
11329 handle_completion_request(
11330 indoc! {"
11331 one.second_completion
11332 two si
11333 three <si|>
11334 additional edit
11335 "},
11336 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11337 true,
11338 counter.clone(),
11339 &mut cx,
11340 )
11341 .await;
11342 cx.condition(|editor, _| editor.context_menu_visible())
11343 .await;
11344 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11345
11346 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11347 editor
11348 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11349 .unwrap()
11350 });
11351 cx.assert_editor_state(indoc! {"
11352 one.second_completion
11353 two sixth_completionˇ
11354 three sixth_completionˇ
11355 additional edit
11356 "});
11357
11358 apply_additional_edits.await.unwrap();
11359
11360 update_test_language_settings(&mut cx, |settings| {
11361 settings.defaults.show_completions_on_input = Some(false);
11362 });
11363 cx.set_state("editorˇ");
11364 cx.simulate_keystroke(".");
11365 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11366 cx.simulate_keystrokes("c l o");
11367 cx.assert_editor_state("editor.cloˇ");
11368 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11369 cx.update_editor(|editor, window, cx| {
11370 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11371 });
11372 handle_completion_request(
11373 "editor.<clo|>",
11374 vec!["close", "clobber"],
11375 true,
11376 counter.clone(),
11377 &mut cx,
11378 )
11379 .await;
11380 cx.condition(|editor, _| editor.context_menu_visible())
11381 .await;
11382 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11383
11384 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11385 editor
11386 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11387 .unwrap()
11388 });
11389 cx.assert_editor_state("editor.closeˇ");
11390 handle_resolve_completion_request(&mut cx, None).await;
11391 apply_additional_edits.await.unwrap();
11392}
11393
11394#[gpui::test]
11395async fn test_completion_reuse(cx: &mut TestAppContext) {
11396 init_test(cx, |_| {});
11397
11398 let mut cx = EditorLspTestContext::new_rust(
11399 lsp::ServerCapabilities {
11400 completion_provider: Some(lsp::CompletionOptions {
11401 trigger_characters: Some(vec![".".to_string()]),
11402 ..Default::default()
11403 }),
11404 ..Default::default()
11405 },
11406 cx,
11407 )
11408 .await;
11409
11410 let counter = Arc::new(AtomicUsize::new(0));
11411 cx.set_state("objˇ");
11412 cx.simulate_keystroke(".");
11413
11414 // Initial completion request returns complete results
11415 let is_incomplete = false;
11416 handle_completion_request(
11417 "obj.|<>",
11418 vec!["a", "ab", "abc"],
11419 is_incomplete,
11420 counter.clone(),
11421 &mut cx,
11422 )
11423 .await;
11424 cx.run_until_parked();
11425 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11426 cx.assert_editor_state("obj.ˇ");
11427 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11428
11429 // Type "a" - filters existing completions
11430 cx.simulate_keystroke("a");
11431 cx.run_until_parked();
11432 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11433 cx.assert_editor_state("obj.aˇ");
11434 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11435
11436 // Type "b" - filters existing completions
11437 cx.simulate_keystroke("b");
11438 cx.run_until_parked();
11439 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11440 cx.assert_editor_state("obj.abˇ");
11441 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11442
11443 // Type "c" - filters existing completions
11444 cx.simulate_keystroke("c");
11445 cx.run_until_parked();
11446 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11447 cx.assert_editor_state("obj.abcˇ");
11448 check_displayed_completions(vec!["abc"], &mut cx);
11449
11450 // Backspace to delete "c" - filters existing completions
11451 cx.update_editor(|editor, window, cx| {
11452 editor.backspace(&Backspace, window, cx);
11453 });
11454 cx.run_until_parked();
11455 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11456 cx.assert_editor_state("obj.abˇ");
11457 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11458
11459 // Moving cursor to the left dismisses menu.
11460 cx.update_editor(|editor, window, cx| {
11461 editor.move_left(&MoveLeft, window, cx);
11462 });
11463 cx.run_until_parked();
11464 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11465 cx.assert_editor_state("obj.aˇb");
11466 cx.update_editor(|editor, _, _| {
11467 assert_eq!(editor.context_menu_visible(), false);
11468 });
11469
11470 // Type "b" - new request
11471 cx.simulate_keystroke("b");
11472 let is_incomplete = false;
11473 handle_completion_request(
11474 "obj.<ab|>a",
11475 vec!["ab", "abc"],
11476 is_incomplete,
11477 counter.clone(),
11478 &mut cx,
11479 )
11480 .await;
11481 cx.run_until_parked();
11482 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11483 cx.assert_editor_state("obj.abˇb");
11484 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11485
11486 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11487 cx.update_editor(|editor, window, cx| {
11488 editor.backspace(&Backspace, window, cx);
11489 });
11490 let is_incomplete = false;
11491 handle_completion_request(
11492 "obj.<a|>b",
11493 vec!["a", "ab", "abc"],
11494 is_incomplete,
11495 counter.clone(),
11496 &mut cx,
11497 )
11498 .await;
11499 cx.run_until_parked();
11500 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11501 cx.assert_editor_state("obj.aˇb");
11502 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11503
11504 // Backspace to delete "a" - dismisses menu.
11505 cx.update_editor(|editor, window, cx| {
11506 editor.backspace(&Backspace, window, cx);
11507 });
11508 cx.run_until_parked();
11509 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11510 cx.assert_editor_state("obj.ˇb");
11511 cx.update_editor(|editor, _, _| {
11512 assert_eq!(editor.context_menu_visible(), false);
11513 });
11514}
11515
11516#[gpui::test]
11517async fn test_word_completion(cx: &mut TestAppContext) {
11518 let lsp_fetch_timeout_ms = 10;
11519 init_test(cx, |language_settings| {
11520 language_settings.defaults.completions = Some(CompletionSettings {
11521 words: WordsCompletionMode::Fallback,
11522 lsp: true,
11523 lsp_fetch_timeout_ms: 10,
11524 lsp_insert_mode: LspInsertMode::Insert,
11525 });
11526 });
11527
11528 let mut cx = EditorLspTestContext::new_rust(
11529 lsp::ServerCapabilities {
11530 completion_provider: Some(lsp::CompletionOptions {
11531 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11532 ..lsp::CompletionOptions::default()
11533 }),
11534 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11535 ..lsp::ServerCapabilities::default()
11536 },
11537 cx,
11538 )
11539 .await;
11540
11541 let throttle_completions = Arc::new(AtomicBool::new(false));
11542
11543 let lsp_throttle_completions = throttle_completions.clone();
11544 let _completion_requests_handler =
11545 cx.lsp
11546 .server
11547 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11548 let lsp_throttle_completions = lsp_throttle_completions.clone();
11549 let cx = cx.clone();
11550 async move {
11551 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11552 cx.background_executor()
11553 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11554 .await;
11555 }
11556 Ok(Some(lsp::CompletionResponse::Array(vec![
11557 lsp::CompletionItem {
11558 label: "first".into(),
11559 ..lsp::CompletionItem::default()
11560 },
11561 lsp::CompletionItem {
11562 label: "last".into(),
11563 ..lsp::CompletionItem::default()
11564 },
11565 ])))
11566 }
11567 });
11568
11569 cx.set_state(indoc! {"
11570 oneˇ
11571 two
11572 three
11573 "});
11574 cx.simulate_keystroke(".");
11575 cx.executor().run_until_parked();
11576 cx.condition(|editor, _| editor.context_menu_visible())
11577 .await;
11578 cx.update_editor(|editor, window, cx| {
11579 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11580 {
11581 assert_eq!(
11582 completion_menu_entries(&menu),
11583 &["first", "last"],
11584 "When LSP server is fast to reply, no fallback word completions are used"
11585 );
11586 } else {
11587 panic!("expected completion menu to be open");
11588 }
11589 editor.cancel(&Cancel, window, cx);
11590 });
11591 cx.executor().run_until_parked();
11592 cx.condition(|editor, _| !editor.context_menu_visible())
11593 .await;
11594
11595 throttle_completions.store(true, atomic::Ordering::Release);
11596 cx.simulate_keystroke(".");
11597 cx.executor()
11598 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11599 cx.executor().run_until_parked();
11600 cx.condition(|editor, _| editor.context_menu_visible())
11601 .await;
11602 cx.update_editor(|editor, _, _| {
11603 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11604 {
11605 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11606 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11607 } else {
11608 panic!("expected completion menu to be open");
11609 }
11610 });
11611}
11612
11613#[gpui::test]
11614async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11615 init_test(cx, |language_settings| {
11616 language_settings.defaults.completions = Some(CompletionSettings {
11617 words: WordsCompletionMode::Enabled,
11618 lsp: true,
11619 lsp_fetch_timeout_ms: 0,
11620 lsp_insert_mode: LspInsertMode::Insert,
11621 });
11622 });
11623
11624 let mut cx = EditorLspTestContext::new_rust(
11625 lsp::ServerCapabilities {
11626 completion_provider: Some(lsp::CompletionOptions {
11627 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11628 ..lsp::CompletionOptions::default()
11629 }),
11630 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11631 ..lsp::ServerCapabilities::default()
11632 },
11633 cx,
11634 )
11635 .await;
11636
11637 let _completion_requests_handler =
11638 cx.lsp
11639 .server
11640 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11641 Ok(Some(lsp::CompletionResponse::Array(vec![
11642 lsp::CompletionItem {
11643 label: "first".into(),
11644 ..lsp::CompletionItem::default()
11645 },
11646 lsp::CompletionItem {
11647 label: "last".into(),
11648 ..lsp::CompletionItem::default()
11649 },
11650 ])))
11651 });
11652
11653 cx.set_state(indoc! {"ˇ
11654 first
11655 last
11656 second
11657 "});
11658 cx.simulate_keystroke(".");
11659 cx.executor().run_until_parked();
11660 cx.condition(|editor, _| editor.context_menu_visible())
11661 .await;
11662 cx.update_editor(|editor, _, _| {
11663 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11664 {
11665 assert_eq!(
11666 completion_menu_entries(&menu),
11667 &["first", "last", "second"],
11668 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11669 );
11670 } else {
11671 panic!("expected completion menu to be open");
11672 }
11673 });
11674}
11675
11676#[gpui::test]
11677async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11678 init_test(cx, |language_settings| {
11679 language_settings.defaults.completions = Some(CompletionSettings {
11680 words: WordsCompletionMode::Disabled,
11681 lsp: true,
11682 lsp_fetch_timeout_ms: 0,
11683 lsp_insert_mode: LspInsertMode::Insert,
11684 });
11685 });
11686
11687 let mut cx = EditorLspTestContext::new_rust(
11688 lsp::ServerCapabilities {
11689 completion_provider: Some(lsp::CompletionOptions {
11690 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11691 ..lsp::CompletionOptions::default()
11692 }),
11693 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11694 ..lsp::ServerCapabilities::default()
11695 },
11696 cx,
11697 )
11698 .await;
11699
11700 let _completion_requests_handler =
11701 cx.lsp
11702 .server
11703 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11704 panic!("LSP completions should not be queried when dealing with word completions")
11705 });
11706
11707 cx.set_state(indoc! {"ˇ
11708 first
11709 last
11710 second
11711 "});
11712 cx.update_editor(|editor, window, cx| {
11713 editor.show_word_completions(&ShowWordCompletions, window, cx);
11714 });
11715 cx.executor().run_until_parked();
11716 cx.condition(|editor, _| editor.context_menu_visible())
11717 .await;
11718 cx.update_editor(|editor, _, _| {
11719 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11720 {
11721 assert_eq!(
11722 completion_menu_entries(&menu),
11723 &["first", "last", "second"],
11724 "`ShowWordCompletions` action should show word completions"
11725 );
11726 } else {
11727 panic!("expected completion menu to be open");
11728 }
11729 });
11730
11731 cx.simulate_keystroke("l");
11732 cx.executor().run_until_parked();
11733 cx.condition(|editor, _| editor.context_menu_visible())
11734 .await;
11735 cx.update_editor(|editor, _, _| {
11736 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11737 {
11738 assert_eq!(
11739 completion_menu_entries(&menu),
11740 &["last"],
11741 "After showing word completions, further editing should filter them and not query the LSP"
11742 );
11743 } else {
11744 panic!("expected completion menu to be open");
11745 }
11746 });
11747}
11748
11749#[gpui::test]
11750async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11751 init_test(cx, |language_settings| {
11752 language_settings.defaults.completions = Some(CompletionSettings {
11753 words: WordsCompletionMode::Fallback,
11754 lsp: false,
11755 lsp_fetch_timeout_ms: 0,
11756 lsp_insert_mode: LspInsertMode::Insert,
11757 });
11758 });
11759
11760 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11761
11762 cx.set_state(indoc! {"ˇ
11763 0_usize
11764 let
11765 33
11766 4.5f32
11767 "});
11768 cx.update_editor(|editor, window, cx| {
11769 editor.show_completions(&ShowCompletions::default(), window, cx);
11770 });
11771 cx.executor().run_until_parked();
11772 cx.condition(|editor, _| editor.context_menu_visible())
11773 .await;
11774 cx.update_editor(|editor, window, cx| {
11775 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11776 {
11777 assert_eq!(
11778 completion_menu_entries(&menu),
11779 &["let"],
11780 "With no digits in the completion query, no digits should be in the word completions"
11781 );
11782 } else {
11783 panic!("expected completion menu to be open");
11784 }
11785 editor.cancel(&Cancel, window, cx);
11786 });
11787
11788 cx.set_state(indoc! {"3ˇ
11789 0_usize
11790 let
11791 3
11792 33.35f32
11793 "});
11794 cx.update_editor(|editor, window, cx| {
11795 editor.show_completions(&ShowCompletions::default(), window, cx);
11796 });
11797 cx.executor().run_until_parked();
11798 cx.condition(|editor, _| editor.context_menu_visible())
11799 .await;
11800 cx.update_editor(|editor, _, _| {
11801 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11802 {
11803 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11804 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11805 } else {
11806 panic!("expected completion menu to be open");
11807 }
11808 });
11809}
11810
11811fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11812 let position = || lsp::Position {
11813 line: params.text_document_position.position.line,
11814 character: params.text_document_position.position.character,
11815 };
11816 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11817 range: lsp::Range {
11818 start: position(),
11819 end: position(),
11820 },
11821 new_text: text.to_string(),
11822 }))
11823}
11824
11825#[gpui::test]
11826async fn test_multiline_completion(cx: &mut TestAppContext) {
11827 init_test(cx, |_| {});
11828
11829 let fs = FakeFs::new(cx.executor());
11830 fs.insert_tree(
11831 path!("/a"),
11832 json!({
11833 "main.ts": "a",
11834 }),
11835 )
11836 .await;
11837
11838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11839 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11840 let typescript_language = Arc::new(Language::new(
11841 LanguageConfig {
11842 name: "TypeScript".into(),
11843 matcher: LanguageMatcher {
11844 path_suffixes: vec!["ts".to_string()],
11845 ..LanguageMatcher::default()
11846 },
11847 line_comments: vec!["// ".into()],
11848 ..LanguageConfig::default()
11849 },
11850 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11851 ));
11852 language_registry.add(typescript_language.clone());
11853 let mut fake_servers = language_registry.register_fake_lsp(
11854 "TypeScript",
11855 FakeLspAdapter {
11856 capabilities: lsp::ServerCapabilities {
11857 completion_provider: Some(lsp::CompletionOptions {
11858 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11859 ..lsp::CompletionOptions::default()
11860 }),
11861 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11862 ..lsp::ServerCapabilities::default()
11863 },
11864 // Emulate vtsls label generation
11865 label_for_completion: Some(Box::new(|item, _| {
11866 let text = if let Some(description) = item
11867 .label_details
11868 .as_ref()
11869 .and_then(|label_details| label_details.description.as_ref())
11870 {
11871 format!("{} {}", item.label, description)
11872 } else if let Some(detail) = &item.detail {
11873 format!("{} {}", item.label, detail)
11874 } else {
11875 item.label.clone()
11876 };
11877 let len = text.len();
11878 Some(language::CodeLabel {
11879 text,
11880 runs: Vec::new(),
11881 filter_range: 0..len,
11882 })
11883 })),
11884 ..FakeLspAdapter::default()
11885 },
11886 );
11887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11888 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11889 let worktree_id = workspace
11890 .update(cx, |workspace, _window, cx| {
11891 workspace.project().update(cx, |project, cx| {
11892 project.worktrees(cx).next().unwrap().read(cx).id()
11893 })
11894 })
11895 .unwrap();
11896 let _buffer = project
11897 .update(cx, |project, cx| {
11898 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11899 })
11900 .await
11901 .unwrap();
11902 let editor = workspace
11903 .update(cx, |workspace, window, cx| {
11904 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11905 })
11906 .unwrap()
11907 .await
11908 .unwrap()
11909 .downcast::<Editor>()
11910 .unwrap();
11911 let fake_server = fake_servers.next().await.unwrap();
11912
11913 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11914 let multiline_label_2 = "a\nb\nc\n";
11915 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11916 let multiline_description = "d\ne\nf\n";
11917 let multiline_detail_2 = "g\nh\ni\n";
11918
11919 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11920 move |params, _| async move {
11921 Ok(Some(lsp::CompletionResponse::Array(vec![
11922 lsp::CompletionItem {
11923 label: multiline_label.to_string(),
11924 text_edit: gen_text_edit(¶ms, "new_text_1"),
11925 ..lsp::CompletionItem::default()
11926 },
11927 lsp::CompletionItem {
11928 label: "single line label 1".to_string(),
11929 detail: Some(multiline_detail.to_string()),
11930 text_edit: gen_text_edit(¶ms, "new_text_2"),
11931 ..lsp::CompletionItem::default()
11932 },
11933 lsp::CompletionItem {
11934 label: "single line label 2".to_string(),
11935 label_details: Some(lsp::CompletionItemLabelDetails {
11936 description: Some(multiline_description.to_string()),
11937 detail: None,
11938 }),
11939 text_edit: gen_text_edit(¶ms, "new_text_2"),
11940 ..lsp::CompletionItem::default()
11941 },
11942 lsp::CompletionItem {
11943 label: multiline_label_2.to_string(),
11944 detail: Some(multiline_detail_2.to_string()),
11945 text_edit: gen_text_edit(¶ms, "new_text_3"),
11946 ..lsp::CompletionItem::default()
11947 },
11948 lsp::CompletionItem {
11949 label: "Label with many spaces and \t but without newlines".to_string(),
11950 detail: Some(
11951 "Details with many spaces and \t but without newlines".to_string(),
11952 ),
11953 text_edit: gen_text_edit(¶ms, "new_text_4"),
11954 ..lsp::CompletionItem::default()
11955 },
11956 ])))
11957 },
11958 );
11959
11960 editor.update_in(cx, |editor, window, cx| {
11961 cx.focus_self(window);
11962 editor.move_to_end(&MoveToEnd, window, cx);
11963 editor.handle_input(".", window, cx);
11964 });
11965 cx.run_until_parked();
11966 completion_handle.next().await.unwrap();
11967
11968 editor.update(cx, |editor, _| {
11969 assert!(editor.context_menu_visible());
11970 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11971 {
11972 let completion_labels = menu
11973 .completions
11974 .borrow()
11975 .iter()
11976 .map(|c| c.label.text.clone())
11977 .collect::<Vec<_>>();
11978 assert_eq!(
11979 completion_labels,
11980 &[
11981 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11982 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11983 "single line label 2 d e f ",
11984 "a b c g h i ",
11985 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11986 ],
11987 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11988 );
11989
11990 for completion in menu
11991 .completions
11992 .borrow()
11993 .iter() {
11994 assert_eq!(
11995 completion.label.filter_range,
11996 0..completion.label.text.len(),
11997 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11998 );
11999 }
12000 } else {
12001 panic!("expected completion menu to be open");
12002 }
12003 });
12004}
12005
12006#[gpui::test]
12007async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12008 init_test(cx, |_| {});
12009 let mut cx = EditorLspTestContext::new_rust(
12010 lsp::ServerCapabilities {
12011 completion_provider: Some(lsp::CompletionOptions {
12012 trigger_characters: Some(vec![".".to_string()]),
12013 ..Default::default()
12014 }),
12015 ..Default::default()
12016 },
12017 cx,
12018 )
12019 .await;
12020 cx.lsp
12021 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12022 Ok(Some(lsp::CompletionResponse::Array(vec![
12023 lsp::CompletionItem {
12024 label: "first".into(),
12025 ..Default::default()
12026 },
12027 lsp::CompletionItem {
12028 label: "last".into(),
12029 ..Default::default()
12030 },
12031 ])))
12032 });
12033 cx.set_state("variableˇ");
12034 cx.simulate_keystroke(".");
12035 cx.executor().run_until_parked();
12036
12037 cx.update_editor(|editor, _, _| {
12038 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12039 {
12040 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12041 } else {
12042 panic!("expected completion menu to be open");
12043 }
12044 });
12045
12046 cx.update_editor(|editor, window, cx| {
12047 editor.move_page_down(&MovePageDown::default(), window, cx);
12048 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12049 {
12050 assert!(
12051 menu.selected_item == 1,
12052 "expected PageDown to select the last item from the context menu"
12053 );
12054 } else {
12055 panic!("expected completion menu to stay open after PageDown");
12056 }
12057 });
12058
12059 cx.update_editor(|editor, window, cx| {
12060 editor.move_page_up(&MovePageUp::default(), window, cx);
12061 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12062 {
12063 assert!(
12064 menu.selected_item == 0,
12065 "expected PageUp to select the first item from the context menu"
12066 );
12067 } else {
12068 panic!("expected completion menu to stay open after PageUp");
12069 }
12070 });
12071}
12072
12073#[gpui::test]
12074async fn test_as_is_completions(cx: &mut TestAppContext) {
12075 init_test(cx, |_| {});
12076 let mut cx = EditorLspTestContext::new_rust(
12077 lsp::ServerCapabilities {
12078 completion_provider: Some(lsp::CompletionOptions {
12079 ..Default::default()
12080 }),
12081 ..Default::default()
12082 },
12083 cx,
12084 )
12085 .await;
12086 cx.lsp
12087 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12088 Ok(Some(lsp::CompletionResponse::Array(vec![
12089 lsp::CompletionItem {
12090 label: "unsafe".into(),
12091 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12092 range: lsp::Range {
12093 start: lsp::Position {
12094 line: 1,
12095 character: 2,
12096 },
12097 end: lsp::Position {
12098 line: 1,
12099 character: 3,
12100 },
12101 },
12102 new_text: "unsafe".to_string(),
12103 })),
12104 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12105 ..Default::default()
12106 },
12107 ])))
12108 });
12109 cx.set_state("fn a() {}\n nˇ");
12110 cx.executor().run_until_parked();
12111 cx.update_editor(|editor, window, cx| {
12112 editor.show_completions(
12113 &ShowCompletions {
12114 trigger: Some("\n".into()),
12115 },
12116 window,
12117 cx,
12118 );
12119 });
12120 cx.executor().run_until_parked();
12121
12122 cx.update_editor(|editor, window, cx| {
12123 editor.confirm_completion(&Default::default(), window, cx)
12124 });
12125 cx.executor().run_until_parked();
12126 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12127}
12128
12129#[gpui::test]
12130async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12131 init_test(cx, |_| {});
12132
12133 let mut cx = EditorLspTestContext::new_rust(
12134 lsp::ServerCapabilities {
12135 completion_provider: Some(lsp::CompletionOptions {
12136 trigger_characters: Some(vec![".".to_string()]),
12137 resolve_provider: Some(true),
12138 ..Default::default()
12139 }),
12140 ..Default::default()
12141 },
12142 cx,
12143 )
12144 .await;
12145
12146 cx.set_state("fn main() { let a = 2ˇ; }");
12147 cx.simulate_keystroke(".");
12148 let completion_item = lsp::CompletionItem {
12149 label: "Some".into(),
12150 kind: Some(lsp::CompletionItemKind::SNIPPET),
12151 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12152 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12153 kind: lsp::MarkupKind::Markdown,
12154 value: "```rust\nSome(2)\n```".to_string(),
12155 })),
12156 deprecated: Some(false),
12157 sort_text: Some("Some".to_string()),
12158 filter_text: Some("Some".to_string()),
12159 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12160 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12161 range: lsp::Range {
12162 start: lsp::Position {
12163 line: 0,
12164 character: 22,
12165 },
12166 end: lsp::Position {
12167 line: 0,
12168 character: 22,
12169 },
12170 },
12171 new_text: "Some(2)".to_string(),
12172 })),
12173 additional_text_edits: Some(vec![lsp::TextEdit {
12174 range: lsp::Range {
12175 start: lsp::Position {
12176 line: 0,
12177 character: 20,
12178 },
12179 end: lsp::Position {
12180 line: 0,
12181 character: 22,
12182 },
12183 },
12184 new_text: "".to_string(),
12185 }]),
12186 ..Default::default()
12187 };
12188
12189 let closure_completion_item = completion_item.clone();
12190 let counter = Arc::new(AtomicUsize::new(0));
12191 let counter_clone = counter.clone();
12192 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12193 let task_completion_item = closure_completion_item.clone();
12194 counter_clone.fetch_add(1, atomic::Ordering::Release);
12195 async move {
12196 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12197 is_incomplete: true,
12198 item_defaults: None,
12199 items: vec![task_completion_item],
12200 })))
12201 }
12202 });
12203
12204 cx.condition(|editor, _| editor.context_menu_visible())
12205 .await;
12206 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12207 assert!(request.next().await.is_some());
12208 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12209
12210 cx.simulate_keystrokes("S o m");
12211 cx.condition(|editor, _| editor.context_menu_visible())
12212 .await;
12213 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12214 assert!(request.next().await.is_some());
12215 assert!(request.next().await.is_some());
12216 assert!(request.next().await.is_some());
12217 request.close();
12218 assert!(request.next().await.is_none());
12219 assert_eq!(
12220 counter.load(atomic::Ordering::Acquire),
12221 4,
12222 "With the completions menu open, only one LSP request should happen per input"
12223 );
12224}
12225
12226#[gpui::test]
12227async fn test_toggle_comment(cx: &mut TestAppContext) {
12228 init_test(cx, |_| {});
12229 let mut cx = EditorTestContext::new(cx).await;
12230 let language = Arc::new(Language::new(
12231 LanguageConfig {
12232 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12233 ..Default::default()
12234 },
12235 Some(tree_sitter_rust::LANGUAGE.into()),
12236 ));
12237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12238
12239 // If multiple selections intersect a line, the line is only toggled once.
12240 cx.set_state(indoc! {"
12241 fn a() {
12242 «//b();
12243 ˇ»// «c();
12244 //ˇ» d();
12245 }
12246 "});
12247
12248 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12249
12250 cx.assert_editor_state(indoc! {"
12251 fn a() {
12252 «b();
12253 c();
12254 ˇ» d();
12255 }
12256 "});
12257
12258 // The comment prefix is inserted at the same column for every line in a
12259 // selection.
12260 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12261
12262 cx.assert_editor_state(indoc! {"
12263 fn a() {
12264 // «b();
12265 // c();
12266 ˇ»// d();
12267 }
12268 "});
12269
12270 // If a selection ends at the beginning of a line, that line is not toggled.
12271 cx.set_selections_state(indoc! {"
12272 fn a() {
12273 // b();
12274 «// c();
12275 ˇ» // d();
12276 }
12277 "});
12278
12279 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12280
12281 cx.assert_editor_state(indoc! {"
12282 fn a() {
12283 // b();
12284 «c();
12285 ˇ» // d();
12286 }
12287 "});
12288
12289 // If a selection span a single line and is empty, the line is toggled.
12290 cx.set_state(indoc! {"
12291 fn a() {
12292 a();
12293 b();
12294 ˇ
12295 }
12296 "});
12297
12298 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12299
12300 cx.assert_editor_state(indoc! {"
12301 fn a() {
12302 a();
12303 b();
12304 //•ˇ
12305 }
12306 "});
12307
12308 // If a selection span multiple lines, empty lines are not toggled.
12309 cx.set_state(indoc! {"
12310 fn a() {
12311 «a();
12312
12313 c();ˇ»
12314 }
12315 "});
12316
12317 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12318
12319 cx.assert_editor_state(indoc! {"
12320 fn a() {
12321 // «a();
12322
12323 // c();ˇ»
12324 }
12325 "});
12326
12327 // If a selection includes multiple comment prefixes, all lines are uncommented.
12328 cx.set_state(indoc! {"
12329 fn a() {
12330 «// a();
12331 /// b();
12332 //! c();ˇ»
12333 }
12334 "});
12335
12336 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12337
12338 cx.assert_editor_state(indoc! {"
12339 fn a() {
12340 «a();
12341 b();
12342 c();ˇ»
12343 }
12344 "});
12345}
12346
12347#[gpui::test]
12348async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12349 init_test(cx, |_| {});
12350 let mut cx = EditorTestContext::new(cx).await;
12351 let language = Arc::new(Language::new(
12352 LanguageConfig {
12353 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12354 ..Default::default()
12355 },
12356 Some(tree_sitter_rust::LANGUAGE.into()),
12357 ));
12358 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12359
12360 let toggle_comments = &ToggleComments {
12361 advance_downwards: false,
12362 ignore_indent: true,
12363 };
12364
12365 // If multiple selections intersect a line, the line is only toggled once.
12366 cx.set_state(indoc! {"
12367 fn a() {
12368 // «b();
12369 // c();
12370 // ˇ» d();
12371 }
12372 "});
12373
12374 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12375
12376 cx.assert_editor_state(indoc! {"
12377 fn a() {
12378 «b();
12379 c();
12380 ˇ» d();
12381 }
12382 "});
12383
12384 // The comment prefix is inserted at the beginning of each line
12385 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12386
12387 cx.assert_editor_state(indoc! {"
12388 fn a() {
12389 // «b();
12390 // c();
12391 // ˇ» d();
12392 }
12393 "});
12394
12395 // If a selection ends at the beginning of a line, that line is not toggled.
12396 cx.set_selections_state(indoc! {"
12397 fn a() {
12398 // b();
12399 // «c();
12400 ˇ»// d();
12401 }
12402 "});
12403
12404 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12405
12406 cx.assert_editor_state(indoc! {"
12407 fn a() {
12408 // b();
12409 «c();
12410 ˇ»// d();
12411 }
12412 "});
12413
12414 // If a selection span a single line and is empty, the line is toggled.
12415 cx.set_state(indoc! {"
12416 fn a() {
12417 a();
12418 b();
12419 ˇ
12420 }
12421 "});
12422
12423 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12424
12425 cx.assert_editor_state(indoc! {"
12426 fn a() {
12427 a();
12428 b();
12429 //ˇ
12430 }
12431 "});
12432
12433 // If a selection span multiple lines, empty lines are not toggled.
12434 cx.set_state(indoc! {"
12435 fn a() {
12436 «a();
12437
12438 c();ˇ»
12439 }
12440 "});
12441
12442 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12443
12444 cx.assert_editor_state(indoc! {"
12445 fn a() {
12446 // «a();
12447
12448 // c();ˇ»
12449 }
12450 "});
12451
12452 // If a selection includes multiple comment prefixes, all lines are uncommented.
12453 cx.set_state(indoc! {"
12454 fn a() {
12455 // «a();
12456 /// b();
12457 //! c();ˇ»
12458 }
12459 "});
12460
12461 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12462
12463 cx.assert_editor_state(indoc! {"
12464 fn a() {
12465 «a();
12466 b();
12467 c();ˇ»
12468 }
12469 "});
12470}
12471
12472#[gpui::test]
12473async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12474 init_test(cx, |_| {});
12475
12476 let language = Arc::new(Language::new(
12477 LanguageConfig {
12478 line_comments: vec!["// ".into()],
12479 ..Default::default()
12480 },
12481 Some(tree_sitter_rust::LANGUAGE.into()),
12482 ));
12483
12484 let mut cx = EditorTestContext::new(cx).await;
12485
12486 cx.language_registry().add(language.clone());
12487 cx.update_buffer(|buffer, cx| {
12488 buffer.set_language(Some(language), cx);
12489 });
12490
12491 let toggle_comments = &ToggleComments {
12492 advance_downwards: true,
12493 ignore_indent: false,
12494 };
12495
12496 // Single cursor on one line -> advance
12497 // Cursor moves horizontally 3 characters as well on non-blank line
12498 cx.set_state(indoc!(
12499 "fn a() {
12500 ˇdog();
12501 cat();
12502 }"
12503 ));
12504 cx.update_editor(|editor, window, cx| {
12505 editor.toggle_comments(toggle_comments, window, cx);
12506 });
12507 cx.assert_editor_state(indoc!(
12508 "fn a() {
12509 // dog();
12510 catˇ();
12511 }"
12512 ));
12513
12514 // Single selection on one line -> don't advance
12515 cx.set_state(indoc!(
12516 "fn a() {
12517 «dog()ˇ»;
12518 cat();
12519 }"
12520 ));
12521 cx.update_editor(|editor, window, cx| {
12522 editor.toggle_comments(toggle_comments, window, cx);
12523 });
12524 cx.assert_editor_state(indoc!(
12525 "fn a() {
12526 // «dog()ˇ»;
12527 cat();
12528 }"
12529 ));
12530
12531 // Multiple cursors on one line -> advance
12532 cx.set_state(indoc!(
12533 "fn a() {
12534 ˇdˇog();
12535 cat();
12536 }"
12537 ));
12538 cx.update_editor(|editor, window, cx| {
12539 editor.toggle_comments(toggle_comments, window, cx);
12540 });
12541 cx.assert_editor_state(indoc!(
12542 "fn a() {
12543 // dog();
12544 catˇ(ˇ);
12545 }"
12546 ));
12547
12548 // Multiple cursors on one line, with selection -> don't advance
12549 cx.set_state(indoc!(
12550 "fn a() {
12551 ˇdˇog«()ˇ»;
12552 cat();
12553 }"
12554 ));
12555 cx.update_editor(|editor, window, cx| {
12556 editor.toggle_comments(toggle_comments, window, cx);
12557 });
12558 cx.assert_editor_state(indoc!(
12559 "fn a() {
12560 // ˇdˇog«()ˇ»;
12561 cat();
12562 }"
12563 ));
12564
12565 // Single cursor on one line -> advance
12566 // Cursor moves to column 0 on blank line
12567 cx.set_state(indoc!(
12568 "fn a() {
12569 ˇdog();
12570
12571 cat();
12572 }"
12573 ));
12574 cx.update_editor(|editor, window, cx| {
12575 editor.toggle_comments(toggle_comments, window, cx);
12576 });
12577 cx.assert_editor_state(indoc!(
12578 "fn a() {
12579 // dog();
12580 ˇ
12581 cat();
12582 }"
12583 ));
12584
12585 // Single cursor on one line -> advance
12586 // Cursor starts and ends at column 0
12587 cx.set_state(indoc!(
12588 "fn a() {
12589 ˇ dog();
12590 cat();
12591 }"
12592 ));
12593 cx.update_editor(|editor, window, cx| {
12594 editor.toggle_comments(toggle_comments, window, cx);
12595 });
12596 cx.assert_editor_state(indoc!(
12597 "fn a() {
12598 // dog();
12599 ˇ cat();
12600 }"
12601 ));
12602}
12603
12604#[gpui::test]
12605async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12606 init_test(cx, |_| {});
12607
12608 let mut cx = EditorTestContext::new(cx).await;
12609
12610 let html_language = Arc::new(
12611 Language::new(
12612 LanguageConfig {
12613 name: "HTML".into(),
12614 block_comment: Some(("<!-- ".into(), " -->".into())),
12615 ..Default::default()
12616 },
12617 Some(tree_sitter_html::LANGUAGE.into()),
12618 )
12619 .with_injection_query(
12620 r#"
12621 (script_element
12622 (raw_text) @injection.content
12623 (#set! injection.language "javascript"))
12624 "#,
12625 )
12626 .unwrap(),
12627 );
12628
12629 let javascript_language = Arc::new(Language::new(
12630 LanguageConfig {
12631 name: "JavaScript".into(),
12632 line_comments: vec!["// ".into()],
12633 ..Default::default()
12634 },
12635 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12636 ));
12637
12638 cx.language_registry().add(html_language.clone());
12639 cx.language_registry().add(javascript_language.clone());
12640 cx.update_buffer(|buffer, cx| {
12641 buffer.set_language(Some(html_language), cx);
12642 });
12643
12644 // Toggle comments for empty selections
12645 cx.set_state(
12646 &r#"
12647 <p>A</p>ˇ
12648 <p>B</p>ˇ
12649 <p>C</p>ˇ
12650 "#
12651 .unindent(),
12652 );
12653 cx.update_editor(|editor, window, cx| {
12654 editor.toggle_comments(&ToggleComments::default(), window, cx)
12655 });
12656 cx.assert_editor_state(
12657 &r#"
12658 <!-- <p>A</p>ˇ -->
12659 <!-- <p>B</p>ˇ -->
12660 <!-- <p>C</p>ˇ -->
12661 "#
12662 .unindent(),
12663 );
12664 cx.update_editor(|editor, window, cx| {
12665 editor.toggle_comments(&ToggleComments::default(), window, cx)
12666 });
12667 cx.assert_editor_state(
12668 &r#"
12669 <p>A</p>ˇ
12670 <p>B</p>ˇ
12671 <p>C</p>ˇ
12672 "#
12673 .unindent(),
12674 );
12675
12676 // Toggle comments for mixture of empty and non-empty selections, where
12677 // multiple selections occupy a given line.
12678 cx.set_state(
12679 &r#"
12680 <p>A«</p>
12681 <p>ˇ»B</p>ˇ
12682 <p>C«</p>
12683 <p>ˇ»D</p>ˇ
12684 "#
12685 .unindent(),
12686 );
12687
12688 cx.update_editor(|editor, window, cx| {
12689 editor.toggle_comments(&ToggleComments::default(), window, cx)
12690 });
12691 cx.assert_editor_state(
12692 &r#"
12693 <!-- <p>A«</p>
12694 <p>ˇ»B</p>ˇ -->
12695 <!-- <p>C«</p>
12696 <p>ˇ»D</p>ˇ -->
12697 "#
12698 .unindent(),
12699 );
12700 cx.update_editor(|editor, window, cx| {
12701 editor.toggle_comments(&ToggleComments::default(), window, cx)
12702 });
12703 cx.assert_editor_state(
12704 &r#"
12705 <p>A«</p>
12706 <p>ˇ»B</p>ˇ
12707 <p>C«</p>
12708 <p>ˇ»D</p>ˇ
12709 "#
12710 .unindent(),
12711 );
12712
12713 // Toggle comments when different languages are active for different
12714 // selections.
12715 cx.set_state(
12716 &r#"
12717 ˇ<script>
12718 ˇvar x = new Y();
12719 ˇ</script>
12720 "#
12721 .unindent(),
12722 );
12723 cx.executor().run_until_parked();
12724 cx.update_editor(|editor, window, cx| {
12725 editor.toggle_comments(&ToggleComments::default(), window, cx)
12726 });
12727 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12728 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12729 cx.assert_editor_state(
12730 &r#"
12731 <!-- ˇ<script> -->
12732 // ˇvar x = new Y();
12733 <!-- ˇ</script> -->
12734 "#
12735 .unindent(),
12736 );
12737}
12738
12739#[gpui::test]
12740fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12741 init_test(cx, |_| {});
12742
12743 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12744 let multibuffer = cx.new(|cx| {
12745 let mut multibuffer = MultiBuffer::new(ReadWrite);
12746 multibuffer.push_excerpts(
12747 buffer.clone(),
12748 [
12749 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12750 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12751 ],
12752 cx,
12753 );
12754 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12755 multibuffer
12756 });
12757
12758 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12759 editor.update_in(cx, |editor, window, cx| {
12760 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12761 editor.change_selections(None, window, cx, |s| {
12762 s.select_ranges([
12763 Point::new(0, 0)..Point::new(0, 0),
12764 Point::new(1, 0)..Point::new(1, 0),
12765 ])
12766 });
12767
12768 editor.handle_input("X", window, cx);
12769 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12770 assert_eq!(
12771 editor.selections.ranges(cx),
12772 [
12773 Point::new(0, 1)..Point::new(0, 1),
12774 Point::new(1, 1)..Point::new(1, 1),
12775 ]
12776 );
12777
12778 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12779 editor.change_selections(None, window, cx, |s| {
12780 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12781 });
12782 editor.backspace(&Default::default(), window, cx);
12783 assert_eq!(editor.text(cx), "Xa\nbbb");
12784 assert_eq!(
12785 editor.selections.ranges(cx),
12786 [Point::new(1, 0)..Point::new(1, 0)]
12787 );
12788
12789 editor.change_selections(None, window, cx, |s| {
12790 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12791 });
12792 editor.backspace(&Default::default(), window, cx);
12793 assert_eq!(editor.text(cx), "X\nbb");
12794 assert_eq!(
12795 editor.selections.ranges(cx),
12796 [Point::new(0, 1)..Point::new(0, 1)]
12797 );
12798 });
12799}
12800
12801#[gpui::test]
12802fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12803 init_test(cx, |_| {});
12804
12805 let markers = vec![('[', ']').into(), ('(', ')').into()];
12806 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12807 indoc! {"
12808 [aaaa
12809 (bbbb]
12810 cccc)",
12811 },
12812 markers.clone(),
12813 );
12814 let excerpt_ranges = markers.into_iter().map(|marker| {
12815 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12816 ExcerptRange::new(context.clone())
12817 });
12818 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12819 let multibuffer = cx.new(|cx| {
12820 let mut multibuffer = MultiBuffer::new(ReadWrite);
12821 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12822 multibuffer
12823 });
12824
12825 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12826 editor.update_in(cx, |editor, window, cx| {
12827 let (expected_text, selection_ranges) = marked_text_ranges(
12828 indoc! {"
12829 aaaa
12830 bˇbbb
12831 bˇbbˇb
12832 cccc"
12833 },
12834 true,
12835 );
12836 assert_eq!(editor.text(cx), expected_text);
12837 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12838
12839 editor.handle_input("X", window, cx);
12840
12841 let (expected_text, expected_selections) = marked_text_ranges(
12842 indoc! {"
12843 aaaa
12844 bXˇbbXb
12845 bXˇbbXˇb
12846 cccc"
12847 },
12848 false,
12849 );
12850 assert_eq!(editor.text(cx), expected_text);
12851 assert_eq!(editor.selections.ranges(cx), expected_selections);
12852
12853 editor.newline(&Newline, window, cx);
12854 let (expected_text, expected_selections) = marked_text_ranges(
12855 indoc! {"
12856 aaaa
12857 bX
12858 ˇbbX
12859 b
12860 bX
12861 ˇbbX
12862 ˇb
12863 cccc"
12864 },
12865 false,
12866 );
12867 assert_eq!(editor.text(cx), expected_text);
12868 assert_eq!(editor.selections.ranges(cx), expected_selections);
12869 });
12870}
12871
12872#[gpui::test]
12873fn test_refresh_selections(cx: &mut TestAppContext) {
12874 init_test(cx, |_| {});
12875
12876 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12877 let mut excerpt1_id = None;
12878 let multibuffer = cx.new(|cx| {
12879 let mut multibuffer = MultiBuffer::new(ReadWrite);
12880 excerpt1_id = multibuffer
12881 .push_excerpts(
12882 buffer.clone(),
12883 [
12884 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12885 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12886 ],
12887 cx,
12888 )
12889 .into_iter()
12890 .next();
12891 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12892 multibuffer
12893 });
12894
12895 let editor = cx.add_window(|window, cx| {
12896 let mut editor = build_editor(multibuffer.clone(), window, cx);
12897 let snapshot = editor.snapshot(window, cx);
12898 editor.change_selections(None, window, cx, |s| {
12899 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12900 });
12901 editor.begin_selection(
12902 Point::new(2, 1).to_display_point(&snapshot),
12903 true,
12904 1,
12905 window,
12906 cx,
12907 );
12908 assert_eq!(
12909 editor.selections.ranges(cx),
12910 [
12911 Point::new(1, 3)..Point::new(1, 3),
12912 Point::new(2, 1)..Point::new(2, 1),
12913 ]
12914 );
12915 editor
12916 });
12917
12918 // Refreshing selections is a no-op when excerpts haven't changed.
12919 _ = editor.update(cx, |editor, window, cx| {
12920 editor.change_selections(None, window, cx, |s| s.refresh());
12921 assert_eq!(
12922 editor.selections.ranges(cx),
12923 [
12924 Point::new(1, 3)..Point::new(1, 3),
12925 Point::new(2, 1)..Point::new(2, 1),
12926 ]
12927 );
12928 });
12929
12930 multibuffer.update(cx, |multibuffer, cx| {
12931 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12932 });
12933 _ = editor.update(cx, |editor, window, cx| {
12934 // Removing an excerpt causes the first selection to become degenerate.
12935 assert_eq!(
12936 editor.selections.ranges(cx),
12937 [
12938 Point::new(0, 0)..Point::new(0, 0),
12939 Point::new(0, 1)..Point::new(0, 1)
12940 ]
12941 );
12942
12943 // Refreshing selections will relocate the first selection to the original buffer
12944 // location.
12945 editor.change_selections(None, window, cx, |s| s.refresh());
12946 assert_eq!(
12947 editor.selections.ranges(cx),
12948 [
12949 Point::new(0, 1)..Point::new(0, 1),
12950 Point::new(0, 3)..Point::new(0, 3)
12951 ]
12952 );
12953 assert!(editor.selections.pending_anchor().is_some());
12954 });
12955}
12956
12957#[gpui::test]
12958fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12959 init_test(cx, |_| {});
12960
12961 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12962 let mut excerpt1_id = None;
12963 let multibuffer = cx.new(|cx| {
12964 let mut multibuffer = MultiBuffer::new(ReadWrite);
12965 excerpt1_id = multibuffer
12966 .push_excerpts(
12967 buffer.clone(),
12968 [
12969 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12970 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12971 ],
12972 cx,
12973 )
12974 .into_iter()
12975 .next();
12976 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12977 multibuffer
12978 });
12979
12980 let editor = cx.add_window(|window, cx| {
12981 let mut editor = build_editor(multibuffer.clone(), window, cx);
12982 let snapshot = editor.snapshot(window, cx);
12983 editor.begin_selection(
12984 Point::new(1, 3).to_display_point(&snapshot),
12985 false,
12986 1,
12987 window,
12988 cx,
12989 );
12990 assert_eq!(
12991 editor.selections.ranges(cx),
12992 [Point::new(1, 3)..Point::new(1, 3)]
12993 );
12994 editor
12995 });
12996
12997 multibuffer.update(cx, |multibuffer, cx| {
12998 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12999 });
13000 _ = editor.update(cx, |editor, window, cx| {
13001 assert_eq!(
13002 editor.selections.ranges(cx),
13003 [Point::new(0, 0)..Point::new(0, 0)]
13004 );
13005
13006 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13007 editor.change_selections(None, window, cx, |s| s.refresh());
13008 assert_eq!(
13009 editor.selections.ranges(cx),
13010 [Point::new(0, 3)..Point::new(0, 3)]
13011 );
13012 assert!(editor.selections.pending_anchor().is_some());
13013 });
13014}
13015
13016#[gpui::test]
13017async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13018 init_test(cx, |_| {});
13019
13020 let language = Arc::new(
13021 Language::new(
13022 LanguageConfig {
13023 brackets: BracketPairConfig {
13024 pairs: vec![
13025 BracketPair {
13026 start: "{".to_string(),
13027 end: "}".to_string(),
13028 close: true,
13029 surround: true,
13030 newline: true,
13031 },
13032 BracketPair {
13033 start: "/* ".to_string(),
13034 end: " */".to_string(),
13035 close: true,
13036 surround: true,
13037 newline: true,
13038 },
13039 ],
13040 ..Default::default()
13041 },
13042 ..Default::default()
13043 },
13044 Some(tree_sitter_rust::LANGUAGE.into()),
13045 )
13046 .with_indents_query("")
13047 .unwrap(),
13048 );
13049
13050 let text = concat!(
13051 "{ }\n", //
13052 " x\n", //
13053 " /* */\n", //
13054 "x\n", //
13055 "{{} }\n", //
13056 );
13057
13058 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13061 editor
13062 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13063 .await;
13064
13065 editor.update_in(cx, |editor, window, cx| {
13066 editor.change_selections(None, window, cx, |s| {
13067 s.select_display_ranges([
13068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13069 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13070 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13071 ])
13072 });
13073 editor.newline(&Newline, window, cx);
13074
13075 assert_eq!(
13076 editor.buffer().read(cx).read(cx).text(),
13077 concat!(
13078 "{ \n", // Suppress rustfmt
13079 "\n", //
13080 "}\n", //
13081 " x\n", //
13082 " /* \n", //
13083 " \n", //
13084 " */\n", //
13085 "x\n", //
13086 "{{} \n", //
13087 "}\n", //
13088 )
13089 );
13090 });
13091}
13092
13093#[gpui::test]
13094fn test_highlighted_ranges(cx: &mut TestAppContext) {
13095 init_test(cx, |_| {});
13096
13097 let editor = cx.add_window(|window, cx| {
13098 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13099 build_editor(buffer.clone(), window, cx)
13100 });
13101
13102 _ = editor.update(cx, |editor, window, cx| {
13103 struct Type1;
13104 struct Type2;
13105
13106 let buffer = editor.buffer.read(cx).snapshot(cx);
13107
13108 let anchor_range =
13109 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13110
13111 editor.highlight_background::<Type1>(
13112 &[
13113 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13114 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13115 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13116 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13117 ],
13118 |_| Hsla::red(),
13119 cx,
13120 );
13121 editor.highlight_background::<Type2>(
13122 &[
13123 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13124 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13125 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13126 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13127 ],
13128 |_| Hsla::green(),
13129 cx,
13130 );
13131
13132 let snapshot = editor.snapshot(window, cx);
13133 let mut highlighted_ranges = editor.background_highlights_in_range(
13134 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13135 &snapshot,
13136 cx.theme().colors(),
13137 );
13138 // Enforce a consistent ordering based on color without relying on the ordering of the
13139 // highlight's `TypeId` which is non-executor.
13140 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13141 assert_eq!(
13142 highlighted_ranges,
13143 &[
13144 (
13145 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13146 Hsla::red(),
13147 ),
13148 (
13149 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13150 Hsla::red(),
13151 ),
13152 (
13153 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13154 Hsla::green(),
13155 ),
13156 (
13157 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13158 Hsla::green(),
13159 ),
13160 ]
13161 );
13162 assert_eq!(
13163 editor.background_highlights_in_range(
13164 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13165 &snapshot,
13166 cx.theme().colors(),
13167 ),
13168 &[(
13169 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13170 Hsla::red(),
13171 )]
13172 );
13173 });
13174}
13175
13176#[gpui::test]
13177async fn test_following(cx: &mut TestAppContext) {
13178 init_test(cx, |_| {});
13179
13180 let fs = FakeFs::new(cx.executor());
13181 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13182
13183 let buffer = project.update(cx, |project, cx| {
13184 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13185 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13186 });
13187 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13188 let follower = cx.update(|cx| {
13189 cx.open_window(
13190 WindowOptions {
13191 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13192 gpui::Point::new(px(0.), px(0.)),
13193 gpui::Point::new(px(10.), px(80.)),
13194 ))),
13195 ..Default::default()
13196 },
13197 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13198 )
13199 .unwrap()
13200 });
13201
13202 let is_still_following = Rc::new(RefCell::new(true));
13203 let follower_edit_event_count = Rc::new(RefCell::new(0));
13204 let pending_update = Rc::new(RefCell::new(None));
13205 let leader_entity = leader.root(cx).unwrap();
13206 let follower_entity = follower.root(cx).unwrap();
13207 _ = follower.update(cx, {
13208 let update = pending_update.clone();
13209 let is_still_following = is_still_following.clone();
13210 let follower_edit_event_count = follower_edit_event_count.clone();
13211 |_, window, cx| {
13212 cx.subscribe_in(
13213 &leader_entity,
13214 window,
13215 move |_, leader, event, window, cx| {
13216 leader.read(cx).add_event_to_update_proto(
13217 event,
13218 &mut update.borrow_mut(),
13219 window,
13220 cx,
13221 );
13222 },
13223 )
13224 .detach();
13225
13226 cx.subscribe_in(
13227 &follower_entity,
13228 window,
13229 move |_, _, event: &EditorEvent, _window, _cx| {
13230 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13231 *is_still_following.borrow_mut() = false;
13232 }
13233
13234 if let EditorEvent::BufferEdited = event {
13235 *follower_edit_event_count.borrow_mut() += 1;
13236 }
13237 },
13238 )
13239 .detach();
13240 }
13241 });
13242
13243 // Update the selections only
13244 _ = leader.update(cx, |leader, window, cx| {
13245 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13246 });
13247 follower
13248 .update(cx, |follower, window, cx| {
13249 follower.apply_update_proto(
13250 &project,
13251 pending_update.borrow_mut().take().unwrap(),
13252 window,
13253 cx,
13254 )
13255 })
13256 .unwrap()
13257 .await
13258 .unwrap();
13259 _ = follower.update(cx, |follower, _, cx| {
13260 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13261 });
13262 assert!(*is_still_following.borrow());
13263 assert_eq!(*follower_edit_event_count.borrow(), 0);
13264
13265 // Update the scroll position only
13266 _ = leader.update(cx, |leader, window, cx| {
13267 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13268 });
13269 follower
13270 .update(cx, |follower, window, cx| {
13271 follower.apply_update_proto(
13272 &project,
13273 pending_update.borrow_mut().take().unwrap(),
13274 window,
13275 cx,
13276 )
13277 })
13278 .unwrap()
13279 .await
13280 .unwrap();
13281 assert_eq!(
13282 follower
13283 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13284 .unwrap(),
13285 gpui::Point::new(1.5, 3.5)
13286 );
13287 assert!(*is_still_following.borrow());
13288 assert_eq!(*follower_edit_event_count.borrow(), 0);
13289
13290 // Update the selections and scroll position. The follower's scroll position is updated
13291 // via autoscroll, not via the leader's exact scroll position.
13292 _ = leader.update(cx, |leader, window, cx| {
13293 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13294 leader.request_autoscroll(Autoscroll::newest(), cx);
13295 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13296 });
13297 follower
13298 .update(cx, |follower, window, cx| {
13299 follower.apply_update_proto(
13300 &project,
13301 pending_update.borrow_mut().take().unwrap(),
13302 window,
13303 cx,
13304 )
13305 })
13306 .unwrap()
13307 .await
13308 .unwrap();
13309 _ = follower.update(cx, |follower, _, cx| {
13310 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13311 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13312 });
13313 assert!(*is_still_following.borrow());
13314
13315 // Creating a pending selection that precedes another selection
13316 _ = leader.update(cx, |leader, window, cx| {
13317 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13318 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13319 });
13320 follower
13321 .update(cx, |follower, window, cx| {
13322 follower.apply_update_proto(
13323 &project,
13324 pending_update.borrow_mut().take().unwrap(),
13325 window,
13326 cx,
13327 )
13328 })
13329 .unwrap()
13330 .await
13331 .unwrap();
13332 _ = follower.update(cx, |follower, _, cx| {
13333 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13334 });
13335 assert!(*is_still_following.borrow());
13336
13337 // Extend the pending selection so that it surrounds another selection
13338 _ = leader.update(cx, |leader, window, cx| {
13339 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13340 });
13341 follower
13342 .update(cx, |follower, window, cx| {
13343 follower.apply_update_proto(
13344 &project,
13345 pending_update.borrow_mut().take().unwrap(),
13346 window,
13347 cx,
13348 )
13349 })
13350 .unwrap()
13351 .await
13352 .unwrap();
13353 _ = follower.update(cx, |follower, _, cx| {
13354 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13355 });
13356
13357 // Scrolling locally breaks the follow
13358 _ = follower.update(cx, |follower, window, cx| {
13359 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13360 follower.set_scroll_anchor(
13361 ScrollAnchor {
13362 anchor: top_anchor,
13363 offset: gpui::Point::new(0.0, 0.5),
13364 },
13365 window,
13366 cx,
13367 );
13368 });
13369 assert!(!(*is_still_following.borrow()));
13370}
13371
13372#[gpui::test]
13373async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13374 init_test(cx, |_| {});
13375
13376 let fs = FakeFs::new(cx.executor());
13377 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13378 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13379 let pane = workspace
13380 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13381 .unwrap();
13382
13383 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13384
13385 let leader = pane.update_in(cx, |_, window, cx| {
13386 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13387 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13388 });
13389
13390 // Start following the editor when it has no excerpts.
13391 let mut state_message =
13392 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13393 let workspace_entity = workspace.root(cx).unwrap();
13394 let follower_1 = cx
13395 .update_window(*workspace.deref(), |_, window, cx| {
13396 Editor::from_state_proto(
13397 workspace_entity,
13398 ViewId {
13399 creator: CollaboratorId::PeerId(PeerId::default()),
13400 id: 0,
13401 },
13402 &mut state_message,
13403 window,
13404 cx,
13405 )
13406 })
13407 .unwrap()
13408 .unwrap()
13409 .await
13410 .unwrap();
13411
13412 let update_message = Rc::new(RefCell::new(None));
13413 follower_1.update_in(cx, {
13414 let update = update_message.clone();
13415 |_, window, cx| {
13416 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13417 leader.read(cx).add_event_to_update_proto(
13418 event,
13419 &mut update.borrow_mut(),
13420 window,
13421 cx,
13422 );
13423 })
13424 .detach();
13425 }
13426 });
13427
13428 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13429 (
13430 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13431 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13432 )
13433 });
13434
13435 // Insert some excerpts.
13436 leader.update(cx, |leader, cx| {
13437 leader.buffer.update(cx, |multibuffer, cx| {
13438 multibuffer.set_excerpts_for_path(
13439 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13440 buffer_1.clone(),
13441 vec![
13442 Point::row_range(0..3),
13443 Point::row_range(1..6),
13444 Point::row_range(12..15),
13445 ],
13446 0,
13447 cx,
13448 );
13449 multibuffer.set_excerpts_for_path(
13450 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13451 buffer_2.clone(),
13452 vec![Point::row_range(0..6), Point::row_range(8..12)],
13453 0,
13454 cx,
13455 );
13456 });
13457 });
13458
13459 // Apply the update of adding the excerpts.
13460 follower_1
13461 .update_in(cx, |follower, window, cx| {
13462 follower.apply_update_proto(
13463 &project,
13464 update_message.borrow().clone().unwrap(),
13465 window,
13466 cx,
13467 )
13468 })
13469 .await
13470 .unwrap();
13471 assert_eq!(
13472 follower_1.update(cx, |editor, cx| editor.text(cx)),
13473 leader.update(cx, |editor, cx| editor.text(cx))
13474 );
13475 update_message.borrow_mut().take();
13476
13477 // Start following separately after it already has excerpts.
13478 let mut state_message =
13479 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13480 let workspace_entity = workspace.root(cx).unwrap();
13481 let follower_2 = cx
13482 .update_window(*workspace.deref(), |_, window, cx| {
13483 Editor::from_state_proto(
13484 workspace_entity,
13485 ViewId {
13486 creator: CollaboratorId::PeerId(PeerId::default()),
13487 id: 0,
13488 },
13489 &mut state_message,
13490 window,
13491 cx,
13492 )
13493 })
13494 .unwrap()
13495 .unwrap()
13496 .await
13497 .unwrap();
13498 assert_eq!(
13499 follower_2.update(cx, |editor, cx| editor.text(cx)),
13500 leader.update(cx, |editor, cx| editor.text(cx))
13501 );
13502
13503 // Remove some excerpts.
13504 leader.update(cx, |leader, cx| {
13505 leader.buffer.update(cx, |multibuffer, cx| {
13506 let excerpt_ids = multibuffer.excerpt_ids();
13507 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13508 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13509 });
13510 });
13511
13512 // Apply the update of removing the excerpts.
13513 follower_1
13514 .update_in(cx, |follower, window, cx| {
13515 follower.apply_update_proto(
13516 &project,
13517 update_message.borrow().clone().unwrap(),
13518 window,
13519 cx,
13520 )
13521 })
13522 .await
13523 .unwrap();
13524 follower_2
13525 .update_in(cx, |follower, window, cx| {
13526 follower.apply_update_proto(
13527 &project,
13528 update_message.borrow().clone().unwrap(),
13529 window,
13530 cx,
13531 )
13532 })
13533 .await
13534 .unwrap();
13535 update_message.borrow_mut().take();
13536 assert_eq!(
13537 follower_1.update(cx, |editor, cx| editor.text(cx)),
13538 leader.update(cx, |editor, cx| editor.text(cx))
13539 );
13540}
13541
13542#[gpui::test]
13543async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13544 init_test(cx, |_| {});
13545
13546 let mut cx = EditorTestContext::new(cx).await;
13547 let lsp_store =
13548 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13549
13550 cx.set_state(indoc! {"
13551 ˇfn func(abc def: i32) -> u32 {
13552 }
13553 "});
13554
13555 cx.update(|_, cx| {
13556 lsp_store.update(cx, |lsp_store, cx| {
13557 lsp_store
13558 .update_diagnostics(
13559 LanguageServerId(0),
13560 lsp::PublishDiagnosticsParams {
13561 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13562 version: None,
13563 diagnostics: vec![
13564 lsp::Diagnostic {
13565 range: lsp::Range::new(
13566 lsp::Position::new(0, 11),
13567 lsp::Position::new(0, 12),
13568 ),
13569 severity: Some(lsp::DiagnosticSeverity::ERROR),
13570 ..Default::default()
13571 },
13572 lsp::Diagnostic {
13573 range: lsp::Range::new(
13574 lsp::Position::new(0, 12),
13575 lsp::Position::new(0, 15),
13576 ),
13577 severity: Some(lsp::DiagnosticSeverity::ERROR),
13578 ..Default::default()
13579 },
13580 lsp::Diagnostic {
13581 range: lsp::Range::new(
13582 lsp::Position::new(0, 25),
13583 lsp::Position::new(0, 28),
13584 ),
13585 severity: Some(lsp::DiagnosticSeverity::ERROR),
13586 ..Default::default()
13587 },
13588 ],
13589 },
13590 &[],
13591 cx,
13592 )
13593 .unwrap()
13594 });
13595 });
13596
13597 executor.run_until_parked();
13598
13599 cx.update_editor(|editor, window, cx| {
13600 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13601 });
13602
13603 cx.assert_editor_state(indoc! {"
13604 fn func(abc def: i32) -> ˇu32 {
13605 }
13606 "});
13607
13608 cx.update_editor(|editor, window, cx| {
13609 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13610 });
13611
13612 cx.assert_editor_state(indoc! {"
13613 fn func(abc ˇdef: i32) -> u32 {
13614 }
13615 "});
13616
13617 cx.update_editor(|editor, window, cx| {
13618 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13619 });
13620
13621 cx.assert_editor_state(indoc! {"
13622 fn func(abcˇ def: i32) -> u32 {
13623 }
13624 "});
13625
13626 cx.update_editor(|editor, window, cx| {
13627 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13628 });
13629
13630 cx.assert_editor_state(indoc! {"
13631 fn func(abc def: i32) -> ˇu32 {
13632 }
13633 "});
13634}
13635
13636#[gpui::test]
13637async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13638 init_test(cx, |_| {});
13639
13640 let mut cx = EditorTestContext::new(cx).await;
13641
13642 let diff_base = r#"
13643 use some::mod;
13644
13645 const A: u32 = 42;
13646
13647 fn main() {
13648 println!("hello");
13649
13650 println!("world");
13651 }
13652 "#
13653 .unindent();
13654
13655 // Edits are modified, removed, modified, added
13656 cx.set_state(
13657 &r#"
13658 use some::modified;
13659
13660 ˇ
13661 fn main() {
13662 println!("hello there");
13663
13664 println!("around the");
13665 println!("world");
13666 }
13667 "#
13668 .unindent(),
13669 );
13670
13671 cx.set_head_text(&diff_base);
13672 executor.run_until_parked();
13673
13674 cx.update_editor(|editor, window, cx| {
13675 //Wrap around the bottom of the buffer
13676 for _ in 0..3 {
13677 editor.go_to_next_hunk(&GoToHunk, window, cx);
13678 }
13679 });
13680
13681 cx.assert_editor_state(
13682 &r#"
13683 ˇuse some::modified;
13684
13685
13686 fn main() {
13687 println!("hello there");
13688
13689 println!("around the");
13690 println!("world");
13691 }
13692 "#
13693 .unindent(),
13694 );
13695
13696 cx.update_editor(|editor, window, cx| {
13697 //Wrap around the top of the buffer
13698 for _ in 0..2 {
13699 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13700 }
13701 });
13702
13703 cx.assert_editor_state(
13704 &r#"
13705 use some::modified;
13706
13707
13708 fn main() {
13709 ˇ println!("hello there");
13710
13711 println!("around the");
13712 println!("world");
13713 }
13714 "#
13715 .unindent(),
13716 );
13717
13718 cx.update_editor(|editor, window, cx| {
13719 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13720 });
13721
13722 cx.assert_editor_state(
13723 &r#"
13724 use some::modified;
13725
13726 ˇ
13727 fn main() {
13728 println!("hello there");
13729
13730 println!("around the");
13731 println!("world");
13732 }
13733 "#
13734 .unindent(),
13735 );
13736
13737 cx.update_editor(|editor, window, cx| {
13738 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13739 });
13740
13741 cx.assert_editor_state(
13742 &r#"
13743 ˇuse some::modified;
13744
13745
13746 fn main() {
13747 println!("hello there");
13748
13749 println!("around the");
13750 println!("world");
13751 }
13752 "#
13753 .unindent(),
13754 );
13755
13756 cx.update_editor(|editor, window, cx| {
13757 for _ in 0..2 {
13758 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13759 }
13760 });
13761
13762 cx.assert_editor_state(
13763 &r#"
13764 use some::modified;
13765
13766
13767 fn main() {
13768 ˇ println!("hello there");
13769
13770 println!("around the");
13771 println!("world");
13772 }
13773 "#
13774 .unindent(),
13775 );
13776
13777 cx.update_editor(|editor, window, cx| {
13778 editor.fold(&Fold, window, cx);
13779 });
13780
13781 cx.update_editor(|editor, window, cx| {
13782 editor.go_to_next_hunk(&GoToHunk, window, cx);
13783 });
13784
13785 cx.assert_editor_state(
13786 &r#"
13787 ˇuse some::modified;
13788
13789
13790 fn main() {
13791 println!("hello there");
13792
13793 println!("around the");
13794 println!("world");
13795 }
13796 "#
13797 .unindent(),
13798 );
13799}
13800
13801#[test]
13802fn test_split_words() {
13803 fn split(text: &str) -> Vec<&str> {
13804 split_words(text).collect()
13805 }
13806
13807 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13808 assert_eq!(split("hello_world"), &["hello_", "world"]);
13809 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13810 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13811 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13812 assert_eq!(split("helloworld"), &["helloworld"]);
13813
13814 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13815}
13816
13817#[gpui::test]
13818async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13819 init_test(cx, |_| {});
13820
13821 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13822 let mut assert = |before, after| {
13823 let _state_context = cx.set_state(before);
13824 cx.run_until_parked();
13825 cx.update_editor(|editor, window, cx| {
13826 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13827 });
13828 cx.run_until_parked();
13829 cx.assert_editor_state(after);
13830 };
13831
13832 // Outside bracket jumps to outside of matching bracket
13833 assert("console.logˇ(var);", "console.log(var)ˇ;");
13834 assert("console.log(var)ˇ;", "console.logˇ(var);");
13835
13836 // Inside bracket jumps to inside of matching bracket
13837 assert("console.log(ˇvar);", "console.log(varˇ);");
13838 assert("console.log(varˇ);", "console.log(ˇvar);");
13839
13840 // When outside a bracket and inside, favor jumping to the inside bracket
13841 assert(
13842 "console.log('foo', [1, 2, 3]ˇ);",
13843 "console.log(ˇ'foo', [1, 2, 3]);",
13844 );
13845 assert(
13846 "console.log(ˇ'foo', [1, 2, 3]);",
13847 "console.log('foo', [1, 2, 3]ˇ);",
13848 );
13849
13850 // Bias forward if two options are equally likely
13851 assert(
13852 "let result = curried_fun()ˇ();",
13853 "let result = curried_fun()()ˇ;",
13854 );
13855
13856 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13857 assert(
13858 indoc! {"
13859 function test() {
13860 console.log('test')ˇ
13861 }"},
13862 indoc! {"
13863 function test() {
13864 console.logˇ('test')
13865 }"},
13866 );
13867}
13868
13869#[gpui::test]
13870async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13871 init_test(cx, |_| {});
13872
13873 let fs = FakeFs::new(cx.executor());
13874 fs.insert_tree(
13875 path!("/a"),
13876 json!({
13877 "main.rs": "fn main() { let a = 5; }",
13878 "other.rs": "// Test file",
13879 }),
13880 )
13881 .await;
13882 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13883
13884 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13885 language_registry.add(Arc::new(Language::new(
13886 LanguageConfig {
13887 name: "Rust".into(),
13888 matcher: LanguageMatcher {
13889 path_suffixes: vec!["rs".to_string()],
13890 ..Default::default()
13891 },
13892 brackets: BracketPairConfig {
13893 pairs: vec![BracketPair {
13894 start: "{".to_string(),
13895 end: "}".to_string(),
13896 close: true,
13897 surround: true,
13898 newline: true,
13899 }],
13900 disabled_scopes_by_bracket_ix: Vec::new(),
13901 },
13902 ..Default::default()
13903 },
13904 Some(tree_sitter_rust::LANGUAGE.into()),
13905 )));
13906 let mut fake_servers = language_registry.register_fake_lsp(
13907 "Rust",
13908 FakeLspAdapter {
13909 capabilities: lsp::ServerCapabilities {
13910 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13911 first_trigger_character: "{".to_string(),
13912 more_trigger_character: None,
13913 }),
13914 ..Default::default()
13915 },
13916 ..Default::default()
13917 },
13918 );
13919
13920 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13921
13922 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13923
13924 let worktree_id = workspace
13925 .update(cx, |workspace, _, cx| {
13926 workspace.project().update(cx, |project, cx| {
13927 project.worktrees(cx).next().unwrap().read(cx).id()
13928 })
13929 })
13930 .unwrap();
13931
13932 let buffer = project
13933 .update(cx, |project, cx| {
13934 project.open_local_buffer(path!("/a/main.rs"), cx)
13935 })
13936 .await
13937 .unwrap();
13938 let editor_handle = workspace
13939 .update(cx, |workspace, window, cx| {
13940 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13941 })
13942 .unwrap()
13943 .await
13944 .unwrap()
13945 .downcast::<Editor>()
13946 .unwrap();
13947
13948 cx.executor().start_waiting();
13949 let fake_server = fake_servers.next().await.unwrap();
13950
13951 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13952 |params, _| async move {
13953 assert_eq!(
13954 params.text_document_position.text_document.uri,
13955 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13956 );
13957 assert_eq!(
13958 params.text_document_position.position,
13959 lsp::Position::new(0, 21),
13960 );
13961
13962 Ok(Some(vec![lsp::TextEdit {
13963 new_text: "]".to_string(),
13964 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13965 }]))
13966 },
13967 );
13968
13969 editor_handle.update_in(cx, |editor, window, cx| {
13970 window.focus(&editor.focus_handle(cx));
13971 editor.change_selections(None, window, cx, |s| {
13972 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13973 });
13974 editor.handle_input("{", window, cx);
13975 });
13976
13977 cx.executor().run_until_parked();
13978
13979 buffer.update(cx, |buffer, _| {
13980 assert_eq!(
13981 buffer.text(),
13982 "fn main() { let a = {5}; }",
13983 "No extra braces from on type formatting should appear in the buffer"
13984 )
13985 });
13986}
13987
13988#[gpui::test]
13989async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13990 init_test(cx, |_| {});
13991
13992 let fs = FakeFs::new(cx.executor());
13993 fs.insert_tree(
13994 path!("/a"),
13995 json!({
13996 "main.rs": "fn main() { let a = 5; }",
13997 "other.rs": "// Test file",
13998 }),
13999 )
14000 .await;
14001
14002 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14003
14004 let server_restarts = Arc::new(AtomicUsize::new(0));
14005 let closure_restarts = Arc::clone(&server_restarts);
14006 let language_server_name = "test language server";
14007 let language_name: LanguageName = "Rust".into();
14008
14009 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14010 language_registry.add(Arc::new(Language::new(
14011 LanguageConfig {
14012 name: language_name.clone(),
14013 matcher: LanguageMatcher {
14014 path_suffixes: vec!["rs".to_string()],
14015 ..Default::default()
14016 },
14017 ..Default::default()
14018 },
14019 Some(tree_sitter_rust::LANGUAGE.into()),
14020 )));
14021 let mut fake_servers = language_registry.register_fake_lsp(
14022 "Rust",
14023 FakeLspAdapter {
14024 name: language_server_name,
14025 initialization_options: Some(json!({
14026 "testOptionValue": true
14027 })),
14028 initializer: Some(Box::new(move |fake_server| {
14029 let task_restarts = Arc::clone(&closure_restarts);
14030 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14031 task_restarts.fetch_add(1, atomic::Ordering::Release);
14032 futures::future::ready(Ok(()))
14033 });
14034 })),
14035 ..Default::default()
14036 },
14037 );
14038
14039 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14040 let _buffer = project
14041 .update(cx, |project, cx| {
14042 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14043 })
14044 .await
14045 .unwrap();
14046 let _fake_server = fake_servers.next().await.unwrap();
14047 update_test_language_settings(cx, |language_settings| {
14048 language_settings.languages.insert(
14049 language_name.clone(),
14050 LanguageSettingsContent {
14051 tab_size: NonZeroU32::new(8),
14052 ..Default::default()
14053 },
14054 );
14055 });
14056 cx.executor().run_until_parked();
14057 assert_eq!(
14058 server_restarts.load(atomic::Ordering::Acquire),
14059 0,
14060 "Should not restart LSP server on an unrelated change"
14061 );
14062
14063 update_test_project_settings(cx, |project_settings| {
14064 project_settings.lsp.insert(
14065 "Some other server name".into(),
14066 LspSettings {
14067 binary: None,
14068 settings: None,
14069 initialization_options: Some(json!({
14070 "some other init value": false
14071 })),
14072 enable_lsp_tasks: false,
14073 },
14074 );
14075 });
14076 cx.executor().run_until_parked();
14077 assert_eq!(
14078 server_restarts.load(atomic::Ordering::Acquire),
14079 0,
14080 "Should not restart LSP server on an unrelated LSP settings change"
14081 );
14082
14083 update_test_project_settings(cx, |project_settings| {
14084 project_settings.lsp.insert(
14085 language_server_name.into(),
14086 LspSettings {
14087 binary: None,
14088 settings: None,
14089 initialization_options: Some(json!({
14090 "anotherInitValue": false
14091 })),
14092 enable_lsp_tasks: false,
14093 },
14094 );
14095 });
14096 cx.executor().run_until_parked();
14097 assert_eq!(
14098 server_restarts.load(atomic::Ordering::Acquire),
14099 1,
14100 "Should restart LSP server on a related LSP settings change"
14101 );
14102
14103 update_test_project_settings(cx, |project_settings| {
14104 project_settings.lsp.insert(
14105 language_server_name.into(),
14106 LspSettings {
14107 binary: None,
14108 settings: None,
14109 initialization_options: Some(json!({
14110 "anotherInitValue": false
14111 })),
14112 enable_lsp_tasks: false,
14113 },
14114 );
14115 });
14116 cx.executor().run_until_parked();
14117 assert_eq!(
14118 server_restarts.load(atomic::Ordering::Acquire),
14119 1,
14120 "Should not restart LSP server on a related LSP settings change that is the same"
14121 );
14122
14123 update_test_project_settings(cx, |project_settings| {
14124 project_settings.lsp.insert(
14125 language_server_name.into(),
14126 LspSettings {
14127 binary: None,
14128 settings: None,
14129 initialization_options: None,
14130 enable_lsp_tasks: false,
14131 },
14132 );
14133 });
14134 cx.executor().run_until_parked();
14135 assert_eq!(
14136 server_restarts.load(atomic::Ordering::Acquire),
14137 2,
14138 "Should restart LSP server on another related LSP settings change"
14139 );
14140}
14141
14142#[gpui::test]
14143async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14144 init_test(cx, |_| {});
14145
14146 let mut cx = EditorLspTestContext::new_rust(
14147 lsp::ServerCapabilities {
14148 completion_provider: Some(lsp::CompletionOptions {
14149 trigger_characters: Some(vec![".".to_string()]),
14150 resolve_provider: Some(true),
14151 ..Default::default()
14152 }),
14153 ..Default::default()
14154 },
14155 cx,
14156 )
14157 .await;
14158
14159 cx.set_state("fn main() { let a = 2ˇ; }");
14160 cx.simulate_keystroke(".");
14161 let completion_item = lsp::CompletionItem {
14162 label: "some".into(),
14163 kind: Some(lsp::CompletionItemKind::SNIPPET),
14164 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14165 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14166 kind: lsp::MarkupKind::Markdown,
14167 value: "```rust\nSome(2)\n```".to_string(),
14168 })),
14169 deprecated: Some(false),
14170 sort_text: Some("fffffff2".to_string()),
14171 filter_text: Some("some".to_string()),
14172 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14173 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14174 range: lsp::Range {
14175 start: lsp::Position {
14176 line: 0,
14177 character: 22,
14178 },
14179 end: lsp::Position {
14180 line: 0,
14181 character: 22,
14182 },
14183 },
14184 new_text: "Some(2)".to_string(),
14185 })),
14186 additional_text_edits: Some(vec![lsp::TextEdit {
14187 range: lsp::Range {
14188 start: lsp::Position {
14189 line: 0,
14190 character: 20,
14191 },
14192 end: lsp::Position {
14193 line: 0,
14194 character: 22,
14195 },
14196 },
14197 new_text: "".to_string(),
14198 }]),
14199 ..Default::default()
14200 };
14201
14202 let closure_completion_item = completion_item.clone();
14203 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14204 let task_completion_item = closure_completion_item.clone();
14205 async move {
14206 Ok(Some(lsp::CompletionResponse::Array(vec![
14207 task_completion_item,
14208 ])))
14209 }
14210 });
14211
14212 request.next().await;
14213
14214 cx.condition(|editor, _| editor.context_menu_visible())
14215 .await;
14216 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14217 editor
14218 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14219 .unwrap()
14220 });
14221 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14222
14223 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14224 let task_completion_item = completion_item.clone();
14225 async move { Ok(task_completion_item) }
14226 })
14227 .next()
14228 .await
14229 .unwrap();
14230 apply_additional_edits.await.unwrap();
14231 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14232}
14233
14234#[gpui::test]
14235async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14236 init_test(cx, |_| {});
14237
14238 let mut cx = EditorLspTestContext::new_rust(
14239 lsp::ServerCapabilities {
14240 completion_provider: Some(lsp::CompletionOptions {
14241 trigger_characters: Some(vec![".".to_string()]),
14242 resolve_provider: Some(true),
14243 ..Default::default()
14244 }),
14245 ..Default::default()
14246 },
14247 cx,
14248 )
14249 .await;
14250
14251 cx.set_state("fn main() { let a = 2ˇ; }");
14252 cx.simulate_keystroke(".");
14253
14254 let item1 = lsp::CompletionItem {
14255 label: "method id()".to_string(),
14256 filter_text: Some("id".to_string()),
14257 detail: None,
14258 documentation: None,
14259 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14260 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14261 new_text: ".id".to_string(),
14262 })),
14263 ..lsp::CompletionItem::default()
14264 };
14265
14266 let item2 = lsp::CompletionItem {
14267 label: "other".to_string(),
14268 filter_text: Some("other".to_string()),
14269 detail: None,
14270 documentation: None,
14271 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14272 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14273 new_text: ".other".to_string(),
14274 })),
14275 ..lsp::CompletionItem::default()
14276 };
14277
14278 let item1 = item1.clone();
14279 cx.set_request_handler::<lsp::request::Completion, _, _>({
14280 let item1 = item1.clone();
14281 move |_, _, _| {
14282 let item1 = item1.clone();
14283 let item2 = item2.clone();
14284 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14285 }
14286 })
14287 .next()
14288 .await;
14289
14290 cx.condition(|editor, _| editor.context_menu_visible())
14291 .await;
14292 cx.update_editor(|editor, _, _| {
14293 let context_menu = editor.context_menu.borrow_mut();
14294 let context_menu = context_menu
14295 .as_ref()
14296 .expect("Should have the context menu deployed");
14297 match context_menu {
14298 CodeContextMenu::Completions(completions_menu) => {
14299 let completions = completions_menu.completions.borrow_mut();
14300 assert_eq!(
14301 completions
14302 .iter()
14303 .map(|completion| &completion.label.text)
14304 .collect::<Vec<_>>(),
14305 vec!["method id()", "other"]
14306 )
14307 }
14308 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14309 }
14310 });
14311
14312 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14313 let item1 = item1.clone();
14314 move |_, item_to_resolve, _| {
14315 let item1 = item1.clone();
14316 async move {
14317 if item1 == item_to_resolve {
14318 Ok(lsp::CompletionItem {
14319 label: "method id()".to_string(),
14320 filter_text: Some("id".to_string()),
14321 detail: Some("Now resolved!".to_string()),
14322 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14324 range: lsp::Range::new(
14325 lsp::Position::new(0, 22),
14326 lsp::Position::new(0, 22),
14327 ),
14328 new_text: ".id".to_string(),
14329 })),
14330 ..lsp::CompletionItem::default()
14331 })
14332 } else {
14333 Ok(item_to_resolve)
14334 }
14335 }
14336 }
14337 })
14338 .next()
14339 .await
14340 .unwrap();
14341 cx.run_until_parked();
14342
14343 cx.update_editor(|editor, window, cx| {
14344 editor.context_menu_next(&Default::default(), window, cx);
14345 });
14346
14347 cx.update_editor(|editor, _, _| {
14348 let context_menu = editor.context_menu.borrow_mut();
14349 let context_menu = context_menu
14350 .as_ref()
14351 .expect("Should have the context menu deployed");
14352 match context_menu {
14353 CodeContextMenu::Completions(completions_menu) => {
14354 let completions = completions_menu.completions.borrow_mut();
14355 assert_eq!(
14356 completions
14357 .iter()
14358 .map(|completion| &completion.label.text)
14359 .collect::<Vec<_>>(),
14360 vec!["method id() Now resolved!", "other"],
14361 "Should update first completion label, but not second as the filter text did not match."
14362 );
14363 }
14364 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14365 }
14366 });
14367}
14368
14369#[gpui::test]
14370async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14371 init_test(cx, |_| {});
14372 let mut cx = EditorLspTestContext::new_rust(
14373 lsp::ServerCapabilities {
14374 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14375 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14376 completion_provider: Some(lsp::CompletionOptions {
14377 resolve_provider: Some(true),
14378 ..Default::default()
14379 }),
14380 ..Default::default()
14381 },
14382 cx,
14383 )
14384 .await;
14385 cx.set_state(indoc! {"
14386 struct TestStruct {
14387 field: i32
14388 }
14389
14390 fn mainˇ() {
14391 let unused_var = 42;
14392 let test_struct = TestStruct { field: 42 };
14393 }
14394 "});
14395 let symbol_range = cx.lsp_range(indoc! {"
14396 struct TestStruct {
14397 field: i32
14398 }
14399
14400 «fn main»() {
14401 let unused_var = 42;
14402 let test_struct = TestStruct { field: 42 };
14403 }
14404 "});
14405 let mut hover_requests =
14406 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14407 Ok(Some(lsp::Hover {
14408 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14409 kind: lsp::MarkupKind::Markdown,
14410 value: "Function documentation".to_string(),
14411 }),
14412 range: Some(symbol_range),
14413 }))
14414 });
14415
14416 // Case 1: Test that code action menu hide hover popover
14417 cx.dispatch_action(Hover);
14418 hover_requests.next().await;
14419 cx.condition(|editor, _| editor.hover_state.visible()).await;
14420 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14421 move |_, _, _| async move {
14422 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14423 lsp::CodeAction {
14424 title: "Remove unused variable".to_string(),
14425 kind: Some(CodeActionKind::QUICKFIX),
14426 edit: Some(lsp::WorkspaceEdit {
14427 changes: Some(
14428 [(
14429 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14430 vec![lsp::TextEdit {
14431 range: lsp::Range::new(
14432 lsp::Position::new(5, 4),
14433 lsp::Position::new(5, 27),
14434 ),
14435 new_text: "".to_string(),
14436 }],
14437 )]
14438 .into_iter()
14439 .collect(),
14440 ),
14441 ..Default::default()
14442 }),
14443 ..Default::default()
14444 },
14445 )]))
14446 },
14447 );
14448 cx.update_editor(|editor, window, cx| {
14449 editor.toggle_code_actions(
14450 &ToggleCodeActions {
14451 deployed_from: None,
14452 quick_launch: false,
14453 },
14454 window,
14455 cx,
14456 );
14457 });
14458 code_action_requests.next().await;
14459 cx.run_until_parked();
14460 cx.condition(|editor, _| editor.context_menu_visible())
14461 .await;
14462 cx.update_editor(|editor, _, _| {
14463 assert!(
14464 !editor.hover_state.visible(),
14465 "Hover popover should be hidden when code action menu is shown"
14466 );
14467 // Hide code actions
14468 editor.context_menu.take();
14469 });
14470
14471 // Case 2: Test that code completions hide hover popover
14472 cx.dispatch_action(Hover);
14473 hover_requests.next().await;
14474 cx.condition(|editor, _| editor.hover_state.visible()).await;
14475 let counter = Arc::new(AtomicUsize::new(0));
14476 let mut completion_requests =
14477 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14478 let counter = counter.clone();
14479 async move {
14480 counter.fetch_add(1, atomic::Ordering::Release);
14481 Ok(Some(lsp::CompletionResponse::Array(vec![
14482 lsp::CompletionItem {
14483 label: "main".into(),
14484 kind: Some(lsp::CompletionItemKind::FUNCTION),
14485 detail: Some("() -> ()".to_string()),
14486 ..Default::default()
14487 },
14488 lsp::CompletionItem {
14489 label: "TestStruct".into(),
14490 kind: Some(lsp::CompletionItemKind::STRUCT),
14491 detail: Some("struct TestStruct".to_string()),
14492 ..Default::default()
14493 },
14494 ])))
14495 }
14496 });
14497 cx.update_editor(|editor, window, cx| {
14498 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14499 });
14500 completion_requests.next().await;
14501 cx.condition(|editor, _| editor.context_menu_visible())
14502 .await;
14503 cx.update_editor(|editor, _, _| {
14504 assert!(
14505 !editor.hover_state.visible(),
14506 "Hover popover should be hidden when completion menu is shown"
14507 );
14508 });
14509}
14510
14511#[gpui::test]
14512async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14513 init_test(cx, |_| {});
14514
14515 let mut cx = EditorLspTestContext::new_rust(
14516 lsp::ServerCapabilities {
14517 completion_provider: Some(lsp::CompletionOptions {
14518 trigger_characters: Some(vec![".".to_string()]),
14519 resolve_provider: Some(true),
14520 ..Default::default()
14521 }),
14522 ..Default::default()
14523 },
14524 cx,
14525 )
14526 .await;
14527
14528 cx.set_state("fn main() { let a = 2ˇ; }");
14529 cx.simulate_keystroke(".");
14530
14531 let unresolved_item_1 = lsp::CompletionItem {
14532 label: "id".to_string(),
14533 filter_text: Some("id".to_string()),
14534 detail: None,
14535 documentation: None,
14536 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14537 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14538 new_text: ".id".to_string(),
14539 })),
14540 ..lsp::CompletionItem::default()
14541 };
14542 let resolved_item_1 = lsp::CompletionItem {
14543 additional_text_edits: Some(vec![lsp::TextEdit {
14544 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14545 new_text: "!!".to_string(),
14546 }]),
14547 ..unresolved_item_1.clone()
14548 };
14549 let unresolved_item_2 = lsp::CompletionItem {
14550 label: "other".to_string(),
14551 filter_text: Some("other".to_string()),
14552 detail: None,
14553 documentation: None,
14554 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14555 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14556 new_text: ".other".to_string(),
14557 })),
14558 ..lsp::CompletionItem::default()
14559 };
14560 let resolved_item_2 = lsp::CompletionItem {
14561 additional_text_edits: Some(vec![lsp::TextEdit {
14562 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14563 new_text: "??".to_string(),
14564 }]),
14565 ..unresolved_item_2.clone()
14566 };
14567
14568 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14569 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14570 cx.lsp
14571 .server
14572 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14573 let unresolved_item_1 = unresolved_item_1.clone();
14574 let resolved_item_1 = resolved_item_1.clone();
14575 let unresolved_item_2 = unresolved_item_2.clone();
14576 let resolved_item_2 = resolved_item_2.clone();
14577 let resolve_requests_1 = resolve_requests_1.clone();
14578 let resolve_requests_2 = resolve_requests_2.clone();
14579 move |unresolved_request, _| {
14580 let unresolved_item_1 = unresolved_item_1.clone();
14581 let resolved_item_1 = resolved_item_1.clone();
14582 let unresolved_item_2 = unresolved_item_2.clone();
14583 let resolved_item_2 = resolved_item_2.clone();
14584 let resolve_requests_1 = resolve_requests_1.clone();
14585 let resolve_requests_2 = resolve_requests_2.clone();
14586 async move {
14587 if unresolved_request == unresolved_item_1 {
14588 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14589 Ok(resolved_item_1.clone())
14590 } else if unresolved_request == unresolved_item_2 {
14591 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14592 Ok(resolved_item_2.clone())
14593 } else {
14594 panic!("Unexpected completion item {unresolved_request:?}")
14595 }
14596 }
14597 }
14598 })
14599 .detach();
14600
14601 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14602 let unresolved_item_1 = unresolved_item_1.clone();
14603 let unresolved_item_2 = unresolved_item_2.clone();
14604 async move {
14605 Ok(Some(lsp::CompletionResponse::Array(vec![
14606 unresolved_item_1,
14607 unresolved_item_2,
14608 ])))
14609 }
14610 })
14611 .next()
14612 .await;
14613
14614 cx.condition(|editor, _| editor.context_menu_visible())
14615 .await;
14616 cx.update_editor(|editor, _, _| {
14617 let context_menu = editor.context_menu.borrow_mut();
14618 let context_menu = context_menu
14619 .as_ref()
14620 .expect("Should have the context menu deployed");
14621 match context_menu {
14622 CodeContextMenu::Completions(completions_menu) => {
14623 let completions = completions_menu.completions.borrow_mut();
14624 assert_eq!(
14625 completions
14626 .iter()
14627 .map(|completion| &completion.label.text)
14628 .collect::<Vec<_>>(),
14629 vec!["id", "other"]
14630 )
14631 }
14632 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14633 }
14634 });
14635 cx.run_until_parked();
14636
14637 cx.update_editor(|editor, window, cx| {
14638 editor.context_menu_next(&ContextMenuNext, window, cx);
14639 });
14640 cx.run_until_parked();
14641 cx.update_editor(|editor, window, cx| {
14642 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14643 });
14644 cx.run_until_parked();
14645 cx.update_editor(|editor, window, cx| {
14646 editor.context_menu_next(&ContextMenuNext, window, cx);
14647 });
14648 cx.run_until_parked();
14649 cx.update_editor(|editor, window, cx| {
14650 editor
14651 .compose_completion(&ComposeCompletion::default(), window, cx)
14652 .expect("No task returned")
14653 })
14654 .await
14655 .expect("Completion failed");
14656 cx.run_until_parked();
14657
14658 cx.update_editor(|editor, _, cx| {
14659 assert_eq!(
14660 resolve_requests_1.load(atomic::Ordering::Acquire),
14661 1,
14662 "Should always resolve once despite multiple selections"
14663 );
14664 assert_eq!(
14665 resolve_requests_2.load(atomic::Ordering::Acquire),
14666 1,
14667 "Should always resolve once after multiple selections and applying the completion"
14668 );
14669 assert_eq!(
14670 editor.text(cx),
14671 "fn main() { let a = ??.other; }",
14672 "Should use resolved data when applying the completion"
14673 );
14674 });
14675}
14676
14677#[gpui::test]
14678async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14679 init_test(cx, |_| {});
14680
14681 let item_0 = lsp::CompletionItem {
14682 label: "abs".into(),
14683 insert_text: Some("abs".into()),
14684 data: Some(json!({ "very": "special"})),
14685 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14686 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14687 lsp::InsertReplaceEdit {
14688 new_text: "abs".to_string(),
14689 insert: lsp::Range::default(),
14690 replace: lsp::Range::default(),
14691 },
14692 )),
14693 ..lsp::CompletionItem::default()
14694 };
14695 let items = iter::once(item_0.clone())
14696 .chain((11..51).map(|i| lsp::CompletionItem {
14697 label: format!("item_{}", i),
14698 insert_text: Some(format!("item_{}", i)),
14699 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14700 ..lsp::CompletionItem::default()
14701 }))
14702 .collect::<Vec<_>>();
14703
14704 let default_commit_characters = vec!["?".to_string()];
14705 let default_data = json!({ "default": "data"});
14706 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14707 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14708 let default_edit_range = lsp::Range {
14709 start: lsp::Position {
14710 line: 0,
14711 character: 5,
14712 },
14713 end: lsp::Position {
14714 line: 0,
14715 character: 5,
14716 },
14717 };
14718
14719 let mut cx = EditorLspTestContext::new_rust(
14720 lsp::ServerCapabilities {
14721 completion_provider: Some(lsp::CompletionOptions {
14722 trigger_characters: Some(vec![".".to_string()]),
14723 resolve_provider: Some(true),
14724 ..Default::default()
14725 }),
14726 ..Default::default()
14727 },
14728 cx,
14729 )
14730 .await;
14731
14732 cx.set_state("fn main() { let a = 2ˇ; }");
14733 cx.simulate_keystroke(".");
14734
14735 let completion_data = default_data.clone();
14736 let completion_characters = default_commit_characters.clone();
14737 let completion_items = items.clone();
14738 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14739 let default_data = completion_data.clone();
14740 let default_commit_characters = completion_characters.clone();
14741 let items = completion_items.clone();
14742 async move {
14743 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14744 items,
14745 item_defaults: Some(lsp::CompletionListItemDefaults {
14746 data: Some(default_data.clone()),
14747 commit_characters: Some(default_commit_characters.clone()),
14748 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14749 default_edit_range,
14750 )),
14751 insert_text_format: Some(default_insert_text_format),
14752 insert_text_mode: Some(default_insert_text_mode),
14753 }),
14754 ..lsp::CompletionList::default()
14755 })))
14756 }
14757 })
14758 .next()
14759 .await;
14760
14761 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14762 cx.lsp
14763 .server
14764 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14765 let closure_resolved_items = resolved_items.clone();
14766 move |item_to_resolve, _| {
14767 let closure_resolved_items = closure_resolved_items.clone();
14768 async move {
14769 closure_resolved_items.lock().push(item_to_resolve.clone());
14770 Ok(item_to_resolve)
14771 }
14772 }
14773 })
14774 .detach();
14775
14776 cx.condition(|editor, _| editor.context_menu_visible())
14777 .await;
14778 cx.run_until_parked();
14779 cx.update_editor(|editor, _, _| {
14780 let menu = editor.context_menu.borrow_mut();
14781 match menu.as_ref().expect("should have the completions menu") {
14782 CodeContextMenu::Completions(completions_menu) => {
14783 assert_eq!(
14784 completions_menu
14785 .entries
14786 .borrow()
14787 .iter()
14788 .map(|mat| mat.string.clone())
14789 .collect::<Vec<String>>(),
14790 items
14791 .iter()
14792 .map(|completion| completion.label.clone())
14793 .collect::<Vec<String>>()
14794 );
14795 }
14796 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14797 }
14798 });
14799 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14800 // with 4 from the end.
14801 assert_eq!(
14802 *resolved_items.lock(),
14803 [&items[0..16], &items[items.len() - 4..items.len()]]
14804 .concat()
14805 .iter()
14806 .cloned()
14807 .map(|mut item| {
14808 if item.data.is_none() {
14809 item.data = Some(default_data.clone());
14810 }
14811 item
14812 })
14813 .collect::<Vec<lsp::CompletionItem>>(),
14814 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14815 );
14816 resolved_items.lock().clear();
14817
14818 cx.update_editor(|editor, window, cx| {
14819 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14820 });
14821 cx.run_until_parked();
14822 // Completions that have already been resolved are skipped.
14823 assert_eq!(
14824 *resolved_items.lock(),
14825 items[items.len() - 16..items.len() - 4]
14826 .iter()
14827 .cloned()
14828 .map(|mut item| {
14829 if item.data.is_none() {
14830 item.data = Some(default_data.clone());
14831 }
14832 item
14833 })
14834 .collect::<Vec<lsp::CompletionItem>>()
14835 );
14836 resolved_items.lock().clear();
14837}
14838
14839#[gpui::test]
14840async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14841 init_test(cx, |_| {});
14842
14843 let mut cx = EditorLspTestContext::new(
14844 Language::new(
14845 LanguageConfig {
14846 matcher: LanguageMatcher {
14847 path_suffixes: vec!["jsx".into()],
14848 ..Default::default()
14849 },
14850 overrides: [(
14851 "element".into(),
14852 LanguageConfigOverride {
14853 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14854 ..Default::default()
14855 },
14856 )]
14857 .into_iter()
14858 .collect(),
14859 ..Default::default()
14860 },
14861 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14862 )
14863 .with_override_query("(jsx_self_closing_element) @element")
14864 .unwrap(),
14865 lsp::ServerCapabilities {
14866 completion_provider: Some(lsp::CompletionOptions {
14867 trigger_characters: Some(vec![":".to_string()]),
14868 ..Default::default()
14869 }),
14870 ..Default::default()
14871 },
14872 cx,
14873 )
14874 .await;
14875
14876 cx.lsp
14877 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14878 Ok(Some(lsp::CompletionResponse::Array(vec![
14879 lsp::CompletionItem {
14880 label: "bg-blue".into(),
14881 ..Default::default()
14882 },
14883 lsp::CompletionItem {
14884 label: "bg-red".into(),
14885 ..Default::default()
14886 },
14887 lsp::CompletionItem {
14888 label: "bg-yellow".into(),
14889 ..Default::default()
14890 },
14891 ])))
14892 });
14893
14894 cx.set_state(r#"<p class="bgˇ" />"#);
14895
14896 // Trigger completion when typing a dash, because the dash is an extra
14897 // word character in the 'element' scope, which contains the cursor.
14898 cx.simulate_keystroke("-");
14899 cx.executor().run_until_parked();
14900 cx.update_editor(|editor, _, _| {
14901 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14902 {
14903 assert_eq!(
14904 completion_menu_entries(&menu),
14905 &["bg-red", "bg-blue", "bg-yellow"]
14906 );
14907 } else {
14908 panic!("expected completion menu to be open");
14909 }
14910 });
14911
14912 cx.simulate_keystroke("l");
14913 cx.executor().run_until_parked();
14914 cx.update_editor(|editor, _, _| {
14915 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14916 {
14917 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14918 } else {
14919 panic!("expected completion menu to be open");
14920 }
14921 });
14922
14923 // When filtering completions, consider the character after the '-' to
14924 // be the start of a subword.
14925 cx.set_state(r#"<p class="yelˇ" />"#);
14926 cx.simulate_keystroke("l");
14927 cx.executor().run_until_parked();
14928 cx.update_editor(|editor, _, _| {
14929 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14930 {
14931 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14932 } else {
14933 panic!("expected completion menu to be open");
14934 }
14935 });
14936}
14937
14938fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14939 let entries = menu.entries.borrow();
14940 entries.iter().map(|mat| mat.string.clone()).collect()
14941}
14942
14943#[gpui::test]
14944async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14945 init_test(cx, |settings| {
14946 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14947 FormatterList(vec![Formatter::Prettier].into()),
14948 ))
14949 });
14950
14951 let fs = FakeFs::new(cx.executor());
14952 fs.insert_file(path!("/file.ts"), Default::default()).await;
14953
14954 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14955 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14956
14957 language_registry.add(Arc::new(Language::new(
14958 LanguageConfig {
14959 name: "TypeScript".into(),
14960 matcher: LanguageMatcher {
14961 path_suffixes: vec!["ts".to_string()],
14962 ..Default::default()
14963 },
14964 ..Default::default()
14965 },
14966 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14967 )));
14968 update_test_language_settings(cx, |settings| {
14969 settings.defaults.prettier = Some(PrettierSettings {
14970 allowed: true,
14971 ..PrettierSettings::default()
14972 });
14973 });
14974
14975 let test_plugin = "test_plugin";
14976 let _ = language_registry.register_fake_lsp(
14977 "TypeScript",
14978 FakeLspAdapter {
14979 prettier_plugins: vec![test_plugin],
14980 ..Default::default()
14981 },
14982 );
14983
14984 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14985 let buffer = project
14986 .update(cx, |project, cx| {
14987 project.open_local_buffer(path!("/file.ts"), cx)
14988 })
14989 .await
14990 .unwrap();
14991
14992 let buffer_text = "one\ntwo\nthree\n";
14993 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14994 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14995 editor.update_in(cx, |editor, window, cx| {
14996 editor.set_text(buffer_text, window, cx)
14997 });
14998
14999 editor
15000 .update_in(cx, |editor, window, cx| {
15001 editor.perform_format(
15002 project.clone(),
15003 FormatTrigger::Manual,
15004 FormatTarget::Buffers,
15005 window,
15006 cx,
15007 )
15008 })
15009 .unwrap()
15010 .await;
15011 assert_eq!(
15012 editor.update(cx, |editor, cx| editor.text(cx)),
15013 buffer_text.to_string() + prettier_format_suffix,
15014 "Test prettier formatting was not applied to the original buffer text",
15015 );
15016
15017 update_test_language_settings(cx, |settings| {
15018 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15019 });
15020 let format = editor.update_in(cx, |editor, window, cx| {
15021 editor.perform_format(
15022 project.clone(),
15023 FormatTrigger::Manual,
15024 FormatTarget::Buffers,
15025 window,
15026 cx,
15027 )
15028 });
15029 format.await.unwrap();
15030 assert_eq!(
15031 editor.update(cx, |editor, cx| editor.text(cx)),
15032 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15033 "Autoformatting (via test prettier) was not applied to the original buffer text",
15034 );
15035}
15036
15037#[gpui::test]
15038async fn test_addition_reverts(cx: &mut TestAppContext) {
15039 init_test(cx, |_| {});
15040 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15041 let base_text = indoc! {r#"
15042 struct Row;
15043 struct Row1;
15044 struct Row2;
15045
15046 struct Row4;
15047 struct Row5;
15048 struct Row6;
15049
15050 struct Row8;
15051 struct Row9;
15052 struct Row10;"#};
15053
15054 // When addition hunks are not adjacent to carets, no hunk revert is performed
15055 assert_hunk_revert(
15056 indoc! {r#"struct Row;
15057 struct Row1;
15058 struct Row1.1;
15059 struct Row1.2;
15060 struct Row2;ˇ
15061
15062 struct Row4;
15063 struct Row5;
15064 struct Row6;
15065
15066 struct Row8;
15067 ˇstruct Row9;
15068 struct Row9.1;
15069 struct Row9.2;
15070 struct Row9.3;
15071 struct Row10;"#},
15072 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15073 indoc! {r#"struct Row;
15074 struct Row1;
15075 struct Row1.1;
15076 struct Row1.2;
15077 struct Row2;ˇ
15078
15079 struct Row4;
15080 struct Row5;
15081 struct Row6;
15082
15083 struct Row8;
15084 ˇstruct Row9;
15085 struct Row9.1;
15086 struct Row9.2;
15087 struct Row9.3;
15088 struct Row10;"#},
15089 base_text,
15090 &mut cx,
15091 );
15092 // Same for selections
15093 assert_hunk_revert(
15094 indoc! {r#"struct Row;
15095 struct Row1;
15096 struct Row2;
15097 struct Row2.1;
15098 struct Row2.2;
15099 «ˇ
15100 struct Row4;
15101 struct» Row5;
15102 «struct Row6;
15103 ˇ»
15104 struct Row9.1;
15105 struct Row9.2;
15106 struct Row9.3;
15107 struct Row8;
15108 struct Row9;
15109 struct Row10;"#},
15110 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15111 indoc! {r#"struct Row;
15112 struct Row1;
15113 struct Row2;
15114 struct Row2.1;
15115 struct Row2.2;
15116 «ˇ
15117 struct Row4;
15118 struct» Row5;
15119 «struct Row6;
15120 ˇ»
15121 struct Row9.1;
15122 struct Row9.2;
15123 struct Row9.3;
15124 struct Row8;
15125 struct Row9;
15126 struct Row10;"#},
15127 base_text,
15128 &mut cx,
15129 );
15130
15131 // When carets and selections intersect the addition hunks, those are reverted.
15132 // Adjacent carets got merged.
15133 assert_hunk_revert(
15134 indoc! {r#"struct Row;
15135 ˇ// something on the top
15136 struct Row1;
15137 struct Row2;
15138 struct Roˇw3.1;
15139 struct Row2.2;
15140 struct Row2.3;ˇ
15141
15142 struct Row4;
15143 struct ˇRow5.1;
15144 struct Row5.2;
15145 struct «Rowˇ»5.3;
15146 struct Row5;
15147 struct Row6;
15148 ˇ
15149 struct Row9.1;
15150 struct «Rowˇ»9.2;
15151 struct «ˇRow»9.3;
15152 struct Row8;
15153 struct Row9;
15154 «ˇ// something on bottom»
15155 struct Row10;"#},
15156 vec![
15157 DiffHunkStatusKind::Added,
15158 DiffHunkStatusKind::Added,
15159 DiffHunkStatusKind::Added,
15160 DiffHunkStatusKind::Added,
15161 DiffHunkStatusKind::Added,
15162 ],
15163 indoc! {r#"struct Row;
15164 ˇstruct Row1;
15165 struct Row2;
15166 ˇ
15167 struct Row4;
15168 ˇstruct Row5;
15169 struct Row6;
15170 ˇ
15171 ˇstruct Row8;
15172 struct Row9;
15173 ˇstruct Row10;"#},
15174 base_text,
15175 &mut cx,
15176 );
15177}
15178
15179#[gpui::test]
15180async fn test_modification_reverts(cx: &mut TestAppContext) {
15181 init_test(cx, |_| {});
15182 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15183 let base_text = indoc! {r#"
15184 struct Row;
15185 struct Row1;
15186 struct Row2;
15187
15188 struct Row4;
15189 struct Row5;
15190 struct Row6;
15191
15192 struct Row8;
15193 struct Row9;
15194 struct Row10;"#};
15195
15196 // Modification hunks behave the same as the addition ones.
15197 assert_hunk_revert(
15198 indoc! {r#"struct Row;
15199 struct Row1;
15200 struct Row33;
15201 ˇ
15202 struct Row4;
15203 struct Row5;
15204 struct Row6;
15205 ˇ
15206 struct Row99;
15207 struct Row9;
15208 struct Row10;"#},
15209 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15210 indoc! {r#"struct Row;
15211 struct Row1;
15212 struct Row33;
15213 ˇ
15214 struct Row4;
15215 struct Row5;
15216 struct Row6;
15217 ˇ
15218 struct Row99;
15219 struct Row9;
15220 struct Row10;"#},
15221 base_text,
15222 &mut cx,
15223 );
15224 assert_hunk_revert(
15225 indoc! {r#"struct Row;
15226 struct Row1;
15227 struct Row33;
15228 «ˇ
15229 struct Row4;
15230 struct» Row5;
15231 «struct Row6;
15232 ˇ»
15233 struct Row99;
15234 struct Row9;
15235 struct Row10;"#},
15236 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15237 indoc! {r#"struct Row;
15238 struct Row1;
15239 struct Row33;
15240 «ˇ
15241 struct Row4;
15242 struct» Row5;
15243 «struct Row6;
15244 ˇ»
15245 struct Row99;
15246 struct Row9;
15247 struct Row10;"#},
15248 base_text,
15249 &mut cx,
15250 );
15251
15252 assert_hunk_revert(
15253 indoc! {r#"ˇstruct Row1.1;
15254 struct Row1;
15255 «ˇstr»uct Row22;
15256
15257 struct ˇRow44;
15258 struct Row5;
15259 struct «Rˇ»ow66;ˇ
15260
15261 «struˇ»ct Row88;
15262 struct Row9;
15263 struct Row1011;ˇ"#},
15264 vec![
15265 DiffHunkStatusKind::Modified,
15266 DiffHunkStatusKind::Modified,
15267 DiffHunkStatusKind::Modified,
15268 DiffHunkStatusKind::Modified,
15269 DiffHunkStatusKind::Modified,
15270 DiffHunkStatusKind::Modified,
15271 ],
15272 indoc! {r#"struct Row;
15273 ˇstruct Row1;
15274 struct Row2;
15275 ˇ
15276 struct Row4;
15277 ˇstruct Row5;
15278 struct Row6;
15279 ˇ
15280 struct Row8;
15281 ˇstruct Row9;
15282 struct Row10;ˇ"#},
15283 base_text,
15284 &mut cx,
15285 );
15286}
15287
15288#[gpui::test]
15289async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15290 init_test(cx, |_| {});
15291 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15292 let base_text = indoc! {r#"
15293 one
15294
15295 two
15296 three
15297 "#};
15298
15299 cx.set_head_text(base_text);
15300 cx.set_state("\nˇ\n");
15301 cx.executor().run_until_parked();
15302 cx.update_editor(|editor, _window, cx| {
15303 editor.expand_selected_diff_hunks(cx);
15304 });
15305 cx.executor().run_until_parked();
15306 cx.update_editor(|editor, window, cx| {
15307 editor.backspace(&Default::default(), window, cx);
15308 });
15309 cx.run_until_parked();
15310 cx.assert_state_with_diff(
15311 indoc! {r#"
15312
15313 - two
15314 - threeˇ
15315 +
15316 "#}
15317 .to_string(),
15318 );
15319}
15320
15321#[gpui::test]
15322async fn test_deletion_reverts(cx: &mut TestAppContext) {
15323 init_test(cx, |_| {});
15324 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15325 let base_text = indoc! {r#"struct Row;
15326struct Row1;
15327struct Row2;
15328
15329struct Row4;
15330struct Row5;
15331struct Row6;
15332
15333struct Row8;
15334struct Row9;
15335struct Row10;"#};
15336
15337 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15338 assert_hunk_revert(
15339 indoc! {r#"struct Row;
15340 struct Row2;
15341
15342 ˇstruct Row4;
15343 struct Row5;
15344 struct Row6;
15345 ˇ
15346 struct Row8;
15347 struct Row10;"#},
15348 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15349 indoc! {r#"struct Row;
15350 struct Row2;
15351
15352 ˇstruct Row4;
15353 struct Row5;
15354 struct Row6;
15355 ˇ
15356 struct Row8;
15357 struct Row10;"#},
15358 base_text,
15359 &mut cx,
15360 );
15361 assert_hunk_revert(
15362 indoc! {r#"struct Row;
15363 struct Row2;
15364
15365 «ˇstruct Row4;
15366 struct» Row5;
15367 «struct Row6;
15368 ˇ»
15369 struct Row8;
15370 struct Row10;"#},
15371 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15372 indoc! {r#"struct Row;
15373 struct Row2;
15374
15375 «ˇstruct Row4;
15376 struct» Row5;
15377 «struct Row6;
15378 ˇ»
15379 struct Row8;
15380 struct Row10;"#},
15381 base_text,
15382 &mut cx,
15383 );
15384
15385 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15386 assert_hunk_revert(
15387 indoc! {r#"struct Row;
15388 ˇstruct Row2;
15389
15390 struct Row4;
15391 struct Row5;
15392 struct Row6;
15393
15394 struct Row8;ˇ
15395 struct Row10;"#},
15396 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15397 indoc! {r#"struct Row;
15398 struct Row1;
15399 ˇstruct Row2;
15400
15401 struct Row4;
15402 struct Row5;
15403 struct Row6;
15404
15405 struct Row8;ˇ
15406 struct Row9;
15407 struct Row10;"#},
15408 base_text,
15409 &mut cx,
15410 );
15411 assert_hunk_revert(
15412 indoc! {r#"struct Row;
15413 struct Row2«ˇ;
15414 struct Row4;
15415 struct» Row5;
15416 «struct Row6;
15417
15418 struct Row8;ˇ»
15419 struct Row10;"#},
15420 vec![
15421 DiffHunkStatusKind::Deleted,
15422 DiffHunkStatusKind::Deleted,
15423 DiffHunkStatusKind::Deleted,
15424 ],
15425 indoc! {r#"struct Row;
15426 struct Row1;
15427 struct Row2«ˇ;
15428
15429 struct Row4;
15430 struct» Row5;
15431 «struct Row6;
15432
15433 struct Row8;ˇ»
15434 struct Row9;
15435 struct Row10;"#},
15436 base_text,
15437 &mut cx,
15438 );
15439}
15440
15441#[gpui::test]
15442async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15443 init_test(cx, |_| {});
15444
15445 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15446 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15447 let base_text_3 =
15448 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15449
15450 let text_1 = edit_first_char_of_every_line(base_text_1);
15451 let text_2 = edit_first_char_of_every_line(base_text_2);
15452 let text_3 = edit_first_char_of_every_line(base_text_3);
15453
15454 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15455 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15456 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15457
15458 let multibuffer = cx.new(|cx| {
15459 let mut multibuffer = MultiBuffer::new(ReadWrite);
15460 multibuffer.push_excerpts(
15461 buffer_1.clone(),
15462 [
15463 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15464 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15465 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15466 ],
15467 cx,
15468 );
15469 multibuffer.push_excerpts(
15470 buffer_2.clone(),
15471 [
15472 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15473 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15474 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15475 ],
15476 cx,
15477 );
15478 multibuffer.push_excerpts(
15479 buffer_3.clone(),
15480 [
15481 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15482 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15483 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15484 ],
15485 cx,
15486 );
15487 multibuffer
15488 });
15489
15490 let fs = FakeFs::new(cx.executor());
15491 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15492 let (editor, cx) = cx
15493 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15494 editor.update_in(cx, |editor, _window, cx| {
15495 for (buffer, diff_base) in [
15496 (buffer_1.clone(), base_text_1),
15497 (buffer_2.clone(), base_text_2),
15498 (buffer_3.clone(), base_text_3),
15499 ] {
15500 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15501 editor
15502 .buffer
15503 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15504 }
15505 });
15506 cx.executor().run_until_parked();
15507
15508 editor.update_in(cx, |editor, window, cx| {
15509 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}");
15510 editor.select_all(&SelectAll, window, cx);
15511 editor.git_restore(&Default::default(), window, cx);
15512 });
15513 cx.executor().run_until_parked();
15514
15515 // When all ranges are selected, all buffer hunks are reverted.
15516 editor.update(cx, |editor, cx| {
15517 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");
15518 });
15519 buffer_1.update(cx, |buffer, _| {
15520 assert_eq!(buffer.text(), base_text_1);
15521 });
15522 buffer_2.update(cx, |buffer, _| {
15523 assert_eq!(buffer.text(), base_text_2);
15524 });
15525 buffer_3.update(cx, |buffer, _| {
15526 assert_eq!(buffer.text(), base_text_3);
15527 });
15528
15529 editor.update_in(cx, |editor, window, cx| {
15530 editor.undo(&Default::default(), window, cx);
15531 });
15532
15533 editor.update_in(cx, |editor, window, cx| {
15534 editor.change_selections(None, window, cx, |s| {
15535 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15536 });
15537 editor.git_restore(&Default::default(), window, cx);
15538 });
15539
15540 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15541 // but not affect buffer_2 and its related excerpts.
15542 editor.update(cx, |editor, cx| {
15543 assert_eq!(
15544 editor.text(cx),
15545 "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}"
15546 );
15547 });
15548 buffer_1.update(cx, |buffer, _| {
15549 assert_eq!(buffer.text(), base_text_1);
15550 });
15551 buffer_2.update(cx, |buffer, _| {
15552 assert_eq!(
15553 buffer.text(),
15554 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15555 );
15556 });
15557 buffer_3.update(cx, |buffer, _| {
15558 assert_eq!(
15559 buffer.text(),
15560 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15561 );
15562 });
15563
15564 fn edit_first_char_of_every_line(text: &str) -> String {
15565 text.split('\n')
15566 .map(|line| format!("X{}", &line[1..]))
15567 .collect::<Vec<_>>()
15568 .join("\n")
15569 }
15570}
15571
15572#[gpui::test]
15573async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15574 init_test(cx, |_| {});
15575
15576 let cols = 4;
15577 let rows = 10;
15578 let sample_text_1 = sample_text(rows, cols, 'a');
15579 assert_eq!(
15580 sample_text_1,
15581 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15582 );
15583 let sample_text_2 = sample_text(rows, cols, 'l');
15584 assert_eq!(
15585 sample_text_2,
15586 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15587 );
15588 let sample_text_3 = sample_text(rows, cols, 'v');
15589 assert_eq!(
15590 sample_text_3,
15591 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15592 );
15593
15594 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15595 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15596 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15597
15598 let multi_buffer = cx.new(|cx| {
15599 let mut multibuffer = MultiBuffer::new(ReadWrite);
15600 multibuffer.push_excerpts(
15601 buffer_1.clone(),
15602 [
15603 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15604 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15605 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15606 ],
15607 cx,
15608 );
15609 multibuffer.push_excerpts(
15610 buffer_2.clone(),
15611 [
15612 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15613 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15614 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15615 ],
15616 cx,
15617 );
15618 multibuffer.push_excerpts(
15619 buffer_3.clone(),
15620 [
15621 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15622 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15623 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15624 ],
15625 cx,
15626 );
15627 multibuffer
15628 });
15629
15630 let fs = FakeFs::new(cx.executor());
15631 fs.insert_tree(
15632 "/a",
15633 json!({
15634 "main.rs": sample_text_1,
15635 "other.rs": sample_text_2,
15636 "lib.rs": sample_text_3,
15637 }),
15638 )
15639 .await;
15640 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15641 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15642 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15643 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15644 Editor::new(
15645 EditorMode::full(),
15646 multi_buffer,
15647 Some(project.clone()),
15648 window,
15649 cx,
15650 )
15651 });
15652 let multibuffer_item_id = workspace
15653 .update(cx, |workspace, window, cx| {
15654 assert!(
15655 workspace.active_item(cx).is_none(),
15656 "active item should be None before the first item is added"
15657 );
15658 workspace.add_item_to_active_pane(
15659 Box::new(multi_buffer_editor.clone()),
15660 None,
15661 true,
15662 window,
15663 cx,
15664 );
15665 let active_item = workspace
15666 .active_item(cx)
15667 .expect("should have an active item after adding the multi buffer");
15668 assert!(
15669 !active_item.is_singleton(cx),
15670 "A multi buffer was expected to active after adding"
15671 );
15672 active_item.item_id()
15673 })
15674 .unwrap();
15675 cx.executor().run_until_parked();
15676
15677 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15678 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15679 s.select_ranges(Some(1..2))
15680 });
15681 editor.open_excerpts(&OpenExcerpts, window, cx);
15682 });
15683 cx.executor().run_until_parked();
15684 let first_item_id = workspace
15685 .update(cx, |workspace, window, cx| {
15686 let active_item = workspace
15687 .active_item(cx)
15688 .expect("should have an active item after navigating into the 1st buffer");
15689 let first_item_id = active_item.item_id();
15690 assert_ne!(
15691 first_item_id, multibuffer_item_id,
15692 "Should navigate into the 1st buffer and activate it"
15693 );
15694 assert!(
15695 active_item.is_singleton(cx),
15696 "New active item should be a singleton buffer"
15697 );
15698 assert_eq!(
15699 active_item
15700 .act_as::<Editor>(cx)
15701 .expect("should have navigated into an editor for the 1st buffer")
15702 .read(cx)
15703 .text(cx),
15704 sample_text_1
15705 );
15706
15707 workspace
15708 .go_back(workspace.active_pane().downgrade(), window, cx)
15709 .detach_and_log_err(cx);
15710
15711 first_item_id
15712 })
15713 .unwrap();
15714 cx.executor().run_until_parked();
15715 workspace
15716 .update(cx, |workspace, _, cx| {
15717 let active_item = workspace
15718 .active_item(cx)
15719 .expect("should have an active item after navigating back");
15720 assert_eq!(
15721 active_item.item_id(),
15722 multibuffer_item_id,
15723 "Should navigate back to the multi buffer"
15724 );
15725 assert!(!active_item.is_singleton(cx));
15726 })
15727 .unwrap();
15728
15729 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15730 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15731 s.select_ranges(Some(39..40))
15732 });
15733 editor.open_excerpts(&OpenExcerpts, window, cx);
15734 });
15735 cx.executor().run_until_parked();
15736 let second_item_id = workspace
15737 .update(cx, |workspace, window, cx| {
15738 let active_item = workspace
15739 .active_item(cx)
15740 .expect("should have an active item after navigating into the 2nd buffer");
15741 let second_item_id = active_item.item_id();
15742 assert_ne!(
15743 second_item_id, multibuffer_item_id,
15744 "Should navigate away from the multibuffer"
15745 );
15746 assert_ne!(
15747 second_item_id, first_item_id,
15748 "Should navigate into the 2nd buffer and activate it"
15749 );
15750 assert!(
15751 active_item.is_singleton(cx),
15752 "New active item should be a singleton buffer"
15753 );
15754 assert_eq!(
15755 active_item
15756 .act_as::<Editor>(cx)
15757 .expect("should have navigated into an editor")
15758 .read(cx)
15759 .text(cx),
15760 sample_text_2
15761 );
15762
15763 workspace
15764 .go_back(workspace.active_pane().downgrade(), window, cx)
15765 .detach_and_log_err(cx);
15766
15767 second_item_id
15768 })
15769 .unwrap();
15770 cx.executor().run_until_parked();
15771 workspace
15772 .update(cx, |workspace, _, cx| {
15773 let active_item = workspace
15774 .active_item(cx)
15775 .expect("should have an active item after navigating back from the 2nd buffer");
15776 assert_eq!(
15777 active_item.item_id(),
15778 multibuffer_item_id,
15779 "Should navigate back from the 2nd buffer to the multi buffer"
15780 );
15781 assert!(!active_item.is_singleton(cx));
15782 })
15783 .unwrap();
15784
15785 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15786 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15787 s.select_ranges(Some(70..70))
15788 });
15789 editor.open_excerpts(&OpenExcerpts, window, cx);
15790 });
15791 cx.executor().run_until_parked();
15792 workspace
15793 .update(cx, |workspace, window, cx| {
15794 let active_item = workspace
15795 .active_item(cx)
15796 .expect("should have an active item after navigating into the 3rd buffer");
15797 let third_item_id = active_item.item_id();
15798 assert_ne!(
15799 third_item_id, multibuffer_item_id,
15800 "Should navigate into the 3rd buffer and activate it"
15801 );
15802 assert_ne!(third_item_id, first_item_id);
15803 assert_ne!(third_item_id, second_item_id);
15804 assert!(
15805 active_item.is_singleton(cx),
15806 "New active item should be a singleton buffer"
15807 );
15808 assert_eq!(
15809 active_item
15810 .act_as::<Editor>(cx)
15811 .expect("should have navigated into an editor")
15812 .read(cx)
15813 .text(cx),
15814 sample_text_3
15815 );
15816
15817 workspace
15818 .go_back(workspace.active_pane().downgrade(), window, cx)
15819 .detach_and_log_err(cx);
15820 })
15821 .unwrap();
15822 cx.executor().run_until_parked();
15823 workspace
15824 .update(cx, |workspace, _, cx| {
15825 let active_item = workspace
15826 .active_item(cx)
15827 .expect("should have an active item after navigating back from the 3rd buffer");
15828 assert_eq!(
15829 active_item.item_id(),
15830 multibuffer_item_id,
15831 "Should navigate back from the 3rd buffer to the multi buffer"
15832 );
15833 assert!(!active_item.is_singleton(cx));
15834 })
15835 .unwrap();
15836}
15837
15838#[gpui::test]
15839async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15840 init_test(cx, |_| {});
15841
15842 let mut cx = EditorTestContext::new(cx).await;
15843
15844 let diff_base = r#"
15845 use some::mod;
15846
15847 const A: u32 = 42;
15848
15849 fn main() {
15850 println!("hello");
15851
15852 println!("world");
15853 }
15854 "#
15855 .unindent();
15856
15857 cx.set_state(
15858 &r#"
15859 use some::modified;
15860
15861 ˇ
15862 fn main() {
15863 println!("hello there");
15864
15865 println!("around the");
15866 println!("world");
15867 }
15868 "#
15869 .unindent(),
15870 );
15871
15872 cx.set_head_text(&diff_base);
15873 executor.run_until_parked();
15874
15875 cx.update_editor(|editor, window, cx| {
15876 editor.go_to_next_hunk(&GoToHunk, window, cx);
15877 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15878 });
15879 executor.run_until_parked();
15880 cx.assert_state_with_diff(
15881 r#"
15882 use some::modified;
15883
15884
15885 fn main() {
15886 - println!("hello");
15887 + ˇ println!("hello there");
15888
15889 println!("around the");
15890 println!("world");
15891 }
15892 "#
15893 .unindent(),
15894 );
15895
15896 cx.update_editor(|editor, window, cx| {
15897 for _ in 0..2 {
15898 editor.go_to_next_hunk(&GoToHunk, window, cx);
15899 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15900 }
15901 });
15902 executor.run_until_parked();
15903 cx.assert_state_with_diff(
15904 r#"
15905 - use some::mod;
15906 + ˇuse some::modified;
15907
15908
15909 fn main() {
15910 - println!("hello");
15911 + println!("hello there");
15912
15913 + println!("around the");
15914 println!("world");
15915 }
15916 "#
15917 .unindent(),
15918 );
15919
15920 cx.update_editor(|editor, window, cx| {
15921 editor.go_to_next_hunk(&GoToHunk, window, cx);
15922 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15923 });
15924 executor.run_until_parked();
15925 cx.assert_state_with_diff(
15926 r#"
15927 - use some::mod;
15928 + use some::modified;
15929
15930 - const A: u32 = 42;
15931 ˇ
15932 fn main() {
15933 - println!("hello");
15934 + println!("hello there");
15935
15936 + println!("around the");
15937 println!("world");
15938 }
15939 "#
15940 .unindent(),
15941 );
15942
15943 cx.update_editor(|editor, window, cx| {
15944 editor.cancel(&Cancel, window, cx);
15945 });
15946
15947 cx.assert_state_with_diff(
15948 r#"
15949 use some::modified;
15950
15951 ˇ
15952 fn main() {
15953 println!("hello there");
15954
15955 println!("around the");
15956 println!("world");
15957 }
15958 "#
15959 .unindent(),
15960 );
15961}
15962
15963#[gpui::test]
15964async fn test_diff_base_change_with_expanded_diff_hunks(
15965 executor: BackgroundExecutor,
15966 cx: &mut TestAppContext,
15967) {
15968 init_test(cx, |_| {});
15969
15970 let mut cx = EditorTestContext::new(cx).await;
15971
15972 let diff_base = r#"
15973 use some::mod1;
15974 use some::mod2;
15975
15976 const A: u32 = 42;
15977 const B: u32 = 42;
15978 const C: u32 = 42;
15979
15980 fn main() {
15981 println!("hello");
15982
15983 println!("world");
15984 }
15985 "#
15986 .unindent();
15987
15988 cx.set_state(
15989 &r#"
15990 use some::mod2;
15991
15992 const A: u32 = 42;
15993 const C: u32 = 42;
15994
15995 fn main(ˇ) {
15996 //println!("hello");
15997
15998 println!("world");
15999 //
16000 //
16001 }
16002 "#
16003 .unindent(),
16004 );
16005
16006 cx.set_head_text(&diff_base);
16007 executor.run_until_parked();
16008
16009 cx.update_editor(|editor, window, cx| {
16010 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16011 });
16012 executor.run_until_parked();
16013 cx.assert_state_with_diff(
16014 r#"
16015 - use some::mod1;
16016 use some::mod2;
16017
16018 const A: u32 = 42;
16019 - const B: u32 = 42;
16020 const C: u32 = 42;
16021
16022 fn main(ˇ) {
16023 - println!("hello");
16024 + //println!("hello");
16025
16026 println!("world");
16027 + //
16028 + //
16029 }
16030 "#
16031 .unindent(),
16032 );
16033
16034 cx.set_head_text("new diff base!");
16035 executor.run_until_parked();
16036 cx.assert_state_with_diff(
16037 r#"
16038 - new diff base!
16039 + use some::mod2;
16040 +
16041 + const A: u32 = 42;
16042 + const C: u32 = 42;
16043 +
16044 + fn main(ˇ) {
16045 + //println!("hello");
16046 +
16047 + println!("world");
16048 + //
16049 + //
16050 + }
16051 "#
16052 .unindent(),
16053 );
16054}
16055
16056#[gpui::test]
16057async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16058 init_test(cx, |_| {});
16059
16060 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16061 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16062 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16063 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16064 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16065 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16066
16067 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16068 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16069 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16070
16071 let multi_buffer = cx.new(|cx| {
16072 let mut multibuffer = MultiBuffer::new(ReadWrite);
16073 multibuffer.push_excerpts(
16074 buffer_1.clone(),
16075 [
16076 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16077 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16078 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16079 ],
16080 cx,
16081 );
16082 multibuffer.push_excerpts(
16083 buffer_2.clone(),
16084 [
16085 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16086 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16087 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16088 ],
16089 cx,
16090 );
16091 multibuffer.push_excerpts(
16092 buffer_3.clone(),
16093 [
16094 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16095 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16096 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16097 ],
16098 cx,
16099 );
16100 multibuffer
16101 });
16102
16103 let editor =
16104 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16105 editor
16106 .update(cx, |editor, _window, cx| {
16107 for (buffer, diff_base) in [
16108 (buffer_1.clone(), file_1_old),
16109 (buffer_2.clone(), file_2_old),
16110 (buffer_3.clone(), file_3_old),
16111 ] {
16112 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16113 editor
16114 .buffer
16115 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16116 }
16117 })
16118 .unwrap();
16119
16120 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16121 cx.run_until_parked();
16122
16123 cx.assert_editor_state(
16124 &"
16125 ˇaaa
16126 ccc
16127 ddd
16128
16129 ggg
16130 hhh
16131
16132
16133 lll
16134 mmm
16135 NNN
16136
16137 qqq
16138 rrr
16139
16140 uuu
16141 111
16142 222
16143 333
16144
16145 666
16146 777
16147
16148 000
16149 !!!"
16150 .unindent(),
16151 );
16152
16153 cx.update_editor(|editor, window, cx| {
16154 editor.select_all(&SelectAll, window, cx);
16155 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16156 });
16157 cx.executor().run_until_parked();
16158
16159 cx.assert_state_with_diff(
16160 "
16161 «aaa
16162 - bbb
16163 ccc
16164 ddd
16165
16166 ggg
16167 hhh
16168
16169
16170 lll
16171 mmm
16172 - nnn
16173 + NNN
16174
16175 qqq
16176 rrr
16177
16178 uuu
16179 111
16180 222
16181 333
16182
16183 + 666
16184 777
16185
16186 000
16187 !!!ˇ»"
16188 .unindent(),
16189 );
16190}
16191
16192#[gpui::test]
16193async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16194 init_test(cx, |_| {});
16195
16196 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16197 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16198
16199 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16200 let multi_buffer = cx.new(|cx| {
16201 let mut multibuffer = MultiBuffer::new(ReadWrite);
16202 multibuffer.push_excerpts(
16203 buffer.clone(),
16204 [
16205 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16206 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16207 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16208 ],
16209 cx,
16210 );
16211 multibuffer
16212 });
16213
16214 let editor =
16215 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16216 editor
16217 .update(cx, |editor, _window, cx| {
16218 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16219 editor
16220 .buffer
16221 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16222 })
16223 .unwrap();
16224
16225 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16226 cx.run_until_parked();
16227
16228 cx.update_editor(|editor, window, cx| {
16229 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16230 });
16231 cx.executor().run_until_parked();
16232
16233 // When the start of a hunk coincides with the start of its excerpt,
16234 // the hunk is expanded. When the start of a a hunk is earlier than
16235 // the start of its excerpt, the hunk is not expanded.
16236 cx.assert_state_with_diff(
16237 "
16238 ˇaaa
16239 - bbb
16240 + BBB
16241
16242 - ddd
16243 - eee
16244 + DDD
16245 + EEE
16246 fff
16247
16248 iii
16249 "
16250 .unindent(),
16251 );
16252}
16253
16254#[gpui::test]
16255async fn test_edits_around_expanded_insertion_hunks(
16256 executor: BackgroundExecutor,
16257 cx: &mut TestAppContext,
16258) {
16259 init_test(cx, |_| {});
16260
16261 let mut cx = EditorTestContext::new(cx).await;
16262
16263 let diff_base = r#"
16264 use some::mod1;
16265 use some::mod2;
16266
16267 const A: u32 = 42;
16268
16269 fn main() {
16270 println!("hello");
16271
16272 println!("world");
16273 }
16274 "#
16275 .unindent();
16276 executor.run_until_parked();
16277 cx.set_state(
16278 &r#"
16279 use some::mod1;
16280 use some::mod2;
16281
16282 const A: u32 = 42;
16283 const B: u32 = 42;
16284 const C: u32 = 42;
16285 ˇ
16286
16287 fn main() {
16288 println!("hello");
16289
16290 println!("world");
16291 }
16292 "#
16293 .unindent(),
16294 );
16295
16296 cx.set_head_text(&diff_base);
16297 executor.run_until_parked();
16298
16299 cx.update_editor(|editor, window, cx| {
16300 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16301 });
16302 executor.run_until_parked();
16303
16304 cx.assert_state_with_diff(
16305 r#"
16306 use some::mod1;
16307 use some::mod2;
16308
16309 const A: u32 = 42;
16310 + const B: u32 = 42;
16311 + const C: u32 = 42;
16312 + ˇ
16313
16314 fn main() {
16315 println!("hello");
16316
16317 println!("world");
16318 }
16319 "#
16320 .unindent(),
16321 );
16322
16323 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16324 executor.run_until_parked();
16325
16326 cx.assert_state_with_diff(
16327 r#"
16328 use some::mod1;
16329 use some::mod2;
16330
16331 const A: u32 = 42;
16332 + const B: u32 = 42;
16333 + const C: u32 = 42;
16334 + const D: u32 = 42;
16335 + ˇ
16336
16337 fn main() {
16338 println!("hello");
16339
16340 println!("world");
16341 }
16342 "#
16343 .unindent(),
16344 );
16345
16346 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16347 executor.run_until_parked();
16348
16349 cx.assert_state_with_diff(
16350 r#"
16351 use some::mod1;
16352 use some::mod2;
16353
16354 const A: u32 = 42;
16355 + const B: u32 = 42;
16356 + const C: u32 = 42;
16357 + const D: u32 = 42;
16358 + const E: u32 = 42;
16359 + ˇ
16360
16361 fn main() {
16362 println!("hello");
16363
16364 println!("world");
16365 }
16366 "#
16367 .unindent(),
16368 );
16369
16370 cx.update_editor(|editor, window, cx| {
16371 editor.delete_line(&DeleteLine, window, cx);
16372 });
16373 executor.run_until_parked();
16374
16375 cx.assert_state_with_diff(
16376 r#"
16377 use some::mod1;
16378 use some::mod2;
16379
16380 const A: u32 = 42;
16381 + const B: u32 = 42;
16382 + const C: u32 = 42;
16383 + const D: u32 = 42;
16384 + const E: u32 = 42;
16385 ˇ
16386 fn main() {
16387 println!("hello");
16388
16389 println!("world");
16390 }
16391 "#
16392 .unindent(),
16393 );
16394
16395 cx.update_editor(|editor, window, cx| {
16396 editor.move_up(&MoveUp, window, cx);
16397 editor.delete_line(&DeleteLine, window, cx);
16398 editor.move_up(&MoveUp, window, cx);
16399 editor.delete_line(&DeleteLine, window, cx);
16400 editor.move_up(&MoveUp, window, cx);
16401 editor.delete_line(&DeleteLine, window, cx);
16402 });
16403 executor.run_until_parked();
16404 cx.assert_state_with_diff(
16405 r#"
16406 use some::mod1;
16407 use some::mod2;
16408
16409 const A: u32 = 42;
16410 + const B: u32 = 42;
16411 ˇ
16412 fn main() {
16413 println!("hello");
16414
16415 println!("world");
16416 }
16417 "#
16418 .unindent(),
16419 );
16420
16421 cx.update_editor(|editor, window, cx| {
16422 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16423 editor.delete_line(&DeleteLine, window, cx);
16424 });
16425 executor.run_until_parked();
16426 cx.assert_state_with_diff(
16427 r#"
16428 ˇ
16429 fn main() {
16430 println!("hello");
16431
16432 println!("world");
16433 }
16434 "#
16435 .unindent(),
16436 );
16437}
16438
16439#[gpui::test]
16440async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16441 init_test(cx, |_| {});
16442
16443 let mut cx = EditorTestContext::new(cx).await;
16444 cx.set_head_text(indoc! { "
16445 one
16446 two
16447 three
16448 four
16449 five
16450 "
16451 });
16452 cx.set_state(indoc! { "
16453 one
16454 ˇthree
16455 five
16456 "});
16457 cx.run_until_parked();
16458 cx.update_editor(|editor, window, cx| {
16459 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16460 });
16461 cx.assert_state_with_diff(
16462 indoc! { "
16463 one
16464 - two
16465 ˇthree
16466 - four
16467 five
16468 "}
16469 .to_string(),
16470 );
16471 cx.update_editor(|editor, window, cx| {
16472 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16473 });
16474
16475 cx.assert_state_with_diff(
16476 indoc! { "
16477 one
16478 ˇthree
16479 five
16480 "}
16481 .to_string(),
16482 );
16483
16484 cx.set_state(indoc! { "
16485 one
16486 ˇTWO
16487 three
16488 four
16489 five
16490 "});
16491 cx.run_until_parked();
16492 cx.update_editor(|editor, window, cx| {
16493 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16494 });
16495
16496 cx.assert_state_with_diff(
16497 indoc! { "
16498 one
16499 - two
16500 + ˇTWO
16501 three
16502 four
16503 five
16504 "}
16505 .to_string(),
16506 );
16507 cx.update_editor(|editor, window, cx| {
16508 editor.move_up(&Default::default(), window, cx);
16509 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16510 });
16511 cx.assert_state_with_diff(
16512 indoc! { "
16513 one
16514 ˇTWO
16515 three
16516 four
16517 five
16518 "}
16519 .to_string(),
16520 );
16521}
16522
16523#[gpui::test]
16524async fn test_edits_around_expanded_deletion_hunks(
16525 executor: BackgroundExecutor,
16526 cx: &mut TestAppContext,
16527) {
16528 init_test(cx, |_| {});
16529
16530 let mut cx = EditorTestContext::new(cx).await;
16531
16532 let diff_base = r#"
16533 use some::mod1;
16534 use some::mod2;
16535
16536 const A: u32 = 42;
16537 const B: u32 = 42;
16538 const C: u32 = 42;
16539
16540
16541 fn main() {
16542 println!("hello");
16543
16544 println!("world");
16545 }
16546 "#
16547 .unindent();
16548 executor.run_until_parked();
16549 cx.set_state(
16550 &r#"
16551 use some::mod1;
16552 use some::mod2;
16553
16554 ˇconst B: u32 = 42;
16555 const C: u32 = 42;
16556
16557
16558 fn main() {
16559 println!("hello");
16560
16561 println!("world");
16562 }
16563 "#
16564 .unindent(),
16565 );
16566
16567 cx.set_head_text(&diff_base);
16568 executor.run_until_parked();
16569
16570 cx.update_editor(|editor, window, cx| {
16571 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16572 });
16573 executor.run_until_parked();
16574
16575 cx.assert_state_with_diff(
16576 r#"
16577 use some::mod1;
16578 use some::mod2;
16579
16580 - const A: u32 = 42;
16581 ˇconst B: u32 = 42;
16582 const C: u32 = 42;
16583
16584
16585 fn main() {
16586 println!("hello");
16587
16588 println!("world");
16589 }
16590 "#
16591 .unindent(),
16592 );
16593
16594 cx.update_editor(|editor, window, cx| {
16595 editor.delete_line(&DeleteLine, window, cx);
16596 });
16597 executor.run_until_parked();
16598 cx.assert_state_with_diff(
16599 r#"
16600 use some::mod1;
16601 use some::mod2;
16602
16603 - const A: u32 = 42;
16604 - const B: u32 = 42;
16605 ˇconst C: u32 = 42;
16606
16607
16608 fn main() {
16609 println!("hello");
16610
16611 println!("world");
16612 }
16613 "#
16614 .unindent(),
16615 );
16616
16617 cx.update_editor(|editor, window, cx| {
16618 editor.delete_line(&DeleteLine, window, cx);
16619 });
16620 executor.run_until_parked();
16621 cx.assert_state_with_diff(
16622 r#"
16623 use some::mod1;
16624 use some::mod2;
16625
16626 - const A: u32 = 42;
16627 - const B: u32 = 42;
16628 - const C: u32 = 42;
16629 ˇ
16630
16631 fn main() {
16632 println!("hello");
16633
16634 println!("world");
16635 }
16636 "#
16637 .unindent(),
16638 );
16639
16640 cx.update_editor(|editor, window, cx| {
16641 editor.handle_input("replacement", window, cx);
16642 });
16643 executor.run_until_parked();
16644 cx.assert_state_with_diff(
16645 r#"
16646 use some::mod1;
16647 use some::mod2;
16648
16649 - const A: u32 = 42;
16650 - const B: u32 = 42;
16651 - const C: u32 = 42;
16652 -
16653 + replacementˇ
16654
16655 fn main() {
16656 println!("hello");
16657
16658 println!("world");
16659 }
16660 "#
16661 .unindent(),
16662 );
16663}
16664
16665#[gpui::test]
16666async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16667 init_test(cx, |_| {});
16668
16669 let mut cx = EditorTestContext::new(cx).await;
16670
16671 let base_text = r#"
16672 one
16673 two
16674 three
16675 four
16676 five
16677 "#
16678 .unindent();
16679 executor.run_until_parked();
16680 cx.set_state(
16681 &r#"
16682 one
16683 two
16684 fˇour
16685 five
16686 "#
16687 .unindent(),
16688 );
16689
16690 cx.set_head_text(&base_text);
16691 executor.run_until_parked();
16692
16693 cx.update_editor(|editor, window, cx| {
16694 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16695 });
16696 executor.run_until_parked();
16697
16698 cx.assert_state_with_diff(
16699 r#"
16700 one
16701 two
16702 - three
16703 fˇour
16704 five
16705 "#
16706 .unindent(),
16707 );
16708
16709 cx.update_editor(|editor, window, cx| {
16710 editor.backspace(&Backspace, window, cx);
16711 editor.backspace(&Backspace, window, cx);
16712 });
16713 executor.run_until_parked();
16714 cx.assert_state_with_diff(
16715 r#"
16716 one
16717 two
16718 - threeˇ
16719 - four
16720 + our
16721 five
16722 "#
16723 .unindent(),
16724 );
16725}
16726
16727#[gpui::test]
16728async fn test_edit_after_expanded_modification_hunk(
16729 executor: BackgroundExecutor,
16730 cx: &mut TestAppContext,
16731) {
16732 init_test(cx, |_| {});
16733
16734 let mut cx = EditorTestContext::new(cx).await;
16735
16736 let diff_base = r#"
16737 use some::mod1;
16738 use some::mod2;
16739
16740 const A: u32 = 42;
16741 const B: u32 = 42;
16742 const C: u32 = 42;
16743 const D: u32 = 42;
16744
16745
16746 fn main() {
16747 println!("hello");
16748
16749 println!("world");
16750 }"#
16751 .unindent();
16752
16753 cx.set_state(
16754 &r#"
16755 use some::mod1;
16756 use some::mod2;
16757
16758 const A: u32 = 42;
16759 const B: u32 = 42;
16760 const C: u32 = 43ˇ
16761 const D: u32 = 42;
16762
16763
16764 fn main() {
16765 println!("hello");
16766
16767 println!("world");
16768 }"#
16769 .unindent(),
16770 );
16771
16772 cx.set_head_text(&diff_base);
16773 executor.run_until_parked();
16774 cx.update_editor(|editor, window, cx| {
16775 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16776 });
16777 executor.run_until_parked();
16778
16779 cx.assert_state_with_diff(
16780 r#"
16781 use some::mod1;
16782 use some::mod2;
16783
16784 const A: u32 = 42;
16785 const B: u32 = 42;
16786 - const C: u32 = 42;
16787 + const C: u32 = 43ˇ
16788 const D: u32 = 42;
16789
16790
16791 fn main() {
16792 println!("hello");
16793
16794 println!("world");
16795 }"#
16796 .unindent(),
16797 );
16798
16799 cx.update_editor(|editor, window, cx| {
16800 editor.handle_input("\nnew_line\n", window, cx);
16801 });
16802 executor.run_until_parked();
16803
16804 cx.assert_state_with_diff(
16805 r#"
16806 use some::mod1;
16807 use some::mod2;
16808
16809 const A: u32 = 42;
16810 const B: u32 = 42;
16811 - const C: u32 = 42;
16812 + const C: u32 = 43
16813 + new_line
16814 + ˇ
16815 const D: u32 = 42;
16816
16817
16818 fn main() {
16819 println!("hello");
16820
16821 println!("world");
16822 }"#
16823 .unindent(),
16824 );
16825}
16826
16827#[gpui::test]
16828async fn test_stage_and_unstage_added_file_hunk(
16829 executor: BackgroundExecutor,
16830 cx: &mut TestAppContext,
16831) {
16832 init_test(cx, |_| {});
16833
16834 let mut cx = EditorTestContext::new(cx).await;
16835 cx.update_editor(|editor, _, cx| {
16836 editor.set_expand_all_diff_hunks(cx);
16837 });
16838
16839 let working_copy = r#"
16840 ˇfn main() {
16841 println!("hello, world!");
16842 }
16843 "#
16844 .unindent();
16845
16846 cx.set_state(&working_copy);
16847 executor.run_until_parked();
16848
16849 cx.assert_state_with_diff(
16850 r#"
16851 + ˇfn main() {
16852 + println!("hello, world!");
16853 + }
16854 "#
16855 .unindent(),
16856 );
16857 cx.assert_index_text(None);
16858
16859 cx.update_editor(|editor, window, cx| {
16860 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16861 });
16862 executor.run_until_parked();
16863 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16864 cx.assert_state_with_diff(
16865 r#"
16866 + ˇfn main() {
16867 + println!("hello, world!");
16868 + }
16869 "#
16870 .unindent(),
16871 );
16872
16873 cx.update_editor(|editor, window, cx| {
16874 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16875 });
16876 executor.run_until_parked();
16877 cx.assert_index_text(None);
16878}
16879
16880async fn setup_indent_guides_editor(
16881 text: &str,
16882 cx: &mut TestAppContext,
16883) -> (BufferId, EditorTestContext) {
16884 init_test(cx, |_| {});
16885
16886 let mut cx = EditorTestContext::new(cx).await;
16887
16888 let buffer_id = cx.update_editor(|editor, window, cx| {
16889 editor.set_text(text, window, cx);
16890 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16891
16892 buffer_ids[0]
16893 });
16894
16895 (buffer_id, cx)
16896}
16897
16898fn assert_indent_guides(
16899 range: Range<u32>,
16900 expected: Vec<IndentGuide>,
16901 active_indices: Option<Vec<usize>>,
16902 cx: &mut EditorTestContext,
16903) {
16904 let indent_guides = cx.update_editor(|editor, window, cx| {
16905 let snapshot = editor.snapshot(window, cx).display_snapshot;
16906 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16907 editor,
16908 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16909 true,
16910 &snapshot,
16911 cx,
16912 );
16913
16914 indent_guides.sort_by(|a, b| {
16915 a.depth.cmp(&b.depth).then(
16916 a.start_row
16917 .cmp(&b.start_row)
16918 .then(a.end_row.cmp(&b.end_row)),
16919 )
16920 });
16921 indent_guides
16922 });
16923
16924 if let Some(expected) = active_indices {
16925 let active_indices = cx.update_editor(|editor, window, cx| {
16926 let snapshot = editor.snapshot(window, cx).display_snapshot;
16927 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16928 });
16929
16930 assert_eq!(
16931 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16932 expected,
16933 "Active indent guide indices do not match"
16934 );
16935 }
16936
16937 assert_eq!(indent_guides, expected, "Indent guides do not match");
16938}
16939
16940fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16941 IndentGuide {
16942 buffer_id,
16943 start_row: MultiBufferRow(start_row),
16944 end_row: MultiBufferRow(end_row),
16945 depth,
16946 tab_size: 4,
16947 settings: IndentGuideSettings {
16948 enabled: true,
16949 line_width: 1,
16950 active_line_width: 1,
16951 ..Default::default()
16952 },
16953 }
16954}
16955
16956#[gpui::test]
16957async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16958 let (buffer_id, mut cx) = setup_indent_guides_editor(
16959 &"
16960 fn main() {
16961 let a = 1;
16962 }"
16963 .unindent(),
16964 cx,
16965 )
16966 .await;
16967
16968 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16969}
16970
16971#[gpui::test]
16972async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16973 let (buffer_id, mut cx) = setup_indent_guides_editor(
16974 &"
16975 fn main() {
16976 let a = 1;
16977 let b = 2;
16978 }"
16979 .unindent(),
16980 cx,
16981 )
16982 .await;
16983
16984 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16985}
16986
16987#[gpui::test]
16988async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16989 let (buffer_id, mut cx) = setup_indent_guides_editor(
16990 &"
16991 fn main() {
16992 let a = 1;
16993 if a == 3 {
16994 let b = 2;
16995 } else {
16996 let c = 3;
16997 }
16998 }"
16999 .unindent(),
17000 cx,
17001 )
17002 .await;
17003
17004 assert_indent_guides(
17005 0..8,
17006 vec![
17007 indent_guide(buffer_id, 1, 6, 0),
17008 indent_guide(buffer_id, 3, 3, 1),
17009 indent_guide(buffer_id, 5, 5, 1),
17010 ],
17011 None,
17012 &mut cx,
17013 );
17014}
17015
17016#[gpui::test]
17017async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17018 let (buffer_id, mut cx) = setup_indent_guides_editor(
17019 &"
17020 fn main() {
17021 let a = 1;
17022 let b = 2;
17023 let c = 3;
17024 }"
17025 .unindent(),
17026 cx,
17027 )
17028 .await;
17029
17030 assert_indent_guides(
17031 0..5,
17032 vec![
17033 indent_guide(buffer_id, 1, 3, 0),
17034 indent_guide(buffer_id, 2, 2, 1),
17035 ],
17036 None,
17037 &mut cx,
17038 );
17039}
17040
17041#[gpui::test]
17042async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17043 let (buffer_id, mut cx) = setup_indent_guides_editor(
17044 &"
17045 fn main() {
17046 let a = 1;
17047
17048 let c = 3;
17049 }"
17050 .unindent(),
17051 cx,
17052 )
17053 .await;
17054
17055 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17056}
17057
17058#[gpui::test]
17059async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17060 let (buffer_id, mut cx) = setup_indent_guides_editor(
17061 &"
17062 fn main() {
17063 let a = 1;
17064
17065 let c = 3;
17066
17067 if a == 3 {
17068 let b = 2;
17069 } else {
17070 let c = 3;
17071 }
17072 }"
17073 .unindent(),
17074 cx,
17075 )
17076 .await;
17077
17078 assert_indent_guides(
17079 0..11,
17080 vec![
17081 indent_guide(buffer_id, 1, 9, 0),
17082 indent_guide(buffer_id, 6, 6, 1),
17083 indent_guide(buffer_id, 8, 8, 1),
17084 ],
17085 None,
17086 &mut cx,
17087 );
17088}
17089
17090#[gpui::test]
17091async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17092 let (buffer_id, mut cx) = setup_indent_guides_editor(
17093 &"
17094 fn main() {
17095 let a = 1;
17096
17097 let c = 3;
17098
17099 if a == 3 {
17100 let b = 2;
17101 } else {
17102 let c = 3;
17103 }
17104 }"
17105 .unindent(),
17106 cx,
17107 )
17108 .await;
17109
17110 assert_indent_guides(
17111 1..11,
17112 vec![
17113 indent_guide(buffer_id, 1, 9, 0),
17114 indent_guide(buffer_id, 6, 6, 1),
17115 indent_guide(buffer_id, 8, 8, 1),
17116 ],
17117 None,
17118 &mut cx,
17119 );
17120}
17121
17122#[gpui::test]
17123async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17124 let (buffer_id, mut cx) = setup_indent_guides_editor(
17125 &"
17126 fn main() {
17127 let a = 1;
17128
17129 let c = 3;
17130
17131 if a == 3 {
17132 let b = 2;
17133 } else {
17134 let c = 3;
17135 }
17136 }"
17137 .unindent(),
17138 cx,
17139 )
17140 .await;
17141
17142 assert_indent_guides(
17143 1..10,
17144 vec![
17145 indent_guide(buffer_id, 1, 9, 0),
17146 indent_guide(buffer_id, 6, 6, 1),
17147 indent_guide(buffer_id, 8, 8, 1),
17148 ],
17149 None,
17150 &mut cx,
17151 );
17152}
17153
17154#[gpui::test]
17155async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17156 let (buffer_id, mut cx) = setup_indent_guides_editor(
17157 &"
17158 fn main() {
17159 if a {
17160 b(
17161 c,
17162 d,
17163 )
17164 } else {
17165 e(
17166 f
17167 )
17168 }
17169 }"
17170 .unindent(),
17171 cx,
17172 )
17173 .await;
17174
17175 assert_indent_guides(
17176 0..11,
17177 vec![
17178 indent_guide(buffer_id, 1, 10, 0),
17179 indent_guide(buffer_id, 2, 5, 1),
17180 indent_guide(buffer_id, 7, 9, 1),
17181 indent_guide(buffer_id, 3, 4, 2),
17182 indent_guide(buffer_id, 8, 8, 2),
17183 ],
17184 None,
17185 &mut cx,
17186 );
17187
17188 cx.update_editor(|editor, window, cx| {
17189 editor.fold_at(MultiBufferRow(2), window, cx);
17190 assert_eq!(
17191 editor.display_text(cx),
17192 "
17193 fn main() {
17194 if a {
17195 b(⋯
17196 )
17197 } else {
17198 e(
17199 f
17200 )
17201 }
17202 }"
17203 .unindent()
17204 );
17205 });
17206
17207 assert_indent_guides(
17208 0..11,
17209 vec![
17210 indent_guide(buffer_id, 1, 10, 0),
17211 indent_guide(buffer_id, 2, 5, 1),
17212 indent_guide(buffer_id, 7, 9, 1),
17213 indent_guide(buffer_id, 8, 8, 2),
17214 ],
17215 None,
17216 &mut cx,
17217 );
17218}
17219
17220#[gpui::test]
17221async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17222 let (buffer_id, mut cx) = setup_indent_guides_editor(
17223 &"
17224 block1
17225 block2
17226 block3
17227 block4
17228 block2
17229 block1
17230 block1"
17231 .unindent(),
17232 cx,
17233 )
17234 .await;
17235
17236 assert_indent_guides(
17237 1..10,
17238 vec![
17239 indent_guide(buffer_id, 1, 4, 0),
17240 indent_guide(buffer_id, 2, 3, 1),
17241 indent_guide(buffer_id, 3, 3, 2),
17242 ],
17243 None,
17244 &mut cx,
17245 );
17246}
17247
17248#[gpui::test]
17249async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17250 let (buffer_id, mut cx) = setup_indent_guides_editor(
17251 &"
17252 block1
17253 block2
17254 block3
17255
17256 block1
17257 block1"
17258 .unindent(),
17259 cx,
17260 )
17261 .await;
17262
17263 assert_indent_guides(
17264 0..6,
17265 vec![
17266 indent_guide(buffer_id, 1, 2, 0),
17267 indent_guide(buffer_id, 2, 2, 1),
17268 ],
17269 None,
17270 &mut cx,
17271 );
17272}
17273
17274#[gpui::test]
17275async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17276 let (buffer_id, mut cx) = setup_indent_guides_editor(
17277 &"
17278 function component() {
17279 \treturn (
17280 \t\t\t
17281 \t\t<div>
17282 \t\t\t<abc></abc>
17283 \t\t</div>
17284 \t)
17285 }"
17286 .unindent(),
17287 cx,
17288 )
17289 .await;
17290
17291 assert_indent_guides(
17292 0..8,
17293 vec![
17294 indent_guide(buffer_id, 1, 6, 0),
17295 indent_guide(buffer_id, 2, 5, 1),
17296 indent_guide(buffer_id, 4, 4, 2),
17297 ],
17298 None,
17299 &mut cx,
17300 );
17301}
17302
17303#[gpui::test]
17304async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17305 let (buffer_id, mut cx) = setup_indent_guides_editor(
17306 &"
17307 function component() {
17308 \treturn (
17309 \t
17310 \t\t<div>
17311 \t\t\t<abc></abc>
17312 \t\t</div>
17313 \t)
17314 }"
17315 .unindent(),
17316 cx,
17317 )
17318 .await;
17319
17320 assert_indent_guides(
17321 0..8,
17322 vec![
17323 indent_guide(buffer_id, 1, 6, 0),
17324 indent_guide(buffer_id, 2, 5, 1),
17325 indent_guide(buffer_id, 4, 4, 2),
17326 ],
17327 None,
17328 &mut cx,
17329 );
17330}
17331
17332#[gpui::test]
17333async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17334 let (buffer_id, mut cx) = setup_indent_guides_editor(
17335 &"
17336 block1
17337
17338
17339
17340 block2
17341 "
17342 .unindent(),
17343 cx,
17344 )
17345 .await;
17346
17347 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17348}
17349
17350#[gpui::test]
17351async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17352 let (buffer_id, mut cx) = setup_indent_guides_editor(
17353 &"
17354 def a:
17355 \tb = 3
17356 \tif True:
17357 \t\tc = 4
17358 \t\td = 5
17359 \tprint(b)
17360 "
17361 .unindent(),
17362 cx,
17363 )
17364 .await;
17365
17366 assert_indent_guides(
17367 0..6,
17368 vec![
17369 indent_guide(buffer_id, 1, 5, 0),
17370 indent_guide(buffer_id, 3, 4, 1),
17371 ],
17372 None,
17373 &mut cx,
17374 );
17375}
17376
17377#[gpui::test]
17378async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17379 let (buffer_id, mut cx) = setup_indent_guides_editor(
17380 &"
17381 fn main() {
17382 let a = 1;
17383 }"
17384 .unindent(),
17385 cx,
17386 )
17387 .await;
17388
17389 cx.update_editor(|editor, window, cx| {
17390 editor.change_selections(None, window, cx, |s| {
17391 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17392 });
17393 });
17394
17395 assert_indent_guides(
17396 0..3,
17397 vec![indent_guide(buffer_id, 1, 1, 0)],
17398 Some(vec![0]),
17399 &mut cx,
17400 );
17401}
17402
17403#[gpui::test]
17404async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17405 let (buffer_id, mut cx) = setup_indent_guides_editor(
17406 &"
17407 fn main() {
17408 if 1 == 2 {
17409 let a = 1;
17410 }
17411 }"
17412 .unindent(),
17413 cx,
17414 )
17415 .await;
17416
17417 cx.update_editor(|editor, window, cx| {
17418 editor.change_selections(None, window, cx, |s| {
17419 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17420 });
17421 });
17422
17423 assert_indent_guides(
17424 0..4,
17425 vec![
17426 indent_guide(buffer_id, 1, 3, 0),
17427 indent_guide(buffer_id, 2, 2, 1),
17428 ],
17429 Some(vec![1]),
17430 &mut cx,
17431 );
17432
17433 cx.update_editor(|editor, window, cx| {
17434 editor.change_selections(None, window, cx, |s| {
17435 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17436 });
17437 });
17438
17439 assert_indent_guides(
17440 0..4,
17441 vec![
17442 indent_guide(buffer_id, 1, 3, 0),
17443 indent_guide(buffer_id, 2, 2, 1),
17444 ],
17445 Some(vec![1]),
17446 &mut cx,
17447 );
17448
17449 cx.update_editor(|editor, window, cx| {
17450 editor.change_selections(None, window, cx, |s| {
17451 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17452 });
17453 });
17454
17455 assert_indent_guides(
17456 0..4,
17457 vec![
17458 indent_guide(buffer_id, 1, 3, 0),
17459 indent_guide(buffer_id, 2, 2, 1),
17460 ],
17461 Some(vec![0]),
17462 &mut cx,
17463 );
17464}
17465
17466#[gpui::test]
17467async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17468 let (buffer_id, mut cx) = setup_indent_guides_editor(
17469 &"
17470 fn main() {
17471 let a = 1;
17472
17473 let b = 2;
17474 }"
17475 .unindent(),
17476 cx,
17477 )
17478 .await;
17479
17480 cx.update_editor(|editor, window, cx| {
17481 editor.change_selections(None, window, cx, |s| {
17482 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17483 });
17484 });
17485
17486 assert_indent_guides(
17487 0..5,
17488 vec![indent_guide(buffer_id, 1, 3, 0)],
17489 Some(vec![0]),
17490 &mut cx,
17491 );
17492}
17493
17494#[gpui::test]
17495async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17496 let (buffer_id, mut cx) = setup_indent_guides_editor(
17497 &"
17498 def m:
17499 a = 1
17500 pass"
17501 .unindent(),
17502 cx,
17503 )
17504 .await;
17505
17506 cx.update_editor(|editor, window, cx| {
17507 editor.change_selections(None, window, cx, |s| {
17508 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17509 });
17510 });
17511
17512 assert_indent_guides(
17513 0..3,
17514 vec![indent_guide(buffer_id, 1, 2, 0)],
17515 Some(vec![0]),
17516 &mut cx,
17517 );
17518}
17519
17520#[gpui::test]
17521async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17522 init_test(cx, |_| {});
17523 let mut cx = EditorTestContext::new(cx).await;
17524 let text = indoc! {
17525 "
17526 impl A {
17527 fn b() {
17528 0;
17529 3;
17530 5;
17531 6;
17532 7;
17533 }
17534 }
17535 "
17536 };
17537 let base_text = indoc! {
17538 "
17539 impl A {
17540 fn b() {
17541 0;
17542 1;
17543 2;
17544 3;
17545 4;
17546 }
17547 fn c() {
17548 5;
17549 6;
17550 7;
17551 }
17552 }
17553 "
17554 };
17555
17556 cx.update_editor(|editor, window, cx| {
17557 editor.set_text(text, window, cx);
17558
17559 editor.buffer().update(cx, |multibuffer, cx| {
17560 let buffer = multibuffer.as_singleton().unwrap();
17561 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17562
17563 multibuffer.set_all_diff_hunks_expanded(cx);
17564 multibuffer.add_diff(diff, cx);
17565
17566 buffer.read(cx).remote_id()
17567 })
17568 });
17569 cx.run_until_parked();
17570
17571 cx.assert_state_with_diff(
17572 indoc! { "
17573 impl A {
17574 fn b() {
17575 0;
17576 - 1;
17577 - 2;
17578 3;
17579 - 4;
17580 - }
17581 - fn c() {
17582 5;
17583 6;
17584 7;
17585 }
17586 }
17587 ˇ"
17588 }
17589 .to_string(),
17590 );
17591
17592 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17593 editor
17594 .snapshot(window, cx)
17595 .buffer_snapshot
17596 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17597 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17598 .collect::<Vec<_>>()
17599 });
17600 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17601 assert_eq!(
17602 actual_guides,
17603 vec![
17604 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17605 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17606 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17607 ]
17608 );
17609}
17610
17611#[gpui::test]
17612async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17613 init_test(cx, |_| {});
17614 let mut cx = EditorTestContext::new(cx).await;
17615
17616 let diff_base = r#"
17617 a
17618 b
17619 c
17620 "#
17621 .unindent();
17622
17623 cx.set_state(
17624 &r#"
17625 ˇA
17626 b
17627 C
17628 "#
17629 .unindent(),
17630 );
17631 cx.set_head_text(&diff_base);
17632 cx.update_editor(|editor, window, cx| {
17633 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17634 });
17635 executor.run_until_parked();
17636
17637 let both_hunks_expanded = r#"
17638 - a
17639 + ˇA
17640 b
17641 - c
17642 + C
17643 "#
17644 .unindent();
17645
17646 cx.assert_state_with_diff(both_hunks_expanded.clone());
17647
17648 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17649 let snapshot = editor.snapshot(window, cx);
17650 let hunks = editor
17651 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17652 .collect::<Vec<_>>();
17653 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17654 let buffer_id = hunks[0].buffer_id;
17655 hunks
17656 .into_iter()
17657 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17658 .collect::<Vec<_>>()
17659 });
17660 assert_eq!(hunk_ranges.len(), 2);
17661
17662 cx.update_editor(|editor, _, cx| {
17663 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17664 });
17665 executor.run_until_parked();
17666
17667 let second_hunk_expanded = r#"
17668 ˇA
17669 b
17670 - c
17671 + C
17672 "#
17673 .unindent();
17674
17675 cx.assert_state_with_diff(second_hunk_expanded);
17676
17677 cx.update_editor(|editor, _, cx| {
17678 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17679 });
17680 executor.run_until_parked();
17681
17682 cx.assert_state_with_diff(both_hunks_expanded.clone());
17683
17684 cx.update_editor(|editor, _, cx| {
17685 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17686 });
17687 executor.run_until_parked();
17688
17689 let first_hunk_expanded = r#"
17690 - a
17691 + ˇA
17692 b
17693 C
17694 "#
17695 .unindent();
17696
17697 cx.assert_state_with_diff(first_hunk_expanded);
17698
17699 cx.update_editor(|editor, _, cx| {
17700 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17701 });
17702 executor.run_until_parked();
17703
17704 cx.assert_state_with_diff(both_hunks_expanded);
17705
17706 cx.set_state(
17707 &r#"
17708 ˇA
17709 b
17710 "#
17711 .unindent(),
17712 );
17713 cx.run_until_parked();
17714
17715 // TODO this cursor position seems bad
17716 cx.assert_state_with_diff(
17717 r#"
17718 - ˇa
17719 + A
17720 b
17721 "#
17722 .unindent(),
17723 );
17724
17725 cx.update_editor(|editor, window, cx| {
17726 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17727 });
17728
17729 cx.assert_state_with_diff(
17730 r#"
17731 - ˇa
17732 + A
17733 b
17734 - c
17735 "#
17736 .unindent(),
17737 );
17738
17739 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17740 let snapshot = editor.snapshot(window, cx);
17741 let hunks = editor
17742 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17743 .collect::<Vec<_>>();
17744 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17745 let buffer_id = hunks[0].buffer_id;
17746 hunks
17747 .into_iter()
17748 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17749 .collect::<Vec<_>>()
17750 });
17751 assert_eq!(hunk_ranges.len(), 2);
17752
17753 cx.update_editor(|editor, _, cx| {
17754 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17755 });
17756 executor.run_until_parked();
17757
17758 cx.assert_state_with_diff(
17759 r#"
17760 - ˇa
17761 + A
17762 b
17763 "#
17764 .unindent(),
17765 );
17766}
17767
17768#[gpui::test]
17769async fn test_toggle_deletion_hunk_at_start_of_file(
17770 executor: BackgroundExecutor,
17771 cx: &mut TestAppContext,
17772) {
17773 init_test(cx, |_| {});
17774 let mut cx = EditorTestContext::new(cx).await;
17775
17776 let diff_base = r#"
17777 a
17778 b
17779 c
17780 "#
17781 .unindent();
17782
17783 cx.set_state(
17784 &r#"
17785 ˇb
17786 c
17787 "#
17788 .unindent(),
17789 );
17790 cx.set_head_text(&diff_base);
17791 cx.update_editor(|editor, window, cx| {
17792 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17793 });
17794 executor.run_until_parked();
17795
17796 let hunk_expanded = r#"
17797 - a
17798 ˇb
17799 c
17800 "#
17801 .unindent();
17802
17803 cx.assert_state_with_diff(hunk_expanded.clone());
17804
17805 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17806 let snapshot = editor.snapshot(window, cx);
17807 let hunks = editor
17808 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17809 .collect::<Vec<_>>();
17810 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17811 let buffer_id = hunks[0].buffer_id;
17812 hunks
17813 .into_iter()
17814 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17815 .collect::<Vec<_>>()
17816 });
17817 assert_eq!(hunk_ranges.len(), 1);
17818
17819 cx.update_editor(|editor, _, cx| {
17820 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17821 });
17822 executor.run_until_parked();
17823
17824 let hunk_collapsed = r#"
17825 ˇb
17826 c
17827 "#
17828 .unindent();
17829
17830 cx.assert_state_with_diff(hunk_collapsed);
17831
17832 cx.update_editor(|editor, _, cx| {
17833 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17834 });
17835 executor.run_until_parked();
17836
17837 cx.assert_state_with_diff(hunk_expanded.clone());
17838}
17839
17840#[gpui::test]
17841async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17842 init_test(cx, |_| {});
17843
17844 let fs = FakeFs::new(cx.executor());
17845 fs.insert_tree(
17846 path!("/test"),
17847 json!({
17848 ".git": {},
17849 "file-1": "ONE\n",
17850 "file-2": "TWO\n",
17851 "file-3": "THREE\n",
17852 }),
17853 )
17854 .await;
17855
17856 fs.set_head_for_repo(
17857 path!("/test/.git").as_ref(),
17858 &[
17859 ("file-1".into(), "one\n".into()),
17860 ("file-2".into(), "two\n".into()),
17861 ("file-3".into(), "three\n".into()),
17862 ],
17863 );
17864
17865 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17866 let mut buffers = vec![];
17867 for i in 1..=3 {
17868 let buffer = project
17869 .update(cx, |project, cx| {
17870 let path = format!(path!("/test/file-{}"), i);
17871 project.open_local_buffer(path, cx)
17872 })
17873 .await
17874 .unwrap();
17875 buffers.push(buffer);
17876 }
17877
17878 let multibuffer = cx.new(|cx| {
17879 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17880 multibuffer.set_all_diff_hunks_expanded(cx);
17881 for buffer in &buffers {
17882 let snapshot = buffer.read(cx).snapshot();
17883 multibuffer.set_excerpts_for_path(
17884 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17885 buffer.clone(),
17886 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17887 DEFAULT_MULTIBUFFER_CONTEXT,
17888 cx,
17889 );
17890 }
17891 multibuffer
17892 });
17893
17894 let editor = cx.add_window(|window, cx| {
17895 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17896 });
17897 cx.run_until_parked();
17898
17899 let snapshot = editor
17900 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17901 .unwrap();
17902 let hunks = snapshot
17903 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17904 .map(|hunk| match hunk {
17905 DisplayDiffHunk::Unfolded {
17906 display_row_range, ..
17907 } => display_row_range,
17908 DisplayDiffHunk::Folded { .. } => unreachable!(),
17909 })
17910 .collect::<Vec<_>>();
17911 assert_eq!(
17912 hunks,
17913 [
17914 DisplayRow(2)..DisplayRow(4),
17915 DisplayRow(7)..DisplayRow(9),
17916 DisplayRow(12)..DisplayRow(14),
17917 ]
17918 );
17919}
17920
17921#[gpui::test]
17922async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17923 init_test(cx, |_| {});
17924
17925 let mut cx = EditorTestContext::new(cx).await;
17926 cx.set_head_text(indoc! { "
17927 one
17928 two
17929 three
17930 four
17931 five
17932 "
17933 });
17934 cx.set_index_text(indoc! { "
17935 one
17936 two
17937 three
17938 four
17939 five
17940 "
17941 });
17942 cx.set_state(indoc! {"
17943 one
17944 TWO
17945 ˇTHREE
17946 FOUR
17947 five
17948 "});
17949 cx.run_until_parked();
17950 cx.update_editor(|editor, window, cx| {
17951 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17952 });
17953 cx.run_until_parked();
17954 cx.assert_index_text(Some(indoc! {"
17955 one
17956 TWO
17957 THREE
17958 FOUR
17959 five
17960 "}));
17961 cx.set_state(indoc! { "
17962 one
17963 TWO
17964 ˇTHREE-HUNDRED
17965 FOUR
17966 five
17967 "});
17968 cx.run_until_parked();
17969 cx.update_editor(|editor, window, cx| {
17970 let snapshot = editor.snapshot(window, cx);
17971 let hunks = editor
17972 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17973 .collect::<Vec<_>>();
17974 assert_eq!(hunks.len(), 1);
17975 assert_eq!(
17976 hunks[0].status(),
17977 DiffHunkStatus {
17978 kind: DiffHunkStatusKind::Modified,
17979 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17980 }
17981 );
17982
17983 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17984 });
17985 cx.run_until_parked();
17986 cx.assert_index_text(Some(indoc! {"
17987 one
17988 TWO
17989 THREE-HUNDRED
17990 FOUR
17991 five
17992 "}));
17993}
17994
17995#[gpui::test]
17996fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17997 init_test(cx, |_| {});
17998
17999 let editor = cx.add_window(|window, cx| {
18000 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18001 build_editor(buffer, window, cx)
18002 });
18003
18004 let render_args = Arc::new(Mutex::new(None));
18005 let snapshot = editor
18006 .update(cx, |editor, window, cx| {
18007 let snapshot = editor.buffer().read(cx).snapshot(cx);
18008 let range =
18009 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18010
18011 struct RenderArgs {
18012 row: MultiBufferRow,
18013 folded: bool,
18014 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18015 }
18016
18017 let crease = Crease::inline(
18018 range,
18019 FoldPlaceholder::test(),
18020 {
18021 let toggle_callback = render_args.clone();
18022 move |row, folded, callback, _window, _cx| {
18023 *toggle_callback.lock() = Some(RenderArgs {
18024 row,
18025 folded,
18026 callback,
18027 });
18028 div()
18029 }
18030 },
18031 |_row, _folded, _window, _cx| div(),
18032 );
18033
18034 editor.insert_creases(Some(crease), cx);
18035 let snapshot = editor.snapshot(window, cx);
18036 let _div = snapshot.render_crease_toggle(
18037 MultiBufferRow(1),
18038 false,
18039 cx.entity().clone(),
18040 window,
18041 cx,
18042 );
18043 snapshot
18044 })
18045 .unwrap();
18046
18047 let render_args = render_args.lock().take().unwrap();
18048 assert_eq!(render_args.row, MultiBufferRow(1));
18049 assert!(!render_args.folded);
18050 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18051
18052 cx.update_window(*editor, |_, window, cx| {
18053 (render_args.callback)(true, window, cx)
18054 })
18055 .unwrap();
18056 let snapshot = editor
18057 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18058 .unwrap();
18059 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18060
18061 cx.update_window(*editor, |_, window, cx| {
18062 (render_args.callback)(false, window, cx)
18063 })
18064 .unwrap();
18065 let snapshot = editor
18066 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18067 .unwrap();
18068 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18069}
18070
18071#[gpui::test]
18072async fn test_input_text(cx: &mut TestAppContext) {
18073 init_test(cx, |_| {});
18074 let mut cx = EditorTestContext::new(cx).await;
18075
18076 cx.set_state(
18077 &r#"ˇone
18078 two
18079
18080 three
18081 fourˇ
18082 five
18083
18084 siˇx"#
18085 .unindent(),
18086 );
18087
18088 cx.dispatch_action(HandleInput(String::new()));
18089 cx.assert_editor_state(
18090 &r#"ˇone
18091 two
18092
18093 three
18094 fourˇ
18095 five
18096
18097 siˇx"#
18098 .unindent(),
18099 );
18100
18101 cx.dispatch_action(HandleInput("AAAA".to_string()));
18102 cx.assert_editor_state(
18103 &r#"AAAAˇone
18104 two
18105
18106 three
18107 fourAAAAˇ
18108 five
18109
18110 siAAAAˇx"#
18111 .unindent(),
18112 );
18113}
18114
18115#[gpui::test]
18116async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18117 init_test(cx, |_| {});
18118
18119 let mut cx = EditorTestContext::new(cx).await;
18120 cx.set_state(
18121 r#"let foo = 1;
18122let foo = 2;
18123let foo = 3;
18124let fooˇ = 4;
18125let foo = 5;
18126let foo = 6;
18127let foo = 7;
18128let foo = 8;
18129let foo = 9;
18130let foo = 10;
18131let foo = 11;
18132let foo = 12;
18133let foo = 13;
18134let foo = 14;
18135let foo = 15;"#,
18136 );
18137
18138 cx.update_editor(|e, window, cx| {
18139 assert_eq!(
18140 e.next_scroll_position,
18141 NextScrollCursorCenterTopBottom::Center,
18142 "Default next scroll direction is center",
18143 );
18144
18145 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18146 assert_eq!(
18147 e.next_scroll_position,
18148 NextScrollCursorCenterTopBottom::Top,
18149 "After center, next scroll direction should be top",
18150 );
18151
18152 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18153 assert_eq!(
18154 e.next_scroll_position,
18155 NextScrollCursorCenterTopBottom::Bottom,
18156 "After top, next scroll direction should be bottom",
18157 );
18158
18159 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18160 assert_eq!(
18161 e.next_scroll_position,
18162 NextScrollCursorCenterTopBottom::Center,
18163 "After bottom, scrolling should start over",
18164 );
18165
18166 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18167 assert_eq!(
18168 e.next_scroll_position,
18169 NextScrollCursorCenterTopBottom::Top,
18170 "Scrolling continues if retriggered fast enough"
18171 );
18172 });
18173
18174 cx.executor()
18175 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18176 cx.executor().run_until_parked();
18177 cx.update_editor(|e, _, _| {
18178 assert_eq!(
18179 e.next_scroll_position,
18180 NextScrollCursorCenterTopBottom::Center,
18181 "If scrolling is not triggered fast enough, it should reset"
18182 );
18183 });
18184}
18185
18186#[gpui::test]
18187async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18188 init_test(cx, |_| {});
18189 let mut cx = EditorLspTestContext::new_rust(
18190 lsp::ServerCapabilities {
18191 definition_provider: Some(lsp::OneOf::Left(true)),
18192 references_provider: Some(lsp::OneOf::Left(true)),
18193 ..lsp::ServerCapabilities::default()
18194 },
18195 cx,
18196 )
18197 .await;
18198
18199 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18200 let go_to_definition = cx
18201 .lsp
18202 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18203 move |params, _| async move {
18204 if empty_go_to_definition {
18205 Ok(None)
18206 } else {
18207 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18208 uri: params.text_document_position_params.text_document.uri,
18209 range: lsp::Range::new(
18210 lsp::Position::new(4, 3),
18211 lsp::Position::new(4, 6),
18212 ),
18213 })))
18214 }
18215 },
18216 );
18217 let references = cx
18218 .lsp
18219 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18220 Ok(Some(vec![lsp::Location {
18221 uri: params.text_document_position.text_document.uri,
18222 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18223 }]))
18224 });
18225 (go_to_definition, references)
18226 };
18227
18228 cx.set_state(
18229 &r#"fn one() {
18230 let mut a = ˇtwo();
18231 }
18232
18233 fn two() {}"#
18234 .unindent(),
18235 );
18236 set_up_lsp_handlers(false, &mut cx);
18237 let navigated = cx
18238 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18239 .await
18240 .expect("Failed to navigate to definition");
18241 assert_eq!(
18242 navigated,
18243 Navigated::Yes,
18244 "Should have navigated to definition from the GetDefinition response"
18245 );
18246 cx.assert_editor_state(
18247 &r#"fn one() {
18248 let mut a = two();
18249 }
18250
18251 fn «twoˇ»() {}"#
18252 .unindent(),
18253 );
18254
18255 let editors = cx.update_workspace(|workspace, _, cx| {
18256 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18257 });
18258 cx.update_editor(|_, _, test_editor_cx| {
18259 assert_eq!(
18260 editors.len(),
18261 1,
18262 "Initially, only one, test, editor should be open in the workspace"
18263 );
18264 assert_eq!(
18265 test_editor_cx.entity(),
18266 editors.last().expect("Asserted len is 1").clone()
18267 );
18268 });
18269
18270 set_up_lsp_handlers(true, &mut cx);
18271 let navigated = cx
18272 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18273 .await
18274 .expect("Failed to navigate to lookup references");
18275 assert_eq!(
18276 navigated,
18277 Navigated::Yes,
18278 "Should have navigated to references as a fallback after empty GoToDefinition response"
18279 );
18280 // We should not change the selections in the existing file,
18281 // if opening another milti buffer with the references
18282 cx.assert_editor_state(
18283 &r#"fn one() {
18284 let mut a = two();
18285 }
18286
18287 fn «twoˇ»() {}"#
18288 .unindent(),
18289 );
18290 let editors = cx.update_workspace(|workspace, _, cx| {
18291 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18292 });
18293 cx.update_editor(|_, _, test_editor_cx| {
18294 assert_eq!(
18295 editors.len(),
18296 2,
18297 "After falling back to references search, we open a new editor with the results"
18298 );
18299 let references_fallback_text = editors
18300 .into_iter()
18301 .find(|new_editor| *new_editor != test_editor_cx.entity())
18302 .expect("Should have one non-test editor now")
18303 .read(test_editor_cx)
18304 .text(test_editor_cx);
18305 assert_eq!(
18306 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18307 "Should use the range from the references response and not the GoToDefinition one"
18308 );
18309 });
18310}
18311
18312#[gpui::test]
18313async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18314 init_test(cx, |_| {});
18315 cx.update(|cx| {
18316 let mut editor_settings = EditorSettings::get_global(cx).clone();
18317 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18318 EditorSettings::override_global(editor_settings, cx);
18319 });
18320 let mut cx = EditorLspTestContext::new_rust(
18321 lsp::ServerCapabilities {
18322 definition_provider: Some(lsp::OneOf::Left(true)),
18323 references_provider: Some(lsp::OneOf::Left(true)),
18324 ..lsp::ServerCapabilities::default()
18325 },
18326 cx,
18327 )
18328 .await;
18329 let original_state = r#"fn one() {
18330 let mut a = ˇtwo();
18331 }
18332
18333 fn two() {}"#
18334 .unindent();
18335 cx.set_state(&original_state);
18336
18337 let mut go_to_definition = cx
18338 .lsp
18339 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18340 move |_, _| async move { Ok(None) },
18341 );
18342 let _references = cx
18343 .lsp
18344 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18345 panic!("Should not call for references with no go to definition fallback")
18346 });
18347
18348 let navigated = cx
18349 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18350 .await
18351 .expect("Failed to navigate to lookup references");
18352 go_to_definition
18353 .next()
18354 .await
18355 .expect("Should have called the go_to_definition handler");
18356
18357 assert_eq!(
18358 navigated,
18359 Navigated::No,
18360 "Should have navigated to references as a fallback after empty GoToDefinition response"
18361 );
18362 cx.assert_editor_state(&original_state);
18363 let editors = cx.update_workspace(|workspace, _, cx| {
18364 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18365 });
18366 cx.update_editor(|_, _, _| {
18367 assert_eq!(
18368 editors.len(),
18369 1,
18370 "After unsuccessful fallback, no other editor should have been opened"
18371 );
18372 });
18373}
18374
18375#[gpui::test]
18376async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18377 init_test(cx, |_| {});
18378
18379 let language = Arc::new(Language::new(
18380 LanguageConfig::default(),
18381 Some(tree_sitter_rust::LANGUAGE.into()),
18382 ));
18383
18384 let text = r#"
18385 #[cfg(test)]
18386 mod tests() {
18387 #[test]
18388 fn runnable_1() {
18389 let a = 1;
18390 }
18391
18392 #[test]
18393 fn runnable_2() {
18394 let a = 1;
18395 let b = 2;
18396 }
18397 }
18398 "#
18399 .unindent();
18400
18401 let fs = FakeFs::new(cx.executor());
18402 fs.insert_file("/file.rs", Default::default()).await;
18403
18404 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18405 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18406 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18407 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18408 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18409
18410 let editor = cx.new_window_entity(|window, cx| {
18411 Editor::new(
18412 EditorMode::full(),
18413 multi_buffer,
18414 Some(project.clone()),
18415 window,
18416 cx,
18417 )
18418 });
18419
18420 editor.update_in(cx, |editor, window, cx| {
18421 let snapshot = editor.buffer().read(cx).snapshot(cx);
18422 editor.tasks.insert(
18423 (buffer.read(cx).remote_id(), 3),
18424 RunnableTasks {
18425 templates: vec![],
18426 offset: snapshot.anchor_before(43),
18427 column: 0,
18428 extra_variables: HashMap::default(),
18429 context_range: BufferOffset(43)..BufferOffset(85),
18430 },
18431 );
18432 editor.tasks.insert(
18433 (buffer.read(cx).remote_id(), 8),
18434 RunnableTasks {
18435 templates: vec![],
18436 offset: snapshot.anchor_before(86),
18437 column: 0,
18438 extra_variables: HashMap::default(),
18439 context_range: BufferOffset(86)..BufferOffset(191),
18440 },
18441 );
18442
18443 // Test finding task when cursor is inside function body
18444 editor.change_selections(None, window, cx, |s| {
18445 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18446 });
18447 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18448 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18449
18450 // Test finding task when cursor is on function name
18451 editor.change_selections(None, window, cx, |s| {
18452 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18453 });
18454 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18455 assert_eq!(row, 8, "Should find task when cursor is on function name");
18456 });
18457}
18458
18459#[gpui::test]
18460async fn test_folding_buffers(cx: &mut TestAppContext) {
18461 init_test(cx, |_| {});
18462
18463 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18464 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18465 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18466
18467 let fs = FakeFs::new(cx.executor());
18468 fs.insert_tree(
18469 path!("/a"),
18470 json!({
18471 "first.rs": sample_text_1,
18472 "second.rs": sample_text_2,
18473 "third.rs": sample_text_3,
18474 }),
18475 )
18476 .await;
18477 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18478 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18479 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18480 let worktree = project.update(cx, |project, cx| {
18481 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18482 assert_eq!(worktrees.len(), 1);
18483 worktrees.pop().unwrap()
18484 });
18485 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18486
18487 let buffer_1 = project
18488 .update(cx, |project, cx| {
18489 project.open_buffer((worktree_id, "first.rs"), cx)
18490 })
18491 .await
18492 .unwrap();
18493 let buffer_2 = project
18494 .update(cx, |project, cx| {
18495 project.open_buffer((worktree_id, "second.rs"), cx)
18496 })
18497 .await
18498 .unwrap();
18499 let buffer_3 = project
18500 .update(cx, |project, cx| {
18501 project.open_buffer((worktree_id, "third.rs"), cx)
18502 })
18503 .await
18504 .unwrap();
18505
18506 let multi_buffer = cx.new(|cx| {
18507 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18508 multi_buffer.push_excerpts(
18509 buffer_1.clone(),
18510 [
18511 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18512 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18513 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18514 ],
18515 cx,
18516 );
18517 multi_buffer.push_excerpts(
18518 buffer_2.clone(),
18519 [
18520 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18521 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18522 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18523 ],
18524 cx,
18525 );
18526 multi_buffer.push_excerpts(
18527 buffer_3.clone(),
18528 [
18529 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18530 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18531 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18532 ],
18533 cx,
18534 );
18535 multi_buffer
18536 });
18537 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18538 Editor::new(
18539 EditorMode::full(),
18540 multi_buffer.clone(),
18541 Some(project.clone()),
18542 window,
18543 cx,
18544 )
18545 });
18546
18547 assert_eq!(
18548 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18549 "\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",
18550 );
18551
18552 multi_buffer_editor.update(cx, |editor, cx| {
18553 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18554 });
18555 assert_eq!(
18556 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18557 "\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",
18558 "After folding the first buffer, its text should not be displayed"
18559 );
18560
18561 multi_buffer_editor.update(cx, |editor, cx| {
18562 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18563 });
18564 assert_eq!(
18565 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18566 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18567 "After folding the second buffer, its text should not be displayed"
18568 );
18569
18570 multi_buffer_editor.update(cx, |editor, cx| {
18571 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18572 });
18573 assert_eq!(
18574 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18575 "\n\n\n\n\n",
18576 "After folding the third buffer, its text should not be displayed"
18577 );
18578
18579 // Emulate selection inside the fold logic, that should work
18580 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18581 editor
18582 .snapshot(window, cx)
18583 .next_line_boundary(Point::new(0, 4));
18584 });
18585
18586 multi_buffer_editor.update(cx, |editor, cx| {
18587 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18588 });
18589 assert_eq!(
18590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18591 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18592 "After unfolding the second buffer, its text should be displayed"
18593 );
18594
18595 // Typing inside of buffer 1 causes that buffer to be unfolded.
18596 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18597 assert_eq!(
18598 multi_buffer
18599 .read(cx)
18600 .snapshot(cx)
18601 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18602 .collect::<String>(),
18603 "bbbb"
18604 );
18605 editor.change_selections(None, window, cx, |selections| {
18606 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18607 });
18608 editor.handle_input("B", window, cx);
18609 });
18610
18611 assert_eq!(
18612 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18613 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18614 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18615 );
18616
18617 multi_buffer_editor.update(cx, |editor, cx| {
18618 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18619 });
18620 assert_eq!(
18621 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18622 "\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",
18623 "After unfolding the all buffers, all original text should be displayed"
18624 );
18625}
18626
18627#[gpui::test]
18628async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18629 init_test(cx, |_| {});
18630
18631 let sample_text_1 = "1111\n2222\n3333".to_string();
18632 let sample_text_2 = "4444\n5555\n6666".to_string();
18633 let sample_text_3 = "7777\n8888\n9999".to_string();
18634
18635 let fs = FakeFs::new(cx.executor());
18636 fs.insert_tree(
18637 path!("/a"),
18638 json!({
18639 "first.rs": sample_text_1,
18640 "second.rs": sample_text_2,
18641 "third.rs": sample_text_3,
18642 }),
18643 )
18644 .await;
18645 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18646 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18647 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18648 let worktree = project.update(cx, |project, cx| {
18649 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18650 assert_eq!(worktrees.len(), 1);
18651 worktrees.pop().unwrap()
18652 });
18653 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18654
18655 let buffer_1 = project
18656 .update(cx, |project, cx| {
18657 project.open_buffer((worktree_id, "first.rs"), cx)
18658 })
18659 .await
18660 .unwrap();
18661 let buffer_2 = project
18662 .update(cx, |project, cx| {
18663 project.open_buffer((worktree_id, "second.rs"), cx)
18664 })
18665 .await
18666 .unwrap();
18667 let buffer_3 = project
18668 .update(cx, |project, cx| {
18669 project.open_buffer((worktree_id, "third.rs"), cx)
18670 })
18671 .await
18672 .unwrap();
18673
18674 let multi_buffer = cx.new(|cx| {
18675 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18676 multi_buffer.push_excerpts(
18677 buffer_1.clone(),
18678 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18679 cx,
18680 );
18681 multi_buffer.push_excerpts(
18682 buffer_2.clone(),
18683 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18684 cx,
18685 );
18686 multi_buffer.push_excerpts(
18687 buffer_3.clone(),
18688 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18689 cx,
18690 );
18691 multi_buffer
18692 });
18693
18694 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18695 Editor::new(
18696 EditorMode::full(),
18697 multi_buffer,
18698 Some(project.clone()),
18699 window,
18700 cx,
18701 )
18702 });
18703
18704 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18705 assert_eq!(
18706 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18707 full_text,
18708 );
18709
18710 multi_buffer_editor.update(cx, |editor, cx| {
18711 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18712 });
18713 assert_eq!(
18714 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18715 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18716 "After folding the first buffer, its text should not be displayed"
18717 );
18718
18719 multi_buffer_editor.update(cx, |editor, cx| {
18720 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18721 });
18722
18723 assert_eq!(
18724 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18725 "\n\n\n\n\n\n7777\n8888\n9999",
18726 "After folding the second buffer, its text should not be displayed"
18727 );
18728
18729 multi_buffer_editor.update(cx, |editor, cx| {
18730 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18731 });
18732 assert_eq!(
18733 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18734 "\n\n\n\n\n",
18735 "After folding the third buffer, its text should not be displayed"
18736 );
18737
18738 multi_buffer_editor.update(cx, |editor, cx| {
18739 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18740 });
18741 assert_eq!(
18742 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18743 "\n\n\n\n4444\n5555\n6666\n\n",
18744 "After unfolding the second buffer, its text should be displayed"
18745 );
18746
18747 multi_buffer_editor.update(cx, |editor, cx| {
18748 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18749 });
18750 assert_eq!(
18751 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18752 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18753 "After unfolding the first buffer, its text should be displayed"
18754 );
18755
18756 multi_buffer_editor.update(cx, |editor, cx| {
18757 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18758 });
18759 assert_eq!(
18760 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18761 full_text,
18762 "After unfolding all buffers, all original text should be displayed"
18763 );
18764}
18765
18766#[gpui::test]
18767async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18768 init_test(cx, |_| {});
18769
18770 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18771
18772 let fs = FakeFs::new(cx.executor());
18773 fs.insert_tree(
18774 path!("/a"),
18775 json!({
18776 "main.rs": sample_text,
18777 }),
18778 )
18779 .await;
18780 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18781 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18782 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18783 let worktree = project.update(cx, |project, cx| {
18784 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18785 assert_eq!(worktrees.len(), 1);
18786 worktrees.pop().unwrap()
18787 });
18788 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18789
18790 let buffer_1 = project
18791 .update(cx, |project, cx| {
18792 project.open_buffer((worktree_id, "main.rs"), cx)
18793 })
18794 .await
18795 .unwrap();
18796
18797 let multi_buffer = cx.new(|cx| {
18798 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18799 multi_buffer.push_excerpts(
18800 buffer_1.clone(),
18801 [ExcerptRange::new(
18802 Point::new(0, 0)
18803 ..Point::new(
18804 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18805 0,
18806 ),
18807 )],
18808 cx,
18809 );
18810 multi_buffer
18811 });
18812 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18813 Editor::new(
18814 EditorMode::full(),
18815 multi_buffer,
18816 Some(project.clone()),
18817 window,
18818 cx,
18819 )
18820 });
18821
18822 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18823 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18824 enum TestHighlight {}
18825 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18826 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18827 editor.highlight_text::<TestHighlight>(
18828 vec![highlight_range.clone()],
18829 HighlightStyle::color(Hsla::green()),
18830 cx,
18831 );
18832 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18833 });
18834
18835 let full_text = format!("\n\n{sample_text}");
18836 assert_eq!(
18837 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18838 full_text,
18839 );
18840}
18841
18842#[gpui::test]
18843async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18844 init_test(cx, |_| {});
18845 cx.update(|cx| {
18846 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18847 "keymaps/default-linux.json",
18848 cx,
18849 )
18850 .unwrap();
18851 cx.bind_keys(default_key_bindings);
18852 });
18853
18854 let (editor, cx) = cx.add_window_view(|window, cx| {
18855 let multi_buffer = MultiBuffer::build_multi(
18856 [
18857 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18858 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18859 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18860 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18861 ],
18862 cx,
18863 );
18864 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18865
18866 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18867 // fold all but the second buffer, so that we test navigating between two
18868 // adjacent folded buffers, as well as folded buffers at the start and
18869 // end the multibuffer
18870 editor.fold_buffer(buffer_ids[0], cx);
18871 editor.fold_buffer(buffer_ids[2], cx);
18872 editor.fold_buffer(buffer_ids[3], cx);
18873
18874 editor
18875 });
18876 cx.simulate_resize(size(px(1000.), px(1000.)));
18877
18878 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18879 cx.assert_excerpts_with_selections(indoc! {"
18880 [EXCERPT]
18881 ˇ[FOLDED]
18882 [EXCERPT]
18883 a1
18884 b1
18885 [EXCERPT]
18886 [FOLDED]
18887 [EXCERPT]
18888 [FOLDED]
18889 "
18890 });
18891 cx.simulate_keystroke("down");
18892 cx.assert_excerpts_with_selections(indoc! {"
18893 [EXCERPT]
18894 [FOLDED]
18895 [EXCERPT]
18896 ˇa1
18897 b1
18898 [EXCERPT]
18899 [FOLDED]
18900 [EXCERPT]
18901 [FOLDED]
18902 "
18903 });
18904 cx.simulate_keystroke("down");
18905 cx.assert_excerpts_with_selections(indoc! {"
18906 [EXCERPT]
18907 [FOLDED]
18908 [EXCERPT]
18909 a1
18910 ˇb1
18911 [EXCERPT]
18912 [FOLDED]
18913 [EXCERPT]
18914 [FOLDED]
18915 "
18916 });
18917 cx.simulate_keystroke("down");
18918 cx.assert_excerpts_with_selections(indoc! {"
18919 [EXCERPT]
18920 [FOLDED]
18921 [EXCERPT]
18922 a1
18923 b1
18924 ˇ[EXCERPT]
18925 [FOLDED]
18926 [EXCERPT]
18927 [FOLDED]
18928 "
18929 });
18930 cx.simulate_keystroke("down");
18931 cx.assert_excerpts_with_selections(indoc! {"
18932 [EXCERPT]
18933 [FOLDED]
18934 [EXCERPT]
18935 a1
18936 b1
18937 [EXCERPT]
18938 ˇ[FOLDED]
18939 [EXCERPT]
18940 [FOLDED]
18941 "
18942 });
18943 for _ in 0..5 {
18944 cx.simulate_keystroke("down");
18945 cx.assert_excerpts_with_selections(indoc! {"
18946 [EXCERPT]
18947 [FOLDED]
18948 [EXCERPT]
18949 a1
18950 b1
18951 [EXCERPT]
18952 [FOLDED]
18953 [EXCERPT]
18954 ˇ[FOLDED]
18955 "
18956 });
18957 }
18958
18959 cx.simulate_keystroke("up");
18960 cx.assert_excerpts_with_selections(indoc! {"
18961 [EXCERPT]
18962 [FOLDED]
18963 [EXCERPT]
18964 a1
18965 b1
18966 [EXCERPT]
18967 ˇ[FOLDED]
18968 [EXCERPT]
18969 [FOLDED]
18970 "
18971 });
18972 cx.simulate_keystroke("up");
18973 cx.assert_excerpts_with_selections(indoc! {"
18974 [EXCERPT]
18975 [FOLDED]
18976 [EXCERPT]
18977 a1
18978 b1
18979 ˇ[EXCERPT]
18980 [FOLDED]
18981 [EXCERPT]
18982 [FOLDED]
18983 "
18984 });
18985 cx.simulate_keystroke("up");
18986 cx.assert_excerpts_with_selections(indoc! {"
18987 [EXCERPT]
18988 [FOLDED]
18989 [EXCERPT]
18990 a1
18991 ˇb1
18992 [EXCERPT]
18993 [FOLDED]
18994 [EXCERPT]
18995 [FOLDED]
18996 "
18997 });
18998 cx.simulate_keystroke("up");
18999 cx.assert_excerpts_with_selections(indoc! {"
19000 [EXCERPT]
19001 [FOLDED]
19002 [EXCERPT]
19003 ˇa1
19004 b1
19005 [EXCERPT]
19006 [FOLDED]
19007 [EXCERPT]
19008 [FOLDED]
19009 "
19010 });
19011 for _ in 0..5 {
19012 cx.simulate_keystroke("up");
19013 cx.assert_excerpts_with_selections(indoc! {"
19014 [EXCERPT]
19015 ˇ[FOLDED]
19016 [EXCERPT]
19017 a1
19018 b1
19019 [EXCERPT]
19020 [FOLDED]
19021 [EXCERPT]
19022 [FOLDED]
19023 "
19024 });
19025 }
19026}
19027
19028#[gpui::test]
19029async fn test_inline_completion_text(cx: &mut TestAppContext) {
19030 init_test(cx, |_| {});
19031
19032 // Simple insertion
19033 assert_highlighted_edits(
19034 "Hello, world!",
19035 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19036 true,
19037 cx,
19038 |highlighted_edits, cx| {
19039 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19040 assert_eq!(highlighted_edits.highlights.len(), 1);
19041 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19042 assert_eq!(
19043 highlighted_edits.highlights[0].1.background_color,
19044 Some(cx.theme().status().created_background)
19045 );
19046 },
19047 )
19048 .await;
19049
19050 // Replacement
19051 assert_highlighted_edits(
19052 "This is a test.",
19053 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19054 false,
19055 cx,
19056 |highlighted_edits, cx| {
19057 assert_eq!(highlighted_edits.text, "That is a test.");
19058 assert_eq!(highlighted_edits.highlights.len(), 1);
19059 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19060 assert_eq!(
19061 highlighted_edits.highlights[0].1.background_color,
19062 Some(cx.theme().status().created_background)
19063 );
19064 },
19065 )
19066 .await;
19067
19068 // Multiple edits
19069 assert_highlighted_edits(
19070 "Hello, world!",
19071 vec![
19072 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19073 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19074 ],
19075 false,
19076 cx,
19077 |highlighted_edits, cx| {
19078 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19079 assert_eq!(highlighted_edits.highlights.len(), 2);
19080 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19081 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19082 assert_eq!(
19083 highlighted_edits.highlights[0].1.background_color,
19084 Some(cx.theme().status().created_background)
19085 );
19086 assert_eq!(
19087 highlighted_edits.highlights[1].1.background_color,
19088 Some(cx.theme().status().created_background)
19089 );
19090 },
19091 )
19092 .await;
19093
19094 // Multiple lines with edits
19095 assert_highlighted_edits(
19096 "First line\nSecond line\nThird line\nFourth line",
19097 vec![
19098 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19099 (
19100 Point::new(2, 0)..Point::new(2, 10),
19101 "New third line".to_string(),
19102 ),
19103 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19104 ],
19105 false,
19106 cx,
19107 |highlighted_edits, cx| {
19108 assert_eq!(
19109 highlighted_edits.text,
19110 "Second modified\nNew third line\nFourth updated line"
19111 );
19112 assert_eq!(highlighted_edits.highlights.len(), 3);
19113 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19114 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19115 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19116 for highlight in &highlighted_edits.highlights {
19117 assert_eq!(
19118 highlight.1.background_color,
19119 Some(cx.theme().status().created_background)
19120 );
19121 }
19122 },
19123 )
19124 .await;
19125}
19126
19127#[gpui::test]
19128async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19129 init_test(cx, |_| {});
19130
19131 // Deletion
19132 assert_highlighted_edits(
19133 "Hello, world!",
19134 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19135 true,
19136 cx,
19137 |highlighted_edits, cx| {
19138 assert_eq!(highlighted_edits.text, "Hello, world!");
19139 assert_eq!(highlighted_edits.highlights.len(), 1);
19140 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19141 assert_eq!(
19142 highlighted_edits.highlights[0].1.background_color,
19143 Some(cx.theme().status().deleted_background)
19144 );
19145 },
19146 )
19147 .await;
19148
19149 // Insertion
19150 assert_highlighted_edits(
19151 "Hello, world!",
19152 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19153 true,
19154 cx,
19155 |highlighted_edits, cx| {
19156 assert_eq!(highlighted_edits.highlights.len(), 1);
19157 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19158 assert_eq!(
19159 highlighted_edits.highlights[0].1.background_color,
19160 Some(cx.theme().status().created_background)
19161 );
19162 },
19163 )
19164 .await;
19165}
19166
19167async fn assert_highlighted_edits(
19168 text: &str,
19169 edits: Vec<(Range<Point>, String)>,
19170 include_deletions: bool,
19171 cx: &mut TestAppContext,
19172 assertion_fn: impl Fn(HighlightedText, &App),
19173) {
19174 let window = cx.add_window(|window, cx| {
19175 let buffer = MultiBuffer::build_simple(text, cx);
19176 Editor::new(EditorMode::full(), buffer, None, window, cx)
19177 });
19178 let cx = &mut VisualTestContext::from_window(*window, cx);
19179
19180 let (buffer, snapshot) = window
19181 .update(cx, |editor, _window, cx| {
19182 (
19183 editor.buffer().clone(),
19184 editor.buffer().read(cx).snapshot(cx),
19185 )
19186 })
19187 .unwrap();
19188
19189 let edits = edits
19190 .into_iter()
19191 .map(|(range, edit)| {
19192 (
19193 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19194 edit,
19195 )
19196 })
19197 .collect::<Vec<_>>();
19198
19199 let text_anchor_edits = edits
19200 .clone()
19201 .into_iter()
19202 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19203 .collect::<Vec<_>>();
19204
19205 let edit_preview = window
19206 .update(cx, |_, _window, cx| {
19207 buffer
19208 .read(cx)
19209 .as_singleton()
19210 .unwrap()
19211 .read(cx)
19212 .preview_edits(text_anchor_edits.into(), cx)
19213 })
19214 .unwrap()
19215 .await;
19216
19217 cx.update(|_window, cx| {
19218 let highlighted_edits = inline_completion_edit_text(
19219 &snapshot.as_singleton().unwrap().2,
19220 &edits,
19221 &edit_preview,
19222 include_deletions,
19223 cx,
19224 );
19225 assertion_fn(highlighted_edits, cx)
19226 });
19227}
19228
19229#[track_caller]
19230fn assert_breakpoint(
19231 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19232 path: &Arc<Path>,
19233 expected: Vec<(u32, Breakpoint)>,
19234) {
19235 if expected.len() == 0usize {
19236 assert!(!breakpoints.contains_key(path), "{}", path.display());
19237 } else {
19238 let mut breakpoint = breakpoints
19239 .get(path)
19240 .unwrap()
19241 .into_iter()
19242 .map(|breakpoint| {
19243 (
19244 breakpoint.row,
19245 Breakpoint {
19246 message: breakpoint.message.clone(),
19247 state: breakpoint.state,
19248 condition: breakpoint.condition.clone(),
19249 hit_condition: breakpoint.hit_condition.clone(),
19250 },
19251 )
19252 })
19253 .collect::<Vec<_>>();
19254
19255 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19256
19257 assert_eq!(expected, breakpoint);
19258 }
19259}
19260
19261fn add_log_breakpoint_at_cursor(
19262 editor: &mut Editor,
19263 log_message: &str,
19264 window: &mut Window,
19265 cx: &mut Context<Editor>,
19266) {
19267 let (anchor, bp) = editor
19268 .breakpoints_at_cursors(window, cx)
19269 .first()
19270 .and_then(|(anchor, bp)| {
19271 if let Some(bp) = bp {
19272 Some((*anchor, bp.clone()))
19273 } else {
19274 None
19275 }
19276 })
19277 .unwrap_or_else(|| {
19278 let cursor_position: Point = editor.selections.newest(cx).head();
19279
19280 let breakpoint_position = editor
19281 .snapshot(window, cx)
19282 .display_snapshot
19283 .buffer_snapshot
19284 .anchor_before(Point::new(cursor_position.row, 0));
19285
19286 (breakpoint_position, Breakpoint::new_log(&log_message))
19287 });
19288
19289 editor.edit_breakpoint_at_anchor(
19290 anchor,
19291 bp,
19292 BreakpointEditAction::EditLogMessage(log_message.into()),
19293 cx,
19294 );
19295}
19296
19297#[gpui::test]
19298async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19299 init_test(cx, |_| {});
19300
19301 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19302 let fs = FakeFs::new(cx.executor());
19303 fs.insert_tree(
19304 path!("/a"),
19305 json!({
19306 "main.rs": sample_text,
19307 }),
19308 )
19309 .await;
19310 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19312 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19313
19314 let fs = FakeFs::new(cx.executor());
19315 fs.insert_tree(
19316 path!("/a"),
19317 json!({
19318 "main.rs": sample_text,
19319 }),
19320 )
19321 .await;
19322 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19323 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19324 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19325 let worktree_id = workspace
19326 .update(cx, |workspace, _window, cx| {
19327 workspace.project().update(cx, |project, cx| {
19328 project.worktrees(cx).next().unwrap().read(cx).id()
19329 })
19330 })
19331 .unwrap();
19332
19333 let buffer = project
19334 .update(cx, |project, cx| {
19335 project.open_buffer((worktree_id, "main.rs"), cx)
19336 })
19337 .await
19338 .unwrap();
19339
19340 let (editor, cx) = cx.add_window_view(|window, cx| {
19341 Editor::new(
19342 EditorMode::full(),
19343 MultiBuffer::build_from_buffer(buffer, cx),
19344 Some(project.clone()),
19345 window,
19346 cx,
19347 )
19348 });
19349
19350 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19351 let abs_path = project.read_with(cx, |project, cx| {
19352 project
19353 .absolute_path(&project_path, cx)
19354 .map(|path_buf| Arc::from(path_buf.to_owned()))
19355 .unwrap()
19356 });
19357
19358 // assert we can add breakpoint on the first line
19359 editor.update_in(cx, |editor, window, cx| {
19360 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19361 editor.move_to_end(&MoveToEnd, window, cx);
19362 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19363 });
19364
19365 let breakpoints = editor.update(cx, |editor, cx| {
19366 editor
19367 .breakpoint_store()
19368 .as_ref()
19369 .unwrap()
19370 .read(cx)
19371 .all_source_breakpoints(cx)
19372 .clone()
19373 });
19374
19375 assert_eq!(1, breakpoints.len());
19376 assert_breakpoint(
19377 &breakpoints,
19378 &abs_path,
19379 vec![
19380 (0, Breakpoint::new_standard()),
19381 (3, Breakpoint::new_standard()),
19382 ],
19383 );
19384
19385 editor.update_in(cx, |editor, window, cx| {
19386 editor.move_to_beginning(&MoveToBeginning, window, cx);
19387 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19388 });
19389
19390 let breakpoints = editor.update(cx, |editor, cx| {
19391 editor
19392 .breakpoint_store()
19393 .as_ref()
19394 .unwrap()
19395 .read(cx)
19396 .all_source_breakpoints(cx)
19397 .clone()
19398 });
19399
19400 assert_eq!(1, breakpoints.len());
19401 assert_breakpoint(
19402 &breakpoints,
19403 &abs_path,
19404 vec![(3, Breakpoint::new_standard())],
19405 );
19406
19407 editor.update_in(cx, |editor, window, cx| {
19408 editor.move_to_end(&MoveToEnd, window, cx);
19409 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19410 });
19411
19412 let breakpoints = editor.update(cx, |editor, cx| {
19413 editor
19414 .breakpoint_store()
19415 .as_ref()
19416 .unwrap()
19417 .read(cx)
19418 .all_source_breakpoints(cx)
19419 .clone()
19420 });
19421
19422 assert_eq!(0, breakpoints.len());
19423 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19424}
19425
19426#[gpui::test]
19427async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19428 init_test(cx, |_| {});
19429
19430 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19431
19432 let fs = FakeFs::new(cx.executor());
19433 fs.insert_tree(
19434 path!("/a"),
19435 json!({
19436 "main.rs": sample_text,
19437 }),
19438 )
19439 .await;
19440 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19441 let (workspace, cx) =
19442 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19443
19444 let worktree_id = workspace.update(cx, |workspace, cx| {
19445 workspace.project().update(cx, |project, cx| {
19446 project.worktrees(cx).next().unwrap().read(cx).id()
19447 })
19448 });
19449
19450 let buffer = project
19451 .update(cx, |project, cx| {
19452 project.open_buffer((worktree_id, "main.rs"), cx)
19453 })
19454 .await
19455 .unwrap();
19456
19457 let (editor, cx) = cx.add_window_view(|window, cx| {
19458 Editor::new(
19459 EditorMode::full(),
19460 MultiBuffer::build_from_buffer(buffer, cx),
19461 Some(project.clone()),
19462 window,
19463 cx,
19464 )
19465 });
19466
19467 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19468 let abs_path = project.read_with(cx, |project, cx| {
19469 project
19470 .absolute_path(&project_path, cx)
19471 .map(|path_buf| Arc::from(path_buf.to_owned()))
19472 .unwrap()
19473 });
19474
19475 editor.update_in(cx, |editor, window, cx| {
19476 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19477 });
19478
19479 let breakpoints = editor.update(cx, |editor, cx| {
19480 editor
19481 .breakpoint_store()
19482 .as_ref()
19483 .unwrap()
19484 .read(cx)
19485 .all_source_breakpoints(cx)
19486 .clone()
19487 });
19488
19489 assert_breakpoint(
19490 &breakpoints,
19491 &abs_path,
19492 vec![(0, Breakpoint::new_log("hello world"))],
19493 );
19494
19495 // Removing a log message from a log breakpoint should remove it
19496 editor.update_in(cx, |editor, window, cx| {
19497 add_log_breakpoint_at_cursor(editor, "", window, cx);
19498 });
19499
19500 let breakpoints = editor.update(cx, |editor, cx| {
19501 editor
19502 .breakpoint_store()
19503 .as_ref()
19504 .unwrap()
19505 .read(cx)
19506 .all_source_breakpoints(cx)
19507 .clone()
19508 });
19509
19510 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19511
19512 editor.update_in(cx, |editor, window, cx| {
19513 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19514 editor.move_to_end(&MoveToEnd, window, cx);
19515 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19516 // Not adding a log message to a standard breakpoint shouldn't remove it
19517 add_log_breakpoint_at_cursor(editor, "", window, cx);
19518 });
19519
19520 let breakpoints = editor.update(cx, |editor, cx| {
19521 editor
19522 .breakpoint_store()
19523 .as_ref()
19524 .unwrap()
19525 .read(cx)
19526 .all_source_breakpoints(cx)
19527 .clone()
19528 });
19529
19530 assert_breakpoint(
19531 &breakpoints,
19532 &abs_path,
19533 vec![
19534 (0, Breakpoint::new_standard()),
19535 (3, Breakpoint::new_standard()),
19536 ],
19537 );
19538
19539 editor.update_in(cx, |editor, window, cx| {
19540 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19541 });
19542
19543 let breakpoints = editor.update(cx, |editor, cx| {
19544 editor
19545 .breakpoint_store()
19546 .as_ref()
19547 .unwrap()
19548 .read(cx)
19549 .all_source_breakpoints(cx)
19550 .clone()
19551 });
19552
19553 assert_breakpoint(
19554 &breakpoints,
19555 &abs_path,
19556 vec![
19557 (0, Breakpoint::new_standard()),
19558 (3, Breakpoint::new_log("hello world")),
19559 ],
19560 );
19561
19562 editor.update_in(cx, |editor, window, cx| {
19563 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19564 });
19565
19566 let breakpoints = editor.update(cx, |editor, cx| {
19567 editor
19568 .breakpoint_store()
19569 .as_ref()
19570 .unwrap()
19571 .read(cx)
19572 .all_source_breakpoints(cx)
19573 .clone()
19574 });
19575
19576 assert_breakpoint(
19577 &breakpoints,
19578 &abs_path,
19579 vec![
19580 (0, Breakpoint::new_standard()),
19581 (3, Breakpoint::new_log("hello Earth!!")),
19582 ],
19583 );
19584}
19585
19586/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19587/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19588/// or when breakpoints were placed out of order. This tests for a regression too
19589#[gpui::test]
19590async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19591 init_test(cx, |_| {});
19592
19593 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19594 let fs = FakeFs::new(cx.executor());
19595 fs.insert_tree(
19596 path!("/a"),
19597 json!({
19598 "main.rs": sample_text,
19599 }),
19600 )
19601 .await;
19602 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19605
19606 let fs = FakeFs::new(cx.executor());
19607 fs.insert_tree(
19608 path!("/a"),
19609 json!({
19610 "main.rs": sample_text,
19611 }),
19612 )
19613 .await;
19614 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19615 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19616 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19617 let worktree_id = workspace
19618 .update(cx, |workspace, _window, cx| {
19619 workspace.project().update(cx, |project, cx| {
19620 project.worktrees(cx).next().unwrap().read(cx).id()
19621 })
19622 })
19623 .unwrap();
19624
19625 let buffer = project
19626 .update(cx, |project, cx| {
19627 project.open_buffer((worktree_id, "main.rs"), cx)
19628 })
19629 .await
19630 .unwrap();
19631
19632 let (editor, cx) = cx.add_window_view(|window, cx| {
19633 Editor::new(
19634 EditorMode::full(),
19635 MultiBuffer::build_from_buffer(buffer, cx),
19636 Some(project.clone()),
19637 window,
19638 cx,
19639 )
19640 });
19641
19642 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19643 let abs_path = project.read_with(cx, |project, cx| {
19644 project
19645 .absolute_path(&project_path, cx)
19646 .map(|path_buf| Arc::from(path_buf.to_owned()))
19647 .unwrap()
19648 });
19649
19650 // assert we can add breakpoint on the first line
19651 editor.update_in(cx, |editor, window, cx| {
19652 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19653 editor.move_to_end(&MoveToEnd, window, cx);
19654 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19655 editor.move_up(&MoveUp, window, cx);
19656 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19657 });
19658
19659 let breakpoints = editor.update(cx, |editor, cx| {
19660 editor
19661 .breakpoint_store()
19662 .as_ref()
19663 .unwrap()
19664 .read(cx)
19665 .all_source_breakpoints(cx)
19666 .clone()
19667 });
19668
19669 assert_eq!(1, breakpoints.len());
19670 assert_breakpoint(
19671 &breakpoints,
19672 &abs_path,
19673 vec![
19674 (0, Breakpoint::new_standard()),
19675 (2, Breakpoint::new_standard()),
19676 (3, Breakpoint::new_standard()),
19677 ],
19678 );
19679
19680 editor.update_in(cx, |editor, window, cx| {
19681 editor.move_to_beginning(&MoveToBeginning, window, cx);
19682 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19683 editor.move_to_end(&MoveToEnd, window, cx);
19684 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19685 // Disabling a breakpoint that doesn't exist should do nothing
19686 editor.move_up(&MoveUp, window, cx);
19687 editor.move_up(&MoveUp, window, cx);
19688 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19689 });
19690
19691 let breakpoints = editor.update(cx, |editor, cx| {
19692 editor
19693 .breakpoint_store()
19694 .as_ref()
19695 .unwrap()
19696 .read(cx)
19697 .all_source_breakpoints(cx)
19698 .clone()
19699 });
19700
19701 let disable_breakpoint = {
19702 let mut bp = Breakpoint::new_standard();
19703 bp.state = BreakpointState::Disabled;
19704 bp
19705 };
19706
19707 assert_eq!(1, breakpoints.len());
19708 assert_breakpoint(
19709 &breakpoints,
19710 &abs_path,
19711 vec![
19712 (0, disable_breakpoint.clone()),
19713 (2, Breakpoint::new_standard()),
19714 (3, disable_breakpoint.clone()),
19715 ],
19716 );
19717
19718 editor.update_in(cx, |editor, window, cx| {
19719 editor.move_to_beginning(&MoveToBeginning, window, cx);
19720 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19721 editor.move_to_end(&MoveToEnd, window, cx);
19722 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19723 editor.move_up(&MoveUp, window, cx);
19724 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19725 });
19726
19727 let breakpoints = editor.update(cx, |editor, cx| {
19728 editor
19729 .breakpoint_store()
19730 .as_ref()
19731 .unwrap()
19732 .read(cx)
19733 .all_source_breakpoints(cx)
19734 .clone()
19735 });
19736
19737 assert_eq!(1, breakpoints.len());
19738 assert_breakpoint(
19739 &breakpoints,
19740 &abs_path,
19741 vec![
19742 (0, Breakpoint::new_standard()),
19743 (2, disable_breakpoint),
19744 (3, Breakpoint::new_standard()),
19745 ],
19746 );
19747}
19748
19749#[gpui::test]
19750async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19751 init_test(cx, |_| {});
19752 let capabilities = lsp::ServerCapabilities {
19753 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19754 prepare_provider: Some(true),
19755 work_done_progress_options: Default::default(),
19756 })),
19757 ..Default::default()
19758 };
19759 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19760
19761 cx.set_state(indoc! {"
19762 struct Fˇoo {}
19763 "});
19764
19765 cx.update_editor(|editor, _, cx| {
19766 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19767 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19768 editor.highlight_background::<DocumentHighlightRead>(
19769 &[highlight_range],
19770 |c| c.editor_document_highlight_read_background,
19771 cx,
19772 );
19773 });
19774
19775 let mut prepare_rename_handler = cx
19776 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19777 move |_, _, _| async move {
19778 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19779 start: lsp::Position {
19780 line: 0,
19781 character: 7,
19782 },
19783 end: lsp::Position {
19784 line: 0,
19785 character: 10,
19786 },
19787 })))
19788 },
19789 );
19790 let prepare_rename_task = cx
19791 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19792 .expect("Prepare rename was not started");
19793 prepare_rename_handler.next().await.unwrap();
19794 prepare_rename_task.await.expect("Prepare rename failed");
19795
19796 let mut rename_handler =
19797 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19798 let edit = lsp::TextEdit {
19799 range: lsp::Range {
19800 start: lsp::Position {
19801 line: 0,
19802 character: 7,
19803 },
19804 end: lsp::Position {
19805 line: 0,
19806 character: 10,
19807 },
19808 },
19809 new_text: "FooRenamed".to_string(),
19810 };
19811 Ok(Some(lsp::WorkspaceEdit::new(
19812 // Specify the same edit twice
19813 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19814 )))
19815 });
19816 let rename_task = cx
19817 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19818 .expect("Confirm rename was not started");
19819 rename_handler.next().await.unwrap();
19820 rename_task.await.expect("Confirm rename failed");
19821 cx.run_until_parked();
19822
19823 // Despite two edits, only one is actually applied as those are identical
19824 cx.assert_editor_state(indoc! {"
19825 struct FooRenamedˇ {}
19826 "});
19827}
19828
19829#[gpui::test]
19830async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19831 init_test(cx, |_| {});
19832 // These capabilities indicate that the server does not support prepare rename.
19833 let capabilities = lsp::ServerCapabilities {
19834 rename_provider: Some(lsp::OneOf::Left(true)),
19835 ..Default::default()
19836 };
19837 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19838
19839 cx.set_state(indoc! {"
19840 struct Fˇoo {}
19841 "});
19842
19843 cx.update_editor(|editor, _window, cx| {
19844 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19845 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19846 editor.highlight_background::<DocumentHighlightRead>(
19847 &[highlight_range],
19848 |c| c.editor_document_highlight_read_background,
19849 cx,
19850 );
19851 });
19852
19853 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19854 .expect("Prepare rename was not started")
19855 .await
19856 .expect("Prepare rename failed");
19857
19858 let mut rename_handler =
19859 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19860 let edit = lsp::TextEdit {
19861 range: lsp::Range {
19862 start: lsp::Position {
19863 line: 0,
19864 character: 7,
19865 },
19866 end: lsp::Position {
19867 line: 0,
19868 character: 10,
19869 },
19870 },
19871 new_text: "FooRenamed".to_string(),
19872 };
19873 Ok(Some(lsp::WorkspaceEdit::new(
19874 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19875 )))
19876 });
19877 let rename_task = cx
19878 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19879 .expect("Confirm rename was not started");
19880 rename_handler.next().await.unwrap();
19881 rename_task.await.expect("Confirm rename failed");
19882 cx.run_until_parked();
19883
19884 // Correct range is renamed, as `surrounding_word` is used to find it.
19885 cx.assert_editor_state(indoc! {"
19886 struct FooRenamedˇ {}
19887 "});
19888}
19889
19890#[gpui::test]
19891async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19892 init_test(cx, |_| {});
19893 let mut cx = EditorTestContext::new(cx).await;
19894
19895 let language = Arc::new(
19896 Language::new(
19897 LanguageConfig::default(),
19898 Some(tree_sitter_html::LANGUAGE.into()),
19899 )
19900 .with_brackets_query(
19901 r#"
19902 ("<" @open "/>" @close)
19903 ("</" @open ">" @close)
19904 ("<" @open ">" @close)
19905 ("\"" @open "\"" @close)
19906 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19907 "#,
19908 )
19909 .unwrap(),
19910 );
19911 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19912
19913 cx.set_state(indoc! {"
19914 <span>ˇ</span>
19915 "});
19916 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19917 cx.assert_editor_state(indoc! {"
19918 <span>
19919 ˇ
19920 </span>
19921 "});
19922
19923 cx.set_state(indoc! {"
19924 <span><span></span>ˇ</span>
19925 "});
19926 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19927 cx.assert_editor_state(indoc! {"
19928 <span><span></span>
19929 ˇ</span>
19930 "});
19931
19932 cx.set_state(indoc! {"
19933 <span>ˇ
19934 </span>
19935 "});
19936 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19937 cx.assert_editor_state(indoc! {"
19938 <span>
19939 ˇ
19940 </span>
19941 "});
19942}
19943
19944#[gpui::test(iterations = 10)]
19945async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19946 init_test(cx, |_| {});
19947
19948 let fs = FakeFs::new(cx.executor());
19949 fs.insert_tree(
19950 path!("/dir"),
19951 json!({
19952 "a.ts": "a",
19953 }),
19954 )
19955 .await;
19956
19957 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19958 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19959 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19960
19961 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19962 language_registry.add(Arc::new(Language::new(
19963 LanguageConfig {
19964 name: "TypeScript".into(),
19965 matcher: LanguageMatcher {
19966 path_suffixes: vec!["ts".to_string()],
19967 ..Default::default()
19968 },
19969 ..Default::default()
19970 },
19971 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19972 )));
19973 let mut fake_language_servers = language_registry.register_fake_lsp(
19974 "TypeScript",
19975 FakeLspAdapter {
19976 capabilities: lsp::ServerCapabilities {
19977 code_lens_provider: Some(lsp::CodeLensOptions {
19978 resolve_provider: Some(true),
19979 }),
19980 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19981 commands: vec!["_the/command".to_string()],
19982 ..lsp::ExecuteCommandOptions::default()
19983 }),
19984 ..lsp::ServerCapabilities::default()
19985 },
19986 ..FakeLspAdapter::default()
19987 },
19988 );
19989
19990 let (buffer, _handle) = project
19991 .update(cx, |p, cx| {
19992 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19993 })
19994 .await
19995 .unwrap();
19996 cx.executor().run_until_parked();
19997
19998 let fake_server = fake_language_servers.next().await.unwrap();
19999
20000 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20001 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20002 drop(buffer_snapshot);
20003 let actions = cx
20004 .update_window(*workspace, |_, window, cx| {
20005 project.code_actions(&buffer, anchor..anchor, window, cx)
20006 })
20007 .unwrap();
20008
20009 fake_server
20010 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20011 Ok(Some(vec![
20012 lsp::CodeLens {
20013 range: lsp::Range::default(),
20014 command: Some(lsp::Command {
20015 title: "Code lens command".to_owned(),
20016 command: "_the/command".to_owned(),
20017 arguments: None,
20018 }),
20019 data: None,
20020 },
20021 lsp::CodeLens {
20022 range: lsp::Range::default(),
20023 command: Some(lsp::Command {
20024 title: "Command not in capabilities".to_owned(),
20025 command: "not in capabilities".to_owned(),
20026 arguments: None,
20027 }),
20028 data: None,
20029 },
20030 lsp::CodeLens {
20031 range: lsp::Range {
20032 start: lsp::Position {
20033 line: 1,
20034 character: 1,
20035 },
20036 end: lsp::Position {
20037 line: 1,
20038 character: 1,
20039 },
20040 },
20041 command: Some(lsp::Command {
20042 title: "Command not in range".to_owned(),
20043 command: "_the/command".to_owned(),
20044 arguments: None,
20045 }),
20046 data: None,
20047 },
20048 ]))
20049 })
20050 .next()
20051 .await;
20052
20053 let actions = actions.await.unwrap();
20054 assert_eq!(
20055 actions.len(),
20056 1,
20057 "Should have only one valid action for the 0..0 range"
20058 );
20059 let action = actions[0].clone();
20060 let apply = project.update(cx, |project, cx| {
20061 project.apply_code_action(buffer.clone(), action, true, cx)
20062 });
20063
20064 // Resolving the code action does not populate its edits. In absence of
20065 // edits, we must execute the given command.
20066 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20067 |mut lens, _| async move {
20068 let lens_command = lens.command.as_mut().expect("should have a command");
20069 assert_eq!(lens_command.title, "Code lens command");
20070 lens_command.arguments = Some(vec![json!("the-argument")]);
20071 Ok(lens)
20072 },
20073 );
20074
20075 // While executing the command, the language server sends the editor
20076 // a `workspaceEdit` request.
20077 fake_server
20078 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20079 let fake = fake_server.clone();
20080 move |params, _| {
20081 assert_eq!(params.command, "_the/command");
20082 let fake = fake.clone();
20083 async move {
20084 fake.server
20085 .request::<lsp::request::ApplyWorkspaceEdit>(
20086 lsp::ApplyWorkspaceEditParams {
20087 label: None,
20088 edit: lsp::WorkspaceEdit {
20089 changes: Some(
20090 [(
20091 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20092 vec![lsp::TextEdit {
20093 range: lsp::Range::new(
20094 lsp::Position::new(0, 0),
20095 lsp::Position::new(0, 0),
20096 ),
20097 new_text: "X".into(),
20098 }],
20099 )]
20100 .into_iter()
20101 .collect(),
20102 ),
20103 ..Default::default()
20104 },
20105 },
20106 )
20107 .await
20108 .into_response()
20109 .unwrap();
20110 Ok(Some(json!(null)))
20111 }
20112 }
20113 })
20114 .next()
20115 .await;
20116
20117 // Applying the code lens command returns a project transaction containing the edits
20118 // sent by the language server in its `workspaceEdit` request.
20119 let transaction = apply.await.unwrap();
20120 assert!(transaction.0.contains_key(&buffer));
20121 buffer.update(cx, |buffer, cx| {
20122 assert_eq!(buffer.text(), "Xa");
20123 buffer.undo(cx);
20124 assert_eq!(buffer.text(), "a");
20125 });
20126}
20127
20128#[gpui::test]
20129async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20130 init_test(cx, |_| {});
20131
20132 let fs = FakeFs::new(cx.executor());
20133 let main_text = r#"fn main() {
20134println!("1");
20135println!("2");
20136println!("3");
20137println!("4");
20138println!("5");
20139}"#;
20140 let lib_text = "mod foo {}";
20141 fs.insert_tree(
20142 path!("/a"),
20143 json!({
20144 "lib.rs": lib_text,
20145 "main.rs": main_text,
20146 }),
20147 )
20148 .await;
20149
20150 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20151 let (workspace, cx) =
20152 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20153 let worktree_id = workspace.update(cx, |workspace, cx| {
20154 workspace.project().update(cx, |project, cx| {
20155 project.worktrees(cx).next().unwrap().read(cx).id()
20156 })
20157 });
20158
20159 let expected_ranges = vec![
20160 Point::new(0, 0)..Point::new(0, 0),
20161 Point::new(1, 0)..Point::new(1, 1),
20162 Point::new(2, 0)..Point::new(2, 2),
20163 Point::new(3, 0)..Point::new(3, 3),
20164 ];
20165
20166 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20167 let editor_1 = workspace
20168 .update_in(cx, |workspace, window, cx| {
20169 workspace.open_path(
20170 (worktree_id, "main.rs"),
20171 Some(pane_1.downgrade()),
20172 true,
20173 window,
20174 cx,
20175 )
20176 })
20177 .unwrap()
20178 .await
20179 .downcast::<Editor>()
20180 .unwrap();
20181 pane_1.update(cx, |pane, cx| {
20182 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20183 open_editor.update(cx, |editor, cx| {
20184 assert_eq!(
20185 editor.display_text(cx),
20186 main_text,
20187 "Original main.rs text on initial open",
20188 );
20189 assert_eq!(
20190 editor
20191 .selections
20192 .all::<Point>(cx)
20193 .into_iter()
20194 .map(|s| s.range())
20195 .collect::<Vec<_>>(),
20196 vec![Point::zero()..Point::zero()],
20197 "Default selections on initial open",
20198 );
20199 })
20200 });
20201 editor_1.update_in(cx, |editor, window, cx| {
20202 editor.change_selections(None, window, cx, |s| {
20203 s.select_ranges(expected_ranges.clone());
20204 });
20205 });
20206
20207 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20208 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20209 });
20210 let editor_2 = workspace
20211 .update_in(cx, |workspace, window, cx| {
20212 workspace.open_path(
20213 (worktree_id, "main.rs"),
20214 Some(pane_2.downgrade()),
20215 true,
20216 window,
20217 cx,
20218 )
20219 })
20220 .unwrap()
20221 .await
20222 .downcast::<Editor>()
20223 .unwrap();
20224 pane_2.update(cx, |pane, cx| {
20225 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20226 open_editor.update(cx, |editor, cx| {
20227 assert_eq!(
20228 editor.display_text(cx),
20229 main_text,
20230 "Original main.rs text on initial open in another panel",
20231 );
20232 assert_eq!(
20233 editor
20234 .selections
20235 .all::<Point>(cx)
20236 .into_iter()
20237 .map(|s| s.range())
20238 .collect::<Vec<_>>(),
20239 vec![Point::zero()..Point::zero()],
20240 "Default selections on initial open in another panel",
20241 );
20242 })
20243 });
20244
20245 editor_2.update_in(cx, |editor, window, cx| {
20246 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20247 });
20248
20249 let _other_editor_1 = workspace
20250 .update_in(cx, |workspace, window, cx| {
20251 workspace.open_path(
20252 (worktree_id, "lib.rs"),
20253 Some(pane_1.downgrade()),
20254 true,
20255 window,
20256 cx,
20257 )
20258 })
20259 .unwrap()
20260 .await
20261 .downcast::<Editor>()
20262 .unwrap();
20263 pane_1
20264 .update_in(cx, |pane, window, cx| {
20265 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20266 })
20267 .await
20268 .unwrap();
20269 drop(editor_1);
20270 pane_1.update(cx, |pane, cx| {
20271 pane.active_item()
20272 .unwrap()
20273 .downcast::<Editor>()
20274 .unwrap()
20275 .update(cx, |editor, cx| {
20276 assert_eq!(
20277 editor.display_text(cx),
20278 lib_text,
20279 "Other file should be open and active",
20280 );
20281 });
20282 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20283 });
20284
20285 let _other_editor_2 = workspace
20286 .update_in(cx, |workspace, window, cx| {
20287 workspace.open_path(
20288 (worktree_id, "lib.rs"),
20289 Some(pane_2.downgrade()),
20290 true,
20291 window,
20292 cx,
20293 )
20294 })
20295 .unwrap()
20296 .await
20297 .downcast::<Editor>()
20298 .unwrap();
20299 pane_2
20300 .update_in(cx, |pane, window, cx| {
20301 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20302 })
20303 .await
20304 .unwrap();
20305 drop(editor_2);
20306 pane_2.update(cx, |pane, cx| {
20307 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20308 open_editor.update(cx, |editor, cx| {
20309 assert_eq!(
20310 editor.display_text(cx),
20311 lib_text,
20312 "Other file should be open and active in another panel too",
20313 );
20314 });
20315 assert_eq!(
20316 pane.items().count(),
20317 1,
20318 "No other editors should be open in another pane",
20319 );
20320 });
20321
20322 let _editor_1_reopened = workspace
20323 .update_in(cx, |workspace, window, cx| {
20324 workspace.open_path(
20325 (worktree_id, "main.rs"),
20326 Some(pane_1.downgrade()),
20327 true,
20328 window,
20329 cx,
20330 )
20331 })
20332 .unwrap()
20333 .await
20334 .downcast::<Editor>()
20335 .unwrap();
20336 let _editor_2_reopened = workspace
20337 .update_in(cx, |workspace, window, cx| {
20338 workspace.open_path(
20339 (worktree_id, "main.rs"),
20340 Some(pane_2.downgrade()),
20341 true,
20342 window,
20343 cx,
20344 )
20345 })
20346 .unwrap()
20347 .await
20348 .downcast::<Editor>()
20349 .unwrap();
20350 pane_1.update(cx, |pane, cx| {
20351 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20352 open_editor.update(cx, |editor, cx| {
20353 assert_eq!(
20354 editor.display_text(cx),
20355 main_text,
20356 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20357 );
20358 assert_eq!(
20359 editor
20360 .selections
20361 .all::<Point>(cx)
20362 .into_iter()
20363 .map(|s| s.range())
20364 .collect::<Vec<_>>(),
20365 expected_ranges,
20366 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20367 );
20368 })
20369 });
20370 pane_2.update(cx, |pane, cx| {
20371 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20372 open_editor.update(cx, |editor, cx| {
20373 assert_eq!(
20374 editor.display_text(cx),
20375 r#"fn main() {
20376⋯rintln!("1");
20377⋯intln!("2");
20378⋯ntln!("3");
20379println!("4");
20380println!("5");
20381}"#,
20382 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20383 );
20384 assert_eq!(
20385 editor
20386 .selections
20387 .all::<Point>(cx)
20388 .into_iter()
20389 .map(|s| s.range())
20390 .collect::<Vec<_>>(),
20391 vec![Point::zero()..Point::zero()],
20392 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20393 );
20394 })
20395 });
20396}
20397
20398#[gpui::test]
20399async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20400 init_test(cx, |_| {});
20401
20402 let fs = FakeFs::new(cx.executor());
20403 let main_text = r#"fn main() {
20404println!("1");
20405println!("2");
20406println!("3");
20407println!("4");
20408println!("5");
20409}"#;
20410 let lib_text = "mod foo {}";
20411 fs.insert_tree(
20412 path!("/a"),
20413 json!({
20414 "lib.rs": lib_text,
20415 "main.rs": main_text,
20416 }),
20417 )
20418 .await;
20419
20420 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20421 let (workspace, cx) =
20422 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20423 let worktree_id = workspace.update(cx, |workspace, cx| {
20424 workspace.project().update(cx, |project, cx| {
20425 project.worktrees(cx).next().unwrap().read(cx).id()
20426 })
20427 });
20428
20429 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20430 let editor = workspace
20431 .update_in(cx, |workspace, window, cx| {
20432 workspace.open_path(
20433 (worktree_id, "main.rs"),
20434 Some(pane.downgrade()),
20435 true,
20436 window,
20437 cx,
20438 )
20439 })
20440 .unwrap()
20441 .await
20442 .downcast::<Editor>()
20443 .unwrap();
20444 pane.update(cx, |pane, cx| {
20445 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20446 open_editor.update(cx, |editor, cx| {
20447 assert_eq!(
20448 editor.display_text(cx),
20449 main_text,
20450 "Original main.rs text on initial open",
20451 );
20452 })
20453 });
20454 editor.update_in(cx, |editor, window, cx| {
20455 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20456 });
20457
20458 cx.update_global(|store: &mut SettingsStore, cx| {
20459 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20460 s.restore_on_file_reopen = Some(false);
20461 });
20462 });
20463 editor.update_in(cx, |editor, window, cx| {
20464 editor.fold_ranges(
20465 vec![
20466 Point::new(1, 0)..Point::new(1, 1),
20467 Point::new(2, 0)..Point::new(2, 2),
20468 Point::new(3, 0)..Point::new(3, 3),
20469 ],
20470 false,
20471 window,
20472 cx,
20473 );
20474 });
20475 pane.update_in(cx, |pane, window, cx| {
20476 pane.close_all_items(&CloseAllItems::default(), window, cx)
20477 })
20478 .await
20479 .unwrap();
20480 pane.update(cx, |pane, _| {
20481 assert!(pane.active_item().is_none());
20482 });
20483 cx.update_global(|store: &mut SettingsStore, cx| {
20484 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20485 s.restore_on_file_reopen = Some(true);
20486 });
20487 });
20488
20489 let _editor_reopened = workspace
20490 .update_in(cx, |workspace, window, cx| {
20491 workspace.open_path(
20492 (worktree_id, "main.rs"),
20493 Some(pane.downgrade()),
20494 true,
20495 window,
20496 cx,
20497 )
20498 })
20499 .unwrap()
20500 .await
20501 .downcast::<Editor>()
20502 .unwrap();
20503 pane.update(cx, |pane, cx| {
20504 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20505 open_editor.update(cx, |editor, cx| {
20506 assert_eq!(
20507 editor.display_text(cx),
20508 main_text,
20509 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20510 );
20511 })
20512 });
20513}
20514
20515#[gpui::test]
20516async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20517 struct EmptyModalView {
20518 focus_handle: gpui::FocusHandle,
20519 }
20520 impl EventEmitter<DismissEvent> for EmptyModalView {}
20521 impl Render for EmptyModalView {
20522 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20523 div()
20524 }
20525 }
20526 impl Focusable for EmptyModalView {
20527 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20528 self.focus_handle.clone()
20529 }
20530 }
20531 impl workspace::ModalView for EmptyModalView {}
20532 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20533 EmptyModalView {
20534 focus_handle: cx.focus_handle(),
20535 }
20536 }
20537
20538 init_test(cx, |_| {});
20539
20540 let fs = FakeFs::new(cx.executor());
20541 let project = Project::test(fs, [], cx).await;
20542 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20543 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20544 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20545 let editor = cx.new_window_entity(|window, cx| {
20546 Editor::new(
20547 EditorMode::full(),
20548 buffer,
20549 Some(project.clone()),
20550 window,
20551 cx,
20552 )
20553 });
20554 workspace
20555 .update(cx, |workspace, window, cx| {
20556 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20557 })
20558 .unwrap();
20559 editor.update_in(cx, |editor, window, cx| {
20560 editor.open_context_menu(&OpenContextMenu, window, cx);
20561 assert!(editor.mouse_context_menu.is_some());
20562 });
20563 workspace
20564 .update(cx, |workspace, window, cx| {
20565 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20566 })
20567 .unwrap();
20568 cx.read(|cx| {
20569 assert!(editor.read(cx).mouse_context_menu.is_none());
20570 });
20571}
20572
20573#[gpui::test]
20574async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20575 init_test(cx, |_| {});
20576
20577 let fs = FakeFs::new(cx.executor());
20578 fs.insert_file(path!("/file.html"), Default::default())
20579 .await;
20580
20581 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20582
20583 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20584 let html_language = Arc::new(Language::new(
20585 LanguageConfig {
20586 name: "HTML".into(),
20587 matcher: LanguageMatcher {
20588 path_suffixes: vec!["html".to_string()],
20589 ..LanguageMatcher::default()
20590 },
20591 brackets: BracketPairConfig {
20592 pairs: vec![BracketPair {
20593 start: "<".into(),
20594 end: ">".into(),
20595 close: true,
20596 ..Default::default()
20597 }],
20598 ..Default::default()
20599 },
20600 ..Default::default()
20601 },
20602 Some(tree_sitter_html::LANGUAGE.into()),
20603 ));
20604 language_registry.add(html_language);
20605 let mut fake_servers = language_registry.register_fake_lsp(
20606 "HTML",
20607 FakeLspAdapter {
20608 capabilities: lsp::ServerCapabilities {
20609 completion_provider: Some(lsp::CompletionOptions {
20610 resolve_provider: Some(true),
20611 ..Default::default()
20612 }),
20613 ..Default::default()
20614 },
20615 ..Default::default()
20616 },
20617 );
20618
20619 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20620 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20621
20622 let worktree_id = workspace
20623 .update(cx, |workspace, _window, cx| {
20624 workspace.project().update(cx, |project, cx| {
20625 project.worktrees(cx).next().unwrap().read(cx).id()
20626 })
20627 })
20628 .unwrap();
20629 project
20630 .update(cx, |project, cx| {
20631 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20632 })
20633 .await
20634 .unwrap();
20635 let editor = workspace
20636 .update(cx, |workspace, window, cx| {
20637 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20638 })
20639 .unwrap()
20640 .await
20641 .unwrap()
20642 .downcast::<Editor>()
20643 .unwrap();
20644
20645 let fake_server = fake_servers.next().await.unwrap();
20646 editor.update_in(cx, |editor, window, cx| {
20647 editor.set_text("<ad></ad>", window, cx);
20648 editor.change_selections(None, window, cx, |selections| {
20649 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20650 });
20651 let Some((buffer, _)) = editor
20652 .buffer
20653 .read(cx)
20654 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20655 else {
20656 panic!("Failed to get buffer for selection position");
20657 };
20658 let buffer = buffer.read(cx);
20659 let buffer_id = buffer.remote_id();
20660 let opening_range =
20661 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20662 let closing_range =
20663 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20664 let mut linked_ranges = HashMap::default();
20665 linked_ranges.insert(
20666 buffer_id,
20667 vec![(opening_range.clone(), vec![closing_range.clone()])],
20668 );
20669 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20670 });
20671 let mut completion_handle =
20672 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20673 Ok(Some(lsp::CompletionResponse::Array(vec![
20674 lsp::CompletionItem {
20675 label: "head".to_string(),
20676 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20677 lsp::InsertReplaceEdit {
20678 new_text: "head".to_string(),
20679 insert: lsp::Range::new(
20680 lsp::Position::new(0, 1),
20681 lsp::Position::new(0, 3),
20682 ),
20683 replace: lsp::Range::new(
20684 lsp::Position::new(0, 1),
20685 lsp::Position::new(0, 3),
20686 ),
20687 },
20688 )),
20689 ..Default::default()
20690 },
20691 ])))
20692 });
20693 editor.update_in(cx, |editor, window, cx| {
20694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20695 });
20696 cx.run_until_parked();
20697 completion_handle.next().await.unwrap();
20698 editor.update(cx, |editor, _| {
20699 assert!(
20700 editor.context_menu_visible(),
20701 "Completion menu should be visible"
20702 );
20703 });
20704 editor.update_in(cx, |editor, window, cx| {
20705 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20706 });
20707 cx.executor().run_until_parked();
20708 editor.update(cx, |editor, cx| {
20709 assert_eq!(editor.text(cx), "<head></head>");
20710 });
20711}
20712
20713#[gpui::test]
20714async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20715 init_test(cx, |_| {});
20716
20717 let fs = FakeFs::new(cx.executor());
20718 fs.insert_tree(
20719 path!("/root"),
20720 json!({
20721 "a": {
20722 "main.rs": "fn main() {}",
20723 },
20724 "foo": {
20725 "bar": {
20726 "external_file.rs": "pub mod external {}",
20727 }
20728 }
20729 }),
20730 )
20731 .await;
20732
20733 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20734 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20735 language_registry.add(rust_lang());
20736 let _fake_servers = language_registry.register_fake_lsp(
20737 "Rust",
20738 FakeLspAdapter {
20739 ..FakeLspAdapter::default()
20740 },
20741 );
20742 let (workspace, cx) =
20743 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20744 let worktree_id = workspace.update(cx, |workspace, cx| {
20745 workspace.project().update(cx, |project, cx| {
20746 project.worktrees(cx).next().unwrap().read(cx).id()
20747 })
20748 });
20749
20750 let assert_language_servers_count =
20751 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20752 project.update(cx, |project, cx| {
20753 let current = project
20754 .lsp_store()
20755 .read(cx)
20756 .as_local()
20757 .unwrap()
20758 .language_servers
20759 .len();
20760 assert_eq!(expected, current, "{context}");
20761 });
20762 };
20763
20764 assert_language_servers_count(
20765 0,
20766 "No servers should be running before any file is open",
20767 cx,
20768 );
20769 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20770 let main_editor = workspace
20771 .update_in(cx, |workspace, window, cx| {
20772 workspace.open_path(
20773 (worktree_id, "main.rs"),
20774 Some(pane.downgrade()),
20775 true,
20776 window,
20777 cx,
20778 )
20779 })
20780 .unwrap()
20781 .await
20782 .downcast::<Editor>()
20783 .unwrap();
20784 pane.update(cx, |pane, cx| {
20785 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20786 open_editor.update(cx, |editor, cx| {
20787 assert_eq!(
20788 editor.display_text(cx),
20789 "fn main() {}",
20790 "Original main.rs text on initial open",
20791 );
20792 });
20793 assert_eq!(open_editor, main_editor);
20794 });
20795 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20796
20797 let external_editor = workspace
20798 .update_in(cx, |workspace, window, cx| {
20799 workspace.open_abs_path(
20800 PathBuf::from("/root/foo/bar/external_file.rs"),
20801 OpenOptions::default(),
20802 window,
20803 cx,
20804 )
20805 })
20806 .await
20807 .expect("opening external file")
20808 .downcast::<Editor>()
20809 .expect("downcasted external file's open element to editor");
20810 pane.update(cx, |pane, cx| {
20811 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20812 open_editor.update(cx, |editor, cx| {
20813 assert_eq!(
20814 editor.display_text(cx),
20815 "pub mod external {}",
20816 "External file is open now",
20817 );
20818 });
20819 assert_eq!(open_editor, external_editor);
20820 });
20821 assert_language_servers_count(
20822 1,
20823 "Second, external, *.rs file should join the existing server",
20824 cx,
20825 );
20826
20827 pane.update_in(cx, |pane, window, cx| {
20828 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20829 })
20830 .await
20831 .unwrap();
20832 pane.update_in(cx, |pane, window, cx| {
20833 pane.navigate_backward(window, cx);
20834 });
20835 cx.run_until_parked();
20836 pane.update(cx, |pane, cx| {
20837 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20838 open_editor.update(cx, |editor, cx| {
20839 assert_eq!(
20840 editor.display_text(cx),
20841 "pub mod external {}",
20842 "External file is open now",
20843 );
20844 });
20845 });
20846 assert_language_servers_count(
20847 1,
20848 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20849 cx,
20850 );
20851
20852 cx.update(|_, cx| {
20853 workspace::reload(&workspace::Reload::default(), cx);
20854 });
20855 assert_language_servers_count(
20856 1,
20857 "After reloading the worktree with local and external files opened, only one project should be started",
20858 cx,
20859 );
20860}
20861
20862#[gpui::test]
20863async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20864 init_test(cx, |_| {});
20865
20866 let mut cx = EditorTestContext::new(cx).await;
20867 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20868 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20869
20870 // test cursor move to start of each line on tab
20871 // for `if`, `elif`, `else`, `while`, `with` and `for`
20872 cx.set_state(indoc! {"
20873 def main():
20874 ˇ for item in items:
20875 ˇ while item.active:
20876 ˇ if item.value > 10:
20877 ˇ continue
20878 ˇ elif item.value < 0:
20879 ˇ break
20880 ˇ else:
20881 ˇ with item.context() as ctx:
20882 ˇ yield count
20883 ˇ else:
20884 ˇ log('while else')
20885 ˇ else:
20886 ˇ log('for else')
20887 "});
20888 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20889 cx.assert_editor_state(indoc! {"
20890 def main():
20891 ˇfor item in items:
20892 ˇwhile item.active:
20893 ˇif item.value > 10:
20894 ˇcontinue
20895 ˇelif item.value < 0:
20896 ˇbreak
20897 ˇelse:
20898 ˇwith item.context() as ctx:
20899 ˇyield count
20900 ˇelse:
20901 ˇlog('while else')
20902 ˇelse:
20903 ˇlog('for else')
20904 "});
20905 // test relative indent is preserved when tab
20906 // for `if`, `elif`, `else`, `while`, `with` and `for`
20907 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20908 cx.assert_editor_state(indoc! {"
20909 def main():
20910 ˇfor item in items:
20911 ˇwhile item.active:
20912 ˇif item.value > 10:
20913 ˇcontinue
20914 ˇelif item.value < 0:
20915 ˇbreak
20916 ˇelse:
20917 ˇwith item.context() as ctx:
20918 ˇyield count
20919 ˇelse:
20920 ˇlog('while else')
20921 ˇelse:
20922 ˇlog('for else')
20923 "});
20924
20925 // test cursor move to start of each line on tab
20926 // for `try`, `except`, `else`, `finally`, `match` and `def`
20927 cx.set_state(indoc! {"
20928 def main():
20929 ˇ try:
20930 ˇ fetch()
20931 ˇ except ValueError:
20932 ˇ handle_error()
20933 ˇ else:
20934 ˇ match value:
20935 ˇ case _:
20936 ˇ finally:
20937 ˇ def status():
20938 ˇ return 0
20939 "});
20940 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20941 cx.assert_editor_state(indoc! {"
20942 def main():
20943 ˇtry:
20944 ˇfetch()
20945 ˇexcept ValueError:
20946 ˇhandle_error()
20947 ˇelse:
20948 ˇmatch value:
20949 ˇcase _:
20950 ˇfinally:
20951 ˇdef status():
20952 ˇreturn 0
20953 "});
20954 // test relative indent is preserved when tab
20955 // for `try`, `except`, `else`, `finally`, `match` and `def`
20956 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20957 cx.assert_editor_state(indoc! {"
20958 def main():
20959 ˇtry:
20960 ˇfetch()
20961 ˇexcept ValueError:
20962 ˇhandle_error()
20963 ˇelse:
20964 ˇmatch value:
20965 ˇcase _:
20966 ˇfinally:
20967 ˇdef status():
20968 ˇreturn 0
20969 "});
20970}
20971
20972#[gpui::test]
20973async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20974 init_test(cx, |_| {});
20975
20976 let mut cx = EditorTestContext::new(cx).await;
20977 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20978 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20979
20980 // test `else` auto outdents when typed inside `if` block
20981 cx.set_state(indoc! {"
20982 def main():
20983 if i == 2:
20984 return
20985 ˇ
20986 "});
20987 cx.update_editor(|editor, window, cx| {
20988 editor.handle_input("else:", window, cx);
20989 });
20990 cx.assert_editor_state(indoc! {"
20991 def main():
20992 if i == 2:
20993 return
20994 else:ˇ
20995 "});
20996
20997 // test `except` auto outdents when typed inside `try` block
20998 cx.set_state(indoc! {"
20999 def main():
21000 try:
21001 i = 2
21002 ˇ
21003 "});
21004 cx.update_editor(|editor, window, cx| {
21005 editor.handle_input("except:", window, cx);
21006 });
21007 cx.assert_editor_state(indoc! {"
21008 def main():
21009 try:
21010 i = 2
21011 except:ˇ
21012 "});
21013
21014 // test `else` auto outdents when typed inside `except` block
21015 cx.set_state(indoc! {"
21016 def main():
21017 try:
21018 i = 2
21019 except:
21020 j = 2
21021 ˇ
21022 "});
21023 cx.update_editor(|editor, window, cx| {
21024 editor.handle_input("else:", window, cx);
21025 });
21026 cx.assert_editor_state(indoc! {"
21027 def main():
21028 try:
21029 i = 2
21030 except:
21031 j = 2
21032 else:ˇ
21033 "});
21034
21035 // test `finally` auto outdents when typed inside `else` block
21036 cx.set_state(indoc! {"
21037 def main():
21038 try:
21039 i = 2
21040 except:
21041 j = 2
21042 else:
21043 k = 2
21044 ˇ
21045 "});
21046 cx.update_editor(|editor, window, cx| {
21047 editor.handle_input("finally:", window, cx);
21048 });
21049 cx.assert_editor_state(indoc! {"
21050 def main():
21051 try:
21052 i = 2
21053 except:
21054 j = 2
21055 else:
21056 k = 2
21057 finally:ˇ
21058 "});
21059
21060 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21061 // cx.set_state(indoc! {"
21062 // def main():
21063 // try:
21064 // for i in range(n):
21065 // pass
21066 // ˇ
21067 // "});
21068 // cx.update_editor(|editor, window, cx| {
21069 // editor.handle_input("except:", window, cx);
21070 // });
21071 // cx.assert_editor_state(indoc! {"
21072 // def main():
21073 // try:
21074 // for i in range(n):
21075 // pass
21076 // except:ˇ
21077 // "});
21078
21079 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21080 // cx.set_state(indoc! {"
21081 // def main():
21082 // try:
21083 // i = 2
21084 // except:
21085 // for i in range(n):
21086 // pass
21087 // ˇ
21088 // "});
21089 // cx.update_editor(|editor, window, cx| {
21090 // editor.handle_input("else:", window, cx);
21091 // });
21092 // cx.assert_editor_state(indoc! {"
21093 // def main():
21094 // try:
21095 // i = 2
21096 // except:
21097 // for i in range(n):
21098 // pass
21099 // else:ˇ
21100 // "});
21101
21102 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21103 // cx.set_state(indoc! {"
21104 // def main():
21105 // try:
21106 // i = 2
21107 // except:
21108 // j = 2
21109 // else:
21110 // for i in range(n):
21111 // pass
21112 // ˇ
21113 // "});
21114 // cx.update_editor(|editor, window, cx| {
21115 // editor.handle_input("finally:", window, cx);
21116 // });
21117 // cx.assert_editor_state(indoc! {"
21118 // def main():
21119 // try:
21120 // i = 2
21121 // except:
21122 // j = 2
21123 // else:
21124 // for i in range(n):
21125 // pass
21126 // finally:ˇ
21127 // "});
21128
21129 // test `else` stays at correct indent when typed after `for` block
21130 cx.set_state(indoc! {"
21131 def main():
21132 for i in range(10):
21133 if i == 3:
21134 break
21135 ˇ
21136 "});
21137 cx.update_editor(|editor, window, cx| {
21138 editor.handle_input("else:", window, cx);
21139 });
21140 cx.assert_editor_state(indoc! {"
21141 def main():
21142 for i in range(10):
21143 if i == 3:
21144 break
21145 else:ˇ
21146 "});
21147
21148 // test does not outdent on typing after line with square brackets
21149 cx.set_state(indoc! {"
21150 def f() -> list[str]:
21151 ˇ
21152 "});
21153 cx.update_editor(|editor, window, cx| {
21154 editor.handle_input("a", window, cx);
21155 });
21156 cx.assert_editor_state(indoc! {"
21157 def f() -> list[str]:
21158 aˇ
21159 "});
21160}
21161
21162#[gpui::test]
21163async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21164 init_test(cx, |_| {});
21165 update_test_language_settings(cx, |settings| {
21166 settings.defaults.extend_comment_on_newline = Some(false);
21167 });
21168 let mut cx = EditorTestContext::new(cx).await;
21169 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21170 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21171
21172 // test correct indent after newline on comment
21173 cx.set_state(indoc! {"
21174 # COMMENT:ˇ
21175 "});
21176 cx.update_editor(|editor, window, cx| {
21177 editor.newline(&Newline, window, cx);
21178 });
21179 cx.assert_editor_state(indoc! {"
21180 # COMMENT:
21181 ˇ
21182 "});
21183
21184 // test correct indent after newline in brackets
21185 cx.set_state(indoc! {"
21186 {ˇ}
21187 "});
21188 cx.update_editor(|editor, window, cx| {
21189 editor.newline(&Newline, window, cx);
21190 });
21191 cx.run_until_parked();
21192 cx.assert_editor_state(indoc! {"
21193 {
21194 ˇ
21195 }
21196 "});
21197
21198 cx.set_state(indoc! {"
21199 (ˇ)
21200 "});
21201 cx.update_editor(|editor, window, cx| {
21202 editor.newline(&Newline, window, cx);
21203 });
21204 cx.run_until_parked();
21205 cx.assert_editor_state(indoc! {"
21206 (
21207 ˇ
21208 )
21209 "});
21210
21211 // do not indent after empty lists or dictionaries
21212 cx.set_state(indoc! {"
21213 a = []ˇ
21214 "});
21215 cx.update_editor(|editor, window, cx| {
21216 editor.newline(&Newline, window, cx);
21217 });
21218 cx.run_until_parked();
21219 cx.assert_editor_state(indoc! {"
21220 a = []
21221 ˇ
21222 "});
21223}
21224
21225fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21226 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21227 point..point
21228}
21229
21230fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21231 let (text, ranges) = marked_text_ranges(marked_text, true);
21232 assert_eq!(editor.text(cx), text);
21233 assert_eq!(
21234 editor.selections.ranges(cx),
21235 ranges,
21236 "Assert selections are {}",
21237 marked_text
21238 );
21239}
21240
21241pub fn handle_signature_help_request(
21242 cx: &mut EditorLspTestContext,
21243 mocked_response: lsp::SignatureHelp,
21244) -> impl Future<Output = ()> + use<> {
21245 let mut request =
21246 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21247 let mocked_response = mocked_response.clone();
21248 async move { Ok(Some(mocked_response)) }
21249 });
21250
21251 async move {
21252 request.next().await;
21253 }
21254}
21255
21256#[track_caller]
21257pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21258 cx.update_editor(|editor, _, _| {
21259 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21260 let entries = menu.entries.borrow();
21261 let entries = entries
21262 .iter()
21263 .map(|entry| entry.string.as_str())
21264 .collect::<Vec<_>>();
21265 assert_eq!(entries, expected);
21266 } else {
21267 panic!("Expected completions menu");
21268 }
21269 });
21270}
21271
21272/// Handle completion request passing a marked string specifying where the completion
21273/// should be triggered from using '|' character, what range should be replaced, and what completions
21274/// should be returned using '<' and '>' to delimit the range.
21275///
21276/// Also see `handle_completion_request_with_insert_and_replace`.
21277#[track_caller]
21278pub fn handle_completion_request(
21279 marked_string: &str,
21280 completions: Vec<&'static str>,
21281 is_incomplete: bool,
21282 counter: Arc<AtomicUsize>,
21283 cx: &mut EditorLspTestContext,
21284) -> impl Future<Output = ()> {
21285 let complete_from_marker: TextRangeMarker = '|'.into();
21286 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21287 let (_, mut marked_ranges) = marked_text_ranges_by(
21288 marked_string,
21289 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21290 );
21291
21292 let complete_from_position =
21293 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21294 let replace_range =
21295 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21296
21297 let mut request =
21298 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21299 let completions = completions.clone();
21300 counter.fetch_add(1, atomic::Ordering::Release);
21301 async move {
21302 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21303 assert_eq!(
21304 params.text_document_position.position,
21305 complete_from_position
21306 );
21307 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21308 is_incomplete: is_incomplete,
21309 item_defaults: None,
21310 items: completions
21311 .iter()
21312 .map(|completion_text| lsp::CompletionItem {
21313 label: completion_text.to_string(),
21314 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21315 range: replace_range,
21316 new_text: completion_text.to_string(),
21317 })),
21318 ..Default::default()
21319 })
21320 .collect(),
21321 })))
21322 }
21323 });
21324
21325 async move {
21326 request.next().await;
21327 }
21328}
21329
21330/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21331/// given instead, which also contains an `insert` range.
21332///
21333/// This function uses markers to define ranges:
21334/// - `|` marks the cursor position
21335/// - `<>` marks the replace range
21336/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21337pub fn handle_completion_request_with_insert_and_replace(
21338 cx: &mut EditorLspTestContext,
21339 marked_string: &str,
21340 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21341 counter: Arc<AtomicUsize>,
21342) -> impl Future<Output = ()> {
21343 let complete_from_marker: TextRangeMarker = '|'.into();
21344 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21345 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21346
21347 let (_, mut marked_ranges) = marked_text_ranges_by(
21348 marked_string,
21349 vec![
21350 complete_from_marker.clone(),
21351 replace_range_marker.clone(),
21352 insert_range_marker.clone(),
21353 ],
21354 );
21355
21356 let complete_from_position =
21357 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21358 let replace_range =
21359 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21360
21361 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21362 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21363 _ => lsp::Range {
21364 start: replace_range.start,
21365 end: complete_from_position,
21366 },
21367 };
21368
21369 let mut request =
21370 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21371 let completions = completions.clone();
21372 counter.fetch_add(1, atomic::Ordering::Release);
21373 async move {
21374 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21375 assert_eq!(
21376 params.text_document_position.position, complete_from_position,
21377 "marker `|` position doesn't match",
21378 );
21379 Ok(Some(lsp::CompletionResponse::Array(
21380 completions
21381 .iter()
21382 .map(|(label, new_text)| lsp::CompletionItem {
21383 label: label.to_string(),
21384 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21385 lsp::InsertReplaceEdit {
21386 insert: insert_range,
21387 replace: replace_range,
21388 new_text: new_text.to_string(),
21389 },
21390 )),
21391 ..Default::default()
21392 })
21393 .collect(),
21394 )))
21395 }
21396 });
21397
21398 async move {
21399 request.next().await;
21400 }
21401}
21402
21403fn handle_resolve_completion_request(
21404 cx: &mut EditorLspTestContext,
21405 edits: Option<Vec<(&'static str, &'static str)>>,
21406) -> impl Future<Output = ()> {
21407 let edits = edits.map(|edits| {
21408 edits
21409 .iter()
21410 .map(|(marked_string, new_text)| {
21411 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21412 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21413 lsp::TextEdit::new(replace_range, new_text.to_string())
21414 })
21415 .collect::<Vec<_>>()
21416 });
21417
21418 let mut request =
21419 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21420 let edits = edits.clone();
21421 async move {
21422 Ok(lsp::CompletionItem {
21423 additional_text_edits: edits,
21424 ..Default::default()
21425 })
21426 }
21427 });
21428
21429 async move {
21430 request.next().await;
21431 }
21432}
21433
21434pub(crate) fn update_test_language_settings(
21435 cx: &mut TestAppContext,
21436 f: impl Fn(&mut AllLanguageSettingsContent),
21437) {
21438 cx.update(|cx| {
21439 SettingsStore::update_global(cx, |store, cx| {
21440 store.update_user_settings::<AllLanguageSettings>(cx, f);
21441 });
21442 });
21443}
21444
21445pub(crate) fn update_test_project_settings(
21446 cx: &mut TestAppContext,
21447 f: impl Fn(&mut ProjectSettings),
21448) {
21449 cx.update(|cx| {
21450 SettingsStore::update_global(cx, |store, cx| {
21451 store.update_user_settings::<ProjectSettings>(cx, f);
21452 });
21453 });
21454}
21455
21456pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21457 cx.update(|cx| {
21458 assets::Assets.load_test_fonts(cx);
21459 let store = SettingsStore::test(cx);
21460 cx.set_global(store);
21461 theme::init(theme::LoadThemes::JustBase, cx);
21462 release_channel::init(SemanticVersion::default(), cx);
21463 client::init_settings(cx);
21464 language::init(cx);
21465 Project::init_settings(cx);
21466 workspace::init_settings(cx);
21467 crate::init(cx);
21468 });
21469
21470 update_test_language_settings(cx, f);
21471}
21472
21473#[track_caller]
21474fn assert_hunk_revert(
21475 not_reverted_text_with_selections: &str,
21476 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21477 expected_reverted_text_with_selections: &str,
21478 base_text: &str,
21479 cx: &mut EditorLspTestContext,
21480) {
21481 cx.set_state(not_reverted_text_with_selections);
21482 cx.set_head_text(base_text);
21483 cx.executor().run_until_parked();
21484
21485 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21486 let snapshot = editor.snapshot(window, cx);
21487 let reverted_hunk_statuses = snapshot
21488 .buffer_snapshot
21489 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21490 .map(|hunk| hunk.status().kind)
21491 .collect::<Vec<_>>();
21492
21493 editor.git_restore(&Default::default(), window, cx);
21494 reverted_hunk_statuses
21495 });
21496 cx.executor().run_until_parked();
21497 cx.assert_editor_state(expected_reverted_text_with_selections);
21498 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21499}