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 (text, insertion_ranges) = marked_text_ranges(
8517 indoc! {"
8518 a.ˇ b
8519 a.ˇ b
8520 a.ˇ b
8521 "},
8522 false,
8523 );
8524
8525 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8526 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8527
8528 editor.update_in(cx, |editor, window, cx| {
8529 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8530
8531 editor
8532 .insert_snippet(&insertion_ranges, snippet, window, cx)
8533 .unwrap();
8534
8535 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8536 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8537 assert_eq!(editor.text(cx), expected_text);
8538 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8539 }
8540
8541 assert(
8542 editor,
8543 cx,
8544 indoc! {"
8545 a.f(«one», two, «three») b
8546 a.f(«one», two, «three») b
8547 a.f(«one», two, «three») b
8548 "},
8549 );
8550
8551 // Can't move earlier than the first tab stop
8552 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8553 assert(
8554 editor,
8555 cx,
8556 indoc! {"
8557 a.f(«one», two, «three») b
8558 a.f(«one», two, «three») b
8559 a.f(«one», two, «three») b
8560 "},
8561 );
8562
8563 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8564 assert(
8565 editor,
8566 cx,
8567 indoc! {"
8568 a.f(one, «two», three) b
8569 a.f(one, «two», three) b
8570 a.f(one, «two», three) b
8571 "},
8572 );
8573
8574 editor.move_to_prev_snippet_tabstop(window, cx);
8575 assert(
8576 editor,
8577 cx,
8578 indoc! {"
8579 a.f(«one», two, «three») b
8580 a.f(«one», two, «three») b
8581 a.f(«one», two, «three») b
8582 "},
8583 );
8584
8585 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8586 assert(
8587 editor,
8588 cx,
8589 indoc! {"
8590 a.f(one, «two», three) b
8591 a.f(one, «two», three) b
8592 a.f(one, «two», three) b
8593 "},
8594 );
8595 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8596 assert(
8597 editor,
8598 cx,
8599 indoc! {"
8600 a.f(one, two, three)ˇ b
8601 a.f(one, two, three)ˇ b
8602 a.f(one, two, three)ˇ b
8603 "},
8604 );
8605
8606 // As soon as the last tab stop is reached, snippet state is gone
8607 editor.move_to_prev_snippet_tabstop(window, cx);
8608 assert(
8609 editor,
8610 cx,
8611 indoc! {"
8612 a.f(one, two, three)ˇ b
8613 a.f(one, two, three)ˇ b
8614 a.f(one, two, three)ˇ b
8615 "},
8616 );
8617 });
8618}
8619
8620#[gpui::test]
8621async fn test_document_format_during_save(cx: &mut TestAppContext) {
8622 init_test(cx, |_| {});
8623
8624 let fs = FakeFs::new(cx.executor());
8625 fs.insert_file(path!("/file.rs"), Default::default()).await;
8626
8627 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8628
8629 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8630 language_registry.add(rust_lang());
8631 let mut fake_servers = language_registry.register_fake_lsp(
8632 "Rust",
8633 FakeLspAdapter {
8634 capabilities: lsp::ServerCapabilities {
8635 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8636 ..Default::default()
8637 },
8638 ..Default::default()
8639 },
8640 );
8641
8642 let buffer = project
8643 .update(cx, |project, cx| {
8644 project.open_local_buffer(path!("/file.rs"), cx)
8645 })
8646 .await
8647 .unwrap();
8648
8649 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8650 let (editor, cx) = cx.add_window_view(|window, cx| {
8651 build_editor_with_project(project.clone(), buffer, window, cx)
8652 });
8653 editor.update_in(cx, |editor, window, cx| {
8654 editor.set_text("one\ntwo\nthree\n", window, cx)
8655 });
8656 assert!(cx.read(|cx| editor.is_dirty(cx)));
8657
8658 cx.executor().start_waiting();
8659 let fake_server = fake_servers.next().await.unwrap();
8660
8661 {
8662 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8663 move |params, _| async move {
8664 assert_eq!(
8665 params.text_document.uri,
8666 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8667 );
8668 assert_eq!(params.options.tab_size, 4);
8669 Ok(Some(vec![lsp::TextEdit::new(
8670 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8671 ", ".to_string(),
8672 )]))
8673 },
8674 );
8675 let save = editor
8676 .update_in(cx, |editor, window, cx| {
8677 editor.save(true, project.clone(), window, cx)
8678 })
8679 .unwrap();
8680 cx.executor().start_waiting();
8681 save.await;
8682
8683 assert_eq!(
8684 editor.update(cx, |editor, cx| editor.text(cx)),
8685 "one, two\nthree\n"
8686 );
8687 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8688 }
8689
8690 {
8691 editor.update_in(cx, |editor, window, cx| {
8692 editor.set_text("one\ntwo\nthree\n", window, cx)
8693 });
8694 assert!(cx.read(|cx| editor.is_dirty(cx)));
8695
8696 // Ensure we can still save even if formatting hangs.
8697 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8698 move |params, _| async move {
8699 assert_eq!(
8700 params.text_document.uri,
8701 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8702 );
8703 futures::future::pending::<()>().await;
8704 unreachable!()
8705 },
8706 );
8707 let save = editor
8708 .update_in(cx, |editor, window, cx| {
8709 editor.save(true, project.clone(), window, cx)
8710 })
8711 .unwrap();
8712 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8713 cx.executor().start_waiting();
8714 save.await;
8715 assert_eq!(
8716 editor.update(cx, |editor, cx| editor.text(cx)),
8717 "one\ntwo\nthree\n"
8718 );
8719 }
8720
8721 // For non-dirty buffer, no formatting request should be sent
8722 {
8723 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8724
8725 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8726 panic!("Should not be invoked on non-dirty buffer");
8727 });
8728 let save = editor
8729 .update_in(cx, |editor, window, cx| {
8730 editor.save(true, project.clone(), window, cx)
8731 })
8732 .unwrap();
8733 cx.executor().start_waiting();
8734 save.await;
8735 }
8736
8737 // Set rust language override and assert overridden tabsize is sent to language server
8738 update_test_language_settings(cx, |settings| {
8739 settings.languages.insert(
8740 "Rust".into(),
8741 LanguageSettingsContent {
8742 tab_size: NonZeroU32::new(8),
8743 ..Default::default()
8744 },
8745 );
8746 });
8747
8748 {
8749 editor.update_in(cx, |editor, window, cx| {
8750 editor.set_text("somehting_new\n", window, cx)
8751 });
8752 assert!(cx.read(|cx| editor.is_dirty(cx)));
8753 let _formatting_request_signal = fake_server
8754 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8755 assert_eq!(
8756 params.text_document.uri,
8757 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8758 );
8759 assert_eq!(params.options.tab_size, 8);
8760 Ok(Some(vec![]))
8761 });
8762 let save = editor
8763 .update_in(cx, |editor, window, cx| {
8764 editor.save(true, project.clone(), window, cx)
8765 })
8766 .unwrap();
8767 cx.executor().start_waiting();
8768 save.await;
8769 }
8770}
8771
8772#[gpui::test]
8773async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8774 init_test(cx, |_| {});
8775
8776 let cols = 4;
8777 let rows = 10;
8778 let sample_text_1 = sample_text(rows, cols, 'a');
8779 assert_eq!(
8780 sample_text_1,
8781 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8782 );
8783 let sample_text_2 = sample_text(rows, cols, 'l');
8784 assert_eq!(
8785 sample_text_2,
8786 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8787 );
8788 let sample_text_3 = sample_text(rows, cols, 'v');
8789 assert_eq!(
8790 sample_text_3,
8791 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8792 );
8793
8794 let fs = FakeFs::new(cx.executor());
8795 fs.insert_tree(
8796 path!("/a"),
8797 json!({
8798 "main.rs": sample_text_1,
8799 "other.rs": sample_text_2,
8800 "lib.rs": sample_text_3,
8801 }),
8802 )
8803 .await;
8804
8805 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8806 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8807 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8808
8809 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8810 language_registry.add(rust_lang());
8811 let mut fake_servers = language_registry.register_fake_lsp(
8812 "Rust",
8813 FakeLspAdapter {
8814 capabilities: lsp::ServerCapabilities {
8815 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8816 ..Default::default()
8817 },
8818 ..Default::default()
8819 },
8820 );
8821
8822 let worktree = project.update(cx, |project, cx| {
8823 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8824 assert_eq!(worktrees.len(), 1);
8825 worktrees.pop().unwrap()
8826 });
8827 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8828
8829 let buffer_1 = project
8830 .update(cx, |project, cx| {
8831 project.open_buffer((worktree_id, "main.rs"), cx)
8832 })
8833 .await
8834 .unwrap();
8835 let buffer_2 = project
8836 .update(cx, |project, cx| {
8837 project.open_buffer((worktree_id, "other.rs"), cx)
8838 })
8839 .await
8840 .unwrap();
8841 let buffer_3 = project
8842 .update(cx, |project, cx| {
8843 project.open_buffer((worktree_id, "lib.rs"), cx)
8844 })
8845 .await
8846 .unwrap();
8847
8848 let multi_buffer = cx.new(|cx| {
8849 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8850 multi_buffer.push_excerpts(
8851 buffer_1.clone(),
8852 [
8853 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8854 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8855 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8856 ],
8857 cx,
8858 );
8859 multi_buffer.push_excerpts(
8860 buffer_2.clone(),
8861 [
8862 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8863 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8864 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8865 ],
8866 cx,
8867 );
8868 multi_buffer.push_excerpts(
8869 buffer_3.clone(),
8870 [
8871 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8872 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8873 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8874 ],
8875 cx,
8876 );
8877 multi_buffer
8878 });
8879 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8880 Editor::new(
8881 EditorMode::full(),
8882 multi_buffer,
8883 Some(project.clone()),
8884 window,
8885 cx,
8886 )
8887 });
8888
8889 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8890 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8891 s.select_ranges(Some(1..2))
8892 });
8893 editor.insert("|one|two|three|", window, cx);
8894 });
8895 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8896 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8897 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8898 s.select_ranges(Some(60..70))
8899 });
8900 editor.insert("|four|five|six|", window, cx);
8901 });
8902 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8903
8904 // First two buffers should be edited, but not the third one.
8905 assert_eq!(
8906 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8907 "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}",
8908 );
8909 buffer_1.update(cx, |buffer, _| {
8910 assert!(buffer.is_dirty());
8911 assert_eq!(
8912 buffer.text(),
8913 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8914 )
8915 });
8916 buffer_2.update(cx, |buffer, _| {
8917 assert!(buffer.is_dirty());
8918 assert_eq!(
8919 buffer.text(),
8920 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8921 )
8922 });
8923 buffer_3.update(cx, |buffer, _| {
8924 assert!(!buffer.is_dirty());
8925 assert_eq!(buffer.text(), sample_text_3,)
8926 });
8927 cx.executor().run_until_parked();
8928
8929 cx.executor().start_waiting();
8930 let save = multi_buffer_editor
8931 .update_in(cx, |editor, window, cx| {
8932 editor.save(true, project.clone(), window, cx)
8933 })
8934 .unwrap();
8935
8936 let fake_server = fake_servers.next().await.unwrap();
8937 fake_server
8938 .server
8939 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8940 Ok(Some(vec![lsp::TextEdit::new(
8941 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8942 format!("[{} formatted]", params.text_document.uri),
8943 )]))
8944 })
8945 .detach();
8946 save.await;
8947
8948 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8949 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8950 assert_eq!(
8951 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8952 uri!(
8953 "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}"
8954 ),
8955 );
8956 buffer_1.update(cx, |buffer, _| {
8957 assert!(!buffer.is_dirty());
8958 assert_eq!(
8959 buffer.text(),
8960 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8961 )
8962 });
8963 buffer_2.update(cx, |buffer, _| {
8964 assert!(!buffer.is_dirty());
8965 assert_eq!(
8966 buffer.text(),
8967 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8968 )
8969 });
8970 buffer_3.update(cx, |buffer, _| {
8971 assert!(!buffer.is_dirty());
8972 assert_eq!(buffer.text(), sample_text_3,)
8973 });
8974}
8975
8976#[gpui::test]
8977async fn test_range_format_during_save(cx: &mut TestAppContext) {
8978 init_test(cx, |_| {});
8979
8980 let fs = FakeFs::new(cx.executor());
8981 fs.insert_file(path!("/file.rs"), Default::default()).await;
8982
8983 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8984
8985 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8986 language_registry.add(rust_lang());
8987 let mut fake_servers = language_registry.register_fake_lsp(
8988 "Rust",
8989 FakeLspAdapter {
8990 capabilities: lsp::ServerCapabilities {
8991 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8992 ..Default::default()
8993 },
8994 ..Default::default()
8995 },
8996 );
8997
8998 let buffer = project
8999 .update(cx, |project, cx| {
9000 project.open_local_buffer(path!("/file.rs"), cx)
9001 })
9002 .await
9003 .unwrap();
9004
9005 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9006 let (editor, cx) = cx.add_window_view(|window, cx| {
9007 build_editor_with_project(project.clone(), buffer, window, cx)
9008 });
9009 editor.update_in(cx, |editor, window, cx| {
9010 editor.set_text("one\ntwo\nthree\n", window, cx)
9011 });
9012 assert!(cx.read(|cx| editor.is_dirty(cx)));
9013
9014 cx.executor().start_waiting();
9015 let fake_server = fake_servers.next().await.unwrap();
9016
9017 let save = editor
9018 .update_in(cx, |editor, window, cx| {
9019 editor.save(true, project.clone(), window, cx)
9020 })
9021 .unwrap();
9022 fake_server
9023 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9024 assert_eq!(
9025 params.text_document.uri,
9026 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9027 );
9028 assert_eq!(params.options.tab_size, 4);
9029 Ok(Some(vec![lsp::TextEdit::new(
9030 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9031 ", ".to_string(),
9032 )]))
9033 })
9034 .next()
9035 .await;
9036 cx.executor().start_waiting();
9037 save.await;
9038 assert_eq!(
9039 editor.update(cx, |editor, cx| editor.text(cx)),
9040 "one, two\nthree\n"
9041 );
9042 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9043
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.set_text("one\ntwo\nthree\n", window, cx)
9046 });
9047 assert!(cx.read(|cx| editor.is_dirty(cx)));
9048
9049 // Ensure we can still save even if formatting hangs.
9050 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9051 move |params, _| async move {
9052 assert_eq!(
9053 params.text_document.uri,
9054 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9055 );
9056 futures::future::pending::<()>().await;
9057 unreachable!()
9058 },
9059 );
9060 let save = editor
9061 .update_in(cx, |editor, window, cx| {
9062 editor.save(true, project.clone(), window, cx)
9063 })
9064 .unwrap();
9065 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9066 cx.executor().start_waiting();
9067 save.await;
9068 assert_eq!(
9069 editor.update(cx, |editor, cx| editor.text(cx)),
9070 "one\ntwo\nthree\n"
9071 );
9072 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9073
9074 // For non-dirty buffer, no formatting request should be sent
9075 let save = editor
9076 .update_in(cx, |editor, window, cx| {
9077 editor.save(true, project.clone(), window, cx)
9078 })
9079 .unwrap();
9080 let _pending_format_request = fake_server
9081 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9082 panic!("Should not be invoked on non-dirty buffer");
9083 })
9084 .next();
9085 cx.executor().start_waiting();
9086 save.await;
9087
9088 // Set Rust language override and assert overridden tabsize is sent to language server
9089 update_test_language_settings(cx, |settings| {
9090 settings.languages.insert(
9091 "Rust".into(),
9092 LanguageSettingsContent {
9093 tab_size: NonZeroU32::new(8),
9094 ..Default::default()
9095 },
9096 );
9097 });
9098
9099 editor.update_in(cx, |editor, window, cx| {
9100 editor.set_text("somehting_new\n", window, cx)
9101 });
9102 assert!(cx.read(|cx| editor.is_dirty(cx)));
9103 let save = editor
9104 .update_in(cx, |editor, window, cx| {
9105 editor.save(true, project.clone(), window, cx)
9106 })
9107 .unwrap();
9108 fake_server
9109 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9110 assert_eq!(
9111 params.text_document.uri,
9112 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9113 );
9114 assert_eq!(params.options.tab_size, 8);
9115 Ok(Some(Vec::new()))
9116 })
9117 .next()
9118 .await;
9119 save.await;
9120}
9121
9122#[gpui::test]
9123async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9124 init_test(cx, |settings| {
9125 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9126 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9127 ))
9128 });
9129
9130 let fs = FakeFs::new(cx.executor());
9131 fs.insert_file(path!("/file.rs"), Default::default()).await;
9132
9133 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9134
9135 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9136 language_registry.add(Arc::new(Language::new(
9137 LanguageConfig {
9138 name: "Rust".into(),
9139 matcher: LanguageMatcher {
9140 path_suffixes: vec!["rs".to_string()],
9141 ..Default::default()
9142 },
9143 ..LanguageConfig::default()
9144 },
9145 Some(tree_sitter_rust::LANGUAGE.into()),
9146 )));
9147 update_test_language_settings(cx, |settings| {
9148 // Enable Prettier formatting for the same buffer, and ensure
9149 // LSP is called instead of Prettier.
9150 settings.defaults.prettier = Some(PrettierSettings {
9151 allowed: true,
9152 ..PrettierSettings::default()
9153 });
9154 });
9155 let mut fake_servers = language_registry.register_fake_lsp(
9156 "Rust",
9157 FakeLspAdapter {
9158 capabilities: lsp::ServerCapabilities {
9159 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9160 ..Default::default()
9161 },
9162 ..Default::default()
9163 },
9164 );
9165
9166 let buffer = project
9167 .update(cx, |project, cx| {
9168 project.open_local_buffer(path!("/file.rs"), cx)
9169 })
9170 .await
9171 .unwrap();
9172
9173 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9174 let (editor, cx) = cx.add_window_view(|window, cx| {
9175 build_editor_with_project(project.clone(), buffer, window, cx)
9176 });
9177 editor.update_in(cx, |editor, window, cx| {
9178 editor.set_text("one\ntwo\nthree\n", window, cx)
9179 });
9180
9181 cx.executor().start_waiting();
9182 let fake_server = fake_servers.next().await.unwrap();
9183
9184 let format = editor
9185 .update_in(cx, |editor, window, cx| {
9186 editor.perform_format(
9187 project.clone(),
9188 FormatTrigger::Manual,
9189 FormatTarget::Buffers,
9190 window,
9191 cx,
9192 )
9193 })
9194 .unwrap();
9195 fake_server
9196 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9197 assert_eq!(
9198 params.text_document.uri,
9199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9200 );
9201 assert_eq!(params.options.tab_size, 4);
9202 Ok(Some(vec![lsp::TextEdit::new(
9203 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9204 ", ".to_string(),
9205 )]))
9206 })
9207 .next()
9208 .await;
9209 cx.executor().start_waiting();
9210 format.await;
9211 assert_eq!(
9212 editor.update(cx, |editor, cx| editor.text(cx)),
9213 "one, two\nthree\n"
9214 );
9215
9216 editor.update_in(cx, |editor, window, cx| {
9217 editor.set_text("one\ntwo\nthree\n", window, cx)
9218 });
9219 // Ensure we don't lock if formatting hangs.
9220 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9221 move |params, _| async move {
9222 assert_eq!(
9223 params.text_document.uri,
9224 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9225 );
9226 futures::future::pending::<()>().await;
9227 unreachable!()
9228 },
9229 );
9230 let format = editor
9231 .update_in(cx, |editor, window, cx| {
9232 editor.perform_format(
9233 project,
9234 FormatTrigger::Manual,
9235 FormatTarget::Buffers,
9236 window,
9237 cx,
9238 )
9239 })
9240 .unwrap();
9241 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9242 cx.executor().start_waiting();
9243 format.await;
9244 assert_eq!(
9245 editor.update(cx, |editor, cx| editor.text(cx)),
9246 "one\ntwo\nthree\n"
9247 );
9248}
9249
9250#[gpui::test]
9251async fn test_multiple_formatters(cx: &mut TestAppContext) {
9252 init_test(cx, |settings| {
9253 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9254 settings.defaults.formatter =
9255 Some(language_settings::SelectedFormatter::List(FormatterList(
9256 vec![
9257 Formatter::LanguageServer { name: None },
9258 Formatter::CodeActions(
9259 [
9260 ("code-action-1".into(), true),
9261 ("code-action-2".into(), true),
9262 ]
9263 .into_iter()
9264 .collect(),
9265 ),
9266 ]
9267 .into(),
9268 )))
9269 });
9270
9271 let fs = FakeFs::new(cx.executor());
9272 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9273 .await;
9274
9275 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9276 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9277 language_registry.add(rust_lang());
9278
9279 let mut fake_servers = language_registry.register_fake_lsp(
9280 "Rust",
9281 FakeLspAdapter {
9282 capabilities: lsp::ServerCapabilities {
9283 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9284 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9285 commands: vec!["the-command-for-code-action-1".into()],
9286 ..Default::default()
9287 }),
9288 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9289 ..Default::default()
9290 },
9291 ..Default::default()
9292 },
9293 );
9294
9295 let buffer = project
9296 .update(cx, |project, cx| {
9297 project.open_local_buffer(path!("/file.rs"), cx)
9298 })
9299 .await
9300 .unwrap();
9301
9302 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9303 let (editor, cx) = cx.add_window_view(|window, cx| {
9304 build_editor_with_project(project.clone(), buffer, window, cx)
9305 });
9306
9307 cx.executor().start_waiting();
9308
9309 let fake_server = fake_servers.next().await.unwrap();
9310 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9311 move |_params, _| async move {
9312 Ok(Some(vec![lsp::TextEdit::new(
9313 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9314 "applied-formatting\n".to_string(),
9315 )]))
9316 },
9317 );
9318 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9319 move |params, _| async move {
9320 assert_eq!(
9321 params.context.only,
9322 Some(vec!["code-action-1".into(), "code-action-2".into()])
9323 );
9324 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9325 Ok(Some(vec![
9326 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9327 kind: Some("code-action-1".into()),
9328 edit: Some(lsp::WorkspaceEdit::new(
9329 [(
9330 uri.clone(),
9331 vec![lsp::TextEdit::new(
9332 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9333 "applied-code-action-1-edit\n".to_string(),
9334 )],
9335 )]
9336 .into_iter()
9337 .collect(),
9338 )),
9339 command: Some(lsp::Command {
9340 command: "the-command-for-code-action-1".into(),
9341 ..Default::default()
9342 }),
9343 ..Default::default()
9344 }),
9345 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9346 kind: Some("code-action-2".into()),
9347 edit: Some(lsp::WorkspaceEdit::new(
9348 [(
9349 uri.clone(),
9350 vec![lsp::TextEdit::new(
9351 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9352 "applied-code-action-2-edit\n".to_string(),
9353 )],
9354 )]
9355 .into_iter()
9356 .collect(),
9357 )),
9358 ..Default::default()
9359 }),
9360 ]))
9361 },
9362 );
9363
9364 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9365 move |params, _| async move { Ok(params) }
9366 });
9367
9368 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9369 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9370 let fake = fake_server.clone();
9371 let lock = command_lock.clone();
9372 move |params, _| {
9373 assert_eq!(params.command, "the-command-for-code-action-1");
9374 let fake = fake.clone();
9375 let lock = lock.clone();
9376 async move {
9377 lock.lock().await;
9378 fake.server
9379 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9380 label: None,
9381 edit: lsp::WorkspaceEdit {
9382 changes: Some(
9383 [(
9384 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9385 vec![lsp::TextEdit {
9386 range: lsp::Range::new(
9387 lsp::Position::new(0, 0),
9388 lsp::Position::new(0, 0),
9389 ),
9390 new_text: "applied-code-action-1-command\n".into(),
9391 }],
9392 )]
9393 .into_iter()
9394 .collect(),
9395 ),
9396 ..Default::default()
9397 },
9398 })
9399 .await
9400 .into_response()
9401 .unwrap();
9402 Ok(Some(json!(null)))
9403 }
9404 }
9405 });
9406
9407 cx.executor().start_waiting();
9408 editor
9409 .update_in(cx, |editor, window, cx| {
9410 editor.perform_format(
9411 project.clone(),
9412 FormatTrigger::Manual,
9413 FormatTarget::Buffers,
9414 window,
9415 cx,
9416 )
9417 })
9418 .unwrap()
9419 .await;
9420 editor.update(cx, |editor, cx| {
9421 assert_eq!(
9422 editor.text(cx),
9423 r#"
9424 applied-code-action-2-edit
9425 applied-code-action-1-command
9426 applied-code-action-1-edit
9427 applied-formatting
9428 one
9429 two
9430 three
9431 "#
9432 .unindent()
9433 );
9434 });
9435
9436 editor.update_in(cx, |editor, window, cx| {
9437 editor.undo(&Default::default(), window, cx);
9438 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9439 });
9440
9441 // Perform a manual edit while waiting for an LSP command
9442 // that's being run as part of a formatting code action.
9443 let lock_guard = command_lock.lock().await;
9444 let format = editor
9445 .update_in(cx, |editor, window, cx| {
9446 editor.perform_format(
9447 project.clone(),
9448 FormatTrigger::Manual,
9449 FormatTarget::Buffers,
9450 window,
9451 cx,
9452 )
9453 })
9454 .unwrap();
9455 cx.run_until_parked();
9456 editor.update(cx, |editor, cx| {
9457 assert_eq!(
9458 editor.text(cx),
9459 r#"
9460 applied-code-action-1-edit
9461 applied-formatting
9462 one
9463 two
9464 three
9465 "#
9466 .unindent()
9467 );
9468
9469 editor.buffer.update(cx, |buffer, cx| {
9470 let ix = buffer.len(cx);
9471 buffer.edit([(ix..ix, "edited\n")], None, cx);
9472 });
9473 });
9474
9475 // Allow the LSP command to proceed. Because the buffer was edited,
9476 // the second code action will not be run.
9477 drop(lock_guard);
9478 format.await;
9479 editor.update_in(cx, |editor, window, cx| {
9480 assert_eq!(
9481 editor.text(cx),
9482 r#"
9483 applied-code-action-1-command
9484 applied-code-action-1-edit
9485 applied-formatting
9486 one
9487 two
9488 three
9489 edited
9490 "#
9491 .unindent()
9492 );
9493
9494 // The manual edit is undone first, because it is the last thing the user did
9495 // (even though the command completed afterwards).
9496 editor.undo(&Default::default(), window, cx);
9497 assert_eq!(
9498 editor.text(cx),
9499 r#"
9500 applied-code-action-1-command
9501 applied-code-action-1-edit
9502 applied-formatting
9503 one
9504 two
9505 three
9506 "#
9507 .unindent()
9508 );
9509
9510 // All the formatting (including the command, which completed after the manual edit)
9511 // is undone together.
9512 editor.undo(&Default::default(), window, cx);
9513 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9514 });
9515}
9516
9517#[gpui::test]
9518async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9519 init_test(cx, |settings| {
9520 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9521 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9522 ))
9523 });
9524
9525 let fs = FakeFs::new(cx.executor());
9526 fs.insert_file(path!("/file.ts"), Default::default()).await;
9527
9528 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9529
9530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9531 language_registry.add(Arc::new(Language::new(
9532 LanguageConfig {
9533 name: "TypeScript".into(),
9534 matcher: LanguageMatcher {
9535 path_suffixes: vec!["ts".to_string()],
9536 ..Default::default()
9537 },
9538 ..LanguageConfig::default()
9539 },
9540 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9541 )));
9542 update_test_language_settings(cx, |settings| {
9543 settings.defaults.prettier = Some(PrettierSettings {
9544 allowed: true,
9545 ..PrettierSettings::default()
9546 });
9547 });
9548 let mut fake_servers = language_registry.register_fake_lsp(
9549 "TypeScript",
9550 FakeLspAdapter {
9551 capabilities: lsp::ServerCapabilities {
9552 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9553 ..Default::default()
9554 },
9555 ..Default::default()
9556 },
9557 );
9558
9559 let buffer = project
9560 .update(cx, |project, cx| {
9561 project.open_local_buffer(path!("/file.ts"), cx)
9562 })
9563 .await
9564 .unwrap();
9565
9566 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9567 let (editor, cx) = cx.add_window_view(|window, cx| {
9568 build_editor_with_project(project.clone(), buffer, window, cx)
9569 });
9570 editor.update_in(cx, |editor, window, cx| {
9571 editor.set_text(
9572 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9573 window,
9574 cx,
9575 )
9576 });
9577
9578 cx.executor().start_waiting();
9579 let fake_server = fake_servers.next().await.unwrap();
9580
9581 let format = editor
9582 .update_in(cx, |editor, window, cx| {
9583 editor.perform_code_action_kind(
9584 project.clone(),
9585 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9586 window,
9587 cx,
9588 )
9589 })
9590 .unwrap();
9591 fake_server
9592 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9593 assert_eq!(
9594 params.text_document.uri,
9595 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9596 );
9597 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9598 lsp::CodeAction {
9599 title: "Organize Imports".to_string(),
9600 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9601 edit: Some(lsp::WorkspaceEdit {
9602 changes: Some(
9603 [(
9604 params.text_document.uri.clone(),
9605 vec![lsp::TextEdit::new(
9606 lsp::Range::new(
9607 lsp::Position::new(1, 0),
9608 lsp::Position::new(2, 0),
9609 ),
9610 "".to_string(),
9611 )],
9612 )]
9613 .into_iter()
9614 .collect(),
9615 ),
9616 ..Default::default()
9617 }),
9618 ..Default::default()
9619 },
9620 )]))
9621 })
9622 .next()
9623 .await;
9624 cx.executor().start_waiting();
9625 format.await;
9626 assert_eq!(
9627 editor.update(cx, |editor, cx| editor.text(cx)),
9628 "import { a } from 'module';\n\nconst x = a;\n"
9629 );
9630
9631 editor.update_in(cx, |editor, window, cx| {
9632 editor.set_text(
9633 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9634 window,
9635 cx,
9636 )
9637 });
9638 // Ensure we don't lock if code action hangs.
9639 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9640 move |params, _| async move {
9641 assert_eq!(
9642 params.text_document.uri,
9643 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9644 );
9645 futures::future::pending::<()>().await;
9646 unreachable!()
9647 },
9648 );
9649 let format = editor
9650 .update_in(cx, |editor, window, cx| {
9651 editor.perform_code_action_kind(
9652 project,
9653 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9654 window,
9655 cx,
9656 )
9657 })
9658 .unwrap();
9659 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9660 cx.executor().start_waiting();
9661 format.await;
9662 assert_eq!(
9663 editor.update(cx, |editor, cx| editor.text(cx)),
9664 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9665 );
9666}
9667
9668#[gpui::test]
9669async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9670 init_test(cx, |_| {});
9671
9672 let mut cx = EditorLspTestContext::new_rust(
9673 lsp::ServerCapabilities {
9674 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9675 ..Default::default()
9676 },
9677 cx,
9678 )
9679 .await;
9680
9681 cx.set_state(indoc! {"
9682 one.twoˇ
9683 "});
9684
9685 // The format request takes a long time. When it completes, it inserts
9686 // a newline and an indent before the `.`
9687 cx.lsp
9688 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9689 let executor = cx.background_executor().clone();
9690 async move {
9691 executor.timer(Duration::from_millis(100)).await;
9692 Ok(Some(vec![lsp::TextEdit {
9693 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9694 new_text: "\n ".into(),
9695 }]))
9696 }
9697 });
9698
9699 // Submit a format request.
9700 let format_1 = cx
9701 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9702 .unwrap();
9703 cx.executor().run_until_parked();
9704
9705 // Submit a second format request.
9706 let format_2 = cx
9707 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9708 .unwrap();
9709 cx.executor().run_until_parked();
9710
9711 // Wait for both format requests to complete
9712 cx.executor().advance_clock(Duration::from_millis(200));
9713 cx.executor().start_waiting();
9714 format_1.await.unwrap();
9715 cx.executor().start_waiting();
9716 format_2.await.unwrap();
9717
9718 // The formatting edits only happens once.
9719 cx.assert_editor_state(indoc! {"
9720 one
9721 .twoˇ
9722 "});
9723}
9724
9725#[gpui::test]
9726async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9727 init_test(cx, |settings| {
9728 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9729 });
9730
9731 let mut cx = EditorLspTestContext::new_rust(
9732 lsp::ServerCapabilities {
9733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9734 ..Default::default()
9735 },
9736 cx,
9737 )
9738 .await;
9739
9740 // Set up a buffer white some trailing whitespace and no trailing newline.
9741 cx.set_state(
9742 &[
9743 "one ", //
9744 "twoˇ", //
9745 "three ", //
9746 "four", //
9747 ]
9748 .join("\n"),
9749 );
9750
9751 // Submit a format request.
9752 let format = cx
9753 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9754 .unwrap();
9755
9756 // Record which buffer changes have been sent to the language server
9757 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9758 cx.lsp
9759 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9760 let buffer_changes = buffer_changes.clone();
9761 move |params, _| {
9762 buffer_changes.lock().extend(
9763 params
9764 .content_changes
9765 .into_iter()
9766 .map(|e| (e.range.unwrap(), e.text)),
9767 );
9768 }
9769 });
9770
9771 // Handle formatting requests to the language server.
9772 cx.lsp
9773 .set_request_handler::<lsp::request::Formatting, _, _>({
9774 let buffer_changes = buffer_changes.clone();
9775 move |_, _| {
9776 // When formatting is requested, trailing whitespace has already been stripped,
9777 // and the trailing newline has already been added.
9778 assert_eq!(
9779 &buffer_changes.lock()[1..],
9780 &[
9781 (
9782 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9783 "".into()
9784 ),
9785 (
9786 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9787 "".into()
9788 ),
9789 (
9790 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9791 "\n".into()
9792 ),
9793 ]
9794 );
9795
9796 // Insert blank lines between each line of the buffer.
9797 async move {
9798 Ok(Some(vec![
9799 lsp::TextEdit {
9800 range: lsp::Range::new(
9801 lsp::Position::new(1, 0),
9802 lsp::Position::new(1, 0),
9803 ),
9804 new_text: "\n".into(),
9805 },
9806 lsp::TextEdit {
9807 range: lsp::Range::new(
9808 lsp::Position::new(2, 0),
9809 lsp::Position::new(2, 0),
9810 ),
9811 new_text: "\n".into(),
9812 },
9813 ]))
9814 }
9815 }
9816 });
9817
9818 // After formatting the buffer, the trailing whitespace is stripped,
9819 // a newline is appended, and the edits provided by the language server
9820 // have been applied.
9821 format.await.unwrap();
9822 cx.assert_editor_state(
9823 &[
9824 "one", //
9825 "", //
9826 "twoˇ", //
9827 "", //
9828 "three", //
9829 "four", //
9830 "", //
9831 ]
9832 .join("\n"),
9833 );
9834
9835 // Undoing the formatting undoes the trailing whitespace removal, the
9836 // trailing newline, and the LSP edits.
9837 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9838 cx.assert_editor_state(
9839 &[
9840 "one ", //
9841 "twoˇ", //
9842 "three ", //
9843 "four", //
9844 ]
9845 .join("\n"),
9846 );
9847}
9848
9849#[gpui::test]
9850async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9851 cx: &mut TestAppContext,
9852) {
9853 init_test(cx, |_| {});
9854
9855 cx.update(|cx| {
9856 cx.update_global::<SettingsStore, _>(|settings, cx| {
9857 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9858 settings.auto_signature_help = Some(true);
9859 });
9860 });
9861 });
9862
9863 let mut cx = EditorLspTestContext::new_rust(
9864 lsp::ServerCapabilities {
9865 signature_help_provider: Some(lsp::SignatureHelpOptions {
9866 ..Default::default()
9867 }),
9868 ..Default::default()
9869 },
9870 cx,
9871 )
9872 .await;
9873
9874 let language = Language::new(
9875 LanguageConfig {
9876 name: "Rust".into(),
9877 brackets: BracketPairConfig {
9878 pairs: vec![
9879 BracketPair {
9880 start: "{".to_string(),
9881 end: "}".to_string(),
9882 close: true,
9883 surround: true,
9884 newline: true,
9885 },
9886 BracketPair {
9887 start: "(".to_string(),
9888 end: ")".to_string(),
9889 close: true,
9890 surround: true,
9891 newline: true,
9892 },
9893 BracketPair {
9894 start: "/*".to_string(),
9895 end: " */".to_string(),
9896 close: true,
9897 surround: true,
9898 newline: true,
9899 },
9900 BracketPair {
9901 start: "[".to_string(),
9902 end: "]".to_string(),
9903 close: false,
9904 surround: false,
9905 newline: true,
9906 },
9907 BracketPair {
9908 start: "\"".to_string(),
9909 end: "\"".to_string(),
9910 close: true,
9911 surround: true,
9912 newline: false,
9913 },
9914 BracketPair {
9915 start: "<".to_string(),
9916 end: ">".to_string(),
9917 close: false,
9918 surround: true,
9919 newline: true,
9920 },
9921 ],
9922 ..Default::default()
9923 },
9924 autoclose_before: "})]".to_string(),
9925 ..Default::default()
9926 },
9927 Some(tree_sitter_rust::LANGUAGE.into()),
9928 );
9929 let language = Arc::new(language);
9930
9931 cx.language_registry().add(language.clone());
9932 cx.update_buffer(|buffer, cx| {
9933 buffer.set_language(Some(language), cx);
9934 });
9935
9936 cx.set_state(
9937 &r#"
9938 fn main() {
9939 sampleˇ
9940 }
9941 "#
9942 .unindent(),
9943 );
9944
9945 cx.update_editor(|editor, window, cx| {
9946 editor.handle_input("(", window, cx);
9947 });
9948 cx.assert_editor_state(
9949 &"
9950 fn main() {
9951 sample(ˇ)
9952 }
9953 "
9954 .unindent(),
9955 );
9956
9957 let mocked_response = lsp::SignatureHelp {
9958 signatures: vec![lsp::SignatureInformation {
9959 label: "fn sample(param1: u8, param2: u8)".to_string(),
9960 documentation: None,
9961 parameters: Some(vec![
9962 lsp::ParameterInformation {
9963 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9964 documentation: None,
9965 },
9966 lsp::ParameterInformation {
9967 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9968 documentation: None,
9969 },
9970 ]),
9971 active_parameter: None,
9972 }],
9973 active_signature: Some(0),
9974 active_parameter: Some(0),
9975 };
9976 handle_signature_help_request(&mut cx, mocked_response).await;
9977
9978 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9979 .await;
9980
9981 cx.editor(|editor, _, _| {
9982 let signature_help_state = editor.signature_help_state.popover().cloned();
9983 assert_eq!(
9984 signature_help_state.unwrap().label,
9985 "param1: u8, param2: u8"
9986 );
9987 });
9988}
9989
9990#[gpui::test]
9991async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9992 init_test(cx, |_| {});
9993
9994 cx.update(|cx| {
9995 cx.update_global::<SettingsStore, _>(|settings, cx| {
9996 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9997 settings.auto_signature_help = Some(false);
9998 settings.show_signature_help_after_edits = Some(false);
9999 });
10000 });
10001 });
10002
10003 let mut cx = EditorLspTestContext::new_rust(
10004 lsp::ServerCapabilities {
10005 signature_help_provider: Some(lsp::SignatureHelpOptions {
10006 ..Default::default()
10007 }),
10008 ..Default::default()
10009 },
10010 cx,
10011 )
10012 .await;
10013
10014 let language = Language::new(
10015 LanguageConfig {
10016 name: "Rust".into(),
10017 brackets: BracketPairConfig {
10018 pairs: vec![
10019 BracketPair {
10020 start: "{".to_string(),
10021 end: "}".to_string(),
10022 close: true,
10023 surround: true,
10024 newline: true,
10025 },
10026 BracketPair {
10027 start: "(".to_string(),
10028 end: ")".to_string(),
10029 close: true,
10030 surround: true,
10031 newline: true,
10032 },
10033 BracketPair {
10034 start: "/*".to_string(),
10035 end: " */".to_string(),
10036 close: true,
10037 surround: true,
10038 newline: true,
10039 },
10040 BracketPair {
10041 start: "[".to_string(),
10042 end: "]".to_string(),
10043 close: false,
10044 surround: false,
10045 newline: true,
10046 },
10047 BracketPair {
10048 start: "\"".to_string(),
10049 end: "\"".to_string(),
10050 close: true,
10051 surround: true,
10052 newline: false,
10053 },
10054 BracketPair {
10055 start: "<".to_string(),
10056 end: ">".to_string(),
10057 close: false,
10058 surround: true,
10059 newline: true,
10060 },
10061 ],
10062 ..Default::default()
10063 },
10064 autoclose_before: "})]".to_string(),
10065 ..Default::default()
10066 },
10067 Some(tree_sitter_rust::LANGUAGE.into()),
10068 );
10069 let language = Arc::new(language);
10070
10071 cx.language_registry().add(language.clone());
10072 cx.update_buffer(|buffer, cx| {
10073 buffer.set_language(Some(language), cx);
10074 });
10075
10076 // Ensure that signature_help is not called when no signature help is enabled.
10077 cx.set_state(
10078 &r#"
10079 fn main() {
10080 sampleˇ
10081 }
10082 "#
10083 .unindent(),
10084 );
10085 cx.update_editor(|editor, window, cx| {
10086 editor.handle_input("(", window, cx);
10087 });
10088 cx.assert_editor_state(
10089 &"
10090 fn main() {
10091 sample(ˇ)
10092 }
10093 "
10094 .unindent(),
10095 );
10096 cx.editor(|editor, _, _| {
10097 assert!(editor.signature_help_state.task().is_none());
10098 });
10099
10100 let mocked_response = lsp::SignatureHelp {
10101 signatures: vec![lsp::SignatureInformation {
10102 label: "fn sample(param1: u8, param2: u8)".to_string(),
10103 documentation: None,
10104 parameters: Some(vec![
10105 lsp::ParameterInformation {
10106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10107 documentation: None,
10108 },
10109 lsp::ParameterInformation {
10110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10111 documentation: None,
10112 },
10113 ]),
10114 active_parameter: None,
10115 }],
10116 active_signature: Some(0),
10117 active_parameter: Some(0),
10118 };
10119
10120 // Ensure that signature_help is called when enabled afte edits
10121 cx.update(|_, cx| {
10122 cx.update_global::<SettingsStore, _>(|settings, cx| {
10123 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10124 settings.auto_signature_help = Some(false);
10125 settings.show_signature_help_after_edits = Some(true);
10126 });
10127 });
10128 });
10129 cx.set_state(
10130 &r#"
10131 fn main() {
10132 sampleˇ
10133 }
10134 "#
10135 .unindent(),
10136 );
10137 cx.update_editor(|editor, window, cx| {
10138 editor.handle_input("(", window, cx);
10139 });
10140 cx.assert_editor_state(
10141 &"
10142 fn main() {
10143 sample(ˇ)
10144 }
10145 "
10146 .unindent(),
10147 );
10148 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10149 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10150 .await;
10151 cx.update_editor(|editor, _, _| {
10152 let signature_help_state = editor.signature_help_state.popover().cloned();
10153 assert!(signature_help_state.is_some());
10154 assert_eq!(
10155 signature_help_state.unwrap().label,
10156 "param1: u8, param2: u8"
10157 );
10158 editor.signature_help_state = SignatureHelpState::default();
10159 });
10160
10161 // Ensure that signature_help is called when auto signature help override is enabled
10162 cx.update(|_, cx| {
10163 cx.update_global::<SettingsStore, _>(|settings, cx| {
10164 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10165 settings.auto_signature_help = Some(true);
10166 settings.show_signature_help_after_edits = Some(false);
10167 });
10168 });
10169 });
10170 cx.set_state(
10171 &r#"
10172 fn main() {
10173 sampleˇ
10174 }
10175 "#
10176 .unindent(),
10177 );
10178 cx.update_editor(|editor, window, cx| {
10179 editor.handle_input("(", window, cx);
10180 });
10181 cx.assert_editor_state(
10182 &"
10183 fn main() {
10184 sample(ˇ)
10185 }
10186 "
10187 .unindent(),
10188 );
10189 handle_signature_help_request(&mut cx, mocked_response).await;
10190 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10191 .await;
10192 cx.editor(|editor, _, _| {
10193 let signature_help_state = editor.signature_help_state.popover().cloned();
10194 assert!(signature_help_state.is_some());
10195 assert_eq!(
10196 signature_help_state.unwrap().label,
10197 "param1: u8, param2: u8"
10198 );
10199 });
10200}
10201
10202#[gpui::test]
10203async fn test_signature_help(cx: &mut TestAppContext) {
10204 init_test(cx, |_| {});
10205 cx.update(|cx| {
10206 cx.update_global::<SettingsStore, _>(|settings, cx| {
10207 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10208 settings.auto_signature_help = Some(true);
10209 });
10210 });
10211 });
10212
10213 let mut cx = EditorLspTestContext::new_rust(
10214 lsp::ServerCapabilities {
10215 signature_help_provider: Some(lsp::SignatureHelpOptions {
10216 ..Default::default()
10217 }),
10218 ..Default::default()
10219 },
10220 cx,
10221 )
10222 .await;
10223
10224 // A test that directly calls `show_signature_help`
10225 cx.update_editor(|editor, window, cx| {
10226 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10227 });
10228
10229 let mocked_response = lsp::SignatureHelp {
10230 signatures: vec![lsp::SignatureInformation {
10231 label: "fn sample(param1: u8, param2: u8)".to_string(),
10232 documentation: None,
10233 parameters: Some(vec![
10234 lsp::ParameterInformation {
10235 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10236 documentation: None,
10237 },
10238 lsp::ParameterInformation {
10239 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10240 documentation: None,
10241 },
10242 ]),
10243 active_parameter: None,
10244 }],
10245 active_signature: Some(0),
10246 active_parameter: Some(0),
10247 };
10248 handle_signature_help_request(&mut cx, mocked_response).await;
10249
10250 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10251 .await;
10252
10253 cx.editor(|editor, _, _| {
10254 let signature_help_state = editor.signature_help_state.popover().cloned();
10255 assert!(signature_help_state.is_some());
10256 assert_eq!(
10257 signature_help_state.unwrap().label,
10258 "param1: u8, param2: u8"
10259 );
10260 });
10261
10262 // When exiting outside from inside the brackets, `signature_help` is closed.
10263 cx.set_state(indoc! {"
10264 fn main() {
10265 sample(ˇ);
10266 }
10267
10268 fn sample(param1: u8, param2: u8) {}
10269 "});
10270
10271 cx.update_editor(|editor, window, cx| {
10272 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10273 });
10274
10275 let mocked_response = lsp::SignatureHelp {
10276 signatures: Vec::new(),
10277 active_signature: None,
10278 active_parameter: None,
10279 };
10280 handle_signature_help_request(&mut cx, mocked_response).await;
10281
10282 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10283 .await;
10284
10285 cx.editor(|editor, _, _| {
10286 assert!(!editor.signature_help_state.is_shown());
10287 });
10288
10289 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10290 cx.set_state(indoc! {"
10291 fn main() {
10292 sample(ˇ);
10293 }
10294
10295 fn sample(param1: u8, param2: u8) {}
10296 "});
10297
10298 let mocked_response = lsp::SignatureHelp {
10299 signatures: vec![lsp::SignatureInformation {
10300 label: "fn sample(param1: u8, param2: u8)".to_string(),
10301 documentation: None,
10302 parameters: Some(vec![
10303 lsp::ParameterInformation {
10304 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10305 documentation: None,
10306 },
10307 lsp::ParameterInformation {
10308 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10309 documentation: None,
10310 },
10311 ]),
10312 active_parameter: None,
10313 }],
10314 active_signature: Some(0),
10315 active_parameter: Some(0),
10316 };
10317 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10318 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10319 .await;
10320 cx.editor(|editor, _, _| {
10321 assert!(editor.signature_help_state.is_shown());
10322 });
10323
10324 // Restore the popover with more parameter input
10325 cx.set_state(indoc! {"
10326 fn main() {
10327 sample(param1, param2ˇ);
10328 }
10329
10330 fn sample(param1: u8, param2: u8) {}
10331 "});
10332
10333 let mocked_response = lsp::SignatureHelp {
10334 signatures: vec![lsp::SignatureInformation {
10335 label: "fn sample(param1: u8, param2: u8)".to_string(),
10336 documentation: None,
10337 parameters: Some(vec![
10338 lsp::ParameterInformation {
10339 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10340 documentation: None,
10341 },
10342 lsp::ParameterInformation {
10343 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10344 documentation: None,
10345 },
10346 ]),
10347 active_parameter: None,
10348 }],
10349 active_signature: Some(0),
10350 active_parameter: Some(1),
10351 };
10352 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10353 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10354 .await;
10355
10356 // When selecting a range, the popover is gone.
10357 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10358 cx.update_editor(|editor, window, cx| {
10359 editor.change_selections(None, window, cx, |s| {
10360 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10361 })
10362 });
10363 cx.assert_editor_state(indoc! {"
10364 fn main() {
10365 sample(param1, «ˇparam2»);
10366 }
10367
10368 fn sample(param1: u8, param2: u8) {}
10369 "});
10370 cx.editor(|editor, _, _| {
10371 assert!(!editor.signature_help_state.is_shown());
10372 });
10373
10374 // When unselecting again, the popover is back if within the brackets.
10375 cx.update_editor(|editor, window, cx| {
10376 editor.change_selections(None, window, cx, |s| {
10377 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10378 })
10379 });
10380 cx.assert_editor_state(indoc! {"
10381 fn main() {
10382 sample(param1, ˇparam2);
10383 }
10384
10385 fn sample(param1: u8, param2: u8) {}
10386 "});
10387 handle_signature_help_request(&mut cx, mocked_response).await;
10388 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10389 .await;
10390 cx.editor(|editor, _, _| {
10391 assert!(editor.signature_help_state.is_shown());
10392 });
10393
10394 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10395 cx.update_editor(|editor, window, cx| {
10396 editor.change_selections(None, window, cx, |s| {
10397 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10398 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10399 })
10400 });
10401 cx.assert_editor_state(indoc! {"
10402 fn main() {
10403 sample(param1, ˇparam2);
10404 }
10405
10406 fn sample(param1: u8, param2: u8) {}
10407 "});
10408
10409 let mocked_response = lsp::SignatureHelp {
10410 signatures: vec![lsp::SignatureInformation {
10411 label: "fn sample(param1: u8, param2: u8)".to_string(),
10412 documentation: None,
10413 parameters: Some(vec![
10414 lsp::ParameterInformation {
10415 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10416 documentation: None,
10417 },
10418 lsp::ParameterInformation {
10419 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10420 documentation: None,
10421 },
10422 ]),
10423 active_parameter: None,
10424 }],
10425 active_signature: Some(0),
10426 active_parameter: Some(1),
10427 };
10428 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10429 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10430 .await;
10431 cx.update_editor(|editor, _, cx| {
10432 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10433 });
10434 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10435 .await;
10436 cx.update_editor(|editor, window, cx| {
10437 editor.change_selections(None, window, cx, |s| {
10438 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10439 })
10440 });
10441 cx.assert_editor_state(indoc! {"
10442 fn main() {
10443 sample(param1, «ˇparam2»);
10444 }
10445
10446 fn sample(param1: u8, param2: u8) {}
10447 "});
10448 cx.update_editor(|editor, window, cx| {
10449 editor.change_selections(None, window, cx, |s| {
10450 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10451 })
10452 });
10453 cx.assert_editor_state(indoc! {"
10454 fn main() {
10455 sample(param1, ˇparam2);
10456 }
10457
10458 fn sample(param1: u8, param2: u8) {}
10459 "});
10460 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10461 .await;
10462}
10463
10464#[gpui::test]
10465async fn test_completion_mode(cx: &mut TestAppContext) {
10466 init_test(cx, |_| {});
10467 let mut cx = EditorLspTestContext::new_rust(
10468 lsp::ServerCapabilities {
10469 completion_provider: Some(lsp::CompletionOptions {
10470 resolve_provider: Some(true),
10471 ..Default::default()
10472 }),
10473 ..Default::default()
10474 },
10475 cx,
10476 )
10477 .await;
10478
10479 struct Run {
10480 run_description: &'static str,
10481 initial_state: String,
10482 buffer_marked_text: String,
10483 completion_label: &'static str,
10484 completion_text: &'static str,
10485 expected_with_insert_mode: String,
10486 expected_with_replace_mode: String,
10487 expected_with_replace_subsequence_mode: String,
10488 expected_with_replace_suffix_mode: String,
10489 }
10490
10491 let runs = [
10492 Run {
10493 run_description: "Start of word matches completion text",
10494 initial_state: "before ediˇ after".into(),
10495 buffer_marked_text: "before <edi|> after".into(),
10496 completion_label: "editor",
10497 completion_text: "editor",
10498 expected_with_insert_mode: "before editorˇ after".into(),
10499 expected_with_replace_mode: "before editorˇ after".into(),
10500 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10501 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10502 },
10503 Run {
10504 run_description: "Accept same text at the middle of the word",
10505 initial_state: "before ediˇtor after".into(),
10506 buffer_marked_text: "before <edi|tor> after".into(),
10507 completion_label: "editor",
10508 completion_text: "editor",
10509 expected_with_insert_mode: "before editorˇtor after".into(),
10510 expected_with_replace_mode: "before editorˇ after".into(),
10511 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10512 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10513 },
10514 Run {
10515 run_description: "End of word matches completion text -- cursor at end",
10516 initial_state: "before torˇ after".into(),
10517 buffer_marked_text: "before <tor|> after".into(),
10518 completion_label: "editor",
10519 completion_text: "editor",
10520 expected_with_insert_mode: "before editorˇ after".into(),
10521 expected_with_replace_mode: "before editorˇ after".into(),
10522 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10523 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10524 },
10525 Run {
10526 run_description: "End of word matches completion text -- cursor at start",
10527 initial_state: "before ˇtor after".into(),
10528 buffer_marked_text: "before <|tor> after".into(),
10529 completion_label: "editor",
10530 completion_text: "editor",
10531 expected_with_insert_mode: "before editorˇtor after".into(),
10532 expected_with_replace_mode: "before editorˇ after".into(),
10533 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10534 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10535 },
10536 Run {
10537 run_description: "Prepend text containing whitespace",
10538 initial_state: "pˇfield: bool".into(),
10539 buffer_marked_text: "<p|field>: bool".into(),
10540 completion_label: "pub ",
10541 completion_text: "pub ",
10542 expected_with_insert_mode: "pub ˇfield: bool".into(),
10543 expected_with_replace_mode: "pub ˇ: bool".into(),
10544 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10545 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10546 },
10547 Run {
10548 run_description: "Add element to start of list",
10549 initial_state: "[element_ˇelement_2]".into(),
10550 buffer_marked_text: "[<element_|element_2>]".into(),
10551 completion_label: "element_1",
10552 completion_text: "element_1",
10553 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10554 expected_with_replace_mode: "[element_1ˇ]".into(),
10555 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10556 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10557 },
10558 Run {
10559 run_description: "Add element to start of list -- first and second elements are equal",
10560 initial_state: "[elˇelement]".into(),
10561 buffer_marked_text: "[<el|element>]".into(),
10562 completion_label: "element",
10563 completion_text: "element",
10564 expected_with_insert_mode: "[elementˇelement]".into(),
10565 expected_with_replace_mode: "[elementˇ]".into(),
10566 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10567 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10568 },
10569 Run {
10570 run_description: "Ends with matching suffix",
10571 initial_state: "SubˇError".into(),
10572 buffer_marked_text: "<Sub|Error>".into(),
10573 completion_label: "SubscriptionError",
10574 completion_text: "SubscriptionError",
10575 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10576 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10577 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10578 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10579 },
10580 Run {
10581 run_description: "Suffix is a subsequence -- contiguous",
10582 initial_state: "SubˇErr".into(),
10583 buffer_marked_text: "<Sub|Err>".into(),
10584 completion_label: "SubscriptionError",
10585 completion_text: "SubscriptionError",
10586 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10587 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10588 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10589 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10590 },
10591 Run {
10592 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10593 initial_state: "Suˇscrirr".into(),
10594 buffer_marked_text: "<Su|scrirr>".into(),
10595 completion_label: "SubscriptionError",
10596 completion_text: "SubscriptionError",
10597 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10598 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10599 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10600 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10601 },
10602 Run {
10603 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10604 initial_state: "foo(indˇix)".into(),
10605 buffer_marked_text: "foo(<ind|ix>)".into(),
10606 completion_label: "node_index",
10607 completion_text: "node_index",
10608 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10609 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10610 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10611 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10612 },
10613 Run {
10614 run_description: "Replace range ends before cursor - should extend to cursor",
10615 initial_state: "before editˇo after".into(),
10616 buffer_marked_text: "before <{ed}>it|o after".into(),
10617 completion_label: "editor",
10618 completion_text: "editor",
10619 expected_with_insert_mode: "before editorˇo after".into(),
10620 expected_with_replace_mode: "before editorˇo after".into(),
10621 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10622 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10623 },
10624 Run {
10625 run_description: "Uses label for suffix matching",
10626 initial_state: "before ediˇtor after".into(),
10627 buffer_marked_text: "before <edi|tor> after".into(),
10628 completion_label: "editor",
10629 completion_text: "editor()",
10630 expected_with_insert_mode: "before editor()ˇtor after".into(),
10631 expected_with_replace_mode: "before editor()ˇ after".into(),
10632 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
10633 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
10634 },
10635 Run {
10636 run_description: "Case insensitive subsequence and suffix matching",
10637 initial_state: "before EDiˇtoR after".into(),
10638 buffer_marked_text: "before <EDi|toR> after".into(),
10639 completion_label: "editor",
10640 completion_text: "editor",
10641 expected_with_insert_mode: "before editorˇtoR after".into(),
10642 expected_with_replace_mode: "before editorˇ after".into(),
10643 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10644 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10645 },
10646 ];
10647
10648 for run in runs {
10649 let run_variations = [
10650 (LspInsertMode::Insert, run.expected_with_insert_mode),
10651 (LspInsertMode::Replace, run.expected_with_replace_mode),
10652 (
10653 LspInsertMode::ReplaceSubsequence,
10654 run.expected_with_replace_subsequence_mode,
10655 ),
10656 (
10657 LspInsertMode::ReplaceSuffix,
10658 run.expected_with_replace_suffix_mode,
10659 ),
10660 ];
10661
10662 for (lsp_insert_mode, expected_text) in run_variations {
10663 eprintln!(
10664 "run = {:?}, mode = {lsp_insert_mode:.?}",
10665 run.run_description,
10666 );
10667
10668 update_test_language_settings(&mut cx, |settings| {
10669 settings.defaults.completions = Some(CompletionSettings {
10670 lsp_insert_mode,
10671 words: WordsCompletionMode::Disabled,
10672 lsp: true,
10673 lsp_fetch_timeout_ms: 0,
10674 });
10675 });
10676
10677 cx.set_state(&run.initial_state);
10678 cx.update_editor(|editor, window, cx| {
10679 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10680 });
10681
10682 let counter = Arc::new(AtomicUsize::new(0));
10683 handle_completion_request_with_insert_and_replace(
10684 &mut cx,
10685 &run.buffer_marked_text,
10686 vec![(run.completion_label, run.completion_text)],
10687 counter.clone(),
10688 )
10689 .await;
10690 cx.condition(|editor, _| editor.context_menu_visible())
10691 .await;
10692 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10693
10694 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10695 editor
10696 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10697 .unwrap()
10698 });
10699 cx.assert_editor_state(&expected_text);
10700 handle_resolve_completion_request(&mut cx, None).await;
10701 apply_additional_edits.await.unwrap();
10702 }
10703 }
10704}
10705
10706#[gpui::test]
10707async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10708 init_test(cx, |_| {});
10709 let mut cx = EditorLspTestContext::new_rust(
10710 lsp::ServerCapabilities {
10711 completion_provider: Some(lsp::CompletionOptions {
10712 resolve_provider: Some(true),
10713 ..Default::default()
10714 }),
10715 ..Default::default()
10716 },
10717 cx,
10718 )
10719 .await;
10720
10721 let initial_state = "SubˇError";
10722 let buffer_marked_text = "<Sub|Error>";
10723 let completion_text = "SubscriptionError";
10724 let expected_with_insert_mode = "SubscriptionErrorˇError";
10725 let expected_with_replace_mode = "SubscriptionErrorˇ";
10726
10727 update_test_language_settings(&mut cx, |settings| {
10728 settings.defaults.completions = Some(CompletionSettings {
10729 words: WordsCompletionMode::Disabled,
10730 // set the opposite here to ensure that the action is overriding the default behavior
10731 lsp_insert_mode: LspInsertMode::Insert,
10732 lsp: true,
10733 lsp_fetch_timeout_ms: 0,
10734 });
10735 });
10736
10737 cx.set_state(initial_state);
10738 cx.update_editor(|editor, window, cx| {
10739 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10740 });
10741
10742 let counter = Arc::new(AtomicUsize::new(0));
10743 handle_completion_request_with_insert_and_replace(
10744 &mut cx,
10745 &buffer_marked_text,
10746 vec![(completion_text, completion_text)],
10747 counter.clone(),
10748 )
10749 .await;
10750 cx.condition(|editor, _| editor.context_menu_visible())
10751 .await;
10752 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10753
10754 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10755 editor
10756 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10757 .unwrap()
10758 });
10759 cx.assert_editor_state(&expected_with_replace_mode);
10760 handle_resolve_completion_request(&mut cx, None).await;
10761 apply_additional_edits.await.unwrap();
10762
10763 update_test_language_settings(&mut cx, |settings| {
10764 settings.defaults.completions = Some(CompletionSettings {
10765 words: WordsCompletionMode::Disabled,
10766 // set the opposite here to ensure that the action is overriding the default behavior
10767 lsp_insert_mode: LspInsertMode::Replace,
10768 lsp: true,
10769 lsp_fetch_timeout_ms: 0,
10770 });
10771 });
10772
10773 cx.set_state(initial_state);
10774 cx.update_editor(|editor, window, cx| {
10775 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10776 });
10777 handle_completion_request_with_insert_and_replace(
10778 &mut cx,
10779 &buffer_marked_text,
10780 vec![(completion_text, completion_text)],
10781 counter.clone(),
10782 )
10783 .await;
10784 cx.condition(|editor, _| editor.context_menu_visible())
10785 .await;
10786 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10787
10788 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10789 editor
10790 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10791 .unwrap()
10792 });
10793 cx.assert_editor_state(&expected_with_insert_mode);
10794 handle_resolve_completion_request(&mut cx, None).await;
10795 apply_additional_edits.await.unwrap();
10796}
10797
10798#[gpui::test]
10799async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10800 init_test(cx, |_| {});
10801 let mut cx = EditorLspTestContext::new_rust(
10802 lsp::ServerCapabilities {
10803 completion_provider: Some(lsp::CompletionOptions {
10804 resolve_provider: Some(true),
10805 ..Default::default()
10806 }),
10807 ..Default::default()
10808 },
10809 cx,
10810 )
10811 .await;
10812
10813 // scenario: surrounding text matches completion text
10814 let completion_text = "to_offset";
10815 let initial_state = indoc! {"
10816 1. buf.to_offˇsuffix
10817 2. buf.to_offˇsuf
10818 3. buf.to_offˇfix
10819 4. buf.to_offˇ
10820 5. into_offˇensive
10821 6. ˇsuffix
10822 7. let ˇ //
10823 8. aaˇzz
10824 9. buf.to_off«zzzzzˇ»suffix
10825 10. buf.«ˇzzzzz»suffix
10826 11. to_off«ˇzzzzz»
10827
10828 buf.to_offˇsuffix // newest cursor
10829 "};
10830 let completion_marked_buffer = indoc! {"
10831 1. buf.to_offsuffix
10832 2. buf.to_offsuf
10833 3. buf.to_offfix
10834 4. buf.to_off
10835 5. into_offensive
10836 6. suffix
10837 7. let //
10838 8. aazz
10839 9. buf.to_offzzzzzsuffix
10840 10. buf.zzzzzsuffix
10841 11. to_offzzzzz
10842
10843 buf.<to_off|suffix> // newest cursor
10844 "};
10845 let expected = indoc! {"
10846 1. buf.to_offsetˇ
10847 2. buf.to_offsetˇsuf
10848 3. buf.to_offsetˇfix
10849 4. buf.to_offsetˇ
10850 5. into_offsetˇensive
10851 6. to_offsetˇsuffix
10852 7. let to_offsetˇ //
10853 8. aato_offsetˇzz
10854 9. buf.to_offsetˇ
10855 10. buf.to_offsetˇsuffix
10856 11. to_offsetˇ
10857
10858 buf.to_offsetˇ // newest cursor
10859 "};
10860 cx.set_state(initial_state);
10861 cx.update_editor(|editor, window, cx| {
10862 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10863 });
10864 handle_completion_request_with_insert_and_replace(
10865 &mut cx,
10866 completion_marked_buffer,
10867 vec![(completion_text, completion_text)],
10868 Arc::new(AtomicUsize::new(0)),
10869 )
10870 .await;
10871 cx.condition(|editor, _| editor.context_menu_visible())
10872 .await;
10873 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10874 editor
10875 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10876 .unwrap()
10877 });
10878 cx.assert_editor_state(expected);
10879 handle_resolve_completion_request(&mut cx, None).await;
10880 apply_additional_edits.await.unwrap();
10881
10882 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10883 let completion_text = "foo_and_bar";
10884 let initial_state = indoc! {"
10885 1. ooanbˇ
10886 2. zooanbˇ
10887 3. ooanbˇz
10888 4. zooanbˇz
10889 5. ooanˇ
10890 6. oanbˇ
10891
10892 ooanbˇ
10893 "};
10894 let completion_marked_buffer = indoc! {"
10895 1. ooanb
10896 2. zooanb
10897 3. ooanbz
10898 4. zooanbz
10899 5. ooan
10900 6. oanb
10901
10902 <ooanb|>
10903 "};
10904 let expected = indoc! {"
10905 1. foo_and_barˇ
10906 2. zfoo_and_barˇ
10907 3. foo_and_barˇz
10908 4. zfoo_and_barˇz
10909 5. ooanfoo_and_barˇ
10910 6. oanbfoo_and_barˇ
10911
10912 foo_and_barˇ
10913 "};
10914 cx.set_state(initial_state);
10915 cx.update_editor(|editor, window, cx| {
10916 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10917 });
10918 handle_completion_request_with_insert_and_replace(
10919 &mut cx,
10920 completion_marked_buffer,
10921 vec![(completion_text, completion_text)],
10922 Arc::new(AtomicUsize::new(0)),
10923 )
10924 .await;
10925 cx.condition(|editor, _| editor.context_menu_visible())
10926 .await;
10927 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10928 editor
10929 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10930 .unwrap()
10931 });
10932 cx.assert_editor_state(expected);
10933 handle_resolve_completion_request(&mut cx, None).await;
10934 apply_additional_edits.await.unwrap();
10935
10936 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10937 // (expects the same as if it was inserted at the end)
10938 let completion_text = "foo_and_bar";
10939 let initial_state = indoc! {"
10940 1. ooˇanb
10941 2. zooˇanb
10942 3. ooˇanbz
10943 4. zooˇanbz
10944
10945 ooˇanb
10946 "};
10947 let completion_marked_buffer = indoc! {"
10948 1. ooanb
10949 2. zooanb
10950 3. ooanbz
10951 4. zooanbz
10952
10953 <oo|anb>
10954 "};
10955 let expected = indoc! {"
10956 1. foo_and_barˇ
10957 2. zfoo_and_barˇ
10958 3. foo_and_barˇz
10959 4. zfoo_and_barˇz
10960
10961 foo_and_barˇ
10962 "};
10963 cx.set_state(initial_state);
10964 cx.update_editor(|editor, window, cx| {
10965 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10966 });
10967 handle_completion_request_with_insert_and_replace(
10968 &mut cx,
10969 completion_marked_buffer,
10970 vec![(completion_text, completion_text)],
10971 Arc::new(AtomicUsize::new(0)),
10972 )
10973 .await;
10974 cx.condition(|editor, _| editor.context_menu_visible())
10975 .await;
10976 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10977 editor
10978 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10979 .unwrap()
10980 });
10981 cx.assert_editor_state(expected);
10982 handle_resolve_completion_request(&mut cx, None).await;
10983 apply_additional_edits.await.unwrap();
10984}
10985
10986// This used to crash
10987#[gpui::test]
10988async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10989 init_test(cx, |_| {});
10990
10991 let buffer_text = indoc! {"
10992 fn main() {
10993 10.satu;
10994
10995 //
10996 // separate cursors so they open in different excerpts (manually reproducible)
10997 //
10998
10999 10.satu20;
11000 }
11001 "};
11002 let multibuffer_text_with_selections = indoc! {"
11003 fn main() {
11004 10.satuˇ;
11005
11006 //
11007
11008 //
11009
11010 10.satuˇ20;
11011 }
11012 "};
11013 let expected_multibuffer = indoc! {"
11014 fn main() {
11015 10.saturating_sub()ˇ;
11016
11017 //
11018
11019 //
11020
11021 10.saturating_sub()ˇ;
11022 }
11023 "};
11024
11025 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11026 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11027
11028 let fs = FakeFs::new(cx.executor());
11029 fs.insert_tree(
11030 path!("/a"),
11031 json!({
11032 "main.rs": buffer_text,
11033 }),
11034 )
11035 .await;
11036
11037 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11038 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11039 language_registry.add(rust_lang());
11040 let mut fake_servers = language_registry.register_fake_lsp(
11041 "Rust",
11042 FakeLspAdapter {
11043 capabilities: lsp::ServerCapabilities {
11044 completion_provider: Some(lsp::CompletionOptions {
11045 resolve_provider: None,
11046 ..lsp::CompletionOptions::default()
11047 }),
11048 ..lsp::ServerCapabilities::default()
11049 },
11050 ..FakeLspAdapter::default()
11051 },
11052 );
11053 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11054 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11055 let buffer = project
11056 .update(cx, |project, cx| {
11057 project.open_local_buffer(path!("/a/main.rs"), cx)
11058 })
11059 .await
11060 .unwrap();
11061
11062 let multi_buffer = cx.new(|cx| {
11063 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11064 multi_buffer.push_excerpts(
11065 buffer.clone(),
11066 [ExcerptRange::new(0..first_excerpt_end)],
11067 cx,
11068 );
11069 multi_buffer.push_excerpts(
11070 buffer.clone(),
11071 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11072 cx,
11073 );
11074 multi_buffer
11075 });
11076
11077 let editor = workspace
11078 .update(cx, |_, window, cx| {
11079 cx.new(|cx| {
11080 Editor::new(
11081 EditorMode::Full {
11082 scale_ui_elements_with_buffer_font_size: false,
11083 show_active_line_background: false,
11084 sized_by_content: false,
11085 },
11086 multi_buffer.clone(),
11087 Some(project.clone()),
11088 window,
11089 cx,
11090 )
11091 })
11092 })
11093 .unwrap();
11094
11095 let pane = workspace
11096 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11097 .unwrap();
11098 pane.update_in(cx, |pane, window, cx| {
11099 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11100 });
11101
11102 let fake_server = fake_servers.next().await.unwrap();
11103
11104 editor.update_in(cx, |editor, window, cx| {
11105 editor.change_selections(None, window, cx, |s| {
11106 s.select_ranges([
11107 Point::new(1, 11)..Point::new(1, 11),
11108 Point::new(7, 11)..Point::new(7, 11),
11109 ])
11110 });
11111
11112 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11113 });
11114
11115 editor.update_in(cx, |editor, window, cx| {
11116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11117 });
11118
11119 fake_server
11120 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11121 let completion_item = lsp::CompletionItem {
11122 label: "saturating_sub()".into(),
11123 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11124 lsp::InsertReplaceEdit {
11125 new_text: "saturating_sub()".to_owned(),
11126 insert: lsp::Range::new(
11127 lsp::Position::new(7, 7),
11128 lsp::Position::new(7, 11),
11129 ),
11130 replace: lsp::Range::new(
11131 lsp::Position::new(7, 7),
11132 lsp::Position::new(7, 13),
11133 ),
11134 },
11135 )),
11136 ..lsp::CompletionItem::default()
11137 };
11138
11139 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11140 })
11141 .next()
11142 .await
11143 .unwrap();
11144
11145 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11146 .await;
11147
11148 editor
11149 .update_in(cx, |editor, window, cx| {
11150 editor
11151 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11152 .unwrap()
11153 })
11154 .await
11155 .unwrap();
11156
11157 editor.update(cx, |editor, cx| {
11158 assert_text_with_selections(editor, expected_multibuffer, cx);
11159 })
11160}
11161
11162#[gpui::test]
11163async fn test_completion(cx: &mut TestAppContext) {
11164 init_test(cx, |_| {});
11165
11166 let mut cx = EditorLspTestContext::new_rust(
11167 lsp::ServerCapabilities {
11168 completion_provider: Some(lsp::CompletionOptions {
11169 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11170 resolve_provider: Some(true),
11171 ..Default::default()
11172 }),
11173 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11174 ..Default::default()
11175 },
11176 cx,
11177 )
11178 .await;
11179 let counter = Arc::new(AtomicUsize::new(0));
11180
11181 cx.set_state(indoc! {"
11182 oneˇ
11183 two
11184 three
11185 "});
11186 cx.simulate_keystroke(".");
11187 handle_completion_request(
11188 indoc! {"
11189 one.|<>
11190 two
11191 three
11192 "},
11193 vec!["first_completion", "second_completion"],
11194 true,
11195 counter.clone(),
11196 &mut cx,
11197 )
11198 .await;
11199 cx.condition(|editor, _| editor.context_menu_visible())
11200 .await;
11201 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11202
11203 let _handler = handle_signature_help_request(
11204 &mut cx,
11205 lsp::SignatureHelp {
11206 signatures: vec![lsp::SignatureInformation {
11207 label: "test signature".to_string(),
11208 documentation: None,
11209 parameters: Some(vec![lsp::ParameterInformation {
11210 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11211 documentation: None,
11212 }]),
11213 active_parameter: None,
11214 }],
11215 active_signature: None,
11216 active_parameter: None,
11217 },
11218 );
11219 cx.update_editor(|editor, window, cx| {
11220 assert!(
11221 !editor.signature_help_state.is_shown(),
11222 "No signature help was called for"
11223 );
11224 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11225 });
11226 cx.run_until_parked();
11227 cx.update_editor(|editor, _, _| {
11228 assert!(
11229 !editor.signature_help_state.is_shown(),
11230 "No signature help should be shown when completions menu is open"
11231 );
11232 });
11233
11234 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11235 editor.context_menu_next(&Default::default(), window, cx);
11236 editor
11237 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11238 .unwrap()
11239 });
11240 cx.assert_editor_state(indoc! {"
11241 one.second_completionˇ
11242 two
11243 three
11244 "});
11245
11246 handle_resolve_completion_request(
11247 &mut cx,
11248 Some(vec![
11249 (
11250 //This overlaps with the primary completion edit which is
11251 //misbehavior from the LSP spec, test that we filter it out
11252 indoc! {"
11253 one.second_ˇcompletion
11254 two
11255 threeˇ
11256 "},
11257 "overlapping additional edit",
11258 ),
11259 (
11260 indoc! {"
11261 one.second_completion
11262 two
11263 threeˇ
11264 "},
11265 "\nadditional edit",
11266 ),
11267 ]),
11268 )
11269 .await;
11270 apply_additional_edits.await.unwrap();
11271 cx.assert_editor_state(indoc! {"
11272 one.second_completionˇ
11273 two
11274 three
11275 additional edit
11276 "});
11277
11278 cx.set_state(indoc! {"
11279 one.second_completion
11280 twoˇ
11281 threeˇ
11282 additional edit
11283 "});
11284 cx.simulate_keystroke(" ");
11285 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11286 cx.simulate_keystroke("s");
11287 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11288
11289 cx.assert_editor_state(indoc! {"
11290 one.second_completion
11291 two sˇ
11292 three sˇ
11293 additional edit
11294 "});
11295 handle_completion_request(
11296 indoc! {"
11297 one.second_completion
11298 two s
11299 three <s|>
11300 additional edit
11301 "},
11302 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11303 true,
11304 counter.clone(),
11305 &mut cx,
11306 )
11307 .await;
11308 cx.condition(|editor, _| editor.context_menu_visible())
11309 .await;
11310 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11311
11312 cx.simulate_keystroke("i");
11313
11314 handle_completion_request(
11315 indoc! {"
11316 one.second_completion
11317 two si
11318 three <si|>
11319 additional edit
11320 "},
11321 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11322 true,
11323 counter.clone(),
11324 &mut cx,
11325 )
11326 .await;
11327 cx.condition(|editor, _| editor.context_menu_visible())
11328 .await;
11329 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11330
11331 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11332 editor
11333 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11334 .unwrap()
11335 });
11336 cx.assert_editor_state(indoc! {"
11337 one.second_completion
11338 two sixth_completionˇ
11339 three sixth_completionˇ
11340 additional edit
11341 "});
11342
11343 apply_additional_edits.await.unwrap();
11344
11345 update_test_language_settings(&mut cx, |settings| {
11346 settings.defaults.show_completions_on_input = Some(false);
11347 });
11348 cx.set_state("editorˇ");
11349 cx.simulate_keystroke(".");
11350 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11351 cx.simulate_keystrokes("c l o");
11352 cx.assert_editor_state("editor.cloˇ");
11353 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11354 cx.update_editor(|editor, window, cx| {
11355 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11356 });
11357 handle_completion_request(
11358 "editor.<clo|>",
11359 vec!["close", "clobber"],
11360 true,
11361 counter.clone(),
11362 &mut cx,
11363 )
11364 .await;
11365 cx.condition(|editor, _| editor.context_menu_visible())
11366 .await;
11367 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11368
11369 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11370 editor
11371 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11372 .unwrap()
11373 });
11374 cx.assert_editor_state("editor.closeˇ");
11375 handle_resolve_completion_request(&mut cx, None).await;
11376 apply_additional_edits.await.unwrap();
11377}
11378
11379#[gpui::test]
11380async fn test_completion_reuse(cx: &mut TestAppContext) {
11381 init_test(cx, |_| {});
11382
11383 let mut cx = EditorLspTestContext::new_rust(
11384 lsp::ServerCapabilities {
11385 completion_provider: Some(lsp::CompletionOptions {
11386 trigger_characters: Some(vec![".".to_string()]),
11387 ..Default::default()
11388 }),
11389 ..Default::default()
11390 },
11391 cx,
11392 )
11393 .await;
11394
11395 let counter = Arc::new(AtomicUsize::new(0));
11396 cx.set_state("objˇ");
11397 cx.simulate_keystroke(".");
11398
11399 // Initial completion request returns complete results
11400 let is_incomplete = false;
11401 handle_completion_request(
11402 "obj.|<>",
11403 vec!["a", "ab", "abc"],
11404 is_incomplete,
11405 counter.clone(),
11406 &mut cx,
11407 )
11408 .await;
11409 cx.run_until_parked();
11410 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11411 cx.assert_editor_state("obj.ˇ");
11412 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11413
11414 // Type "a" - filters existing completions
11415 cx.simulate_keystroke("a");
11416 cx.run_until_parked();
11417 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11418 cx.assert_editor_state("obj.aˇ");
11419 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11420
11421 // Type "b" - filters existing completions
11422 cx.simulate_keystroke("b");
11423 cx.run_until_parked();
11424 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11425 cx.assert_editor_state("obj.abˇ");
11426 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11427
11428 // Type "c" - filters existing completions
11429 cx.simulate_keystroke("c");
11430 cx.run_until_parked();
11431 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11432 cx.assert_editor_state("obj.abcˇ");
11433 check_displayed_completions(vec!["abc"], &mut cx);
11434
11435 // Backspace to delete "c" - filters existing completions
11436 cx.update_editor(|editor, window, cx| {
11437 editor.backspace(&Backspace, window, cx);
11438 });
11439 cx.run_until_parked();
11440 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11441 cx.assert_editor_state("obj.abˇ");
11442 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11443
11444 // Moving cursor to the left dismisses menu.
11445 cx.update_editor(|editor, window, cx| {
11446 editor.move_left(&MoveLeft, window, cx);
11447 });
11448 cx.run_until_parked();
11449 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11450 cx.assert_editor_state("obj.aˇb");
11451 cx.update_editor(|editor, _, _| {
11452 assert_eq!(editor.context_menu_visible(), false);
11453 });
11454
11455 // Type "b" - new request
11456 cx.simulate_keystroke("b");
11457 let is_incomplete = false;
11458 handle_completion_request(
11459 "obj.<ab|>a",
11460 vec!["ab", "abc"],
11461 is_incomplete,
11462 counter.clone(),
11463 &mut cx,
11464 )
11465 .await;
11466 cx.run_until_parked();
11467 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11468 cx.assert_editor_state("obj.abˇb");
11469 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11470
11471 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11472 cx.update_editor(|editor, window, cx| {
11473 editor.backspace(&Backspace, window, cx);
11474 });
11475 let is_incomplete = false;
11476 handle_completion_request(
11477 "obj.<a|>b",
11478 vec!["a", "ab", "abc"],
11479 is_incomplete,
11480 counter.clone(),
11481 &mut cx,
11482 )
11483 .await;
11484 cx.run_until_parked();
11485 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11486 cx.assert_editor_state("obj.aˇb");
11487 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11488
11489 // Backspace to delete "a" - dismisses menu.
11490 cx.update_editor(|editor, window, cx| {
11491 editor.backspace(&Backspace, window, cx);
11492 });
11493 cx.run_until_parked();
11494 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11495 cx.assert_editor_state("obj.ˇb");
11496 cx.update_editor(|editor, _, _| {
11497 assert_eq!(editor.context_menu_visible(), false);
11498 });
11499}
11500
11501#[gpui::test]
11502async fn test_word_completion(cx: &mut TestAppContext) {
11503 let lsp_fetch_timeout_ms = 10;
11504 init_test(cx, |language_settings| {
11505 language_settings.defaults.completions = Some(CompletionSettings {
11506 words: WordsCompletionMode::Fallback,
11507 lsp: true,
11508 lsp_fetch_timeout_ms: 10,
11509 lsp_insert_mode: LspInsertMode::Insert,
11510 });
11511 });
11512
11513 let mut cx = EditorLspTestContext::new_rust(
11514 lsp::ServerCapabilities {
11515 completion_provider: Some(lsp::CompletionOptions {
11516 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11517 ..lsp::CompletionOptions::default()
11518 }),
11519 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11520 ..lsp::ServerCapabilities::default()
11521 },
11522 cx,
11523 )
11524 .await;
11525
11526 let throttle_completions = Arc::new(AtomicBool::new(false));
11527
11528 let lsp_throttle_completions = throttle_completions.clone();
11529 let _completion_requests_handler =
11530 cx.lsp
11531 .server
11532 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11533 let lsp_throttle_completions = lsp_throttle_completions.clone();
11534 let cx = cx.clone();
11535 async move {
11536 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11537 cx.background_executor()
11538 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11539 .await;
11540 }
11541 Ok(Some(lsp::CompletionResponse::Array(vec![
11542 lsp::CompletionItem {
11543 label: "first".into(),
11544 ..lsp::CompletionItem::default()
11545 },
11546 lsp::CompletionItem {
11547 label: "last".into(),
11548 ..lsp::CompletionItem::default()
11549 },
11550 ])))
11551 }
11552 });
11553
11554 cx.set_state(indoc! {"
11555 oneˇ
11556 two
11557 three
11558 "});
11559 cx.simulate_keystroke(".");
11560 cx.executor().run_until_parked();
11561 cx.condition(|editor, _| editor.context_menu_visible())
11562 .await;
11563 cx.update_editor(|editor, window, cx| {
11564 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11565 {
11566 assert_eq!(
11567 completion_menu_entries(&menu),
11568 &["first", "last"],
11569 "When LSP server is fast to reply, no fallback word completions are used"
11570 );
11571 } else {
11572 panic!("expected completion menu to be open");
11573 }
11574 editor.cancel(&Cancel, window, cx);
11575 });
11576 cx.executor().run_until_parked();
11577 cx.condition(|editor, _| !editor.context_menu_visible())
11578 .await;
11579
11580 throttle_completions.store(true, atomic::Ordering::Release);
11581 cx.simulate_keystroke(".");
11582 cx.executor()
11583 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11584 cx.executor().run_until_parked();
11585 cx.condition(|editor, _| editor.context_menu_visible())
11586 .await;
11587 cx.update_editor(|editor, _, _| {
11588 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11589 {
11590 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11591 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11592 } else {
11593 panic!("expected completion menu to be open");
11594 }
11595 });
11596}
11597
11598#[gpui::test]
11599async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11600 init_test(cx, |language_settings| {
11601 language_settings.defaults.completions = Some(CompletionSettings {
11602 words: WordsCompletionMode::Enabled,
11603 lsp: true,
11604 lsp_fetch_timeout_ms: 0,
11605 lsp_insert_mode: LspInsertMode::Insert,
11606 });
11607 });
11608
11609 let mut cx = EditorLspTestContext::new_rust(
11610 lsp::ServerCapabilities {
11611 completion_provider: Some(lsp::CompletionOptions {
11612 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11613 ..lsp::CompletionOptions::default()
11614 }),
11615 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11616 ..lsp::ServerCapabilities::default()
11617 },
11618 cx,
11619 )
11620 .await;
11621
11622 let _completion_requests_handler =
11623 cx.lsp
11624 .server
11625 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11626 Ok(Some(lsp::CompletionResponse::Array(vec![
11627 lsp::CompletionItem {
11628 label: "first".into(),
11629 ..lsp::CompletionItem::default()
11630 },
11631 lsp::CompletionItem {
11632 label: "last".into(),
11633 ..lsp::CompletionItem::default()
11634 },
11635 ])))
11636 });
11637
11638 cx.set_state(indoc! {"ˇ
11639 first
11640 last
11641 second
11642 "});
11643 cx.simulate_keystroke(".");
11644 cx.executor().run_until_parked();
11645 cx.condition(|editor, _| editor.context_menu_visible())
11646 .await;
11647 cx.update_editor(|editor, _, _| {
11648 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11649 {
11650 assert_eq!(
11651 completion_menu_entries(&menu),
11652 &["first", "last", "second"],
11653 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11654 );
11655 } else {
11656 panic!("expected completion menu to be open");
11657 }
11658 });
11659}
11660
11661#[gpui::test]
11662async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11663 init_test(cx, |language_settings| {
11664 language_settings.defaults.completions = Some(CompletionSettings {
11665 words: WordsCompletionMode::Disabled,
11666 lsp: true,
11667 lsp_fetch_timeout_ms: 0,
11668 lsp_insert_mode: LspInsertMode::Insert,
11669 });
11670 });
11671
11672 let mut cx = EditorLspTestContext::new_rust(
11673 lsp::ServerCapabilities {
11674 completion_provider: Some(lsp::CompletionOptions {
11675 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11676 ..lsp::CompletionOptions::default()
11677 }),
11678 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11679 ..lsp::ServerCapabilities::default()
11680 },
11681 cx,
11682 )
11683 .await;
11684
11685 let _completion_requests_handler =
11686 cx.lsp
11687 .server
11688 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11689 panic!("LSP completions should not be queried when dealing with word completions")
11690 });
11691
11692 cx.set_state(indoc! {"ˇ
11693 first
11694 last
11695 second
11696 "});
11697 cx.update_editor(|editor, window, cx| {
11698 editor.show_word_completions(&ShowWordCompletions, window, cx);
11699 });
11700 cx.executor().run_until_parked();
11701 cx.condition(|editor, _| editor.context_menu_visible())
11702 .await;
11703 cx.update_editor(|editor, _, _| {
11704 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11705 {
11706 assert_eq!(
11707 completion_menu_entries(&menu),
11708 &["first", "last", "second"],
11709 "`ShowWordCompletions` action should show word completions"
11710 );
11711 } else {
11712 panic!("expected completion menu to be open");
11713 }
11714 });
11715
11716 cx.simulate_keystroke("l");
11717 cx.executor().run_until_parked();
11718 cx.condition(|editor, _| editor.context_menu_visible())
11719 .await;
11720 cx.update_editor(|editor, _, _| {
11721 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11722 {
11723 assert_eq!(
11724 completion_menu_entries(&menu),
11725 &["last"],
11726 "After showing word completions, further editing should filter them and not query the LSP"
11727 );
11728 } else {
11729 panic!("expected completion menu to be open");
11730 }
11731 });
11732}
11733
11734#[gpui::test]
11735async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11736 init_test(cx, |language_settings| {
11737 language_settings.defaults.completions = Some(CompletionSettings {
11738 words: WordsCompletionMode::Fallback,
11739 lsp: false,
11740 lsp_fetch_timeout_ms: 0,
11741 lsp_insert_mode: LspInsertMode::Insert,
11742 });
11743 });
11744
11745 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11746
11747 cx.set_state(indoc! {"ˇ
11748 0_usize
11749 let
11750 33
11751 4.5f32
11752 "});
11753 cx.update_editor(|editor, window, cx| {
11754 editor.show_completions(&ShowCompletions::default(), window, cx);
11755 });
11756 cx.executor().run_until_parked();
11757 cx.condition(|editor, _| editor.context_menu_visible())
11758 .await;
11759 cx.update_editor(|editor, window, cx| {
11760 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11761 {
11762 assert_eq!(
11763 completion_menu_entries(&menu),
11764 &["let"],
11765 "With no digits in the completion query, no digits should be in the word completions"
11766 );
11767 } else {
11768 panic!("expected completion menu to be open");
11769 }
11770 editor.cancel(&Cancel, window, cx);
11771 });
11772
11773 cx.set_state(indoc! {"3ˇ
11774 0_usize
11775 let
11776 3
11777 33.35f32
11778 "});
11779 cx.update_editor(|editor, window, cx| {
11780 editor.show_completions(&ShowCompletions::default(), window, cx);
11781 });
11782 cx.executor().run_until_parked();
11783 cx.condition(|editor, _| editor.context_menu_visible())
11784 .await;
11785 cx.update_editor(|editor, _, _| {
11786 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11787 {
11788 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11789 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11790 } else {
11791 panic!("expected completion menu to be open");
11792 }
11793 });
11794}
11795
11796fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11797 let position = || lsp::Position {
11798 line: params.text_document_position.position.line,
11799 character: params.text_document_position.position.character,
11800 };
11801 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11802 range: lsp::Range {
11803 start: position(),
11804 end: position(),
11805 },
11806 new_text: text.to_string(),
11807 }))
11808}
11809
11810#[gpui::test]
11811async fn test_multiline_completion(cx: &mut TestAppContext) {
11812 init_test(cx, |_| {});
11813
11814 let fs = FakeFs::new(cx.executor());
11815 fs.insert_tree(
11816 path!("/a"),
11817 json!({
11818 "main.ts": "a",
11819 }),
11820 )
11821 .await;
11822
11823 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11824 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11825 let typescript_language = Arc::new(Language::new(
11826 LanguageConfig {
11827 name: "TypeScript".into(),
11828 matcher: LanguageMatcher {
11829 path_suffixes: vec!["ts".to_string()],
11830 ..LanguageMatcher::default()
11831 },
11832 line_comments: vec!["// ".into()],
11833 ..LanguageConfig::default()
11834 },
11835 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11836 ));
11837 language_registry.add(typescript_language.clone());
11838 let mut fake_servers = language_registry.register_fake_lsp(
11839 "TypeScript",
11840 FakeLspAdapter {
11841 capabilities: lsp::ServerCapabilities {
11842 completion_provider: Some(lsp::CompletionOptions {
11843 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11844 ..lsp::CompletionOptions::default()
11845 }),
11846 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11847 ..lsp::ServerCapabilities::default()
11848 },
11849 // Emulate vtsls label generation
11850 label_for_completion: Some(Box::new(|item, _| {
11851 let text = if let Some(description) = item
11852 .label_details
11853 .as_ref()
11854 .and_then(|label_details| label_details.description.as_ref())
11855 {
11856 format!("{} {}", item.label, description)
11857 } else if let Some(detail) = &item.detail {
11858 format!("{} {}", item.label, detail)
11859 } else {
11860 item.label.clone()
11861 };
11862 let len = text.len();
11863 Some(language::CodeLabel {
11864 text,
11865 runs: Vec::new(),
11866 filter_range: 0..len,
11867 })
11868 })),
11869 ..FakeLspAdapter::default()
11870 },
11871 );
11872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11873 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11874 let worktree_id = workspace
11875 .update(cx, |workspace, _window, cx| {
11876 workspace.project().update(cx, |project, cx| {
11877 project.worktrees(cx).next().unwrap().read(cx).id()
11878 })
11879 })
11880 .unwrap();
11881 let _buffer = project
11882 .update(cx, |project, cx| {
11883 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11884 })
11885 .await
11886 .unwrap();
11887 let editor = workspace
11888 .update(cx, |workspace, window, cx| {
11889 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11890 })
11891 .unwrap()
11892 .await
11893 .unwrap()
11894 .downcast::<Editor>()
11895 .unwrap();
11896 let fake_server = fake_servers.next().await.unwrap();
11897
11898 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11899 let multiline_label_2 = "a\nb\nc\n";
11900 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11901 let multiline_description = "d\ne\nf\n";
11902 let multiline_detail_2 = "g\nh\ni\n";
11903
11904 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11905 move |params, _| async move {
11906 Ok(Some(lsp::CompletionResponse::Array(vec![
11907 lsp::CompletionItem {
11908 label: multiline_label.to_string(),
11909 text_edit: gen_text_edit(¶ms, "new_text_1"),
11910 ..lsp::CompletionItem::default()
11911 },
11912 lsp::CompletionItem {
11913 label: "single line label 1".to_string(),
11914 detail: Some(multiline_detail.to_string()),
11915 text_edit: gen_text_edit(¶ms, "new_text_2"),
11916 ..lsp::CompletionItem::default()
11917 },
11918 lsp::CompletionItem {
11919 label: "single line label 2".to_string(),
11920 label_details: Some(lsp::CompletionItemLabelDetails {
11921 description: Some(multiline_description.to_string()),
11922 detail: None,
11923 }),
11924 text_edit: gen_text_edit(¶ms, "new_text_2"),
11925 ..lsp::CompletionItem::default()
11926 },
11927 lsp::CompletionItem {
11928 label: multiline_label_2.to_string(),
11929 detail: Some(multiline_detail_2.to_string()),
11930 text_edit: gen_text_edit(¶ms, "new_text_3"),
11931 ..lsp::CompletionItem::default()
11932 },
11933 lsp::CompletionItem {
11934 label: "Label with many spaces and \t but without newlines".to_string(),
11935 detail: Some(
11936 "Details with many spaces and \t but without newlines".to_string(),
11937 ),
11938 text_edit: gen_text_edit(¶ms, "new_text_4"),
11939 ..lsp::CompletionItem::default()
11940 },
11941 ])))
11942 },
11943 );
11944
11945 editor.update_in(cx, |editor, window, cx| {
11946 cx.focus_self(window);
11947 editor.move_to_end(&MoveToEnd, window, cx);
11948 editor.handle_input(".", window, cx);
11949 });
11950 cx.run_until_parked();
11951 completion_handle.next().await.unwrap();
11952
11953 editor.update(cx, |editor, _| {
11954 assert!(editor.context_menu_visible());
11955 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11956 {
11957 let completion_labels = menu
11958 .completions
11959 .borrow()
11960 .iter()
11961 .map(|c| c.label.text.clone())
11962 .collect::<Vec<_>>();
11963 assert_eq!(
11964 completion_labels,
11965 &[
11966 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11967 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11968 "single line label 2 d e f ",
11969 "a b c g h i ",
11970 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11971 ],
11972 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11973 );
11974
11975 for completion in menu
11976 .completions
11977 .borrow()
11978 .iter() {
11979 assert_eq!(
11980 completion.label.filter_range,
11981 0..completion.label.text.len(),
11982 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11983 );
11984 }
11985 } else {
11986 panic!("expected completion menu to be open");
11987 }
11988 });
11989}
11990
11991#[gpui::test]
11992async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11993 init_test(cx, |_| {});
11994 let mut cx = EditorLspTestContext::new_rust(
11995 lsp::ServerCapabilities {
11996 completion_provider: Some(lsp::CompletionOptions {
11997 trigger_characters: Some(vec![".".to_string()]),
11998 ..Default::default()
11999 }),
12000 ..Default::default()
12001 },
12002 cx,
12003 )
12004 .await;
12005 cx.lsp
12006 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12007 Ok(Some(lsp::CompletionResponse::Array(vec![
12008 lsp::CompletionItem {
12009 label: "first".into(),
12010 ..Default::default()
12011 },
12012 lsp::CompletionItem {
12013 label: "last".into(),
12014 ..Default::default()
12015 },
12016 ])))
12017 });
12018 cx.set_state("variableˇ");
12019 cx.simulate_keystroke(".");
12020 cx.executor().run_until_parked();
12021
12022 cx.update_editor(|editor, _, _| {
12023 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12024 {
12025 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12026 } else {
12027 panic!("expected completion menu to be open");
12028 }
12029 });
12030
12031 cx.update_editor(|editor, window, cx| {
12032 editor.move_page_down(&MovePageDown::default(), window, cx);
12033 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12034 {
12035 assert!(
12036 menu.selected_item == 1,
12037 "expected PageDown to select the last item from the context menu"
12038 );
12039 } else {
12040 panic!("expected completion menu to stay open after PageDown");
12041 }
12042 });
12043
12044 cx.update_editor(|editor, window, cx| {
12045 editor.move_page_up(&MovePageUp::default(), window, cx);
12046 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12047 {
12048 assert!(
12049 menu.selected_item == 0,
12050 "expected PageUp to select the first item from the context menu"
12051 );
12052 } else {
12053 panic!("expected completion menu to stay open after PageUp");
12054 }
12055 });
12056}
12057
12058#[gpui::test]
12059async fn test_as_is_completions(cx: &mut TestAppContext) {
12060 init_test(cx, |_| {});
12061 let mut cx = EditorLspTestContext::new_rust(
12062 lsp::ServerCapabilities {
12063 completion_provider: Some(lsp::CompletionOptions {
12064 ..Default::default()
12065 }),
12066 ..Default::default()
12067 },
12068 cx,
12069 )
12070 .await;
12071 cx.lsp
12072 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12073 Ok(Some(lsp::CompletionResponse::Array(vec![
12074 lsp::CompletionItem {
12075 label: "unsafe".into(),
12076 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12077 range: lsp::Range {
12078 start: lsp::Position {
12079 line: 1,
12080 character: 2,
12081 },
12082 end: lsp::Position {
12083 line: 1,
12084 character: 3,
12085 },
12086 },
12087 new_text: "unsafe".to_string(),
12088 })),
12089 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12090 ..Default::default()
12091 },
12092 ])))
12093 });
12094 cx.set_state("fn a() {}\n nˇ");
12095 cx.executor().run_until_parked();
12096 cx.update_editor(|editor, window, cx| {
12097 editor.show_completions(
12098 &ShowCompletions {
12099 trigger: Some("\n".into()),
12100 },
12101 window,
12102 cx,
12103 );
12104 });
12105 cx.executor().run_until_parked();
12106
12107 cx.update_editor(|editor, window, cx| {
12108 editor.confirm_completion(&Default::default(), window, cx)
12109 });
12110 cx.executor().run_until_parked();
12111 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12112}
12113
12114#[gpui::test]
12115async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12116 init_test(cx, |_| {});
12117
12118 let mut cx = EditorLspTestContext::new_rust(
12119 lsp::ServerCapabilities {
12120 completion_provider: Some(lsp::CompletionOptions {
12121 trigger_characters: Some(vec![".".to_string()]),
12122 resolve_provider: Some(true),
12123 ..Default::default()
12124 }),
12125 ..Default::default()
12126 },
12127 cx,
12128 )
12129 .await;
12130
12131 cx.set_state("fn main() { let a = 2ˇ; }");
12132 cx.simulate_keystroke(".");
12133 let completion_item = lsp::CompletionItem {
12134 label: "Some".into(),
12135 kind: Some(lsp::CompletionItemKind::SNIPPET),
12136 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12137 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12138 kind: lsp::MarkupKind::Markdown,
12139 value: "```rust\nSome(2)\n```".to_string(),
12140 })),
12141 deprecated: Some(false),
12142 sort_text: Some("Some".to_string()),
12143 filter_text: Some("Some".to_string()),
12144 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12145 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12146 range: lsp::Range {
12147 start: lsp::Position {
12148 line: 0,
12149 character: 22,
12150 },
12151 end: lsp::Position {
12152 line: 0,
12153 character: 22,
12154 },
12155 },
12156 new_text: "Some(2)".to_string(),
12157 })),
12158 additional_text_edits: Some(vec![lsp::TextEdit {
12159 range: lsp::Range {
12160 start: lsp::Position {
12161 line: 0,
12162 character: 20,
12163 },
12164 end: lsp::Position {
12165 line: 0,
12166 character: 22,
12167 },
12168 },
12169 new_text: "".to_string(),
12170 }]),
12171 ..Default::default()
12172 };
12173
12174 let closure_completion_item = completion_item.clone();
12175 let counter = Arc::new(AtomicUsize::new(0));
12176 let counter_clone = counter.clone();
12177 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12178 let task_completion_item = closure_completion_item.clone();
12179 counter_clone.fetch_add(1, atomic::Ordering::Release);
12180 async move {
12181 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12182 is_incomplete: true,
12183 item_defaults: None,
12184 items: vec![task_completion_item],
12185 })))
12186 }
12187 });
12188
12189 cx.condition(|editor, _| editor.context_menu_visible())
12190 .await;
12191 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12192 assert!(request.next().await.is_some());
12193 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12194
12195 cx.simulate_keystrokes("S o m");
12196 cx.condition(|editor, _| editor.context_menu_visible())
12197 .await;
12198 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12199 assert!(request.next().await.is_some());
12200 assert!(request.next().await.is_some());
12201 assert!(request.next().await.is_some());
12202 request.close();
12203 assert!(request.next().await.is_none());
12204 assert_eq!(
12205 counter.load(atomic::Ordering::Acquire),
12206 4,
12207 "With the completions menu open, only one LSP request should happen per input"
12208 );
12209}
12210
12211#[gpui::test]
12212async fn test_toggle_comment(cx: &mut TestAppContext) {
12213 init_test(cx, |_| {});
12214 let mut cx = EditorTestContext::new(cx).await;
12215 let language = Arc::new(Language::new(
12216 LanguageConfig {
12217 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12218 ..Default::default()
12219 },
12220 Some(tree_sitter_rust::LANGUAGE.into()),
12221 ));
12222 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12223
12224 // If multiple selections intersect a line, the line is only toggled once.
12225 cx.set_state(indoc! {"
12226 fn a() {
12227 «//b();
12228 ˇ»// «c();
12229 //ˇ» d();
12230 }
12231 "});
12232
12233 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12234
12235 cx.assert_editor_state(indoc! {"
12236 fn a() {
12237 «b();
12238 c();
12239 ˇ» d();
12240 }
12241 "});
12242
12243 // The comment prefix is inserted at the same column for every line in a
12244 // selection.
12245 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12246
12247 cx.assert_editor_state(indoc! {"
12248 fn a() {
12249 // «b();
12250 // c();
12251 ˇ»// d();
12252 }
12253 "});
12254
12255 // If a selection ends at the beginning of a line, that line is not toggled.
12256 cx.set_selections_state(indoc! {"
12257 fn a() {
12258 // b();
12259 «// c();
12260 ˇ» // d();
12261 }
12262 "});
12263
12264 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12265
12266 cx.assert_editor_state(indoc! {"
12267 fn a() {
12268 // b();
12269 «c();
12270 ˇ» // d();
12271 }
12272 "});
12273
12274 // If a selection span a single line and is empty, the line is toggled.
12275 cx.set_state(indoc! {"
12276 fn a() {
12277 a();
12278 b();
12279 ˇ
12280 }
12281 "});
12282
12283 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12284
12285 cx.assert_editor_state(indoc! {"
12286 fn a() {
12287 a();
12288 b();
12289 //•ˇ
12290 }
12291 "});
12292
12293 // If a selection span multiple lines, empty lines are not toggled.
12294 cx.set_state(indoc! {"
12295 fn a() {
12296 «a();
12297
12298 c();ˇ»
12299 }
12300 "});
12301
12302 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12303
12304 cx.assert_editor_state(indoc! {"
12305 fn a() {
12306 // «a();
12307
12308 // c();ˇ»
12309 }
12310 "});
12311
12312 // If a selection includes multiple comment prefixes, all lines are uncommented.
12313 cx.set_state(indoc! {"
12314 fn a() {
12315 «// a();
12316 /// b();
12317 //! c();ˇ»
12318 }
12319 "});
12320
12321 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12322
12323 cx.assert_editor_state(indoc! {"
12324 fn a() {
12325 «a();
12326 b();
12327 c();ˇ»
12328 }
12329 "});
12330}
12331
12332#[gpui::test]
12333async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12334 init_test(cx, |_| {});
12335 let mut cx = EditorTestContext::new(cx).await;
12336 let language = Arc::new(Language::new(
12337 LanguageConfig {
12338 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12339 ..Default::default()
12340 },
12341 Some(tree_sitter_rust::LANGUAGE.into()),
12342 ));
12343 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12344
12345 let toggle_comments = &ToggleComments {
12346 advance_downwards: false,
12347 ignore_indent: true,
12348 };
12349
12350 // If multiple selections intersect a line, the line is only toggled once.
12351 cx.set_state(indoc! {"
12352 fn a() {
12353 // «b();
12354 // c();
12355 // ˇ» d();
12356 }
12357 "});
12358
12359 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12360
12361 cx.assert_editor_state(indoc! {"
12362 fn a() {
12363 «b();
12364 c();
12365 ˇ» d();
12366 }
12367 "});
12368
12369 // The comment prefix is inserted at the beginning of each line
12370 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12371
12372 cx.assert_editor_state(indoc! {"
12373 fn a() {
12374 // «b();
12375 // c();
12376 // ˇ» d();
12377 }
12378 "});
12379
12380 // If a selection ends at the beginning of a line, that line is not toggled.
12381 cx.set_selections_state(indoc! {"
12382 fn a() {
12383 // b();
12384 // «c();
12385 ˇ»// d();
12386 }
12387 "});
12388
12389 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12390
12391 cx.assert_editor_state(indoc! {"
12392 fn a() {
12393 // b();
12394 «c();
12395 ˇ»// d();
12396 }
12397 "});
12398
12399 // If a selection span a single line and is empty, the line is toggled.
12400 cx.set_state(indoc! {"
12401 fn a() {
12402 a();
12403 b();
12404 ˇ
12405 }
12406 "});
12407
12408 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12409
12410 cx.assert_editor_state(indoc! {"
12411 fn a() {
12412 a();
12413 b();
12414 //ˇ
12415 }
12416 "});
12417
12418 // If a selection span multiple lines, empty lines are not toggled.
12419 cx.set_state(indoc! {"
12420 fn a() {
12421 «a();
12422
12423 c();ˇ»
12424 }
12425 "});
12426
12427 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12428
12429 cx.assert_editor_state(indoc! {"
12430 fn a() {
12431 // «a();
12432
12433 // c();ˇ»
12434 }
12435 "});
12436
12437 // If a selection includes multiple comment prefixes, all lines are uncommented.
12438 cx.set_state(indoc! {"
12439 fn a() {
12440 // «a();
12441 /// b();
12442 //! c();ˇ»
12443 }
12444 "});
12445
12446 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12447
12448 cx.assert_editor_state(indoc! {"
12449 fn a() {
12450 «a();
12451 b();
12452 c();ˇ»
12453 }
12454 "});
12455}
12456
12457#[gpui::test]
12458async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12459 init_test(cx, |_| {});
12460
12461 let language = Arc::new(Language::new(
12462 LanguageConfig {
12463 line_comments: vec!["// ".into()],
12464 ..Default::default()
12465 },
12466 Some(tree_sitter_rust::LANGUAGE.into()),
12467 ));
12468
12469 let mut cx = EditorTestContext::new(cx).await;
12470
12471 cx.language_registry().add(language.clone());
12472 cx.update_buffer(|buffer, cx| {
12473 buffer.set_language(Some(language), cx);
12474 });
12475
12476 let toggle_comments = &ToggleComments {
12477 advance_downwards: true,
12478 ignore_indent: false,
12479 };
12480
12481 // Single cursor on one line -> advance
12482 // Cursor moves horizontally 3 characters as well on non-blank line
12483 cx.set_state(indoc!(
12484 "fn a() {
12485 ˇdog();
12486 cat();
12487 }"
12488 ));
12489 cx.update_editor(|editor, window, cx| {
12490 editor.toggle_comments(toggle_comments, window, cx);
12491 });
12492 cx.assert_editor_state(indoc!(
12493 "fn a() {
12494 // dog();
12495 catˇ();
12496 }"
12497 ));
12498
12499 // Single selection on one line -> don't advance
12500 cx.set_state(indoc!(
12501 "fn a() {
12502 «dog()ˇ»;
12503 cat();
12504 }"
12505 ));
12506 cx.update_editor(|editor, window, cx| {
12507 editor.toggle_comments(toggle_comments, window, cx);
12508 });
12509 cx.assert_editor_state(indoc!(
12510 "fn a() {
12511 // «dog()ˇ»;
12512 cat();
12513 }"
12514 ));
12515
12516 // Multiple cursors on one line -> advance
12517 cx.set_state(indoc!(
12518 "fn a() {
12519 ˇdˇog();
12520 cat();
12521 }"
12522 ));
12523 cx.update_editor(|editor, window, cx| {
12524 editor.toggle_comments(toggle_comments, window, cx);
12525 });
12526 cx.assert_editor_state(indoc!(
12527 "fn a() {
12528 // dog();
12529 catˇ(ˇ);
12530 }"
12531 ));
12532
12533 // Multiple cursors on one line, with selection -> don't advance
12534 cx.set_state(indoc!(
12535 "fn a() {
12536 ˇdˇog«()ˇ»;
12537 cat();
12538 }"
12539 ));
12540 cx.update_editor(|editor, window, cx| {
12541 editor.toggle_comments(toggle_comments, window, cx);
12542 });
12543 cx.assert_editor_state(indoc!(
12544 "fn a() {
12545 // ˇdˇog«()ˇ»;
12546 cat();
12547 }"
12548 ));
12549
12550 // Single cursor on one line -> advance
12551 // Cursor moves to column 0 on blank line
12552 cx.set_state(indoc!(
12553 "fn a() {
12554 ˇdog();
12555
12556 cat();
12557 }"
12558 ));
12559 cx.update_editor(|editor, window, cx| {
12560 editor.toggle_comments(toggle_comments, window, cx);
12561 });
12562 cx.assert_editor_state(indoc!(
12563 "fn a() {
12564 // dog();
12565 ˇ
12566 cat();
12567 }"
12568 ));
12569
12570 // Single cursor on one line -> advance
12571 // Cursor starts and ends at column 0
12572 cx.set_state(indoc!(
12573 "fn a() {
12574 ˇ dog();
12575 cat();
12576 }"
12577 ));
12578 cx.update_editor(|editor, window, cx| {
12579 editor.toggle_comments(toggle_comments, window, cx);
12580 });
12581 cx.assert_editor_state(indoc!(
12582 "fn a() {
12583 // dog();
12584 ˇ cat();
12585 }"
12586 ));
12587}
12588
12589#[gpui::test]
12590async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12591 init_test(cx, |_| {});
12592
12593 let mut cx = EditorTestContext::new(cx).await;
12594
12595 let html_language = Arc::new(
12596 Language::new(
12597 LanguageConfig {
12598 name: "HTML".into(),
12599 block_comment: Some(("<!-- ".into(), " -->".into())),
12600 ..Default::default()
12601 },
12602 Some(tree_sitter_html::LANGUAGE.into()),
12603 )
12604 .with_injection_query(
12605 r#"
12606 (script_element
12607 (raw_text) @injection.content
12608 (#set! injection.language "javascript"))
12609 "#,
12610 )
12611 .unwrap(),
12612 );
12613
12614 let javascript_language = Arc::new(Language::new(
12615 LanguageConfig {
12616 name: "JavaScript".into(),
12617 line_comments: vec!["// ".into()],
12618 ..Default::default()
12619 },
12620 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12621 ));
12622
12623 cx.language_registry().add(html_language.clone());
12624 cx.language_registry().add(javascript_language.clone());
12625 cx.update_buffer(|buffer, cx| {
12626 buffer.set_language(Some(html_language), cx);
12627 });
12628
12629 // Toggle comments for empty selections
12630 cx.set_state(
12631 &r#"
12632 <p>A</p>ˇ
12633 <p>B</p>ˇ
12634 <p>C</p>ˇ
12635 "#
12636 .unindent(),
12637 );
12638 cx.update_editor(|editor, window, cx| {
12639 editor.toggle_comments(&ToggleComments::default(), window, cx)
12640 });
12641 cx.assert_editor_state(
12642 &r#"
12643 <!-- <p>A</p>ˇ -->
12644 <!-- <p>B</p>ˇ -->
12645 <!-- <p>C</p>ˇ -->
12646 "#
12647 .unindent(),
12648 );
12649 cx.update_editor(|editor, window, cx| {
12650 editor.toggle_comments(&ToggleComments::default(), window, cx)
12651 });
12652 cx.assert_editor_state(
12653 &r#"
12654 <p>A</p>ˇ
12655 <p>B</p>ˇ
12656 <p>C</p>ˇ
12657 "#
12658 .unindent(),
12659 );
12660
12661 // Toggle comments for mixture of empty and non-empty selections, where
12662 // multiple selections occupy a given line.
12663 cx.set_state(
12664 &r#"
12665 <p>A«</p>
12666 <p>ˇ»B</p>ˇ
12667 <p>C«</p>
12668 <p>ˇ»D</p>ˇ
12669 "#
12670 .unindent(),
12671 );
12672
12673 cx.update_editor(|editor, window, cx| {
12674 editor.toggle_comments(&ToggleComments::default(), window, cx)
12675 });
12676 cx.assert_editor_state(
12677 &r#"
12678 <!-- <p>A«</p>
12679 <p>ˇ»B</p>ˇ -->
12680 <!-- <p>C«</p>
12681 <p>ˇ»D</p>ˇ -->
12682 "#
12683 .unindent(),
12684 );
12685 cx.update_editor(|editor, window, cx| {
12686 editor.toggle_comments(&ToggleComments::default(), window, cx)
12687 });
12688 cx.assert_editor_state(
12689 &r#"
12690 <p>A«</p>
12691 <p>ˇ»B</p>ˇ
12692 <p>C«</p>
12693 <p>ˇ»D</p>ˇ
12694 "#
12695 .unindent(),
12696 );
12697
12698 // Toggle comments when different languages are active for different
12699 // selections.
12700 cx.set_state(
12701 &r#"
12702 ˇ<script>
12703 ˇvar x = new Y();
12704 ˇ</script>
12705 "#
12706 .unindent(),
12707 );
12708 cx.executor().run_until_parked();
12709 cx.update_editor(|editor, window, cx| {
12710 editor.toggle_comments(&ToggleComments::default(), window, cx)
12711 });
12712 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12713 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12714 cx.assert_editor_state(
12715 &r#"
12716 <!-- ˇ<script> -->
12717 // ˇvar x = new Y();
12718 <!-- ˇ</script> -->
12719 "#
12720 .unindent(),
12721 );
12722}
12723
12724#[gpui::test]
12725fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12726 init_test(cx, |_| {});
12727
12728 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12729 let multibuffer = cx.new(|cx| {
12730 let mut multibuffer = MultiBuffer::new(ReadWrite);
12731 multibuffer.push_excerpts(
12732 buffer.clone(),
12733 [
12734 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12735 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12736 ],
12737 cx,
12738 );
12739 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12740 multibuffer
12741 });
12742
12743 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12744 editor.update_in(cx, |editor, window, cx| {
12745 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12746 editor.change_selections(None, window, cx, |s| {
12747 s.select_ranges([
12748 Point::new(0, 0)..Point::new(0, 0),
12749 Point::new(1, 0)..Point::new(1, 0),
12750 ])
12751 });
12752
12753 editor.handle_input("X", window, cx);
12754 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12755 assert_eq!(
12756 editor.selections.ranges(cx),
12757 [
12758 Point::new(0, 1)..Point::new(0, 1),
12759 Point::new(1, 1)..Point::new(1, 1),
12760 ]
12761 );
12762
12763 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12764 editor.change_selections(None, window, cx, |s| {
12765 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12766 });
12767 editor.backspace(&Default::default(), window, cx);
12768 assert_eq!(editor.text(cx), "Xa\nbbb");
12769 assert_eq!(
12770 editor.selections.ranges(cx),
12771 [Point::new(1, 0)..Point::new(1, 0)]
12772 );
12773
12774 editor.change_selections(None, window, cx, |s| {
12775 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12776 });
12777 editor.backspace(&Default::default(), window, cx);
12778 assert_eq!(editor.text(cx), "X\nbb");
12779 assert_eq!(
12780 editor.selections.ranges(cx),
12781 [Point::new(0, 1)..Point::new(0, 1)]
12782 );
12783 });
12784}
12785
12786#[gpui::test]
12787fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12788 init_test(cx, |_| {});
12789
12790 let markers = vec![('[', ']').into(), ('(', ')').into()];
12791 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12792 indoc! {"
12793 [aaaa
12794 (bbbb]
12795 cccc)",
12796 },
12797 markers.clone(),
12798 );
12799 let excerpt_ranges = markers.into_iter().map(|marker| {
12800 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12801 ExcerptRange::new(context.clone())
12802 });
12803 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12804 let multibuffer = cx.new(|cx| {
12805 let mut multibuffer = MultiBuffer::new(ReadWrite);
12806 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12807 multibuffer
12808 });
12809
12810 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12811 editor.update_in(cx, |editor, window, cx| {
12812 let (expected_text, selection_ranges) = marked_text_ranges(
12813 indoc! {"
12814 aaaa
12815 bˇbbb
12816 bˇbbˇb
12817 cccc"
12818 },
12819 true,
12820 );
12821 assert_eq!(editor.text(cx), expected_text);
12822 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12823
12824 editor.handle_input("X", window, cx);
12825
12826 let (expected_text, expected_selections) = marked_text_ranges(
12827 indoc! {"
12828 aaaa
12829 bXˇbbXb
12830 bXˇbbXˇb
12831 cccc"
12832 },
12833 false,
12834 );
12835 assert_eq!(editor.text(cx), expected_text);
12836 assert_eq!(editor.selections.ranges(cx), expected_selections);
12837
12838 editor.newline(&Newline, window, cx);
12839 let (expected_text, expected_selections) = marked_text_ranges(
12840 indoc! {"
12841 aaaa
12842 bX
12843 ˇbbX
12844 b
12845 bX
12846 ˇbbX
12847 ˇb
12848 cccc"
12849 },
12850 false,
12851 );
12852 assert_eq!(editor.text(cx), expected_text);
12853 assert_eq!(editor.selections.ranges(cx), expected_selections);
12854 });
12855}
12856
12857#[gpui::test]
12858fn test_refresh_selections(cx: &mut TestAppContext) {
12859 init_test(cx, |_| {});
12860
12861 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12862 let mut excerpt1_id = None;
12863 let multibuffer = cx.new(|cx| {
12864 let mut multibuffer = MultiBuffer::new(ReadWrite);
12865 excerpt1_id = multibuffer
12866 .push_excerpts(
12867 buffer.clone(),
12868 [
12869 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12870 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12871 ],
12872 cx,
12873 )
12874 .into_iter()
12875 .next();
12876 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12877 multibuffer
12878 });
12879
12880 let editor = cx.add_window(|window, cx| {
12881 let mut editor = build_editor(multibuffer.clone(), window, cx);
12882 let snapshot = editor.snapshot(window, cx);
12883 editor.change_selections(None, window, cx, |s| {
12884 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12885 });
12886 editor.begin_selection(
12887 Point::new(2, 1).to_display_point(&snapshot),
12888 true,
12889 1,
12890 window,
12891 cx,
12892 );
12893 assert_eq!(
12894 editor.selections.ranges(cx),
12895 [
12896 Point::new(1, 3)..Point::new(1, 3),
12897 Point::new(2, 1)..Point::new(2, 1),
12898 ]
12899 );
12900 editor
12901 });
12902
12903 // Refreshing selections is a no-op when excerpts haven't changed.
12904 _ = editor.update(cx, |editor, window, cx| {
12905 editor.change_selections(None, window, cx, |s| s.refresh());
12906 assert_eq!(
12907 editor.selections.ranges(cx),
12908 [
12909 Point::new(1, 3)..Point::new(1, 3),
12910 Point::new(2, 1)..Point::new(2, 1),
12911 ]
12912 );
12913 });
12914
12915 multibuffer.update(cx, |multibuffer, cx| {
12916 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12917 });
12918 _ = editor.update(cx, |editor, window, cx| {
12919 // Removing an excerpt causes the first selection to become degenerate.
12920 assert_eq!(
12921 editor.selections.ranges(cx),
12922 [
12923 Point::new(0, 0)..Point::new(0, 0),
12924 Point::new(0, 1)..Point::new(0, 1)
12925 ]
12926 );
12927
12928 // Refreshing selections will relocate the first selection to the original buffer
12929 // location.
12930 editor.change_selections(None, window, cx, |s| s.refresh());
12931 assert_eq!(
12932 editor.selections.ranges(cx),
12933 [
12934 Point::new(0, 1)..Point::new(0, 1),
12935 Point::new(0, 3)..Point::new(0, 3)
12936 ]
12937 );
12938 assert!(editor.selections.pending_anchor().is_some());
12939 });
12940}
12941
12942#[gpui::test]
12943fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12944 init_test(cx, |_| {});
12945
12946 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12947 let mut excerpt1_id = None;
12948 let multibuffer = cx.new(|cx| {
12949 let mut multibuffer = MultiBuffer::new(ReadWrite);
12950 excerpt1_id = multibuffer
12951 .push_excerpts(
12952 buffer.clone(),
12953 [
12954 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12955 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12956 ],
12957 cx,
12958 )
12959 .into_iter()
12960 .next();
12961 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12962 multibuffer
12963 });
12964
12965 let editor = cx.add_window(|window, cx| {
12966 let mut editor = build_editor(multibuffer.clone(), window, cx);
12967 let snapshot = editor.snapshot(window, cx);
12968 editor.begin_selection(
12969 Point::new(1, 3).to_display_point(&snapshot),
12970 false,
12971 1,
12972 window,
12973 cx,
12974 );
12975 assert_eq!(
12976 editor.selections.ranges(cx),
12977 [Point::new(1, 3)..Point::new(1, 3)]
12978 );
12979 editor
12980 });
12981
12982 multibuffer.update(cx, |multibuffer, cx| {
12983 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12984 });
12985 _ = editor.update(cx, |editor, window, cx| {
12986 assert_eq!(
12987 editor.selections.ranges(cx),
12988 [Point::new(0, 0)..Point::new(0, 0)]
12989 );
12990
12991 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12992 editor.change_selections(None, window, cx, |s| s.refresh());
12993 assert_eq!(
12994 editor.selections.ranges(cx),
12995 [Point::new(0, 3)..Point::new(0, 3)]
12996 );
12997 assert!(editor.selections.pending_anchor().is_some());
12998 });
12999}
13000
13001#[gpui::test]
13002async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13003 init_test(cx, |_| {});
13004
13005 let language = Arc::new(
13006 Language::new(
13007 LanguageConfig {
13008 brackets: BracketPairConfig {
13009 pairs: vec![
13010 BracketPair {
13011 start: "{".to_string(),
13012 end: "}".to_string(),
13013 close: true,
13014 surround: true,
13015 newline: true,
13016 },
13017 BracketPair {
13018 start: "/* ".to_string(),
13019 end: " */".to_string(),
13020 close: true,
13021 surround: true,
13022 newline: true,
13023 },
13024 ],
13025 ..Default::default()
13026 },
13027 ..Default::default()
13028 },
13029 Some(tree_sitter_rust::LANGUAGE.into()),
13030 )
13031 .with_indents_query("")
13032 .unwrap(),
13033 );
13034
13035 let text = concat!(
13036 "{ }\n", //
13037 " x\n", //
13038 " /* */\n", //
13039 "x\n", //
13040 "{{} }\n", //
13041 );
13042
13043 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13044 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13045 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13046 editor
13047 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13048 .await;
13049
13050 editor.update_in(cx, |editor, window, cx| {
13051 editor.change_selections(None, window, cx, |s| {
13052 s.select_display_ranges([
13053 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13054 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13055 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13056 ])
13057 });
13058 editor.newline(&Newline, window, cx);
13059
13060 assert_eq!(
13061 editor.buffer().read(cx).read(cx).text(),
13062 concat!(
13063 "{ \n", // Suppress rustfmt
13064 "\n", //
13065 "}\n", //
13066 " x\n", //
13067 " /* \n", //
13068 " \n", //
13069 " */\n", //
13070 "x\n", //
13071 "{{} \n", //
13072 "}\n", //
13073 )
13074 );
13075 });
13076}
13077
13078#[gpui::test]
13079fn test_highlighted_ranges(cx: &mut TestAppContext) {
13080 init_test(cx, |_| {});
13081
13082 let editor = cx.add_window(|window, cx| {
13083 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13084 build_editor(buffer.clone(), window, cx)
13085 });
13086
13087 _ = editor.update(cx, |editor, window, cx| {
13088 struct Type1;
13089 struct Type2;
13090
13091 let buffer = editor.buffer.read(cx).snapshot(cx);
13092
13093 let anchor_range =
13094 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13095
13096 editor.highlight_background::<Type1>(
13097 &[
13098 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13099 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13100 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13101 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13102 ],
13103 |_| Hsla::red(),
13104 cx,
13105 );
13106 editor.highlight_background::<Type2>(
13107 &[
13108 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13109 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13110 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13111 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13112 ],
13113 |_| Hsla::green(),
13114 cx,
13115 );
13116
13117 let snapshot = editor.snapshot(window, cx);
13118 let mut highlighted_ranges = editor.background_highlights_in_range(
13119 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13120 &snapshot,
13121 cx.theme().colors(),
13122 );
13123 // Enforce a consistent ordering based on color without relying on the ordering of the
13124 // highlight's `TypeId` which is non-executor.
13125 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13126 assert_eq!(
13127 highlighted_ranges,
13128 &[
13129 (
13130 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13131 Hsla::red(),
13132 ),
13133 (
13134 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13135 Hsla::red(),
13136 ),
13137 (
13138 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13139 Hsla::green(),
13140 ),
13141 (
13142 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13143 Hsla::green(),
13144 ),
13145 ]
13146 );
13147 assert_eq!(
13148 editor.background_highlights_in_range(
13149 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13150 &snapshot,
13151 cx.theme().colors(),
13152 ),
13153 &[(
13154 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13155 Hsla::red(),
13156 )]
13157 );
13158 });
13159}
13160
13161#[gpui::test]
13162async fn test_following(cx: &mut TestAppContext) {
13163 init_test(cx, |_| {});
13164
13165 let fs = FakeFs::new(cx.executor());
13166 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13167
13168 let buffer = project.update(cx, |project, cx| {
13169 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13170 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13171 });
13172 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13173 let follower = cx.update(|cx| {
13174 cx.open_window(
13175 WindowOptions {
13176 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13177 gpui::Point::new(px(0.), px(0.)),
13178 gpui::Point::new(px(10.), px(80.)),
13179 ))),
13180 ..Default::default()
13181 },
13182 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13183 )
13184 .unwrap()
13185 });
13186
13187 let is_still_following = Rc::new(RefCell::new(true));
13188 let follower_edit_event_count = Rc::new(RefCell::new(0));
13189 let pending_update = Rc::new(RefCell::new(None));
13190 let leader_entity = leader.root(cx).unwrap();
13191 let follower_entity = follower.root(cx).unwrap();
13192 _ = follower.update(cx, {
13193 let update = pending_update.clone();
13194 let is_still_following = is_still_following.clone();
13195 let follower_edit_event_count = follower_edit_event_count.clone();
13196 |_, window, cx| {
13197 cx.subscribe_in(
13198 &leader_entity,
13199 window,
13200 move |_, leader, event, window, cx| {
13201 leader.read(cx).add_event_to_update_proto(
13202 event,
13203 &mut update.borrow_mut(),
13204 window,
13205 cx,
13206 );
13207 },
13208 )
13209 .detach();
13210
13211 cx.subscribe_in(
13212 &follower_entity,
13213 window,
13214 move |_, _, event: &EditorEvent, _window, _cx| {
13215 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13216 *is_still_following.borrow_mut() = false;
13217 }
13218
13219 if let EditorEvent::BufferEdited = event {
13220 *follower_edit_event_count.borrow_mut() += 1;
13221 }
13222 },
13223 )
13224 .detach();
13225 }
13226 });
13227
13228 // Update the selections only
13229 _ = leader.update(cx, |leader, window, cx| {
13230 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13231 });
13232 follower
13233 .update(cx, |follower, window, cx| {
13234 follower.apply_update_proto(
13235 &project,
13236 pending_update.borrow_mut().take().unwrap(),
13237 window,
13238 cx,
13239 )
13240 })
13241 .unwrap()
13242 .await
13243 .unwrap();
13244 _ = follower.update(cx, |follower, _, cx| {
13245 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13246 });
13247 assert!(*is_still_following.borrow());
13248 assert_eq!(*follower_edit_event_count.borrow(), 0);
13249
13250 // Update the scroll position only
13251 _ = leader.update(cx, |leader, window, cx| {
13252 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13253 });
13254 follower
13255 .update(cx, |follower, window, cx| {
13256 follower.apply_update_proto(
13257 &project,
13258 pending_update.borrow_mut().take().unwrap(),
13259 window,
13260 cx,
13261 )
13262 })
13263 .unwrap()
13264 .await
13265 .unwrap();
13266 assert_eq!(
13267 follower
13268 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13269 .unwrap(),
13270 gpui::Point::new(1.5, 3.5)
13271 );
13272 assert!(*is_still_following.borrow());
13273 assert_eq!(*follower_edit_event_count.borrow(), 0);
13274
13275 // Update the selections and scroll position. The follower's scroll position is updated
13276 // via autoscroll, not via the leader's exact scroll position.
13277 _ = leader.update(cx, |leader, window, cx| {
13278 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13279 leader.request_autoscroll(Autoscroll::newest(), cx);
13280 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13281 });
13282 follower
13283 .update(cx, |follower, window, cx| {
13284 follower.apply_update_proto(
13285 &project,
13286 pending_update.borrow_mut().take().unwrap(),
13287 window,
13288 cx,
13289 )
13290 })
13291 .unwrap()
13292 .await
13293 .unwrap();
13294 _ = follower.update(cx, |follower, _, cx| {
13295 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13296 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13297 });
13298 assert!(*is_still_following.borrow());
13299
13300 // Creating a pending selection that precedes another selection
13301 _ = leader.update(cx, |leader, window, cx| {
13302 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13303 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13304 });
13305 follower
13306 .update(cx, |follower, window, cx| {
13307 follower.apply_update_proto(
13308 &project,
13309 pending_update.borrow_mut().take().unwrap(),
13310 window,
13311 cx,
13312 )
13313 })
13314 .unwrap()
13315 .await
13316 .unwrap();
13317 _ = follower.update(cx, |follower, _, cx| {
13318 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13319 });
13320 assert!(*is_still_following.borrow());
13321
13322 // Extend the pending selection so that it surrounds another selection
13323 _ = leader.update(cx, |leader, window, cx| {
13324 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13325 });
13326 follower
13327 .update(cx, |follower, window, cx| {
13328 follower.apply_update_proto(
13329 &project,
13330 pending_update.borrow_mut().take().unwrap(),
13331 window,
13332 cx,
13333 )
13334 })
13335 .unwrap()
13336 .await
13337 .unwrap();
13338 _ = follower.update(cx, |follower, _, cx| {
13339 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13340 });
13341
13342 // Scrolling locally breaks the follow
13343 _ = follower.update(cx, |follower, window, cx| {
13344 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13345 follower.set_scroll_anchor(
13346 ScrollAnchor {
13347 anchor: top_anchor,
13348 offset: gpui::Point::new(0.0, 0.5),
13349 },
13350 window,
13351 cx,
13352 );
13353 });
13354 assert!(!(*is_still_following.borrow()));
13355}
13356
13357#[gpui::test]
13358async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13359 init_test(cx, |_| {});
13360
13361 let fs = FakeFs::new(cx.executor());
13362 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13363 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13364 let pane = workspace
13365 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13366 .unwrap();
13367
13368 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13369
13370 let leader = pane.update_in(cx, |_, window, cx| {
13371 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13372 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13373 });
13374
13375 // Start following the editor when it has no excerpts.
13376 let mut state_message =
13377 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13378 let workspace_entity = workspace.root(cx).unwrap();
13379 let follower_1 = cx
13380 .update_window(*workspace.deref(), |_, window, cx| {
13381 Editor::from_state_proto(
13382 workspace_entity,
13383 ViewId {
13384 creator: CollaboratorId::PeerId(PeerId::default()),
13385 id: 0,
13386 },
13387 &mut state_message,
13388 window,
13389 cx,
13390 )
13391 })
13392 .unwrap()
13393 .unwrap()
13394 .await
13395 .unwrap();
13396
13397 let update_message = Rc::new(RefCell::new(None));
13398 follower_1.update_in(cx, {
13399 let update = update_message.clone();
13400 |_, window, cx| {
13401 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13402 leader.read(cx).add_event_to_update_proto(
13403 event,
13404 &mut update.borrow_mut(),
13405 window,
13406 cx,
13407 );
13408 })
13409 .detach();
13410 }
13411 });
13412
13413 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13414 (
13415 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13416 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13417 )
13418 });
13419
13420 // Insert some excerpts.
13421 leader.update(cx, |leader, cx| {
13422 leader.buffer.update(cx, |multibuffer, cx| {
13423 multibuffer.set_excerpts_for_path(
13424 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13425 buffer_1.clone(),
13426 vec![
13427 Point::row_range(0..3),
13428 Point::row_range(1..6),
13429 Point::row_range(12..15),
13430 ],
13431 0,
13432 cx,
13433 );
13434 multibuffer.set_excerpts_for_path(
13435 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13436 buffer_2.clone(),
13437 vec![Point::row_range(0..6), Point::row_range(8..12)],
13438 0,
13439 cx,
13440 );
13441 });
13442 });
13443
13444 // Apply the update of adding the excerpts.
13445 follower_1
13446 .update_in(cx, |follower, window, cx| {
13447 follower.apply_update_proto(
13448 &project,
13449 update_message.borrow().clone().unwrap(),
13450 window,
13451 cx,
13452 )
13453 })
13454 .await
13455 .unwrap();
13456 assert_eq!(
13457 follower_1.update(cx, |editor, cx| editor.text(cx)),
13458 leader.update(cx, |editor, cx| editor.text(cx))
13459 );
13460 update_message.borrow_mut().take();
13461
13462 // Start following separately after it already has excerpts.
13463 let mut state_message =
13464 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13465 let workspace_entity = workspace.root(cx).unwrap();
13466 let follower_2 = cx
13467 .update_window(*workspace.deref(), |_, window, cx| {
13468 Editor::from_state_proto(
13469 workspace_entity,
13470 ViewId {
13471 creator: CollaboratorId::PeerId(PeerId::default()),
13472 id: 0,
13473 },
13474 &mut state_message,
13475 window,
13476 cx,
13477 )
13478 })
13479 .unwrap()
13480 .unwrap()
13481 .await
13482 .unwrap();
13483 assert_eq!(
13484 follower_2.update(cx, |editor, cx| editor.text(cx)),
13485 leader.update(cx, |editor, cx| editor.text(cx))
13486 );
13487
13488 // Remove some excerpts.
13489 leader.update(cx, |leader, cx| {
13490 leader.buffer.update(cx, |multibuffer, cx| {
13491 let excerpt_ids = multibuffer.excerpt_ids();
13492 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13493 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13494 });
13495 });
13496
13497 // Apply the update of removing the excerpts.
13498 follower_1
13499 .update_in(cx, |follower, window, cx| {
13500 follower.apply_update_proto(
13501 &project,
13502 update_message.borrow().clone().unwrap(),
13503 window,
13504 cx,
13505 )
13506 })
13507 .await
13508 .unwrap();
13509 follower_2
13510 .update_in(cx, |follower, window, cx| {
13511 follower.apply_update_proto(
13512 &project,
13513 update_message.borrow().clone().unwrap(),
13514 window,
13515 cx,
13516 )
13517 })
13518 .await
13519 .unwrap();
13520 update_message.borrow_mut().take();
13521 assert_eq!(
13522 follower_1.update(cx, |editor, cx| editor.text(cx)),
13523 leader.update(cx, |editor, cx| editor.text(cx))
13524 );
13525}
13526
13527#[gpui::test]
13528async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13529 init_test(cx, |_| {});
13530
13531 let mut cx = EditorTestContext::new(cx).await;
13532 let lsp_store =
13533 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13534
13535 cx.set_state(indoc! {"
13536 ˇfn func(abc def: i32) -> u32 {
13537 }
13538 "});
13539
13540 cx.update(|_, cx| {
13541 lsp_store.update(cx, |lsp_store, cx| {
13542 lsp_store
13543 .update_diagnostics(
13544 LanguageServerId(0),
13545 lsp::PublishDiagnosticsParams {
13546 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13547 version: None,
13548 diagnostics: vec![
13549 lsp::Diagnostic {
13550 range: lsp::Range::new(
13551 lsp::Position::new(0, 11),
13552 lsp::Position::new(0, 12),
13553 ),
13554 severity: Some(lsp::DiagnosticSeverity::ERROR),
13555 ..Default::default()
13556 },
13557 lsp::Diagnostic {
13558 range: lsp::Range::new(
13559 lsp::Position::new(0, 12),
13560 lsp::Position::new(0, 15),
13561 ),
13562 severity: Some(lsp::DiagnosticSeverity::ERROR),
13563 ..Default::default()
13564 },
13565 lsp::Diagnostic {
13566 range: lsp::Range::new(
13567 lsp::Position::new(0, 25),
13568 lsp::Position::new(0, 28),
13569 ),
13570 severity: Some(lsp::DiagnosticSeverity::ERROR),
13571 ..Default::default()
13572 },
13573 ],
13574 },
13575 &[],
13576 cx,
13577 )
13578 .unwrap()
13579 });
13580 });
13581
13582 executor.run_until_parked();
13583
13584 cx.update_editor(|editor, window, cx| {
13585 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13586 });
13587
13588 cx.assert_editor_state(indoc! {"
13589 fn func(abc def: i32) -> ˇu32 {
13590 }
13591 "});
13592
13593 cx.update_editor(|editor, window, cx| {
13594 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13595 });
13596
13597 cx.assert_editor_state(indoc! {"
13598 fn func(abc ˇdef: i32) -> u32 {
13599 }
13600 "});
13601
13602 cx.update_editor(|editor, window, cx| {
13603 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13604 });
13605
13606 cx.assert_editor_state(indoc! {"
13607 fn func(abcˇ def: i32) -> u32 {
13608 }
13609 "});
13610
13611 cx.update_editor(|editor, window, cx| {
13612 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13613 });
13614
13615 cx.assert_editor_state(indoc! {"
13616 fn func(abc def: i32) -> ˇu32 {
13617 }
13618 "});
13619}
13620
13621#[gpui::test]
13622async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13623 init_test(cx, |_| {});
13624
13625 let mut cx = EditorTestContext::new(cx).await;
13626
13627 let diff_base = r#"
13628 use some::mod;
13629
13630 const A: u32 = 42;
13631
13632 fn main() {
13633 println!("hello");
13634
13635 println!("world");
13636 }
13637 "#
13638 .unindent();
13639
13640 // Edits are modified, removed, modified, added
13641 cx.set_state(
13642 &r#"
13643 use some::modified;
13644
13645 ˇ
13646 fn main() {
13647 println!("hello there");
13648
13649 println!("around the");
13650 println!("world");
13651 }
13652 "#
13653 .unindent(),
13654 );
13655
13656 cx.set_head_text(&diff_base);
13657 executor.run_until_parked();
13658
13659 cx.update_editor(|editor, window, cx| {
13660 //Wrap around the bottom of the buffer
13661 for _ in 0..3 {
13662 editor.go_to_next_hunk(&GoToHunk, window, cx);
13663 }
13664 });
13665
13666 cx.assert_editor_state(
13667 &r#"
13668 ˇuse some::modified;
13669
13670
13671 fn main() {
13672 println!("hello there");
13673
13674 println!("around the");
13675 println!("world");
13676 }
13677 "#
13678 .unindent(),
13679 );
13680
13681 cx.update_editor(|editor, window, cx| {
13682 //Wrap around the top of the buffer
13683 for _ in 0..2 {
13684 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13685 }
13686 });
13687
13688 cx.assert_editor_state(
13689 &r#"
13690 use some::modified;
13691
13692
13693 fn main() {
13694 ˇ println!("hello there");
13695
13696 println!("around the");
13697 println!("world");
13698 }
13699 "#
13700 .unindent(),
13701 );
13702
13703 cx.update_editor(|editor, window, cx| {
13704 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13705 });
13706
13707 cx.assert_editor_state(
13708 &r#"
13709 use some::modified;
13710
13711 ˇ
13712 fn main() {
13713 println!("hello there");
13714
13715 println!("around the");
13716 println!("world");
13717 }
13718 "#
13719 .unindent(),
13720 );
13721
13722 cx.update_editor(|editor, window, cx| {
13723 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13724 });
13725
13726 cx.assert_editor_state(
13727 &r#"
13728 ˇuse some::modified;
13729
13730
13731 fn main() {
13732 println!("hello there");
13733
13734 println!("around the");
13735 println!("world");
13736 }
13737 "#
13738 .unindent(),
13739 );
13740
13741 cx.update_editor(|editor, window, cx| {
13742 for _ in 0..2 {
13743 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13744 }
13745 });
13746
13747 cx.assert_editor_state(
13748 &r#"
13749 use some::modified;
13750
13751
13752 fn main() {
13753 ˇ println!("hello there");
13754
13755 println!("around the");
13756 println!("world");
13757 }
13758 "#
13759 .unindent(),
13760 );
13761
13762 cx.update_editor(|editor, window, cx| {
13763 editor.fold(&Fold, window, cx);
13764 });
13765
13766 cx.update_editor(|editor, window, cx| {
13767 editor.go_to_next_hunk(&GoToHunk, window, cx);
13768 });
13769
13770 cx.assert_editor_state(
13771 &r#"
13772 ˇuse some::modified;
13773
13774
13775 fn main() {
13776 println!("hello there");
13777
13778 println!("around the");
13779 println!("world");
13780 }
13781 "#
13782 .unindent(),
13783 );
13784}
13785
13786#[test]
13787fn test_split_words() {
13788 fn split(text: &str) -> Vec<&str> {
13789 split_words(text).collect()
13790 }
13791
13792 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13793 assert_eq!(split("hello_world"), &["hello_", "world"]);
13794 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13795 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13796 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13797 assert_eq!(split("helloworld"), &["helloworld"]);
13798
13799 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13800}
13801
13802#[gpui::test]
13803async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13804 init_test(cx, |_| {});
13805
13806 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13807 let mut assert = |before, after| {
13808 let _state_context = cx.set_state(before);
13809 cx.run_until_parked();
13810 cx.update_editor(|editor, window, cx| {
13811 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13812 });
13813 cx.run_until_parked();
13814 cx.assert_editor_state(after);
13815 };
13816
13817 // Outside bracket jumps to outside of matching bracket
13818 assert("console.logˇ(var);", "console.log(var)ˇ;");
13819 assert("console.log(var)ˇ;", "console.logˇ(var);");
13820
13821 // Inside bracket jumps to inside of matching bracket
13822 assert("console.log(ˇvar);", "console.log(varˇ);");
13823 assert("console.log(varˇ);", "console.log(ˇvar);");
13824
13825 // When outside a bracket and inside, favor jumping to the inside bracket
13826 assert(
13827 "console.log('foo', [1, 2, 3]ˇ);",
13828 "console.log(ˇ'foo', [1, 2, 3]);",
13829 );
13830 assert(
13831 "console.log(ˇ'foo', [1, 2, 3]);",
13832 "console.log('foo', [1, 2, 3]ˇ);",
13833 );
13834
13835 // Bias forward if two options are equally likely
13836 assert(
13837 "let result = curried_fun()ˇ();",
13838 "let result = curried_fun()()ˇ;",
13839 );
13840
13841 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13842 assert(
13843 indoc! {"
13844 function test() {
13845 console.log('test')ˇ
13846 }"},
13847 indoc! {"
13848 function test() {
13849 console.logˇ('test')
13850 }"},
13851 );
13852}
13853
13854#[gpui::test]
13855async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13856 init_test(cx, |_| {});
13857
13858 let fs = FakeFs::new(cx.executor());
13859 fs.insert_tree(
13860 path!("/a"),
13861 json!({
13862 "main.rs": "fn main() { let a = 5; }",
13863 "other.rs": "// Test file",
13864 }),
13865 )
13866 .await;
13867 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13868
13869 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13870 language_registry.add(Arc::new(Language::new(
13871 LanguageConfig {
13872 name: "Rust".into(),
13873 matcher: LanguageMatcher {
13874 path_suffixes: vec!["rs".to_string()],
13875 ..Default::default()
13876 },
13877 brackets: BracketPairConfig {
13878 pairs: vec![BracketPair {
13879 start: "{".to_string(),
13880 end: "}".to_string(),
13881 close: true,
13882 surround: true,
13883 newline: true,
13884 }],
13885 disabled_scopes_by_bracket_ix: Vec::new(),
13886 },
13887 ..Default::default()
13888 },
13889 Some(tree_sitter_rust::LANGUAGE.into()),
13890 )));
13891 let mut fake_servers = language_registry.register_fake_lsp(
13892 "Rust",
13893 FakeLspAdapter {
13894 capabilities: lsp::ServerCapabilities {
13895 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13896 first_trigger_character: "{".to_string(),
13897 more_trigger_character: None,
13898 }),
13899 ..Default::default()
13900 },
13901 ..Default::default()
13902 },
13903 );
13904
13905 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13906
13907 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13908
13909 let worktree_id = workspace
13910 .update(cx, |workspace, _, cx| {
13911 workspace.project().update(cx, |project, cx| {
13912 project.worktrees(cx).next().unwrap().read(cx).id()
13913 })
13914 })
13915 .unwrap();
13916
13917 let buffer = project
13918 .update(cx, |project, cx| {
13919 project.open_local_buffer(path!("/a/main.rs"), cx)
13920 })
13921 .await
13922 .unwrap();
13923 let editor_handle = workspace
13924 .update(cx, |workspace, window, cx| {
13925 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13926 })
13927 .unwrap()
13928 .await
13929 .unwrap()
13930 .downcast::<Editor>()
13931 .unwrap();
13932
13933 cx.executor().start_waiting();
13934 let fake_server = fake_servers.next().await.unwrap();
13935
13936 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13937 |params, _| async move {
13938 assert_eq!(
13939 params.text_document_position.text_document.uri,
13940 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13941 );
13942 assert_eq!(
13943 params.text_document_position.position,
13944 lsp::Position::new(0, 21),
13945 );
13946
13947 Ok(Some(vec![lsp::TextEdit {
13948 new_text: "]".to_string(),
13949 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13950 }]))
13951 },
13952 );
13953
13954 editor_handle.update_in(cx, |editor, window, cx| {
13955 window.focus(&editor.focus_handle(cx));
13956 editor.change_selections(None, window, cx, |s| {
13957 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13958 });
13959 editor.handle_input("{", window, cx);
13960 });
13961
13962 cx.executor().run_until_parked();
13963
13964 buffer.update(cx, |buffer, _| {
13965 assert_eq!(
13966 buffer.text(),
13967 "fn main() { let a = {5}; }",
13968 "No extra braces from on type formatting should appear in the buffer"
13969 )
13970 });
13971}
13972
13973#[gpui::test]
13974async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13975 init_test(cx, |_| {});
13976
13977 let fs = FakeFs::new(cx.executor());
13978 fs.insert_tree(
13979 path!("/a"),
13980 json!({
13981 "main.rs": "fn main() { let a = 5; }",
13982 "other.rs": "// Test file",
13983 }),
13984 )
13985 .await;
13986
13987 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13988
13989 let server_restarts = Arc::new(AtomicUsize::new(0));
13990 let closure_restarts = Arc::clone(&server_restarts);
13991 let language_server_name = "test language server";
13992 let language_name: LanguageName = "Rust".into();
13993
13994 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13995 language_registry.add(Arc::new(Language::new(
13996 LanguageConfig {
13997 name: language_name.clone(),
13998 matcher: LanguageMatcher {
13999 path_suffixes: vec!["rs".to_string()],
14000 ..Default::default()
14001 },
14002 ..Default::default()
14003 },
14004 Some(tree_sitter_rust::LANGUAGE.into()),
14005 )));
14006 let mut fake_servers = language_registry.register_fake_lsp(
14007 "Rust",
14008 FakeLspAdapter {
14009 name: language_server_name,
14010 initialization_options: Some(json!({
14011 "testOptionValue": true
14012 })),
14013 initializer: Some(Box::new(move |fake_server| {
14014 let task_restarts = Arc::clone(&closure_restarts);
14015 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14016 task_restarts.fetch_add(1, atomic::Ordering::Release);
14017 futures::future::ready(Ok(()))
14018 });
14019 })),
14020 ..Default::default()
14021 },
14022 );
14023
14024 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14025 let _buffer = project
14026 .update(cx, |project, cx| {
14027 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14028 })
14029 .await
14030 .unwrap();
14031 let _fake_server = fake_servers.next().await.unwrap();
14032 update_test_language_settings(cx, |language_settings| {
14033 language_settings.languages.insert(
14034 language_name.clone(),
14035 LanguageSettingsContent {
14036 tab_size: NonZeroU32::new(8),
14037 ..Default::default()
14038 },
14039 );
14040 });
14041 cx.executor().run_until_parked();
14042 assert_eq!(
14043 server_restarts.load(atomic::Ordering::Acquire),
14044 0,
14045 "Should not restart LSP server on an unrelated change"
14046 );
14047
14048 update_test_project_settings(cx, |project_settings| {
14049 project_settings.lsp.insert(
14050 "Some other server name".into(),
14051 LspSettings {
14052 binary: None,
14053 settings: None,
14054 initialization_options: Some(json!({
14055 "some other init value": false
14056 })),
14057 enable_lsp_tasks: false,
14058 },
14059 );
14060 });
14061 cx.executor().run_until_parked();
14062 assert_eq!(
14063 server_restarts.load(atomic::Ordering::Acquire),
14064 0,
14065 "Should not restart LSP server on an unrelated LSP settings change"
14066 );
14067
14068 update_test_project_settings(cx, |project_settings| {
14069 project_settings.lsp.insert(
14070 language_server_name.into(),
14071 LspSettings {
14072 binary: None,
14073 settings: None,
14074 initialization_options: Some(json!({
14075 "anotherInitValue": false
14076 })),
14077 enable_lsp_tasks: false,
14078 },
14079 );
14080 });
14081 cx.executor().run_until_parked();
14082 assert_eq!(
14083 server_restarts.load(atomic::Ordering::Acquire),
14084 1,
14085 "Should restart LSP server on a related LSP settings change"
14086 );
14087
14088 update_test_project_settings(cx, |project_settings| {
14089 project_settings.lsp.insert(
14090 language_server_name.into(),
14091 LspSettings {
14092 binary: None,
14093 settings: None,
14094 initialization_options: Some(json!({
14095 "anotherInitValue": false
14096 })),
14097 enable_lsp_tasks: false,
14098 },
14099 );
14100 });
14101 cx.executor().run_until_parked();
14102 assert_eq!(
14103 server_restarts.load(atomic::Ordering::Acquire),
14104 1,
14105 "Should not restart LSP server on a related LSP settings change that is the same"
14106 );
14107
14108 update_test_project_settings(cx, |project_settings| {
14109 project_settings.lsp.insert(
14110 language_server_name.into(),
14111 LspSettings {
14112 binary: None,
14113 settings: None,
14114 initialization_options: None,
14115 enable_lsp_tasks: false,
14116 },
14117 );
14118 });
14119 cx.executor().run_until_parked();
14120 assert_eq!(
14121 server_restarts.load(atomic::Ordering::Acquire),
14122 2,
14123 "Should restart LSP server on another related LSP settings change"
14124 );
14125}
14126
14127#[gpui::test]
14128async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14129 init_test(cx, |_| {});
14130
14131 let mut cx = EditorLspTestContext::new_rust(
14132 lsp::ServerCapabilities {
14133 completion_provider: Some(lsp::CompletionOptions {
14134 trigger_characters: Some(vec![".".to_string()]),
14135 resolve_provider: Some(true),
14136 ..Default::default()
14137 }),
14138 ..Default::default()
14139 },
14140 cx,
14141 )
14142 .await;
14143
14144 cx.set_state("fn main() { let a = 2ˇ; }");
14145 cx.simulate_keystroke(".");
14146 let completion_item = lsp::CompletionItem {
14147 label: "some".into(),
14148 kind: Some(lsp::CompletionItemKind::SNIPPET),
14149 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14150 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14151 kind: lsp::MarkupKind::Markdown,
14152 value: "```rust\nSome(2)\n```".to_string(),
14153 })),
14154 deprecated: Some(false),
14155 sort_text: Some("fffffff2".to_string()),
14156 filter_text: Some("some".to_string()),
14157 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14158 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14159 range: lsp::Range {
14160 start: lsp::Position {
14161 line: 0,
14162 character: 22,
14163 },
14164 end: lsp::Position {
14165 line: 0,
14166 character: 22,
14167 },
14168 },
14169 new_text: "Some(2)".to_string(),
14170 })),
14171 additional_text_edits: Some(vec![lsp::TextEdit {
14172 range: lsp::Range {
14173 start: lsp::Position {
14174 line: 0,
14175 character: 20,
14176 },
14177 end: lsp::Position {
14178 line: 0,
14179 character: 22,
14180 },
14181 },
14182 new_text: "".to_string(),
14183 }]),
14184 ..Default::default()
14185 };
14186
14187 let closure_completion_item = completion_item.clone();
14188 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14189 let task_completion_item = closure_completion_item.clone();
14190 async move {
14191 Ok(Some(lsp::CompletionResponse::Array(vec![
14192 task_completion_item,
14193 ])))
14194 }
14195 });
14196
14197 request.next().await;
14198
14199 cx.condition(|editor, _| editor.context_menu_visible())
14200 .await;
14201 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14202 editor
14203 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14204 .unwrap()
14205 });
14206 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14207
14208 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14209 let task_completion_item = completion_item.clone();
14210 async move { Ok(task_completion_item) }
14211 })
14212 .next()
14213 .await
14214 .unwrap();
14215 apply_additional_edits.await.unwrap();
14216 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14217}
14218
14219#[gpui::test]
14220async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14221 init_test(cx, |_| {});
14222
14223 let mut cx = EditorLspTestContext::new_rust(
14224 lsp::ServerCapabilities {
14225 completion_provider: Some(lsp::CompletionOptions {
14226 trigger_characters: Some(vec![".".to_string()]),
14227 resolve_provider: Some(true),
14228 ..Default::default()
14229 }),
14230 ..Default::default()
14231 },
14232 cx,
14233 )
14234 .await;
14235
14236 cx.set_state("fn main() { let a = 2ˇ; }");
14237 cx.simulate_keystroke(".");
14238
14239 let item1 = lsp::CompletionItem {
14240 label: "method id()".to_string(),
14241 filter_text: Some("id".to_string()),
14242 detail: None,
14243 documentation: None,
14244 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14245 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14246 new_text: ".id".to_string(),
14247 })),
14248 ..lsp::CompletionItem::default()
14249 };
14250
14251 let item2 = lsp::CompletionItem {
14252 label: "other".to_string(),
14253 filter_text: Some("other".to_string()),
14254 detail: None,
14255 documentation: None,
14256 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14257 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14258 new_text: ".other".to_string(),
14259 })),
14260 ..lsp::CompletionItem::default()
14261 };
14262
14263 let item1 = item1.clone();
14264 cx.set_request_handler::<lsp::request::Completion, _, _>({
14265 let item1 = item1.clone();
14266 move |_, _, _| {
14267 let item1 = item1.clone();
14268 let item2 = item2.clone();
14269 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14270 }
14271 })
14272 .next()
14273 .await;
14274
14275 cx.condition(|editor, _| editor.context_menu_visible())
14276 .await;
14277 cx.update_editor(|editor, _, _| {
14278 let context_menu = editor.context_menu.borrow_mut();
14279 let context_menu = context_menu
14280 .as_ref()
14281 .expect("Should have the context menu deployed");
14282 match context_menu {
14283 CodeContextMenu::Completions(completions_menu) => {
14284 let completions = completions_menu.completions.borrow_mut();
14285 assert_eq!(
14286 completions
14287 .iter()
14288 .map(|completion| &completion.label.text)
14289 .collect::<Vec<_>>(),
14290 vec!["method id()", "other"]
14291 )
14292 }
14293 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14294 }
14295 });
14296
14297 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14298 let item1 = item1.clone();
14299 move |_, item_to_resolve, _| {
14300 let item1 = item1.clone();
14301 async move {
14302 if item1 == item_to_resolve {
14303 Ok(lsp::CompletionItem {
14304 label: "method id()".to_string(),
14305 filter_text: Some("id".to_string()),
14306 detail: Some("Now resolved!".to_string()),
14307 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14308 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14309 range: lsp::Range::new(
14310 lsp::Position::new(0, 22),
14311 lsp::Position::new(0, 22),
14312 ),
14313 new_text: ".id".to_string(),
14314 })),
14315 ..lsp::CompletionItem::default()
14316 })
14317 } else {
14318 Ok(item_to_resolve)
14319 }
14320 }
14321 }
14322 })
14323 .next()
14324 .await
14325 .unwrap();
14326 cx.run_until_parked();
14327
14328 cx.update_editor(|editor, window, cx| {
14329 editor.context_menu_next(&Default::default(), window, cx);
14330 });
14331
14332 cx.update_editor(|editor, _, _| {
14333 let context_menu = editor.context_menu.borrow_mut();
14334 let context_menu = context_menu
14335 .as_ref()
14336 .expect("Should have the context menu deployed");
14337 match context_menu {
14338 CodeContextMenu::Completions(completions_menu) => {
14339 let completions = completions_menu.completions.borrow_mut();
14340 assert_eq!(
14341 completions
14342 .iter()
14343 .map(|completion| &completion.label.text)
14344 .collect::<Vec<_>>(),
14345 vec!["method id() Now resolved!", "other"],
14346 "Should update first completion label, but not second as the filter text did not match."
14347 );
14348 }
14349 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14350 }
14351 });
14352}
14353
14354#[gpui::test]
14355async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14356 init_test(cx, |_| {});
14357 let mut cx = EditorLspTestContext::new_rust(
14358 lsp::ServerCapabilities {
14359 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14360 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14361 completion_provider: Some(lsp::CompletionOptions {
14362 resolve_provider: Some(true),
14363 ..Default::default()
14364 }),
14365 ..Default::default()
14366 },
14367 cx,
14368 )
14369 .await;
14370 cx.set_state(indoc! {"
14371 struct TestStruct {
14372 field: i32
14373 }
14374
14375 fn mainˇ() {
14376 let unused_var = 42;
14377 let test_struct = TestStruct { field: 42 };
14378 }
14379 "});
14380 let symbol_range = cx.lsp_range(indoc! {"
14381 struct TestStruct {
14382 field: i32
14383 }
14384
14385 «fn main»() {
14386 let unused_var = 42;
14387 let test_struct = TestStruct { field: 42 };
14388 }
14389 "});
14390 let mut hover_requests =
14391 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14392 Ok(Some(lsp::Hover {
14393 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14394 kind: lsp::MarkupKind::Markdown,
14395 value: "Function documentation".to_string(),
14396 }),
14397 range: Some(symbol_range),
14398 }))
14399 });
14400
14401 // Case 1: Test that code action menu hide hover popover
14402 cx.dispatch_action(Hover);
14403 hover_requests.next().await;
14404 cx.condition(|editor, _| editor.hover_state.visible()).await;
14405 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14406 move |_, _, _| async move {
14407 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14408 lsp::CodeAction {
14409 title: "Remove unused variable".to_string(),
14410 kind: Some(CodeActionKind::QUICKFIX),
14411 edit: Some(lsp::WorkspaceEdit {
14412 changes: Some(
14413 [(
14414 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14415 vec![lsp::TextEdit {
14416 range: lsp::Range::new(
14417 lsp::Position::new(5, 4),
14418 lsp::Position::new(5, 27),
14419 ),
14420 new_text: "".to_string(),
14421 }],
14422 )]
14423 .into_iter()
14424 .collect(),
14425 ),
14426 ..Default::default()
14427 }),
14428 ..Default::default()
14429 },
14430 )]))
14431 },
14432 );
14433 cx.update_editor(|editor, window, cx| {
14434 editor.toggle_code_actions(
14435 &ToggleCodeActions {
14436 deployed_from: None,
14437 quick_launch: false,
14438 },
14439 window,
14440 cx,
14441 );
14442 });
14443 code_action_requests.next().await;
14444 cx.run_until_parked();
14445 cx.condition(|editor, _| editor.context_menu_visible())
14446 .await;
14447 cx.update_editor(|editor, _, _| {
14448 assert!(
14449 !editor.hover_state.visible(),
14450 "Hover popover should be hidden when code action menu is shown"
14451 );
14452 // Hide code actions
14453 editor.context_menu.take();
14454 });
14455
14456 // Case 2: Test that code completions hide hover popover
14457 cx.dispatch_action(Hover);
14458 hover_requests.next().await;
14459 cx.condition(|editor, _| editor.hover_state.visible()).await;
14460 let counter = Arc::new(AtomicUsize::new(0));
14461 let mut completion_requests =
14462 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14463 let counter = counter.clone();
14464 async move {
14465 counter.fetch_add(1, atomic::Ordering::Release);
14466 Ok(Some(lsp::CompletionResponse::Array(vec![
14467 lsp::CompletionItem {
14468 label: "main".into(),
14469 kind: Some(lsp::CompletionItemKind::FUNCTION),
14470 detail: Some("() -> ()".to_string()),
14471 ..Default::default()
14472 },
14473 lsp::CompletionItem {
14474 label: "TestStruct".into(),
14475 kind: Some(lsp::CompletionItemKind::STRUCT),
14476 detail: Some("struct TestStruct".to_string()),
14477 ..Default::default()
14478 },
14479 ])))
14480 }
14481 });
14482 cx.update_editor(|editor, window, cx| {
14483 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14484 });
14485 completion_requests.next().await;
14486 cx.condition(|editor, _| editor.context_menu_visible())
14487 .await;
14488 cx.update_editor(|editor, _, _| {
14489 assert!(
14490 !editor.hover_state.visible(),
14491 "Hover popover should be hidden when completion menu is shown"
14492 );
14493 });
14494}
14495
14496#[gpui::test]
14497async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14498 init_test(cx, |_| {});
14499
14500 let mut cx = EditorLspTestContext::new_rust(
14501 lsp::ServerCapabilities {
14502 completion_provider: Some(lsp::CompletionOptions {
14503 trigger_characters: Some(vec![".".to_string()]),
14504 resolve_provider: Some(true),
14505 ..Default::default()
14506 }),
14507 ..Default::default()
14508 },
14509 cx,
14510 )
14511 .await;
14512
14513 cx.set_state("fn main() { let a = 2ˇ; }");
14514 cx.simulate_keystroke(".");
14515
14516 let unresolved_item_1 = lsp::CompletionItem {
14517 label: "id".to_string(),
14518 filter_text: Some("id".to_string()),
14519 detail: None,
14520 documentation: None,
14521 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14522 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14523 new_text: ".id".to_string(),
14524 })),
14525 ..lsp::CompletionItem::default()
14526 };
14527 let resolved_item_1 = lsp::CompletionItem {
14528 additional_text_edits: Some(vec![lsp::TextEdit {
14529 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14530 new_text: "!!".to_string(),
14531 }]),
14532 ..unresolved_item_1.clone()
14533 };
14534 let unresolved_item_2 = lsp::CompletionItem {
14535 label: "other".to_string(),
14536 filter_text: Some("other".to_string()),
14537 detail: None,
14538 documentation: None,
14539 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14540 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14541 new_text: ".other".to_string(),
14542 })),
14543 ..lsp::CompletionItem::default()
14544 };
14545 let resolved_item_2 = lsp::CompletionItem {
14546 additional_text_edits: Some(vec![lsp::TextEdit {
14547 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14548 new_text: "??".to_string(),
14549 }]),
14550 ..unresolved_item_2.clone()
14551 };
14552
14553 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14554 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14555 cx.lsp
14556 .server
14557 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14558 let unresolved_item_1 = unresolved_item_1.clone();
14559 let resolved_item_1 = resolved_item_1.clone();
14560 let unresolved_item_2 = unresolved_item_2.clone();
14561 let resolved_item_2 = resolved_item_2.clone();
14562 let resolve_requests_1 = resolve_requests_1.clone();
14563 let resolve_requests_2 = resolve_requests_2.clone();
14564 move |unresolved_request, _| {
14565 let unresolved_item_1 = unresolved_item_1.clone();
14566 let resolved_item_1 = resolved_item_1.clone();
14567 let unresolved_item_2 = unresolved_item_2.clone();
14568 let resolved_item_2 = resolved_item_2.clone();
14569 let resolve_requests_1 = resolve_requests_1.clone();
14570 let resolve_requests_2 = resolve_requests_2.clone();
14571 async move {
14572 if unresolved_request == unresolved_item_1 {
14573 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14574 Ok(resolved_item_1.clone())
14575 } else if unresolved_request == unresolved_item_2 {
14576 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14577 Ok(resolved_item_2.clone())
14578 } else {
14579 panic!("Unexpected completion item {unresolved_request:?}")
14580 }
14581 }
14582 }
14583 })
14584 .detach();
14585
14586 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14587 let unresolved_item_1 = unresolved_item_1.clone();
14588 let unresolved_item_2 = unresolved_item_2.clone();
14589 async move {
14590 Ok(Some(lsp::CompletionResponse::Array(vec![
14591 unresolved_item_1,
14592 unresolved_item_2,
14593 ])))
14594 }
14595 })
14596 .next()
14597 .await;
14598
14599 cx.condition(|editor, _| editor.context_menu_visible())
14600 .await;
14601 cx.update_editor(|editor, _, _| {
14602 let context_menu = editor.context_menu.borrow_mut();
14603 let context_menu = context_menu
14604 .as_ref()
14605 .expect("Should have the context menu deployed");
14606 match context_menu {
14607 CodeContextMenu::Completions(completions_menu) => {
14608 let completions = completions_menu.completions.borrow_mut();
14609 assert_eq!(
14610 completions
14611 .iter()
14612 .map(|completion| &completion.label.text)
14613 .collect::<Vec<_>>(),
14614 vec!["id", "other"]
14615 )
14616 }
14617 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14618 }
14619 });
14620 cx.run_until_parked();
14621
14622 cx.update_editor(|editor, window, cx| {
14623 editor.context_menu_next(&ContextMenuNext, window, cx);
14624 });
14625 cx.run_until_parked();
14626 cx.update_editor(|editor, window, cx| {
14627 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14628 });
14629 cx.run_until_parked();
14630 cx.update_editor(|editor, window, cx| {
14631 editor.context_menu_next(&ContextMenuNext, window, cx);
14632 });
14633 cx.run_until_parked();
14634 cx.update_editor(|editor, window, cx| {
14635 editor
14636 .compose_completion(&ComposeCompletion::default(), window, cx)
14637 .expect("No task returned")
14638 })
14639 .await
14640 .expect("Completion failed");
14641 cx.run_until_parked();
14642
14643 cx.update_editor(|editor, _, cx| {
14644 assert_eq!(
14645 resolve_requests_1.load(atomic::Ordering::Acquire),
14646 1,
14647 "Should always resolve once despite multiple selections"
14648 );
14649 assert_eq!(
14650 resolve_requests_2.load(atomic::Ordering::Acquire),
14651 1,
14652 "Should always resolve once after multiple selections and applying the completion"
14653 );
14654 assert_eq!(
14655 editor.text(cx),
14656 "fn main() { let a = ??.other; }",
14657 "Should use resolved data when applying the completion"
14658 );
14659 });
14660}
14661
14662#[gpui::test]
14663async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14664 init_test(cx, |_| {});
14665
14666 let item_0 = lsp::CompletionItem {
14667 label: "abs".into(),
14668 insert_text: Some("abs".into()),
14669 data: Some(json!({ "very": "special"})),
14670 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14671 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14672 lsp::InsertReplaceEdit {
14673 new_text: "abs".to_string(),
14674 insert: lsp::Range::default(),
14675 replace: lsp::Range::default(),
14676 },
14677 )),
14678 ..lsp::CompletionItem::default()
14679 };
14680 let items = iter::once(item_0.clone())
14681 .chain((11..51).map(|i| lsp::CompletionItem {
14682 label: format!("item_{}", i),
14683 insert_text: Some(format!("item_{}", i)),
14684 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14685 ..lsp::CompletionItem::default()
14686 }))
14687 .collect::<Vec<_>>();
14688
14689 let default_commit_characters = vec!["?".to_string()];
14690 let default_data = json!({ "default": "data"});
14691 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14692 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14693 let default_edit_range = lsp::Range {
14694 start: lsp::Position {
14695 line: 0,
14696 character: 5,
14697 },
14698 end: lsp::Position {
14699 line: 0,
14700 character: 5,
14701 },
14702 };
14703
14704 let mut cx = EditorLspTestContext::new_rust(
14705 lsp::ServerCapabilities {
14706 completion_provider: Some(lsp::CompletionOptions {
14707 trigger_characters: Some(vec![".".to_string()]),
14708 resolve_provider: Some(true),
14709 ..Default::default()
14710 }),
14711 ..Default::default()
14712 },
14713 cx,
14714 )
14715 .await;
14716
14717 cx.set_state("fn main() { let a = 2ˇ; }");
14718 cx.simulate_keystroke(".");
14719
14720 let completion_data = default_data.clone();
14721 let completion_characters = default_commit_characters.clone();
14722 let completion_items = items.clone();
14723 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14724 let default_data = completion_data.clone();
14725 let default_commit_characters = completion_characters.clone();
14726 let items = completion_items.clone();
14727 async move {
14728 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14729 items,
14730 item_defaults: Some(lsp::CompletionListItemDefaults {
14731 data: Some(default_data.clone()),
14732 commit_characters: Some(default_commit_characters.clone()),
14733 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14734 default_edit_range,
14735 )),
14736 insert_text_format: Some(default_insert_text_format),
14737 insert_text_mode: Some(default_insert_text_mode),
14738 }),
14739 ..lsp::CompletionList::default()
14740 })))
14741 }
14742 })
14743 .next()
14744 .await;
14745
14746 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14747 cx.lsp
14748 .server
14749 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14750 let closure_resolved_items = resolved_items.clone();
14751 move |item_to_resolve, _| {
14752 let closure_resolved_items = closure_resolved_items.clone();
14753 async move {
14754 closure_resolved_items.lock().push(item_to_resolve.clone());
14755 Ok(item_to_resolve)
14756 }
14757 }
14758 })
14759 .detach();
14760
14761 cx.condition(|editor, _| editor.context_menu_visible())
14762 .await;
14763 cx.run_until_parked();
14764 cx.update_editor(|editor, _, _| {
14765 let menu = editor.context_menu.borrow_mut();
14766 match menu.as_ref().expect("should have the completions menu") {
14767 CodeContextMenu::Completions(completions_menu) => {
14768 assert_eq!(
14769 completions_menu
14770 .entries
14771 .borrow()
14772 .iter()
14773 .map(|mat| mat.string.clone())
14774 .collect::<Vec<String>>(),
14775 items
14776 .iter()
14777 .map(|completion| completion.label.clone())
14778 .collect::<Vec<String>>()
14779 );
14780 }
14781 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14782 }
14783 });
14784 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14785 // with 4 from the end.
14786 assert_eq!(
14787 *resolved_items.lock(),
14788 [&items[0..16], &items[items.len() - 4..items.len()]]
14789 .concat()
14790 .iter()
14791 .cloned()
14792 .map(|mut item| {
14793 if item.data.is_none() {
14794 item.data = Some(default_data.clone());
14795 }
14796 item
14797 })
14798 .collect::<Vec<lsp::CompletionItem>>(),
14799 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14800 );
14801 resolved_items.lock().clear();
14802
14803 cx.update_editor(|editor, window, cx| {
14804 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14805 });
14806 cx.run_until_parked();
14807 // Completions that have already been resolved are skipped.
14808 assert_eq!(
14809 *resolved_items.lock(),
14810 items[items.len() - 16..items.len() - 4]
14811 .iter()
14812 .cloned()
14813 .map(|mut item| {
14814 if item.data.is_none() {
14815 item.data = Some(default_data.clone());
14816 }
14817 item
14818 })
14819 .collect::<Vec<lsp::CompletionItem>>()
14820 );
14821 resolved_items.lock().clear();
14822}
14823
14824#[gpui::test]
14825async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14826 init_test(cx, |_| {});
14827
14828 let mut cx = EditorLspTestContext::new(
14829 Language::new(
14830 LanguageConfig {
14831 matcher: LanguageMatcher {
14832 path_suffixes: vec!["jsx".into()],
14833 ..Default::default()
14834 },
14835 overrides: [(
14836 "element".into(),
14837 LanguageConfigOverride {
14838 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14839 ..Default::default()
14840 },
14841 )]
14842 .into_iter()
14843 .collect(),
14844 ..Default::default()
14845 },
14846 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14847 )
14848 .with_override_query("(jsx_self_closing_element) @element")
14849 .unwrap(),
14850 lsp::ServerCapabilities {
14851 completion_provider: Some(lsp::CompletionOptions {
14852 trigger_characters: Some(vec![":".to_string()]),
14853 ..Default::default()
14854 }),
14855 ..Default::default()
14856 },
14857 cx,
14858 )
14859 .await;
14860
14861 cx.lsp
14862 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14863 Ok(Some(lsp::CompletionResponse::Array(vec![
14864 lsp::CompletionItem {
14865 label: "bg-blue".into(),
14866 ..Default::default()
14867 },
14868 lsp::CompletionItem {
14869 label: "bg-red".into(),
14870 ..Default::default()
14871 },
14872 lsp::CompletionItem {
14873 label: "bg-yellow".into(),
14874 ..Default::default()
14875 },
14876 ])))
14877 });
14878
14879 cx.set_state(r#"<p class="bgˇ" />"#);
14880
14881 // Trigger completion when typing a dash, because the dash is an extra
14882 // word character in the 'element' scope, which contains the cursor.
14883 cx.simulate_keystroke("-");
14884 cx.executor().run_until_parked();
14885 cx.update_editor(|editor, _, _| {
14886 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14887 {
14888 assert_eq!(
14889 completion_menu_entries(&menu),
14890 &["bg-red", "bg-blue", "bg-yellow"]
14891 );
14892 } else {
14893 panic!("expected completion menu to be open");
14894 }
14895 });
14896
14897 cx.simulate_keystroke("l");
14898 cx.executor().run_until_parked();
14899 cx.update_editor(|editor, _, _| {
14900 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14901 {
14902 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14903 } else {
14904 panic!("expected completion menu to be open");
14905 }
14906 });
14907
14908 // When filtering completions, consider the character after the '-' to
14909 // be the start of a subword.
14910 cx.set_state(r#"<p class="yelˇ" />"#);
14911 cx.simulate_keystroke("l");
14912 cx.executor().run_until_parked();
14913 cx.update_editor(|editor, _, _| {
14914 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14915 {
14916 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14917 } else {
14918 panic!("expected completion menu to be open");
14919 }
14920 });
14921}
14922
14923fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14924 let entries = menu.entries.borrow();
14925 entries.iter().map(|mat| mat.string.clone()).collect()
14926}
14927
14928#[gpui::test]
14929async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14930 init_test(cx, |settings| {
14931 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14932 FormatterList(vec![Formatter::Prettier].into()),
14933 ))
14934 });
14935
14936 let fs = FakeFs::new(cx.executor());
14937 fs.insert_file(path!("/file.ts"), Default::default()).await;
14938
14939 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14940 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14941
14942 language_registry.add(Arc::new(Language::new(
14943 LanguageConfig {
14944 name: "TypeScript".into(),
14945 matcher: LanguageMatcher {
14946 path_suffixes: vec!["ts".to_string()],
14947 ..Default::default()
14948 },
14949 ..Default::default()
14950 },
14951 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14952 )));
14953 update_test_language_settings(cx, |settings| {
14954 settings.defaults.prettier = Some(PrettierSettings {
14955 allowed: true,
14956 ..PrettierSettings::default()
14957 });
14958 });
14959
14960 let test_plugin = "test_plugin";
14961 let _ = language_registry.register_fake_lsp(
14962 "TypeScript",
14963 FakeLspAdapter {
14964 prettier_plugins: vec![test_plugin],
14965 ..Default::default()
14966 },
14967 );
14968
14969 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14970 let buffer = project
14971 .update(cx, |project, cx| {
14972 project.open_local_buffer(path!("/file.ts"), cx)
14973 })
14974 .await
14975 .unwrap();
14976
14977 let buffer_text = "one\ntwo\nthree\n";
14978 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14979 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14980 editor.update_in(cx, |editor, window, cx| {
14981 editor.set_text(buffer_text, window, cx)
14982 });
14983
14984 editor
14985 .update_in(cx, |editor, window, cx| {
14986 editor.perform_format(
14987 project.clone(),
14988 FormatTrigger::Manual,
14989 FormatTarget::Buffers,
14990 window,
14991 cx,
14992 )
14993 })
14994 .unwrap()
14995 .await;
14996 assert_eq!(
14997 editor.update(cx, |editor, cx| editor.text(cx)),
14998 buffer_text.to_string() + prettier_format_suffix,
14999 "Test prettier formatting was not applied to the original buffer text",
15000 );
15001
15002 update_test_language_settings(cx, |settings| {
15003 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15004 });
15005 let format = editor.update_in(cx, |editor, window, cx| {
15006 editor.perform_format(
15007 project.clone(),
15008 FormatTrigger::Manual,
15009 FormatTarget::Buffers,
15010 window,
15011 cx,
15012 )
15013 });
15014 format.await.unwrap();
15015 assert_eq!(
15016 editor.update(cx, |editor, cx| editor.text(cx)),
15017 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15018 "Autoformatting (via test prettier) was not applied to the original buffer text",
15019 );
15020}
15021
15022#[gpui::test]
15023async fn test_addition_reverts(cx: &mut TestAppContext) {
15024 init_test(cx, |_| {});
15025 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15026 let base_text = indoc! {r#"
15027 struct Row;
15028 struct Row1;
15029 struct Row2;
15030
15031 struct Row4;
15032 struct Row5;
15033 struct Row6;
15034
15035 struct Row8;
15036 struct Row9;
15037 struct Row10;"#};
15038
15039 // When addition hunks are not adjacent to carets, no hunk revert is performed
15040 assert_hunk_revert(
15041 indoc! {r#"struct Row;
15042 struct Row1;
15043 struct Row1.1;
15044 struct Row1.2;
15045 struct Row2;ˇ
15046
15047 struct Row4;
15048 struct Row5;
15049 struct Row6;
15050
15051 struct Row8;
15052 ˇstruct Row9;
15053 struct Row9.1;
15054 struct Row9.2;
15055 struct Row9.3;
15056 struct Row10;"#},
15057 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15058 indoc! {r#"struct Row;
15059 struct Row1;
15060 struct Row1.1;
15061 struct Row1.2;
15062 struct Row2;ˇ
15063
15064 struct Row4;
15065 struct Row5;
15066 struct Row6;
15067
15068 struct Row8;
15069 ˇstruct Row9;
15070 struct Row9.1;
15071 struct Row9.2;
15072 struct Row9.3;
15073 struct Row10;"#},
15074 base_text,
15075 &mut cx,
15076 );
15077 // Same for selections
15078 assert_hunk_revert(
15079 indoc! {r#"struct Row;
15080 struct Row1;
15081 struct Row2;
15082 struct Row2.1;
15083 struct Row2.2;
15084 «ˇ
15085 struct Row4;
15086 struct» Row5;
15087 «struct Row6;
15088 ˇ»
15089 struct Row9.1;
15090 struct Row9.2;
15091 struct Row9.3;
15092 struct Row8;
15093 struct Row9;
15094 struct Row10;"#},
15095 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15096 indoc! {r#"struct Row;
15097 struct Row1;
15098 struct Row2;
15099 struct Row2.1;
15100 struct Row2.2;
15101 «ˇ
15102 struct Row4;
15103 struct» Row5;
15104 «struct Row6;
15105 ˇ»
15106 struct Row9.1;
15107 struct Row9.2;
15108 struct Row9.3;
15109 struct Row8;
15110 struct Row9;
15111 struct Row10;"#},
15112 base_text,
15113 &mut cx,
15114 );
15115
15116 // When carets and selections intersect the addition hunks, those are reverted.
15117 // Adjacent carets got merged.
15118 assert_hunk_revert(
15119 indoc! {r#"struct Row;
15120 ˇ// something on the top
15121 struct Row1;
15122 struct Row2;
15123 struct Roˇw3.1;
15124 struct Row2.2;
15125 struct Row2.3;ˇ
15126
15127 struct Row4;
15128 struct ˇRow5.1;
15129 struct Row5.2;
15130 struct «Rowˇ»5.3;
15131 struct Row5;
15132 struct Row6;
15133 ˇ
15134 struct Row9.1;
15135 struct «Rowˇ»9.2;
15136 struct «ˇRow»9.3;
15137 struct Row8;
15138 struct Row9;
15139 «ˇ// something on bottom»
15140 struct Row10;"#},
15141 vec![
15142 DiffHunkStatusKind::Added,
15143 DiffHunkStatusKind::Added,
15144 DiffHunkStatusKind::Added,
15145 DiffHunkStatusKind::Added,
15146 DiffHunkStatusKind::Added,
15147 ],
15148 indoc! {r#"struct Row;
15149 ˇstruct Row1;
15150 struct Row2;
15151 ˇ
15152 struct Row4;
15153 ˇstruct Row5;
15154 struct Row6;
15155 ˇ
15156 ˇstruct Row8;
15157 struct Row9;
15158 ˇstruct Row10;"#},
15159 base_text,
15160 &mut cx,
15161 );
15162}
15163
15164#[gpui::test]
15165async fn test_modification_reverts(cx: &mut TestAppContext) {
15166 init_test(cx, |_| {});
15167 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15168 let base_text = indoc! {r#"
15169 struct Row;
15170 struct Row1;
15171 struct Row2;
15172
15173 struct Row4;
15174 struct Row5;
15175 struct Row6;
15176
15177 struct Row8;
15178 struct Row9;
15179 struct Row10;"#};
15180
15181 // Modification hunks behave the same as the addition ones.
15182 assert_hunk_revert(
15183 indoc! {r#"struct Row;
15184 struct Row1;
15185 struct Row33;
15186 ˇ
15187 struct Row4;
15188 struct Row5;
15189 struct Row6;
15190 ˇ
15191 struct Row99;
15192 struct Row9;
15193 struct Row10;"#},
15194 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15195 indoc! {r#"struct Row;
15196 struct Row1;
15197 struct Row33;
15198 ˇ
15199 struct Row4;
15200 struct Row5;
15201 struct Row6;
15202 ˇ
15203 struct Row99;
15204 struct Row9;
15205 struct Row10;"#},
15206 base_text,
15207 &mut cx,
15208 );
15209 assert_hunk_revert(
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 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15222 indoc! {r#"struct Row;
15223 struct Row1;
15224 struct Row33;
15225 «ˇ
15226 struct Row4;
15227 struct» Row5;
15228 «struct Row6;
15229 ˇ»
15230 struct Row99;
15231 struct Row9;
15232 struct Row10;"#},
15233 base_text,
15234 &mut cx,
15235 );
15236
15237 assert_hunk_revert(
15238 indoc! {r#"ˇstruct Row1.1;
15239 struct Row1;
15240 «ˇstr»uct Row22;
15241
15242 struct ˇRow44;
15243 struct Row5;
15244 struct «Rˇ»ow66;ˇ
15245
15246 «struˇ»ct Row88;
15247 struct Row9;
15248 struct Row1011;ˇ"#},
15249 vec![
15250 DiffHunkStatusKind::Modified,
15251 DiffHunkStatusKind::Modified,
15252 DiffHunkStatusKind::Modified,
15253 DiffHunkStatusKind::Modified,
15254 DiffHunkStatusKind::Modified,
15255 DiffHunkStatusKind::Modified,
15256 ],
15257 indoc! {r#"struct Row;
15258 ˇstruct Row1;
15259 struct Row2;
15260 ˇ
15261 struct Row4;
15262 ˇstruct Row5;
15263 struct Row6;
15264 ˇ
15265 struct Row8;
15266 ˇstruct Row9;
15267 struct Row10;ˇ"#},
15268 base_text,
15269 &mut cx,
15270 );
15271}
15272
15273#[gpui::test]
15274async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15275 init_test(cx, |_| {});
15276 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15277 let base_text = indoc! {r#"
15278 one
15279
15280 two
15281 three
15282 "#};
15283
15284 cx.set_head_text(base_text);
15285 cx.set_state("\nˇ\n");
15286 cx.executor().run_until_parked();
15287 cx.update_editor(|editor, _window, cx| {
15288 editor.expand_selected_diff_hunks(cx);
15289 });
15290 cx.executor().run_until_parked();
15291 cx.update_editor(|editor, window, cx| {
15292 editor.backspace(&Default::default(), window, cx);
15293 });
15294 cx.run_until_parked();
15295 cx.assert_state_with_diff(
15296 indoc! {r#"
15297
15298 - two
15299 - threeˇ
15300 +
15301 "#}
15302 .to_string(),
15303 );
15304}
15305
15306#[gpui::test]
15307async fn test_deletion_reverts(cx: &mut TestAppContext) {
15308 init_test(cx, |_| {});
15309 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15310 let base_text = indoc! {r#"struct Row;
15311struct Row1;
15312struct Row2;
15313
15314struct Row4;
15315struct Row5;
15316struct Row6;
15317
15318struct Row8;
15319struct Row9;
15320struct Row10;"#};
15321
15322 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15323 assert_hunk_revert(
15324 indoc! {r#"struct Row;
15325 struct Row2;
15326
15327 ˇstruct Row4;
15328 struct Row5;
15329 struct Row6;
15330 ˇ
15331 struct Row8;
15332 struct Row10;"#},
15333 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15334 indoc! {r#"struct Row;
15335 struct Row2;
15336
15337 ˇstruct Row4;
15338 struct Row5;
15339 struct Row6;
15340 ˇ
15341 struct Row8;
15342 struct Row10;"#},
15343 base_text,
15344 &mut cx,
15345 );
15346 assert_hunk_revert(
15347 indoc! {r#"struct Row;
15348 struct Row2;
15349
15350 «ˇstruct Row4;
15351 struct» Row5;
15352 «struct Row6;
15353 ˇ»
15354 struct Row8;
15355 struct Row10;"#},
15356 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15357 indoc! {r#"struct Row;
15358 struct Row2;
15359
15360 «ˇstruct Row4;
15361 struct» Row5;
15362 «struct Row6;
15363 ˇ»
15364 struct Row8;
15365 struct Row10;"#},
15366 base_text,
15367 &mut cx,
15368 );
15369
15370 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15371 assert_hunk_revert(
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 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15382 indoc! {r#"struct Row;
15383 struct Row1;
15384 ˇstruct Row2;
15385
15386 struct Row4;
15387 struct Row5;
15388 struct Row6;
15389
15390 struct Row8;ˇ
15391 struct Row9;
15392 struct Row10;"#},
15393 base_text,
15394 &mut cx,
15395 );
15396 assert_hunk_revert(
15397 indoc! {r#"struct Row;
15398 struct Row2«ˇ;
15399 struct Row4;
15400 struct» Row5;
15401 «struct Row6;
15402
15403 struct Row8;ˇ»
15404 struct Row10;"#},
15405 vec![
15406 DiffHunkStatusKind::Deleted,
15407 DiffHunkStatusKind::Deleted,
15408 DiffHunkStatusKind::Deleted,
15409 ],
15410 indoc! {r#"struct Row;
15411 struct Row1;
15412 struct Row2«ˇ;
15413
15414 struct Row4;
15415 struct» Row5;
15416 «struct Row6;
15417
15418 struct Row8;ˇ»
15419 struct Row9;
15420 struct Row10;"#},
15421 base_text,
15422 &mut cx,
15423 );
15424}
15425
15426#[gpui::test]
15427async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15428 init_test(cx, |_| {});
15429
15430 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15431 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15432 let base_text_3 =
15433 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15434
15435 let text_1 = edit_first_char_of_every_line(base_text_1);
15436 let text_2 = edit_first_char_of_every_line(base_text_2);
15437 let text_3 = edit_first_char_of_every_line(base_text_3);
15438
15439 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15440 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15441 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15442
15443 let multibuffer = cx.new(|cx| {
15444 let mut multibuffer = MultiBuffer::new(ReadWrite);
15445 multibuffer.push_excerpts(
15446 buffer_1.clone(),
15447 [
15448 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15449 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15450 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15451 ],
15452 cx,
15453 );
15454 multibuffer.push_excerpts(
15455 buffer_2.clone(),
15456 [
15457 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15458 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15459 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15460 ],
15461 cx,
15462 );
15463 multibuffer.push_excerpts(
15464 buffer_3.clone(),
15465 [
15466 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15467 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15468 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15469 ],
15470 cx,
15471 );
15472 multibuffer
15473 });
15474
15475 let fs = FakeFs::new(cx.executor());
15476 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15477 let (editor, cx) = cx
15478 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15479 editor.update_in(cx, |editor, _window, cx| {
15480 for (buffer, diff_base) in [
15481 (buffer_1.clone(), base_text_1),
15482 (buffer_2.clone(), base_text_2),
15483 (buffer_3.clone(), base_text_3),
15484 ] {
15485 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15486 editor
15487 .buffer
15488 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15489 }
15490 });
15491 cx.executor().run_until_parked();
15492
15493 editor.update_in(cx, |editor, window, cx| {
15494 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}");
15495 editor.select_all(&SelectAll, window, cx);
15496 editor.git_restore(&Default::default(), window, cx);
15497 });
15498 cx.executor().run_until_parked();
15499
15500 // When all ranges are selected, all buffer hunks are reverted.
15501 editor.update(cx, |editor, cx| {
15502 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");
15503 });
15504 buffer_1.update(cx, |buffer, _| {
15505 assert_eq!(buffer.text(), base_text_1);
15506 });
15507 buffer_2.update(cx, |buffer, _| {
15508 assert_eq!(buffer.text(), base_text_2);
15509 });
15510 buffer_3.update(cx, |buffer, _| {
15511 assert_eq!(buffer.text(), base_text_3);
15512 });
15513
15514 editor.update_in(cx, |editor, window, cx| {
15515 editor.undo(&Default::default(), window, cx);
15516 });
15517
15518 editor.update_in(cx, |editor, window, cx| {
15519 editor.change_selections(None, window, cx, |s| {
15520 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15521 });
15522 editor.git_restore(&Default::default(), window, cx);
15523 });
15524
15525 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15526 // but not affect buffer_2 and its related excerpts.
15527 editor.update(cx, |editor, cx| {
15528 assert_eq!(
15529 editor.text(cx),
15530 "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}"
15531 );
15532 });
15533 buffer_1.update(cx, |buffer, _| {
15534 assert_eq!(buffer.text(), base_text_1);
15535 });
15536 buffer_2.update(cx, |buffer, _| {
15537 assert_eq!(
15538 buffer.text(),
15539 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15540 );
15541 });
15542 buffer_3.update(cx, |buffer, _| {
15543 assert_eq!(
15544 buffer.text(),
15545 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15546 );
15547 });
15548
15549 fn edit_first_char_of_every_line(text: &str) -> String {
15550 text.split('\n')
15551 .map(|line| format!("X{}", &line[1..]))
15552 .collect::<Vec<_>>()
15553 .join("\n")
15554 }
15555}
15556
15557#[gpui::test]
15558async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15559 init_test(cx, |_| {});
15560
15561 let cols = 4;
15562 let rows = 10;
15563 let sample_text_1 = sample_text(rows, cols, 'a');
15564 assert_eq!(
15565 sample_text_1,
15566 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15567 );
15568 let sample_text_2 = sample_text(rows, cols, 'l');
15569 assert_eq!(
15570 sample_text_2,
15571 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15572 );
15573 let sample_text_3 = sample_text(rows, cols, 'v');
15574 assert_eq!(
15575 sample_text_3,
15576 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15577 );
15578
15579 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15580 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15581 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15582
15583 let multi_buffer = cx.new(|cx| {
15584 let mut multibuffer = MultiBuffer::new(ReadWrite);
15585 multibuffer.push_excerpts(
15586 buffer_1.clone(),
15587 [
15588 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15589 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15590 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15591 ],
15592 cx,
15593 );
15594 multibuffer.push_excerpts(
15595 buffer_2.clone(),
15596 [
15597 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15598 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15599 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15600 ],
15601 cx,
15602 );
15603 multibuffer.push_excerpts(
15604 buffer_3.clone(),
15605 [
15606 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15607 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15608 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15609 ],
15610 cx,
15611 );
15612 multibuffer
15613 });
15614
15615 let fs = FakeFs::new(cx.executor());
15616 fs.insert_tree(
15617 "/a",
15618 json!({
15619 "main.rs": sample_text_1,
15620 "other.rs": sample_text_2,
15621 "lib.rs": sample_text_3,
15622 }),
15623 )
15624 .await;
15625 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15626 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15627 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15628 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15629 Editor::new(
15630 EditorMode::full(),
15631 multi_buffer,
15632 Some(project.clone()),
15633 window,
15634 cx,
15635 )
15636 });
15637 let multibuffer_item_id = workspace
15638 .update(cx, |workspace, window, cx| {
15639 assert!(
15640 workspace.active_item(cx).is_none(),
15641 "active item should be None before the first item is added"
15642 );
15643 workspace.add_item_to_active_pane(
15644 Box::new(multi_buffer_editor.clone()),
15645 None,
15646 true,
15647 window,
15648 cx,
15649 );
15650 let active_item = workspace
15651 .active_item(cx)
15652 .expect("should have an active item after adding the multi buffer");
15653 assert!(
15654 !active_item.is_singleton(cx),
15655 "A multi buffer was expected to active after adding"
15656 );
15657 active_item.item_id()
15658 })
15659 .unwrap();
15660 cx.executor().run_until_parked();
15661
15662 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15663 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15664 s.select_ranges(Some(1..2))
15665 });
15666 editor.open_excerpts(&OpenExcerpts, window, cx);
15667 });
15668 cx.executor().run_until_parked();
15669 let first_item_id = workspace
15670 .update(cx, |workspace, window, cx| {
15671 let active_item = workspace
15672 .active_item(cx)
15673 .expect("should have an active item after navigating into the 1st buffer");
15674 let first_item_id = active_item.item_id();
15675 assert_ne!(
15676 first_item_id, multibuffer_item_id,
15677 "Should navigate into the 1st buffer and activate it"
15678 );
15679 assert!(
15680 active_item.is_singleton(cx),
15681 "New active item should be a singleton buffer"
15682 );
15683 assert_eq!(
15684 active_item
15685 .act_as::<Editor>(cx)
15686 .expect("should have navigated into an editor for the 1st buffer")
15687 .read(cx)
15688 .text(cx),
15689 sample_text_1
15690 );
15691
15692 workspace
15693 .go_back(workspace.active_pane().downgrade(), window, cx)
15694 .detach_and_log_err(cx);
15695
15696 first_item_id
15697 })
15698 .unwrap();
15699 cx.executor().run_until_parked();
15700 workspace
15701 .update(cx, |workspace, _, cx| {
15702 let active_item = workspace
15703 .active_item(cx)
15704 .expect("should have an active item after navigating back");
15705 assert_eq!(
15706 active_item.item_id(),
15707 multibuffer_item_id,
15708 "Should navigate back to the multi buffer"
15709 );
15710 assert!(!active_item.is_singleton(cx));
15711 })
15712 .unwrap();
15713
15714 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15715 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15716 s.select_ranges(Some(39..40))
15717 });
15718 editor.open_excerpts(&OpenExcerpts, window, cx);
15719 });
15720 cx.executor().run_until_parked();
15721 let second_item_id = workspace
15722 .update(cx, |workspace, window, cx| {
15723 let active_item = workspace
15724 .active_item(cx)
15725 .expect("should have an active item after navigating into the 2nd buffer");
15726 let second_item_id = active_item.item_id();
15727 assert_ne!(
15728 second_item_id, multibuffer_item_id,
15729 "Should navigate away from the multibuffer"
15730 );
15731 assert_ne!(
15732 second_item_id, first_item_id,
15733 "Should navigate into the 2nd buffer and activate it"
15734 );
15735 assert!(
15736 active_item.is_singleton(cx),
15737 "New active item should be a singleton buffer"
15738 );
15739 assert_eq!(
15740 active_item
15741 .act_as::<Editor>(cx)
15742 .expect("should have navigated into an editor")
15743 .read(cx)
15744 .text(cx),
15745 sample_text_2
15746 );
15747
15748 workspace
15749 .go_back(workspace.active_pane().downgrade(), window, cx)
15750 .detach_and_log_err(cx);
15751
15752 second_item_id
15753 })
15754 .unwrap();
15755 cx.executor().run_until_parked();
15756 workspace
15757 .update(cx, |workspace, _, cx| {
15758 let active_item = workspace
15759 .active_item(cx)
15760 .expect("should have an active item after navigating back from the 2nd buffer");
15761 assert_eq!(
15762 active_item.item_id(),
15763 multibuffer_item_id,
15764 "Should navigate back from the 2nd buffer to the multi buffer"
15765 );
15766 assert!(!active_item.is_singleton(cx));
15767 })
15768 .unwrap();
15769
15770 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15771 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15772 s.select_ranges(Some(70..70))
15773 });
15774 editor.open_excerpts(&OpenExcerpts, window, cx);
15775 });
15776 cx.executor().run_until_parked();
15777 workspace
15778 .update(cx, |workspace, window, cx| {
15779 let active_item = workspace
15780 .active_item(cx)
15781 .expect("should have an active item after navigating into the 3rd buffer");
15782 let third_item_id = active_item.item_id();
15783 assert_ne!(
15784 third_item_id, multibuffer_item_id,
15785 "Should navigate into the 3rd buffer and activate it"
15786 );
15787 assert_ne!(third_item_id, first_item_id);
15788 assert_ne!(third_item_id, second_item_id);
15789 assert!(
15790 active_item.is_singleton(cx),
15791 "New active item should be a singleton buffer"
15792 );
15793 assert_eq!(
15794 active_item
15795 .act_as::<Editor>(cx)
15796 .expect("should have navigated into an editor")
15797 .read(cx)
15798 .text(cx),
15799 sample_text_3
15800 );
15801
15802 workspace
15803 .go_back(workspace.active_pane().downgrade(), window, cx)
15804 .detach_and_log_err(cx);
15805 })
15806 .unwrap();
15807 cx.executor().run_until_parked();
15808 workspace
15809 .update(cx, |workspace, _, cx| {
15810 let active_item = workspace
15811 .active_item(cx)
15812 .expect("should have an active item after navigating back from the 3rd buffer");
15813 assert_eq!(
15814 active_item.item_id(),
15815 multibuffer_item_id,
15816 "Should navigate back from the 3rd buffer to the multi buffer"
15817 );
15818 assert!(!active_item.is_singleton(cx));
15819 })
15820 .unwrap();
15821}
15822
15823#[gpui::test]
15824async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15825 init_test(cx, |_| {});
15826
15827 let mut cx = EditorTestContext::new(cx).await;
15828
15829 let diff_base = r#"
15830 use some::mod;
15831
15832 const A: u32 = 42;
15833
15834 fn main() {
15835 println!("hello");
15836
15837 println!("world");
15838 }
15839 "#
15840 .unindent();
15841
15842 cx.set_state(
15843 &r#"
15844 use some::modified;
15845
15846 ˇ
15847 fn main() {
15848 println!("hello there");
15849
15850 println!("around the");
15851 println!("world");
15852 }
15853 "#
15854 .unindent(),
15855 );
15856
15857 cx.set_head_text(&diff_base);
15858 executor.run_until_parked();
15859
15860 cx.update_editor(|editor, window, cx| {
15861 editor.go_to_next_hunk(&GoToHunk, window, cx);
15862 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15863 });
15864 executor.run_until_parked();
15865 cx.assert_state_with_diff(
15866 r#"
15867 use some::modified;
15868
15869
15870 fn main() {
15871 - println!("hello");
15872 + ˇ println!("hello there");
15873
15874 println!("around the");
15875 println!("world");
15876 }
15877 "#
15878 .unindent(),
15879 );
15880
15881 cx.update_editor(|editor, window, cx| {
15882 for _ in 0..2 {
15883 editor.go_to_next_hunk(&GoToHunk, window, cx);
15884 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15885 }
15886 });
15887 executor.run_until_parked();
15888 cx.assert_state_with_diff(
15889 r#"
15890 - use some::mod;
15891 + ˇuse some::modified;
15892
15893
15894 fn main() {
15895 - println!("hello");
15896 + println!("hello there");
15897
15898 + println!("around the");
15899 println!("world");
15900 }
15901 "#
15902 .unindent(),
15903 );
15904
15905 cx.update_editor(|editor, window, cx| {
15906 editor.go_to_next_hunk(&GoToHunk, window, cx);
15907 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15908 });
15909 executor.run_until_parked();
15910 cx.assert_state_with_diff(
15911 r#"
15912 - use some::mod;
15913 + use some::modified;
15914
15915 - const A: u32 = 42;
15916 ˇ
15917 fn main() {
15918 - println!("hello");
15919 + println!("hello there");
15920
15921 + println!("around the");
15922 println!("world");
15923 }
15924 "#
15925 .unindent(),
15926 );
15927
15928 cx.update_editor(|editor, window, cx| {
15929 editor.cancel(&Cancel, window, cx);
15930 });
15931
15932 cx.assert_state_with_diff(
15933 r#"
15934 use some::modified;
15935
15936 ˇ
15937 fn main() {
15938 println!("hello there");
15939
15940 println!("around the");
15941 println!("world");
15942 }
15943 "#
15944 .unindent(),
15945 );
15946}
15947
15948#[gpui::test]
15949async fn test_diff_base_change_with_expanded_diff_hunks(
15950 executor: BackgroundExecutor,
15951 cx: &mut TestAppContext,
15952) {
15953 init_test(cx, |_| {});
15954
15955 let mut cx = EditorTestContext::new(cx).await;
15956
15957 let diff_base = r#"
15958 use some::mod1;
15959 use some::mod2;
15960
15961 const A: u32 = 42;
15962 const B: u32 = 42;
15963 const C: u32 = 42;
15964
15965 fn main() {
15966 println!("hello");
15967
15968 println!("world");
15969 }
15970 "#
15971 .unindent();
15972
15973 cx.set_state(
15974 &r#"
15975 use some::mod2;
15976
15977 const A: u32 = 42;
15978 const C: u32 = 42;
15979
15980 fn main(ˇ) {
15981 //println!("hello");
15982
15983 println!("world");
15984 //
15985 //
15986 }
15987 "#
15988 .unindent(),
15989 );
15990
15991 cx.set_head_text(&diff_base);
15992 executor.run_until_parked();
15993
15994 cx.update_editor(|editor, window, cx| {
15995 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15996 });
15997 executor.run_until_parked();
15998 cx.assert_state_with_diff(
15999 r#"
16000 - use some::mod1;
16001 use some::mod2;
16002
16003 const A: u32 = 42;
16004 - const B: u32 = 42;
16005 const C: u32 = 42;
16006
16007 fn main(ˇ) {
16008 - println!("hello");
16009 + //println!("hello");
16010
16011 println!("world");
16012 + //
16013 + //
16014 }
16015 "#
16016 .unindent(),
16017 );
16018
16019 cx.set_head_text("new diff base!");
16020 executor.run_until_parked();
16021 cx.assert_state_with_diff(
16022 r#"
16023 - new diff base!
16024 + use some::mod2;
16025 +
16026 + const A: u32 = 42;
16027 + const C: u32 = 42;
16028 +
16029 + fn main(ˇ) {
16030 + //println!("hello");
16031 +
16032 + println!("world");
16033 + //
16034 + //
16035 + }
16036 "#
16037 .unindent(),
16038 );
16039}
16040
16041#[gpui::test]
16042async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16043 init_test(cx, |_| {});
16044
16045 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16046 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16047 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16048 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16049 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16050 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16051
16052 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16053 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16054 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16055
16056 let multi_buffer = cx.new(|cx| {
16057 let mut multibuffer = MultiBuffer::new(ReadWrite);
16058 multibuffer.push_excerpts(
16059 buffer_1.clone(),
16060 [
16061 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16062 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16063 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16064 ],
16065 cx,
16066 );
16067 multibuffer.push_excerpts(
16068 buffer_2.clone(),
16069 [
16070 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16071 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16072 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16073 ],
16074 cx,
16075 );
16076 multibuffer.push_excerpts(
16077 buffer_3.clone(),
16078 [
16079 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16080 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16081 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16082 ],
16083 cx,
16084 );
16085 multibuffer
16086 });
16087
16088 let editor =
16089 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16090 editor
16091 .update(cx, |editor, _window, cx| {
16092 for (buffer, diff_base) in [
16093 (buffer_1.clone(), file_1_old),
16094 (buffer_2.clone(), file_2_old),
16095 (buffer_3.clone(), file_3_old),
16096 ] {
16097 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16098 editor
16099 .buffer
16100 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16101 }
16102 })
16103 .unwrap();
16104
16105 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16106 cx.run_until_parked();
16107
16108 cx.assert_editor_state(
16109 &"
16110 ˇaaa
16111 ccc
16112 ddd
16113
16114 ggg
16115 hhh
16116
16117
16118 lll
16119 mmm
16120 NNN
16121
16122 qqq
16123 rrr
16124
16125 uuu
16126 111
16127 222
16128 333
16129
16130 666
16131 777
16132
16133 000
16134 !!!"
16135 .unindent(),
16136 );
16137
16138 cx.update_editor(|editor, window, cx| {
16139 editor.select_all(&SelectAll, window, cx);
16140 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16141 });
16142 cx.executor().run_until_parked();
16143
16144 cx.assert_state_with_diff(
16145 "
16146 «aaa
16147 - bbb
16148 ccc
16149 ddd
16150
16151 ggg
16152 hhh
16153
16154
16155 lll
16156 mmm
16157 - nnn
16158 + NNN
16159
16160 qqq
16161 rrr
16162
16163 uuu
16164 111
16165 222
16166 333
16167
16168 + 666
16169 777
16170
16171 000
16172 !!!ˇ»"
16173 .unindent(),
16174 );
16175}
16176
16177#[gpui::test]
16178async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16179 init_test(cx, |_| {});
16180
16181 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16182 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16183
16184 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16185 let multi_buffer = cx.new(|cx| {
16186 let mut multibuffer = MultiBuffer::new(ReadWrite);
16187 multibuffer.push_excerpts(
16188 buffer.clone(),
16189 [
16190 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16191 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16192 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16193 ],
16194 cx,
16195 );
16196 multibuffer
16197 });
16198
16199 let editor =
16200 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16201 editor
16202 .update(cx, |editor, _window, cx| {
16203 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16204 editor
16205 .buffer
16206 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16207 })
16208 .unwrap();
16209
16210 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16211 cx.run_until_parked();
16212
16213 cx.update_editor(|editor, window, cx| {
16214 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16215 });
16216 cx.executor().run_until_parked();
16217
16218 // When the start of a hunk coincides with the start of its excerpt,
16219 // the hunk is expanded. When the start of a a hunk is earlier than
16220 // the start of its excerpt, the hunk is not expanded.
16221 cx.assert_state_with_diff(
16222 "
16223 ˇaaa
16224 - bbb
16225 + BBB
16226
16227 - ddd
16228 - eee
16229 + DDD
16230 + EEE
16231 fff
16232
16233 iii
16234 "
16235 .unindent(),
16236 );
16237}
16238
16239#[gpui::test]
16240async fn test_edits_around_expanded_insertion_hunks(
16241 executor: BackgroundExecutor,
16242 cx: &mut TestAppContext,
16243) {
16244 init_test(cx, |_| {});
16245
16246 let mut cx = EditorTestContext::new(cx).await;
16247
16248 let diff_base = r#"
16249 use some::mod1;
16250 use some::mod2;
16251
16252 const A: u32 = 42;
16253
16254 fn main() {
16255 println!("hello");
16256
16257 println!("world");
16258 }
16259 "#
16260 .unindent();
16261 executor.run_until_parked();
16262 cx.set_state(
16263 &r#"
16264 use some::mod1;
16265 use some::mod2;
16266
16267 const A: u32 = 42;
16268 const B: u32 = 42;
16269 const C: u32 = 42;
16270 ˇ
16271
16272 fn main() {
16273 println!("hello");
16274
16275 println!("world");
16276 }
16277 "#
16278 .unindent(),
16279 );
16280
16281 cx.set_head_text(&diff_base);
16282 executor.run_until_parked();
16283
16284 cx.update_editor(|editor, window, cx| {
16285 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16286 });
16287 executor.run_until_parked();
16288
16289 cx.assert_state_with_diff(
16290 r#"
16291 use some::mod1;
16292 use some::mod2;
16293
16294 const A: u32 = 42;
16295 + const B: u32 = 42;
16296 + const C: u32 = 42;
16297 + ˇ
16298
16299 fn main() {
16300 println!("hello");
16301
16302 println!("world");
16303 }
16304 "#
16305 .unindent(),
16306 );
16307
16308 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16309 executor.run_until_parked();
16310
16311 cx.assert_state_with_diff(
16312 r#"
16313 use some::mod1;
16314 use some::mod2;
16315
16316 const A: u32 = 42;
16317 + const B: u32 = 42;
16318 + const C: u32 = 42;
16319 + const D: u32 = 42;
16320 + ˇ
16321
16322 fn main() {
16323 println!("hello");
16324
16325 println!("world");
16326 }
16327 "#
16328 .unindent(),
16329 );
16330
16331 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16332 executor.run_until_parked();
16333
16334 cx.assert_state_with_diff(
16335 r#"
16336 use some::mod1;
16337 use some::mod2;
16338
16339 const A: u32 = 42;
16340 + const B: u32 = 42;
16341 + const C: u32 = 42;
16342 + const D: u32 = 42;
16343 + const E: u32 = 42;
16344 + ˇ
16345
16346 fn main() {
16347 println!("hello");
16348
16349 println!("world");
16350 }
16351 "#
16352 .unindent(),
16353 );
16354
16355 cx.update_editor(|editor, window, cx| {
16356 editor.delete_line(&DeleteLine, window, cx);
16357 });
16358 executor.run_until_parked();
16359
16360 cx.assert_state_with_diff(
16361 r#"
16362 use some::mod1;
16363 use some::mod2;
16364
16365 const A: u32 = 42;
16366 + const B: u32 = 42;
16367 + const C: u32 = 42;
16368 + const D: u32 = 42;
16369 + const E: u32 = 42;
16370 ˇ
16371 fn main() {
16372 println!("hello");
16373
16374 println!("world");
16375 }
16376 "#
16377 .unindent(),
16378 );
16379
16380 cx.update_editor(|editor, window, cx| {
16381 editor.move_up(&MoveUp, window, cx);
16382 editor.delete_line(&DeleteLine, window, cx);
16383 editor.move_up(&MoveUp, window, cx);
16384 editor.delete_line(&DeleteLine, window, cx);
16385 editor.move_up(&MoveUp, window, cx);
16386 editor.delete_line(&DeleteLine, window, cx);
16387 });
16388 executor.run_until_parked();
16389 cx.assert_state_with_diff(
16390 r#"
16391 use some::mod1;
16392 use some::mod2;
16393
16394 const A: u32 = 42;
16395 + const B: u32 = 42;
16396 ˇ
16397 fn main() {
16398 println!("hello");
16399
16400 println!("world");
16401 }
16402 "#
16403 .unindent(),
16404 );
16405
16406 cx.update_editor(|editor, window, cx| {
16407 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16408 editor.delete_line(&DeleteLine, window, cx);
16409 });
16410 executor.run_until_parked();
16411 cx.assert_state_with_diff(
16412 r#"
16413 ˇ
16414 fn main() {
16415 println!("hello");
16416
16417 println!("world");
16418 }
16419 "#
16420 .unindent(),
16421 );
16422}
16423
16424#[gpui::test]
16425async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16426 init_test(cx, |_| {});
16427
16428 let mut cx = EditorTestContext::new(cx).await;
16429 cx.set_head_text(indoc! { "
16430 one
16431 two
16432 three
16433 four
16434 five
16435 "
16436 });
16437 cx.set_state(indoc! { "
16438 one
16439 ˇthree
16440 five
16441 "});
16442 cx.run_until_parked();
16443 cx.update_editor(|editor, window, cx| {
16444 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16445 });
16446 cx.assert_state_with_diff(
16447 indoc! { "
16448 one
16449 - two
16450 ˇthree
16451 - four
16452 five
16453 "}
16454 .to_string(),
16455 );
16456 cx.update_editor(|editor, window, cx| {
16457 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16458 });
16459
16460 cx.assert_state_with_diff(
16461 indoc! { "
16462 one
16463 ˇthree
16464 five
16465 "}
16466 .to_string(),
16467 );
16468
16469 cx.set_state(indoc! { "
16470 one
16471 ˇTWO
16472 three
16473 four
16474 five
16475 "});
16476 cx.run_until_parked();
16477 cx.update_editor(|editor, window, cx| {
16478 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16479 });
16480
16481 cx.assert_state_with_diff(
16482 indoc! { "
16483 one
16484 - two
16485 + ˇTWO
16486 three
16487 four
16488 five
16489 "}
16490 .to_string(),
16491 );
16492 cx.update_editor(|editor, window, cx| {
16493 editor.move_up(&Default::default(), window, cx);
16494 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16495 });
16496 cx.assert_state_with_diff(
16497 indoc! { "
16498 one
16499 ˇTWO
16500 three
16501 four
16502 five
16503 "}
16504 .to_string(),
16505 );
16506}
16507
16508#[gpui::test]
16509async fn test_edits_around_expanded_deletion_hunks(
16510 executor: BackgroundExecutor,
16511 cx: &mut TestAppContext,
16512) {
16513 init_test(cx, |_| {});
16514
16515 let mut cx = EditorTestContext::new(cx).await;
16516
16517 let diff_base = r#"
16518 use some::mod1;
16519 use some::mod2;
16520
16521 const A: u32 = 42;
16522 const B: u32 = 42;
16523 const C: u32 = 42;
16524
16525
16526 fn main() {
16527 println!("hello");
16528
16529 println!("world");
16530 }
16531 "#
16532 .unindent();
16533 executor.run_until_parked();
16534 cx.set_state(
16535 &r#"
16536 use some::mod1;
16537 use some::mod2;
16538
16539 ˇconst B: u32 = 42;
16540 const C: u32 = 42;
16541
16542
16543 fn main() {
16544 println!("hello");
16545
16546 println!("world");
16547 }
16548 "#
16549 .unindent(),
16550 );
16551
16552 cx.set_head_text(&diff_base);
16553 executor.run_until_parked();
16554
16555 cx.update_editor(|editor, window, cx| {
16556 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16557 });
16558 executor.run_until_parked();
16559
16560 cx.assert_state_with_diff(
16561 r#"
16562 use some::mod1;
16563 use some::mod2;
16564
16565 - const A: u32 = 42;
16566 ˇconst B: u32 = 42;
16567 const C: u32 = 42;
16568
16569
16570 fn main() {
16571 println!("hello");
16572
16573 println!("world");
16574 }
16575 "#
16576 .unindent(),
16577 );
16578
16579 cx.update_editor(|editor, window, cx| {
16580 editor.delete_line(&DeleteLine, window, cx);
16581 });
16582 executor.run_until_parked();
16583 cx.assert_state_with_diff(
16584 r#"
16585 use some::mod1;
16586 use some::mod2;
16587
16588 - const A: u32 = 42;
16589 - const B: u32 = 42;
16590 ˇconst C: u32 = 42;
16591
16592
16593 fn main() {
16594 println!("hello");
16595
16596 println!("world");
16597 }
16598 "#
16599 .unindent(),
16600 );
16601
16602 cx.update_editor(|editor, window, cx| {
16603 editor.delete_line(&DeleteLine, window, cx);
16604 });
16605 executor.run_until_parked();
16606 cx.assert_state_with_diff(
16607 r#"
16608 use some::mod1;
16609 use some::mod2;
16610
16611 - const A: u32 = 42;
16612 - const B: u32 = 42;
16613 - const C: u32 = 42;
16614 ˇ
16615
16616 fn main() {
16617 println!("hello");
16618
16619 println!("world");
16620 }
16621 "#
16622 .unindent(),
16623 );
16624
16625 cx.update_editor(|editor, window, cx| {
16626 editor.handle_input("replacement", window, cx);
16627 });
16628 executor.run_until_parked();
16629 cx.assert_state_with_diff(
16630 r#"
16631 use some::mod1;
16632 use some::mod2;
16633
16634 - const A: u32 = 42;
16635 - const B: u32 = 42;
16636 - const C: u32 = 42;
16637 -
16638 + replacementˇ
16639
16640 fn main() {
16641 println!("hello");
16642
16643 println!("world");
16644 }
16645 "#
16646 .unindent(),
16647 );
16648}
16649
16650#[gpui::test]
16651async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16652 init_test(cx, |_| {});
16653
16654 let mut cx = EditorTestContext::new(cx).await;
16655
16656 let base_text = r#"
16657 one
16658 two
16659 three
16660 four
16661 five
16662 "#
16663 .unindent();
16664 executor.run_until_parked();
16665 cx.set_state(
16666 &r#"
16667 one
16668 two
16669 fˇour
16670 five
16671 "#
16672 .unindent(),
16673 );
16674
16675 cx.set_head_text(&base_text);
16676 executor.run_until_parked();
16677
16678 cx.update_editor(|editor, window, cx| {
16679 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16680 });
16681 executor.run_until_parked();
16682
16683 cx.assert_state_with_diff(
16684 r#"
16685 one
16686 two
16687 - three
16688 fˇour
16689 five
16690 "#
16691 .unindent(),
16692 );
16693
16694 cx.update_editor(|editor, window, cx| {
16695 editor.backspace(&Backspace, window, cx);
16696 editor.backspace(&Backspace, window, cx);
16697 });
16698 executor.run_until_parked();
16699 cx.assert_state_with_diff(
16700 r#"
16701 one
16702 two
16703 - threeˇ
16704 - four
16705 + our
16706 five
16707 "#
16708 .unindent(),
16709 );
16710}
16711
16712#[gpui::test]
16713async fn test_edit_after_expanded_modification_hunk(
16714 executor: BackgroundExecutor,
16715 cx: &mut TestAppContext,
16716) {
16717 init_test(cx, |_| {});
16718
16719 let mut cx = EditorTestContext::new(cx).await;
16720
16721 let diff_base = r#"
16722 use some::mod1;
16723 use some::mod2;
16724
16725 const A: u32 = 42;
16726 const B: u32 = 42;
16727 const C: u32 = 42;
16728 const D: u32 = 42;
16729
16730
16731 fn main() {
16732 println!("hello");
16733
16734 println!("world");
16735 }"#
16736 .unindent();
16737
16738 cx.set_state(
16739 &r#"
16740 use some::mod1;
16741 use some::mod2;
16742
16743 const A: u32 = 42;
16744 const B: u32 = 42;
16745 const C: u32 = 43ˇ
16746 const D: u32 = 42;
16747
16748
16749 fn main() {
16750 println!("hello");
16751
16752 println!("world");
16753 }"#
16754 .unindent(),
16755 );
16756
16757 cx.set_head_text(&diff_base);
16758 executor.run_until_parked();
16759 cx.update_editor(|editor, window, cx| {
16760 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16761 });
16762 executor.run_until_parked();
16763
16764 cx.assert_state_with_diff(
16765 r#"
16766 use some::mod1;
16767 use some::mod2;
16768
16769 const A: u32 = 42;
16770 const B: u32 = 42;
16771 - const C: u32 = 42;
16772 + const C: u32 = 43ˇ
16773 const D: u32 = 42;
16774
16775
16776 fn main() {
16777 println!("hello");
16778
16779 println!("world");
16780 }"#
16781 .unindent(),
16782 );
16783
16784 cx.update_editor(|editor, window, cx| {
16785 editor.handle_input("\nnew_line\n", window, cx);
16786 });
16787 executor.run_until_parked();
16788
16789 cx.assert_state_with_diff(
16790 r#"
16791 use some::mod1;
16792 use some::mod2;
16793
16794 const A: u32 = 42;
16795 const B: u32 = 42;
16796 - const C: u32 = 42;
16797 + const C: u32 = 43
16798 + new_line
16799 + ˇ
16800 const D: u32 = 42;
16801
16802
16803 fn main() {
16804 println!("hello");
16805
16806 println!("world");
16807 }"#
16808 .unindent(),
16809 );
16810}
16811
16812#[gpui::test]
16813async fn test_stage_and_unstage_added_file_hunk(
16814 executor: BackgroundExecutor,
16815 cx: &mut TestAppContext,
16816) {
16817 init_test(cx, |_| {});
16818
16819 let mut cx = EditorTestContext::new(cx).await;
16820 cx.update_editor(|editor, _, cx| {
16821 editor.set_expand_all_diff_hunks(cx);
16822 });
16823
16824 let working_copy = r#"
16825 ˇfn main() {
16826 println!("hello, world!");
16827 }
16828 "#
16829 .unindent();
16830
16831 cx.set_state(&working_copy);
16832 executor.run_until_parked();
16833
16834 cx.assert_state_with_diff(
16835 r#"
16836 + ˇfn main() {
16837 + println!("hello, world!");
16838 + }
16839 "#
16840 .unindent(),
16841 );
16842 cx.assert_index_text(None);
16843
16844 cx.update_editor(|editor, window, cx| {
16845 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16846 });
16847 executor.run_until_parked();
16848 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16849 cx.assert_state_with_diff(
16850 r#"
16851 + ˇfn main() {
16852 + println!("hello, world!");
16853 + }
16854 "#
16855 .unindent(),
16856 );
16857
16858 cx.update_editor(|editor, window, cx| {
16859 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16860 });
16861 executor.run_until_parked();
16862 cx.assert_index_text(None);
16863}
16864
16865async fn setup_indent_guides_editor(
16866 text: &str,
16867 cx: &mut TestAppContext,
16868) -> (BufferId, EditorTestContext) {
16869 init_test(cx, |_| {});
16870
16871 let mut cx = EditorTestContext::new(cx).await;
16872
16873 let buffer_id = cx.update_editor(|editor, window, cx| {
16874 editor.set_text(text, window, cx);
16875 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16876
16877 buffer_ids[0]
16878 });
16879
16880 (buffer_id, cx)
16881}
16882
16883fn assert_indent_guides(
16884 range: Range<u32>,
16885 expected: Vec<IndentGuide>,
16886 active_indices: Option<Vec<usize>>,
16887 cx: &mut EditorTestContext,
16888) {
16889 let indent_guides = cx.update_editor(|editor, window, cx| {
16890 let snapshot = editor.snapshot(window, cx).display_snapshot;
16891 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16892 editor,
16893 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16894 true,
16895 &snapshot,
16896 cx,
16897 );
16898
16899 indent_guides.sort_by(|a, b| {
16900 a.depth.cmp(&b.depth).then(
16901 a.start_row
16902 .cmp(&b.start_row)
16903 .then(a.end_row.cmp(&b.end_row)),
16904 )
16905 });
16906 indent_guides
16907 });
16908
16909 if let Some(expected) = active_indices {
16910 let active_indices = cx.update_editor(|editor, window, cx| {
16911 let snapshot = editor.snapshot(window, cx).display_snapshot;
16912 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16913 });
16914
16915 assert_eq!(
16916 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16917 expected,
16918 "Active indent guide indices do not match"
16919 );
16920 }
16921
16922 assert_eq!(indent_guides, expected, "Indent guides do not match");
16923}
16924
16925fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16926 IndentGuide {
16927 buffer_id,
16928 start_row: MultiBufferRow(start_row),
16929 end_row: MultiBufferRow(end_row),
16930 depth,
16931 tab_size: 4,
16932 settings: IndentGuideSettings {
16933 enabled: true,
16934 line_width: 1,
16935 active_line_width: 1,
16936 ..Default::default()
16937 },
16938 }
16939}
16940
16941#[gpui::test]
16942async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16943 let (buffer_id, mut cx) = setup_indent_guides_editor(
16944 &"
16945 fn main() {
16946 let a = 1;
16947 }"
16948 .unindent(),
16949 cx,
16950 )
16951 .await;
16952
16953 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16954}
16955
16956#[gpui::test]
16957async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16958 let (buffer_id, mut cx) = setup_indent_guides_editor(
16959 &"
16960 fn main() {
16961 let a = 1;
16962 let b = 2;
16963 }"
16964 .unindent(),
16965 cx,
16966 )
16967 .await;
16968
16969 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16970}
16971
16972#[gpui::test]
16973async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16974 let (buffer_id, mut cx) = setup_indent_guides_editor(
16975 &"
16976 fn main() {
16977 let a = 1;
16978 if a == 3 {
16979 let b = 2;
16980 } else {
16981 let c = 3;
16982 }
16983 }"
16984 .unindent(),
16985 cx,
16986 )
16987 .await;
16988
16989 assert_indent_guides(
16990 0..8,
16991 vec![
16992 indent_guide(buffer_id, 1, 6, 0),
16993 indent_guide(buffer_id, 3, 3, 1),
16994 indent_guide(buffer_id, 5, 5, 1),
16995 ],
16996 None,
16997 &mut cx,
16998 );
16999}
17000
17001#[gpui::test]
17002async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17003 let (buffer_id, mut cx) = setup_indent_guides_editor(
17004 &"
17005 fn main() {
17006 let a = 1;
17007 let b = 2;
17008 let c = 3;
17009 }"
17010 .unindent(),
17011 cx,
17012 )
17013 .await;
17014
17015 assert_indent_guides(
17016 0..5,
17017 vec![
17018 indent_guide(buffer_id, 1, 3, 0),
17019 indent_guide(buffer_id, 2, 2, 1),
17020 ],
17021 None,
17022 &mut cx,
17023 );
17024}
17025
17026#[gpui::test]
17027async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17028 let (buffer_id, mut cx) = setup_indent_guides_editor(
17029 &"
17030 fn main() {
17031 let a = 1;
17032
17033 let c = 3;
17034 }"
17035 .unindent(),
17036 cx,
17037 )
17038 .await;
17039
17040 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17041}
17042
17043#[gpui::test]
17044async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17045 let (buffer_id, mut cx) = setup_indent_guides_editor(
17046 &"
17047 fn main() {
17048 let a = 1;
17049
17050 let c = 3;
17051
17052 if a == 3 {
17053 let b = 2;
17054 } else {
17055 let c = 3;
17056 }
17057 }"
17058 .unindent(),
17059 cx,
17060 )
17061 .await;
17062
17063 assert_indent_guides(
17064 0..11,
17065 vec![
17066 indent_guide(buffer_id, 1, 9, 0),
17067 indent_guide(buffer_id, 6, 6, 1),
17068 indent_guide(buffer_id, 8, 8, 1),
17069 ],
17070 None,
17071 &mut cx,
17072 );
17073}
17074
17075#[gpui::test]
17076async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17077 let (buffer_id, mut cx) = setup_indent_guides_editor(
17078 &"
17079 fn main() {
17080 let a = 1;
17081
17082 let c = 3;
17083
17084 if a == 3 {
17085 let b = 2;
17086 } else {
17087 let c = 3;
17088 }
17089 }"
17090 .unindent(),
17091 cx,
17092 )
17093 .await;
17094
17095 assert_indent_guides(
17096 1..11,
17097 vec![
17098 indent_guide(buffer_id, 1, 9, 0),
17099 indent_guide(buffer_id, 6, 6, 1),
17100 indent_guide(buffer_id, 8, 8, 1),
17101 ],
17102 None,
17103 &mut cx,
17104 );
17105}
17106
17107#[gpui::test]
17108async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17109 let (buffer_id, mut cx) = setup_indent_guides_editor(
17110 &"
17111 fn main() {
17112 let a = 1;
17113
17114 let c = 3;
17115
17116 if a == 3 {
17117 let b = 2;
17118 } else {
17119 let c = 3;
17120 }
17121 }"
17122 .unindent(),
17123 cx,
17124 )
17125 .await;
17126
17127 assert_indent_guides(
17128 1..10,
17129 vec![
17130 indent_guide(buffer_id, 1, 9, 0),
17131 indent_guide(buffer_id, 6, 6, 1),
17132 indent_guide(buffer_id, 8, 8, 1),
17133 ],
17134 None,
17135 &mut cx,
17136 );
17137}
17138
17139#[gpui::test]
17140async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17141 let (buffer_id, mut cx) = setup_indent_guides_editor(
17142 &"
17143 fn main() {
17144 if a {
17145 b(
17146 c,
17147 d,
17148 )
17149 } else {
17150 e(
17151 f
17152 )
17153 }
17154 }"
17155 .unindent(),
17156 cx,
17157 )
17158 .await;
17159
17160 assert_indent_guides(
17161 0..11,
17162 vec![
17163 indent_guide(buffer_id, 1, 10, 0),
17164 indent_guide(buffer_id, 2, 5, 1),
17165 indent_guide(buffer_id, 7, 9, 1),
17166 indent_guide(buffer_id, 3, 4, 2),
17167 indent_guide(buffer_id, 8, 8, 2),
17168 ],
17169 None,
17170 &mut cx,
17171 );
17172
17173 cx.update_editor(|editor, window, cx| {
17174 editor.fold_at(MultiBufferRow(2), window, cx);
17175 assert_eq!(
17176 editor.display_text(cx),
17177 "
17178 fn main() {
17179 if a {
17180 b(⋯
17181 )
17182 } else {
17183 e(
17184 f
17185 )
17186 }
17187 }"
17188 .unindent()
17189 );
17190 });
17191
17192 assert_indent_guides(
17193 0..11,
17194 vec![
17195 indent_guide(buffer_id, 1, 10, 0),
17196 indent_guide(buffer_id, 2, 5, 1),
17197 indent_guide(buffer_id, 7, 9, 1),
17198 indent_guide(buffer_id, 8, 8, 2),
17199 ],
17200 None,
17201 &mut cx,
17202 );
17203}
17204
17205#[gpui::test]
17206async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17207 let (buffer_id, mut cx) = setup_indent_guides_editor(
17208 &"
17209 block1
17210 block2
17211 block3
17212 block4
17213 block2
17214 block1
17215 block1"
17216 .unindent(),
17217 cx,
17218 )
17219 .await;
17220
17221 assert_indent_guides(
17222 1..10,
17223 vec![
17224 indent_guide(buffer_id, 1, 4, 0),
17225 indent_guide(buffer_id, 2, 3, 1),
17226 indent_guide(buffer_id, 3, 3, 2),
17227 ],
17228 None,
17229 &mut cx,
17230 );
17231}
17232
17233#[gpui::test]
17234async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17235 let (buffer_id, mut cx) = setup_indent_guides_editor(
17236 &"
17237 block1
17238 block2
17239 block3
17240
17241 block1
17242 block1"
17243 .unindent(),
17244 cx,
17245 )
17246 .await;
17247
17248 assert_indent_guides(
17249 0..6,
17250 vec![
17251 indent_guide(buffer_id, 1, 2, 0),
17252 indent_guide(buffer_id, 2, 2, 1),
17253 ],
17254 None,
17255 &mut cx,
17256 );
17257}
17258
17259#[gpui::test]
17260async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17261 let (buffer_id, mut cx) = setup_indent_guides_editor(
17262 &"
17263 function component() {
17264 \treturn (
17265 \t\t\t
17266 \t\t<div>
17267 \t\t\t<abc></abc>
17268 \t\t</div>
17269 \t)
17270 }"
17271 .unindent(),
17272 cx,
17273 )
17274 .await;
17275
17276 assert_indent_guides(
17277 0..8,
17278 vec![
17279 indent_guide(buffer_id, 1, 6, 0),
17280 indent_guide(buffer_id, 2, 5, 1),
17281 indent_guide(buffer_id, 4, 4, 2),
17282 ],
17283 None,
17284 &mut cx,
17285 );
17286}
17287
17288#[gpui::test]
17289async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17290 let (buffer_id, mut cx) = setup_indent_guides_editor(
17291 &"
17292 function component() {
17293 \treturn (
17294 \t
17295 \t\t<div>
17296 \t\t\t<abc></abc>
17297 \t\t</div>
17298 \t)
17299 }"
17300 .unindent(),
17301 cx,
17302 )
17303 .await;
17304
17305 assert_indent_guides(
17306 0..8,
17307 vec![
17308 indent_guide(buffer_id, 1, 6, 0),
17309 indent_guide(buffer_id, 2, 5, 1),
17310 indent_guide(buffer_id, 4, 4, 2),
17311 ],
17312 None,
17313 &mut cx,
17314 );
17315}
17316
17317#[gpui::test]
17318async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17319 let (buffer_id, mut cx) = setup_indent_guides_editor(
17320 &"
17321 block1
17322
17323
17324
17325 block2
17326 "
17327 .unindent(),
17328 cx,
17329 )
17330 .await;
17331
17332 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17333}
17334
17335#[gpui::test]
17336async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17337 let (buffer_id, mut cx) = setup_indent_guides_editor(
17338 &"
17339 def a:
17340 \tb = 3
17341 \tif True:
17342 \t\tc = 4
17343 \t\td = 5
17344 \tprint(b)
17345 "
17346 .unindent(),
17347 cx,
17348 )
17349 .await;
17350
17351 assert_indent_guides(
17352 0..6,
17353 vec![
17354 indent_guide(buffer_id, 1, 5, 0),
17355 indent_guide(buffer_id, 3, 4, 1),
17356 ],
17357 None,
17358 &mut cx,
17359 );
17360}
17361
17362#[gpui::test]
17363async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17364 let (buffer_id, mut cx) = setup_indent_guides_editor(
17365 &"
17366 fn main() {
17367 let a = 1;
17368 }"
17369 .unindent(),
17370 cx,
17371 )
17372 .await;
17373
17374 cx.update_editor(|editor, window, cx| {
17375 editor.change_selections(None, window, cx, |s| {
17376 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17377 });
17378 });
17379
17380 assert_indent_guides(
17381 0..3,
17382 vec![indent_guide(buffer_id, 1, 1, 0)],
17383 Some(vec![0]),
17384 &mut cx,
17385 );
17386}
17387
17388#[gpui::test]
17389async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17390 let (buffer_id, mut cx) = setup_indent_guides_editor(
17391 &"
17392 fn main() {
17393 if 1 == 2 {
17394 let a = 1;
17395 }
17396 }"
17397 .unindent(),
17398 cx,
17399 )
17400 .await;
17401
17402 cx.update_editor(|editor, window, cx| {
17403 editor.change_selections(None, window, cx, |s| {
17404 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17405 });
17406 });
17407
17408 assert_indent_guides(
17409 0..4,
17410 vec![
17411 indent_guide(buffer_id, 1, 3, 0),
17412 indent_guide(buffer_id, 2, 2, 1),
17413 ],
17414 Some(vec![1]),
17415 &mut cx,
17416 );
17417
17418 cx.update_editor(|editor, window, cx| {
17419 editor.change_selections(None, window, cx, |s| {
17420 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17421 });
17422 });
17423
17424 assert_indent_guides(
17425 0..4,
17426 vec![
17427 indent_guide(buffer_id, 1, 3, 0),
17428 indent_guide(buffer_id, 2, 2, 1),
17429 ],
17430 Some(vec![1]),
17431 &mut cx,
17432 );
17433
17434 cx.update_editor(|editor, window, cx| {
17435 editor.change_selections(None, window, cx, |s| {
17436 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17437 });
17438 });
17439
17440 assert_indent_guides(
17441 0..4,
17442 vec![
17443 indent_guide(buffer_id, 1, 3, 0),
17444 indent_guide(buffer_id, 2, 2, 1),
17445 ],
17446 Some(vec![0]),
17447 &mut cx,
17448 );
17449}
17450
17451#[gpui::test]
17452async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17453 let (buffer_id, mut cx) = setup_indent_guides_editor(
17454 &"
17455 fn main() {
17456 let a = 1;
17457
17458 let b = 2;
17459 }"
17460 .unindent(),
17461 cx,
17462 )
17463 .await;
17464
17465 cx.update_editor(|editor, window, cx| {
17466 editor.change_selections(None, window, cx, |s| {
17467 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17468 });
17469 });
17470
17471 assert_indent_guides(
17472 0..5,
17473 vec![indent_guide(buffer_id, 1, 3, 0)],
17474 Some(vec![0]),
17475 &mut cx,
17476 );
17477}
17478
17479#[gpui::test]
17480async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17481 let (buffer_id, mut cx) = setup_indent_guides_editor(
17482 &"
17483 def m:
17484 a = 1
17485 pass"
17486 .unindent(),
17487 cx,
17488 )
17489 .await;
17490
17491 cx.update_editor(|editor, window, cx| {
17492 editor.change_selections(None, window, cx, |s| {
17493 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17494 });
17495 });
17496
17497 assert_indent_guides(
17498 0..3,
17499 vec![indent_guide(buffer_id, 1, 2, 0)],
17500 Some(vec![0]),
17501 &mut cx,
17502 );
17503}
17504
17505#[gpui::test]
17506async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17507 init_test(cx, |_| {});
17508 let mut cx = EditorTestContext::new(cx).await;
17509 let text = indoc! {
17510 "
17511 impl A {
17512 fn b() {
17513 0;
17514 3;
17515 5;
17516 6;
17517 7;
17518 }
17519 }
17520 "
17521 };
17522 let base_text = indoc! {
17523 "
17524 impl A {
17525 fn b() {
17526 0;
17527 1;
17528 2;
17529 3;
17530 4;
17531 }
17532 fn c() {
17533 5;
17534 6;
17535 7;
17536 }
17537 }
17538 "
17539 };
17540
17541 cx.update_editor(|editor, window, cx| {
17542 editor.set_text(text, window, cx);
17543
17544 editor.buffer().update(cx, |multibuffer, cx| {
17545 let buffer = multibuffer.as_singleton().unwrap();
17546 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17547
17548 multibuffer.set_all_diff_hunks_expanded(cx);
17549 multibuffer.add_diff(diff, cx);
17550
17551 buffer.read(cx).remote_id()
17552 })
17553 });
17554 cx.run_until_parked();
17555
17556 cx.assert_state_with_diff(
17557 indoc! { "
17558 impl A {
17559 fn b() {
17560 0;
17561 - 1;
17562 - 2;
17563 3;
17564 - 4;
17565 - }
17566 - fn c() {
17567 5;
17568 6;
17569 7;
17570 }
17571 }
17572 ˇ"
17573 }
17574 .to_string(),
17575 );
17576
17577 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17578 editor
17579 .snapshot(window, cx)
17580 .buffer_snapshot
17581 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17582 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17583 .collect::<Vec<_>>()
17584 });
17585 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17586 assert_eq!(
17587 actual_guides,
17588 vec![
17589 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17590 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17591 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17592 ]
17593 );
17594}
17595
17596#[gpui::test]
17597async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17598 init_test(cx, |_| {});
17599 let mut cx = EditorTestContext::new(cx).await;
17600
17601 let diff_base = r#"
17602 a
17603 b
17604 c
17605 "#
17606 .unindent();
17607
17608 cx.set_state(
17609 &r#"
17610 ˇA
17611 b
17612 C
17613 "#
17614 .unindent(),
17615 );
17616 cx.set_head_text(&diff_base);
17617 cx.update_editor(|editor, window, cx| {
17618 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17619 });
17620 executor.run_until_parked();
17621
17622 let both_hunks_expanded = r#"
17623 - a
17624 + ˇA
17625 b
17626 - c
17627 + C
17628 "#
17629 .unindent();
17630
17631 cx.assert_state_with_diff(both_hunks_expanded.clone());
17632
17633 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17634 let snapshot = editor.snapshot(window, cx);
17635 let hunks = editor
17636 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17637 .collect::<Vec<_>>();
17638 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17639 let buffer_id = hunks[0].buffer_id;
17640 hunks
17641 .into_iter()
17642 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17643 .collect::<Vec<_>>()
17644 });
17645 assert_eq!(hunk_ranges.len(), 2);
17646
17647 cx.update_editor(|editor, _, cx| {
17648 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17649 });
17650 executor.run_until_parked();
17651
17652 let second_hunk_expanded = r#"
17653 ˇA
17654 b
17655 - c
17656 + C
17657 "#
17658 .unindent();
17659
17660 cx.assert_state_with_diff(second_hunk_expanded);
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 cx.assert_state_with_diff(both_hunks_expanded.clone());
17668
17669 cx.update_editor(|editor, _, cx| {
17670 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17671 });
17672 executor.run_until_parked();
17673
17674 let first_hunk_expanded = r#"
17675 - a
17676 + ˇA
17677 b
17678 C
17679 "#
17680 .unindent();
17681
17682 cx.assert_state_with_diff(first_hunk_expanded);
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 cx.assert_state_with_diff(both_hunks_expanded);
17690
17691 cx.set_state(
17692 &r#"
17693 ˇA
17694 b
17695 "#
17696 .unindent(),
17697 );
17698 cx.run_until_parked();
17699
17700 // TODO this cursor position seems bad
17701 cx.assert_state_with_diff(
17702 r#"
17703 - ˇa
17704 + A
17705 b
17706 "#
17707 .unindent(),
17708 );
17709
17710 cx.update_editor(|editor, window, cx| {
17711 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17712 });
17713
17714 cx.assert_state_with_diff(
17715 r#"
17716 - ˇa
17717 + A
17718 b
17719 - c
17720 "#
17721 .unindent(),
17722 );
17723
17724 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17725 let snapshot = editor.snapshot(window, cx);
17726 let hunks = editor
17727 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17728 .collect::<Vec<_>>();
17729 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17730 let buffer_id = hunks[0].buffer_id;
17731 hunks
17732 .into_iter()
17733 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17734 .collect::<Vec<_>>()
17735 });
17736 assert_eq!(hunk_ranges.len(), 2);
17737
17738 cx.update_editor(|editor, _, cx| {
17739 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17740 });
17741 executor.run_until_parked();
17742
17743 cx.assert_state_with_diff(
17744 r#"
17745 - ˇa
17746 + A
17747 b
17748 "#
17749 .unindent(),
17750 );
17751}
17752
17753#[gpui::test]
17754async fn test_toggle_deletion_hunk_at_start_of_file(
17755 executor: BackgroundExecutor,
17756 cx: &mut TestAppContext,
17757) {
17758 init_test(cx, |_| {});
17759 let mut cx = EditorTestContext::new(cx).await;
17760
17761 let diff_base = r#"
17762 a
17763 b
17764 c
17765 "#
17766 .unindent();
17767
17768 cx.set_state(
17769 &r#"
17770 ˇb
17771 c
17772 "#
17773 .unindent(),
17774 );
17775 cx.set_head_text(&diff_base);
17776 cx.update_editor(|editor, window, cx| {
17777 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17778 });
17779 executor.run_until_parked();
17780
17781 let hunk_expanded = r#"
17782 - a
17783 ˇb
17784 c
17785 "#
17786 .unindent();
17787
17788 cx.assert_state_with_diff(hunk_expanded.clone());
17789
17790 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17791 let snapshot = editor.snapshot(window, cx);
17792 let hunks = editor
17793 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17794 .collect::<Vec<_>>();
17795 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17796 let buffer_id = hunks[0].buffer_id;
17797 hunks
17798 .into_iter()
17799 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17800 .collect::<Vec<_>>()
17801 });
17802 assert_eq!(hunk_ranges.len(), 1);
17803
17804 cx.update_editor(|editor, _, cx| {
17805 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17806 });
17807 executor.run_until_parked();
17808
17809 let hunk_collapsed = r#"
17810 ˇb
17811 c
17812 "#
17813 .unindent();
17814
17815 cx.assert_state_with_diff(hunk_collapsed);
17816
17817 cx.update_editor(|editor, _, cx| {
17818 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17819 });
17820 executor.run_until_parked();
17821
17822 cx.assert_state_with_diff(hunk_expanded.clone());
17823}
17824
17825#[gpui::test]
17826async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17827 init_test(cx, |_| {});
17828
17829 let fs = FakeFs::new(cx.executor());
17830 fs.insert_tree(
17831 path!("/test"),
17832 json!({
17833 ".git": {},
17834 "file-1": "ONE\n",
17835 "file-2": "TWO\n",
17836 "file-3": "THREE\n",
17837 }),
17838 )
17839 .await;
17840
17841 fs.set_head_for_repo(
17842 path!("/test/.git").as_ref(),
17843 &[
17844 ("file-1".into(), "one\n".into()),
17845 ("file-2".into(), "two\n".into()),
17846 ("file-3".into(), "three\n".into()),
17847 ],
17848 );
17849
17850 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17851 let mut buffers = vec![];
17852 for i in 1..=3 {
17853 let buffer = project
17854 .update(cx, |project, cx| {
17855 let path = format!(path!("/test/file-{}"), i);
17856 project.open_local_buffer(path, cx)
17857 })
17858 .await
17859 .unwrap();
17860 buffers.push(buffer);
17861 }
17862
17863 let multibuffer = cx.new(|cx| {
17864 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17865 multibuffer.set_all_diff_hunks_expanded(cx);
17866 for buffer in &buffers {
17867 let snapshot = buffer.read(cx).snapshot();
17868 multibuffer.set_excerpts_for_path(
17869 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17870 buffer.clone(),
17871 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17872 DEFAULT_MULTIBUFFER_CONTEXT,
17873 cx,
17874 );
17875 }
17876 multibuffer
17877 });
17878
17879 let editor = cx.add_window(|window, cx| {
17880 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17881 });
17882 cx.run_until_parked();
17883
17884 let snapshot = editor
17885 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17886 .unwrap();
17887 let hunks = snapshot
17888 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17889 .map(|hunk| match hunk {
17890 DisplayDiffHunk::Unfolded {
17891 display_row_range, ..
17892 } => display_row_range,
17893 DisplayDiffHunk::Folded { .. } => unreachable!(),
17894 })
17895 .collect::<Vec<_>>();
17896 assert_eq!(
17897 hunks,
17898 [
17899 DisplayRow(2)..DisplayRow(4),
17900 DisplayRow(7)..DisplayRow(9),
17901 DisplayRow(12)..DisplayRow(14),
17902 ]
17903 );
17904}
17905
17906#[gpui::test]
17907async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17908 init_test(cx, |_| {});
17909
17910 let mut cx = EditorTestContext::new(cx).await;
17911 cx.set_head_text(indoc! { "
17912 one
17913 two
17914 three
17915 four
17916 five
17917 "
17918 });
17919 cx.set_index_text(indoc! { "
17920 one
17921 two
17922 three
17923 four
17924 five
17925 "
17926 });
17927 cx.set_state(indoc! {"
17928 one
17929 TWO
17930 ˇTHREE
17931 FOUR
17932 five
17933 "});
17934 cx.run_until_parked();
17935 cx.update_editor(|editor, window, cx| {
17936 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17937 });
17938 cx.run_until_parked();
17939 cx.assert_index_text(Some(indoc! {"
17940 one
17941 TWO
17942 THREE
17943 FOUR
17944 five
17945 "}));
17946 cx.set_state(indoc! { "
17947 one
17948 TWO
17949 ˇTHREE-HUNDRED
17950 FOUR
17951 five
17952 "});
17953 cx.run_until_parked();
17954 cx.update_editor(|editor, window, cx| {
17955 let snapshot = editor.snapshot(window, cx);
17956 let hunks = editor
17957 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17958 .collect::<Vec<_>>();
17959 assert_eq!(hunks.len(), 1);
17960 assert_eq!(
17961 hunks[0].status(),
17962 DiffHunkStatus {
17963 kind: DiffHunkStatusKind::Modified,
17964 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17965 }
17966 );
17967
17968 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17969 });
17970 cx.run_until_parked();
17971 cx.assert_index_text(Some(indoc! {"
17972 one
17973 TWO
17974 THREE-HUNDRED
17975 FOUR
17976 five
17977 "}));
17978}
17979
17980#[gpui::test]
17981fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17982 init_test(cx, |_| {});
17983
17984 let editor = cx.add_window(|window, cx| {
17985 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17986 build_editor(buffer, window, cx)
17987 });
17988
17989 let render_args = Arc::new(Mutex::new(None));
17990 let snapshot = editor
17991 .update(cx, |editor, window, cx| {
17992 let snapshot = editor.buffer().read(cx).snapshot(cx);
17993 let range =
17994 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17995
17996 struct RenderArgs {
17997 row: MultiBufferRow,
17998 folded: bool,
17999 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18000 }
18001
18002 let crease = Crease::inline(
18003 range,
18004 FoldPlaceholder::test(),
18005 {
18006 let toggle_callback = render_args.clone();
18007 move |row, folded, callback, _window, _cx| {
18008 *toggle_callback.lock() = Some(RenderArgs {
18009 row,
18010 folded,
18011 callback,
18012 });
18013 div()
18014 }
18015 },
18016 |_row, _folded, _window, _cx| div(),
18017 );
18018
18019 editor.insert_creases(Some(crease), cx);
18020 let snapshot = editor.snapshot(window, cx);
18021 let _div = snapshot.render_crease_toggle(
18022 MultiBufferRow(1),
18023 false,
18024 cx.entity().clone(),
18025 window,
18026 cx,
18027 );
18028 snapshot
18029 })
18030 .unwrap();
18031
18032 let render_args = render_args.lock().take().unwrap();
18033 assert_eq!(render_args.row, MultiBufferRow(1));
18034 assert!(!render_args.folded);
18035 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18036
18037 cx.update_window(*editor, |_, window, cx| {
18038 (render_args.callback)(true, window, cx)
18039 })
18040 .unwrap();
18041 let snapshot = editor
18042 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18043 .unwrap();
18044 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18045
18046 cx.update_window(*editor, |_, window, cx| {
18047 (render_args.callback)(false, window, cx)
18048 })
18049 .unwrap();
18050 let snapshot = editor
18051 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18052 .unwrap();
18053 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18054}
18055
18056#[gpui::test]
18057async fn test_input_text(cx: &mut TestAppContext) {
18058 init_test(cx, |_| {});
18059 let mut cx = EditorTestContext::new(cx).await;
18060
18061 cx.set_state(
18062 &r#"ˇone
18063 two
18064
18065 three
18066 fourˇ
18067 five
18068
18069 siˇx"#
18070 .unindent(),
18071 );
18072
18073 cx.dispatch_action(HandleInput(String::new()));
18074 cx.assert_editor_state(
18075 &r#"ˇone
18076 two
18077
18078 three
18079 fourˇ
18080 five
18081
18082 siˇx"#
18083 .unindent(),
18084 );
18085
18086 cx.dispatch_action(HandleInput("AAAA".to_string()));
18087 cx.assert_editor_state(
18088 &r#"AAAAˇone
18089 two
18090
18091 three
18092 fourAAAAˇ
18093 five
18094
18095 siAAAAˇx"#
18096 .unindent(),
18097 );
18098}
18099
18100#[gpui::test]
18101async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18102 init_test(cx, |_| {});
18103
18104 let mut cx = EditorTestContext::new(cx).await;
18105 cx.set_state(
18106 r#"let foo = 1;
18107let foo = 2;
18108let foo = 3;
18109let fooˇ = 4;
18110let foo = 5;
18111let foo = 6;
18112let foo = 7;
18113let foo = 8;
18114let foo = 9;
18115let foo = 10;
18116let foo = 11;
18117let foo = 12;
18118let foo = 13;
18119let foo = 14;
18120let foo = 15;"#,
18121 );
18122
18123 cx.update_editor(|e, window, cx| {
18124 assert_eq!(
18125 e.next_scroll_position,
18126 NextScrollCursorCenterTopBottom::Center,
18127 "Default next scroll direction is center",
18128 );
18129
18130 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18131 assert_eq!(
18132 e.next_scroll_position,
18133 NextScrollCursorCenterTopBottom::Top,
18134 "After center, next scroll direction should be top",
18135 );
18136
18137 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18138 assert_eq!(
18139 e.next_scroll_position,
18140 NextScrollCursorCenterTopBottom::Bottom,
18141 "After top, next scroll direction should be bottom",
18142 );
18143
18144 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18145 assert_eq!(
18146 e.next_scroll_position,
18147 NextScrollCursorCenterTopBottom::Center,
18148 "After bottom, scrolling should start over",
18149 );
18150
18151 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18152 assert_eq!(
18153 e.next_scroll_position,
18154 NextScrollCursorCenterTopBottom::Top,
18155 "Scrolling continues if retriggered fast enough"
18156 );
18157 });
18158
18159 cx.executor()
18160 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18161 cx.executor().run_until_parked();
18162 cx.update_editor(|e, _, _| {
18163 assert_eq!(
18164 e.next_scroll_position,
18165 NextScrollCursorCenterTopBottom::Center,
18166 "If scrolling is not triggered fast enough, it should reset"
18167 );
18168 });
18169}
18170
18171#[gpui::test]
18172async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18173 init_test(cx, |_| {});
18174 let mut cx = EditorLspTestContext::new_rust(
18175 lsp::ServerCapabilities {
18176 definition_provider: Some(lsp::OneOf::Left(true)),
18177 references_provider: Some(lsp::OneOf::Left(true)),
18178 ..lsp::ServerCapabilities::default()
18179 },
18180 cx,
18181 )
18182 .await;
18183
18184 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18185 let go_to_definition = cx
18186 .lsp
18187 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18188 move |params, _| async move {
18189 if empty_go_to_definition {
18190 Ok(None)
18191 } else {
18192 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18193 uri: params.text_document_position_params.text_document.uri,
18194 range: lsp::Range::new(
18195 lsp::Position::new(4, 3),
18196 lsp::Position::new(4, 6),
18197 ),
18198 })))
18199 }
18200 },
18201 );
18202 let references = cx
18203 .lsp
18204 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18205 Ok(Some(vec![lsp::Location {
18206 uri: params.text_document_position.text_document.uri,
18207 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18208 }]))
18209 });
18210 (go_to_definition, references)
18211 };
18212
18213 cx.set_state(
18214 &r#"fn one() {
18215 let mut a = ˇtwo();
18216 }
18217
18218 fn two() {}"#
18219 .unindent(),
18220 );
18221 set_up_lsp_handlers(false, &mut cx);
18222 let navigated = cx
18223 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18224 .await
18225 .expect("Failed to navigate to definition");
18226 assert_eq!(
18227 navigated,
18228 Navigated::Yes,
18229 "Should have navigated to definition from the GetDefinition response"
18230 );
18231 cx.assert_editor_state(
18232 &r#"fn one() {
18233 let mut a = two();
18234 }
18235
18236 fn «twoˇ»() {}"#
18237 .unindent(),
18238 );
18239
18240 let editors = cx.update_workspace(|workspace, _, cx| {
18241 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18242 });
18243 cx.update_editor(|_, _, test_editor_cx| {
18244 assert_eq!(
18245 editors.len(),
18246 1,
18247 "Initially, only one, test, editor should be open in the workspace"
18248 );
18249 assert_eq!(
18250 test_editor_cx.entity(),
18251 editors.last().expect("Asserted len is 1").clone()
18252 );
18253 });
18254
18255 set_up_lsp_handlers(true, &mut cx);
18256 let navigated = cx
18257 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18258 .await
18259 .expect("Failed to navigate to lookup references");
18260 assert_eq!(
18261 navigated,
18262 Navigated::Yes,
18263 "Should have navigated to references as a fallback after empty GoToDefinition response"
18264 );
18265 // We should not change the selections in the existing file,
18266 // if opening another milti buffer with the references
18267 cx.assert_editor_state(
18268 &r#"fn one() {
18269 let mut a = two();
18270 }
18271
18272 fn «twoˇ»() {}"#
18273 .unindent(),
18274 );
18275 let editors = cx.update_workspace(|workspace, _, cx| {
18276 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18277 });
18278 cx.update_editor(|_, _, test_editor_cx| {
18279 assert_eq!(
18280 editors.len(),
18281 2,
18282 "After falling back to references search, we open a new editor with the results"
18283 );
18284 let references_fallback_text = editors
18285 .into_iter()
18286 .find(|new_editor| *new_editor != test_editor_cx.entity())
18287 .expect("Should have one non-test editor now")
18288 .read(test_editor_cx)
18289 .text(test_editor_cx);
18290 assert_eq!(
18291 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18292 "Should use the range from the references response and not the GoToDefinition one"
18293 );
18294 });
18295}
18296
18297#[gpui::test]
18298async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18299 init_test(cx, |_| {});
18300 cx.update(|cx| {
18301 let mut editor_settings = EditorSettings::get_global(cx).clone();
18302 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18303 EditorSettings::override_global(editor_settings, cx);
18304 });
18305 let mut cx = EditorLspTestContext::new_rust(
18306 lsp::ServerCapabilities {
18307 definition_provider: Some(lsp::OneOf::Left(true)),
18308 references_provider: Some(lsp::OneOf::Left(true)),
18309 ..lsp::ServerCapabilities::default()
18310 },
18311 cx,
18312 )
18313 .await;
18314 let original_state = r#"fn one() {
18315 let mut a = ˇtwo();
18316 }
18317
18318 fn two() {}"#
18319 .unindent();
18320 cx.set_state(&original_state);
18321
18322 let mut go_to_definition = cx
18323 .lsp
18324 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18325 move |_, _| async move { Ok(None) },
18326 );
18327 let _references = cx
18328 .lsp
18329 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18330 panic!("Should not call for references with no go to definition fallback")
18331 });
18332
18333 let navigated = cx
18334 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18335 .await
18336 .expect("Failed to navigate to lookup references");
18337 go_to_definition
18338 .next()
18339 .await
18340 .expect("Should have called the go_to_definition handler");
18341
18342 assert_eq!(
18343 navigated,
18344 Navigated::No,
18345 "Should have navigated to references as a fallback after empty GoToDefinition response"
18346 );
18347 cx.assert_editor_state(&original_state);
18348 let editors = cx.update_workspace(|workspace, _, cx| {
18349 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18350 });
18351 cx.update_editor(|_, _, _| {
18352 assert_eq!(
18353 editors.len(),
18354 1,
18355 "After unsuccessful fallback, no other editor should have been opened"
18356 );
18357 });
18358}
18359
18360#[gpui::test]
18361async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18362 init_test(cx, |_| {});
18363
18364 let language = Arc::new(Language::new(
18365 LanguageConfig::default(),
18366 Some(tree_sitter_rust::LANGUAGE.into()),
18367 ));
18368
18369 let text = r#"
18370 #[cfg(test)]
18371 mod tests() {
18372 #[test]
18373 fn runnable_1() {
18374 let a = 1;
18375 }
18376
18377 #[test]
18378 fn runnable_2() {
18379 let a = 1;
18380 let b = 2;
18381 }
18382 }
18383 "#
18384 .unindent();
18385
18386 let fs = FakeFs::new(cx.executor());
18387 fs.insert_file("/file.rs", Default::default()).await;
18388
18389 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18390 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18391 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18392 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18393 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18394
18395 let editor = cx.new_window_entity(|window, cx| {
18396 Editor::new(
18397 EditorMode::full(),
18398 multi_buffer,
18399 Some(project.clone()),
18400 window,
18401 cx,
18402 )
18403 });
18404
18405 editor.update_in(cx, |editor, window, cx| {
18406 let snapshot = editor.buffer().read(cx).snapshot(cx);
18407 editor.tasks.insert(
18408 (buffer.read(cx).remote_id(), 3),
18409 RunnableTasks {
18410 templates: vec![],
18411 offset: snapshot.anchor_before(43),
18412 column: 0,
18413 extra_variables: HashMap::default(),
18414 context_range: BufferOffset(43)..BufferOffset(85),
18415 },
18416 );
18417 editor.tasks.insert(
18418 (buffer.read(cx).remote_id(), 8),
18419 RunnableTasks {
18420 templates: vec![],
18421 offset: snapshot.anchor_before(86),
18422 column: 0,
18423 extra_variables: HashMap::default(),
18424 context_range: BufferOffset(86)..BufferOffset(191),
18425 },
18426 );
18427
18428 // Test finding task when cursor is inside function body
18429 editor.change_selections(None, window, cx, |s| {
18430 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18431 });
18432 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18433 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18434
18435 // Test finding task when cursor is on function name
18436 editor.change_selections(None, window, cx, |s| {
18437 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18438 });
18439 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18440 assert_eq!(row, 8, "Should find task when cursor is on function name");
18441 });
18442}
18443
18444#[gpui::test]
18445async fn test_folding_buffers(cx: &mut TestAppContext) {
18446 init_test(cx, |_| {});
18447
18448 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18449 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18450 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18451
18452 let fs = FakeFs::new(cx.executor());
18453 fs.insert_tree(
18454 path!("/a"),
18455 json!({
18456 "first.rs": sample_text_1,
18457 "second.rs": sample_text_2,
18458 "third.rs": sample_text_3,
18459 }),
18460 )
18461 .await;
18462 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18463 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18464 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18465 let worktree = project.update(cx, |project, cx| {
18466 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18467 assert_eq!(worktrees.len(), 1);
18468 worktrees.pop().unwrap()
18469 });
18470 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18471
18472 let buffer_1 = project
18473 .update(cx, |project, cx| {
18474 project.open_buffer((worktree_id, "first.rs"), cx)
18475 })
18476 .await
18477 .unwrap();
18478 let buffer_2 = project
18479 .update(cx, |project, cx| {
18480 project.open_buffer((worktree_id, "second.rs"), cx)
18481 })
18482 .await
18483 .unwrap();
18484 let buffer_3 = project
18485 .update(cx, |project, cx| {
18486 project.open_buffer((worktree_id, "third.rs"), cx)
18487 })
18488 .await
18489 .unwrap();
18490
18491 let multi_buffer = cx.new(|cx| {
18492 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18493 multi_buffer.push_excerpts(
18494 buffer_1.clone(),
18495 [
18496 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18497 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18498 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18499 ],
18500 cx,
18501 );
18502 multi_buffer.push_excerpts(
18503 buffer_2.clone(),
18504 [
18505 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18506 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18507 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18508 ],
18509 cx,
18510 );
18511 multi_buffer.push_excerpts(
18512 buffer_3.clone(),
18513 [
18514 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18515 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18516 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18517 ],
18518 cx,
18519 );
18520 multi_buffer
18521 });
18522 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18523 Editor::new(
18524 EditorMode::full(),
18525 multi_buffer.clone(),
18526 Some(project.clone()),
18527 window,
18528 cx,
18529 )
18530 });
18531
18532 assert_eq!(
18533 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18534 "\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",
18535 );
18536
18537 multi_buffer_editor.update(cx, |editor, cx| {
18538 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18539 });
18540 assert_eq!(
18541 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18542 "\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",
18543 "After folding the first buffer, its text should not be displayed"
18544 );
18545
18546 multi_buffer_editor.update(cx, |editor, cx| {
18547 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18548 });
18549 assert_eq!(
18550 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18551 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18552 "After folding the second buffer, its text should not be displayed"
18553 );
18554
18555 multi_buffer_editor.update(cx, |editor, cx| {
18556 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18557 });
18558 assert_eq!(
18559 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18560 "\n\n\n\n\n",
18561 "After folding the third buffer, its text should not be displayed"
18562 );
18563
18564 // Emulate selection inside the fold logic, that should work
18565 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18566 editor
18567 .snapshot(window, cx)
18568 .next_line_boundary(Point::new(0, 4));
18569 });
18570
18571 multi_buffer_editor.update(cx, |editor, cx| {
18572 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18573 });
18574 assert_eq!(
18575 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18576 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18577 "After unfolding the second buffer, its text should be displayed"
18578 );
18579
18580 // Typing inside of buffer 1 causes that buffer to be unfolded.
18581 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18582 assert_eq!(
18583 multi_buffer
18584 .read(cx)
18585 .snapshot(cx)
18586 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18587 .collect::<String>(),
18588 "bbbb"
18589 );
18590 editor.change_selections(None, window, cx, |selections| {
18591 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18592 });
18593 editor.handle_input("B", window, cx);
18594 });
18595
18596 assert_eq!(
18597 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18598 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18599 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18600 );
18601
18602 multi_buffer_editor.update(cx, |editor, cx| {
18603 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18604 });
18605 assert_eq!(
18606 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18607 "\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",
18608 "After unfolding the all buffers, all original text should be displayed"
18609 );
18610}
18611
18612#[gpui::test]
18613async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18614 init_test(cx, |_| {});
18615
18616 let sample_text_1 = "1111\n2222\n3333".to_string();
18617 let sample_text_2 = "4444\n5555\n6666".to_string();
18618 let sample_text_3 = "7777\n8888\n9999".to_string();
18619
18620 let fs = FakeFs::new(cx.executor());
18621 fs.insert_tree(
18622 path!("/a"),
18623 json!({
18624 "first.rs": sample_text_1,
18625 "second.rs": sample_text_2,
18626 "third.rs": sample_text_3,
18627 }),
18628 )
18629 .await;
18630 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18631 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18632 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18633 let worktree = project.update(cx, |project, cx| {
18634 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18635 assert_eq!(worktrees.len(), 1);
18636 worktrees.pop().unwrap()
18637 });
18638 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18639
18640 let buffer_1 = project
18641 .update(cx, |project, cx| {
18642 project.open_buffer((worktree_id, "first.rs"), cx)
18643 })
18644 .await
18645 .unwrap();
18646 let buffer_2 = project
18647 .update(cx, |project, cx| {
18648 project.open_buffer((worktree_id, "second.rs"), cx)
18649 })
18650 .await
18651 .unwrap();
18652 let buffer_3 = project
18653 .update(cx, |project, cx| {
18654 project.open_buffer((worktree_id, "third.rs"), cx)
18655 })
18656 .await
18657 .unwrap();
18658
18659 let multi_buffer = cx.new(|cx| {
18660 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18661 multi_buffer.push_excerpts(
18662 buffer_1.clone(),
18663 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18664 cx,
18665 );
18666 multi_buffer.push_excerpts(
18667 buffer_2.clone(),
18668 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18669 cx,
18670 );
18671 multi_buffer.push_excerpts(
18672 buffer_3.clone(),
18673 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18674 cx,
18675 );
18676 multi_buffer
18677 });
18678
18679 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18680 Editor::new(
18681 EditorMode::full(),
18682 multi_buffer,
18683 Some(project.clone()),
18684 window,
18685 cx,
18686 )
18687 });
18688
18689 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18690 assert_eq!(
18691 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18692 full_text,
18693 );
18694
18695 multi_buffer_editor.update(cx, |editor, cx| {
18696 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18697 });
18698 assert_eq!(
18699 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18700 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18701 "After folding the first buffer, its text should not be displayed"
18702 );
18703
18704 multi_buffer_editor.update(cx, |editor, cx| {
18705 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18706 });
18707
18708 assert_eq!(
18709 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18710 "\n\n\n\n\n\n7777\n8888\n9999",
18711 "After folding the second buffer, its text should not be displayed"
18712 );
18713
18714 multi_buffer_editor.update(cx, |editor, cx| {
18715 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18716 });
18717 assert_eq!(
18718 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18719 "\n\n\n\n\n",
18720 "After folding the third buffer, its text should not be displayed"
18721 );
18722
18723 multi_buffer_editor.update(cx, |editor, cx| {
18724 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18725 });
18726 assert_eq!(
18727 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18728 "\n\n\n\n4444\n5555\n6666\n\n",
18729 "After unfolding the second buffer, its text should be displayed"
18730 );
18731
18732 multi_buffer_editor.update(cx, |editor, cx| {
18733 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18734 });
18735 assert_eq!(
18736 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18737 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18738 "After unfolding the first buffer, its text should be displayed"
18739 );
18740
18741 multi_buffer_editor.update(cx, |editor, cx| {
18742 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18743 });
18744 assert_eq!(
18745 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18746 full_text,
18747 "After unfolding all buffers, all original text should be displayed"
18748 );
18749}
18750
18751#[gpui::test]
18752async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18753 init_test(cx, |_| {});
18754
18755 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18756
18757 let fs = FakeFs::new(cx.executor());
18758 fs.insert_tree(
18759 path!("/a"),
18760 json!({
18761 "main.rs": sample_text,
18762 }),
18763 )
18764 .await;
18765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18766 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18768 let worktree = project.update(cx, |project, cx| {
18769 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18770 assert_eq!(worktrees.len(), 1);
18771 worktrees.pop().unwrap()
18772 });
18773 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18774
18775 let buffer_1 = project
18776 .update(cx, |project, cx| {
18777 project.open_buffer((worktree_id, "main.rs"), cx)
18778 })
18779 .await
18780 .unwrap();
18781
18782 let multi_buffer = cx.new(|cx| {
18783 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18784 multi_buffer.push_excerpts(
18785 buffer_1.clone(),
18786 [ExcerptRange::new(
18787 Point::new(0, 0)
18788 ..Point::new(
18789 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18790 0,
18791 ),
18792 )],
18793 cx,
18794 );
18795 multi_buffer
18796 });
18797 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18798 Editor::new(
18799 EditorMode::full(),
18800 multi_buffer,
18801 Some(project.clone()),
18802 window,
18803 cx,
18804 )
18805 });
18806
18807 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18808 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18809 enum TestHighlight {}
18810 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18811 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18812 editor.highlight_text::<TestHighlight>(
18813 vec![highlight_range.clone()],
18814 HighlightStyle::color(Hsla::green()),
18815 cx,
18816 );
18817 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18818 });
18819
18820 let full_text = format!("\n\n{sample_text}");
18821 assert_eq!(
18822 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18823 full_text,
18824 );
18825}
18826
18827#[gpui::test]
18828async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18829 init_test(cx, |_| {});
18830 cx.update(|cx| {
18831 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18832 "keymaps/default-linux.json",
18833 cx,
18834 )
18835 .unwrap();
18836 cx.bind_keys(default_key_bindings);
18837 });
18838
18839 let (editor, cx) = cx.add_window_view(|window, cx| {
18840 let multi_buffer = MultiBuffer::build_multi(
18841 [
18842 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18843 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18844 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18845 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18846 ],
18847 cx,
18848 );
18849 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18850
18851 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18852 // fold all but the second buffer, so that we test navigating between two
18853 // adjacent folded buffers, as well as folded buffers at the start and
18854 // end the multibuffer
18855 editor.fold_buffer(buffer_ids[0], cx);
18856 editor.fold_buffer(buffer_ids[2], cx);
18857 editor.fold_buffer(buffer_ids[3], cx);
18858
18859 editor
18860 });
18861 cx.simulate_resize(size(px(1000.), px(1000.)));
18862
18863 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18864 cx.assert_excerpts_with_selections(indoc! {"
18865 [EXCERPT]
18866 ˇ[FOLDED]
18867 [EXCERPT]
18868 a1
18869 b1
18870 [EXCERPT]
18871 [FOLDED]
18872 [EXCERPT]
18873 [FOLDED]
18874 "
18875 });
18876 cx.simulate_keystroke("down");
18877 cx.assert_excerpts_with_selections(indoc! {"
18878 [EXCERPT]
18879 [FOLDED]
18880 [EXCERPT]
18881 ˇa1
18882 b1
18883 [EXCERPT]
18884 [FOLDED]
18885 [EXCERPT]
18886 [FOLDED]
18887 "
18888 });
18889 cx.simulate_keystroke("down");
18890 cx.assert_excerpts_with_selections(indoc! {"
18891 [EXCERPT]
18892 [FOLDED]
18893 [EXCERPT]
18894 a1
18895 ˇb1
18896 [EXCERPT]
18897 [FOLDED]
18898 [EXCERPT]
18899 [FOLDED]
18900 "
18901 });
18902 cx.simulate_keystroke("down");
18903 cx.assert_excerpts_with_selections(indoc! {"
18904 [EXCERPT]
18905 [FOLDED]
18906 [EXCERPT]
18907 a1
18908 b1
18909 ˇ[EXCERPT]
18910 [FOLDED]
18911 [EXCERPT]
18912 [FOLDED]
18913 "
18914 });
18915 cx.simulate_keystroke("down");
18916 cx.assert_excerpts_with_selections(indoc! {"
18917 [EXCERPT]
18918 [FOLDED]
18919 [EXCERPT]
18920 a1
18921 b1
18922 [EXCERPT]
18923 ˇ[FOLDED]
18924 [EXCERPT]
18925 [FOLDED]
18926 "
18927 });
18928 for _ in 0..5 {
18929 cx.simulate_keystroke("down");
18930 cx.assert_excerpts_with_selections(indoc! {"
18931 [EXCERPT]
18932 [FOLDED]
18933 [EXCERPT]
18934 a1
18935 b1
18936 [EXCERPT]
18937 [FOLDED]
18938 [EXCERPT]
18939 ˇ[FOLDED]
18940 "
18941 });
18942 }
18943
18944 cx.simulate_keystroke("up");
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 cx.simulate_keystroke("up");
18958 cx.assert_excerpts_with_selections(indoc! {"
18959 [EXCERPT]
18960 [FOLDED]
18961 [EXCERPT]
18962 a1
18963 b1
18964 ˇ[EXCERPT]
18965 [FOLDED]
18966 [EXCERPT]
18967 [FOLDED]
18968 "
18969 });
18970 cx.simulate_keystroke("up");
18971 cx.assert_excerpts_with_selections(indoc! {"
18972 [EXCERPT]
18973 [FOLDED]
18974 [EXCERPT]
18975 a1
18976 ˇb1
18977 [EXCERPT]
18978 [FOLDED]
18979 [EXCERPT]
18980 [FOLDED]
18981 "
18982 });
18983 cx.simulate_keystroke("up");
18984 cx.assert_excerpts_with_selections(indoc! {"
18985 [EXCERPT]
18986 [FOLDED]
18987 [EXCERPT]
18988 ˇa1
18989 b1
18990 [EXCERPT]
18991 [FOLDED]
18992 [EXCERPT]
18993 [FOLDED]
18994 "
18995 });
18996 for _ in 0..5 {
18997 cx.simulate_keystroke("up");
18998 cx.assert_excerpts_with_selections(indoc! {"
18999 [EXCERPT]
19000 ˇ[FOLDED]
19001 [EXCERPT]
19002 a1
19003 b1
19004 [EXCERPT]
19005 [FOLDED]
19006 [EXCERPT]
19007 [FOLDED]
19008 "
19009 });
19010 }
19011}
19012
19013#[gpui::test]
19014async fn test_inline_completion_text(cx: &mut TestAppContext) {
19015 init_test(cx, |_| {});
19016
19017 // Simple insertion
19018 assert_highlighted_edits(
19019 "Hello, world!",
19020 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19021 true,
19022 cx,
19023 |highlighted_edits, cx| {
19024 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19025 assert_eq!(highlighted_edits.highlights.len(), 1);
19026 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19027 assert_eq!(
19028 highlighted_edits.highlights[0].1.background_color,
19029 Some(cx.theme().status().created_background)
19030 );
19031 },
19032 )
19033 .await;
19034
19035 // Replacement
19036 assert_highlighted_edits(
19037 "This is a test.",
19038 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19039 false,
19040 cx,
19041 |highlighted_edits, cx| {
19042 assert_eq!(highlighted_edits.text, "That is a test.");
19043 assert_eq!(highlighted_edits.highlights.len(), 1);
19044 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19045 assert_eq!(
19046 highlighted_edits.highlights[0].1.background_color,
19047 Some(cx.theme().status().created_background)
19048 );
19049 },
19050 )
19051 .await;
19052
19053 // Multiple edits
19054 assert_highlighted_edits(
19055 "Hello, world!",
19056 vec![
19057 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19058 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19059 ],
19060 false,
19061 cx,
19062 |highlighted_edits, cx| {
19063 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19064 assert_eq!(highlighted_edits.highlights.len(), 2);
19065 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19066 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19067 assert_eq!(
19068 highlighted_edits.highlights[0].1.background_color,
19069 Some(cx.theme().status().created_background)
19070 );
19071 assert_eq!(
19072 highlighted_edits.highlights[1].1.background_color,
19073 Some(cx.theme().status().created_background)
19074 );
19075 },
19076 )
19077 .await;
19078
19079 // Multiple lines with edits
19080 assert_highlighted_edits(
19081 "First line\nSecond line\nThird line\nFourth line",
19082 vec![
19083 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19084 (
19085 Point::new(2, 0)..Point::new(2, 10),
19086 "New third line".to_string(),
19087 ),
19088 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19089 ],
19090 false,
19091 cx,
19092 |highlighted_edits, cx| {
19093 assert_eq!(
19094 highlighted_edits.text,
19095 "Second modified\nNew third line\nFourth updated line"
19096 );
19097 assert_eq!(highlighted_edits.highlights.len(), 3);
19098 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19099 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19100 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19101 for highlight in &highlighted_edits.highlights {
19102 assert_eq!(
19103 highlight.1.background_color,
19104 Some(cx.theme().status().created_background)
19105 );
19106 }
19107 },
19108 )
19109 .await;
19110}
19111
19112#[gpui::test]
19113async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19114 init_test(cx, |_| {});
19115
19116 // Deletion
19117 assert_highlighted_edits(
19118 "Hello, world!",
19119 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19120 true,
19121 cx,
19122 |highlighted_edits, cx| {
19123 assert_eq!(highlighted_edits.text, "Hello, world!");
19124 assert_eq!(highlighted_edits.highlights.len(), 1);
19125 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19126 assert_eq!(
19127 highlighted_edits.highlights[0].1.background_color,
19128 Some(cx.theme().status().deleted_background)
19129 );
19130 },
19131 )
19132 .await;
19133
19134 // Insertion
19135 assert_highlighted_edits(
19136 "Hello, world!",
19137 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19138 true,
19139 cx,
19140 |highlighted_edits, cx| {
19141 assert_eq!(highlighted_edits.highlights.len(), 1);
19142 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19143 assert_eq!(
19144 highlighted_edits.highlights[0].1.background_color,
19145 Some(cx.theme().status().created_background)
19146 );
19147 },
19148 )
19149 .await;
19150}
19151
19152async fn assert_highlighted_edits(
19153 text: &str,
19154 edits: Vec<(Range<Point>, String)>,
19155 include_deletions: bool,
19156 cx: &mut TestAppContext,
19157 assertion_fn: impl Fn(HighlightedText, &App),
19158) {
19159 let window = cx.add_window(|window, cx| {
19160 let buffer = MultiBuffer::build_simple(text, cx);
19161 Editor::new(EditorMode::full(), buffer, None, window, cx)
19162 });
19163 let cx = &mut VisualTestContext::from_window(*window, cx);
19164
19165 let (buffer, snapshot) = window
19166 .update(cx, |editor, _window, cx| {
19167 (
19168 editor.buffer().clone(),
19169 editor.buffer().read(cx).snapshot(cx),
19170 )
19171 })
19172 .unwrap();
19173
19174 let edits = edits
19175 .into_iter()
19176 .map(|(range, edit)| {
19177 (
19178 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19179 edit,
19180 )
19181 })
19182 .collect::<Vec<_>>();
19183
19184 let text_anchor_edits = edits
19185 .clone()
19186 .into_iter()
19187 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19188 .collect::<Vec<_>>();
19189
19190 let edit_preview = window
19191 .update(cx, |_, _window, cx| {
19192 buffer
19193 .read(cx)
19194 .as_singleton()
19195 .unwrap()
19196 .read(cx)
19197 .preview_edits(text_anchor_edits.into(), cx)
19198 })
19199 .unwrap()
19200 .await;
19201
19202 cx.update(|_window, cx| {
19203 let highlighted_edits = inline_completion_edit_text(
19204 &snapshot.as_singleton().unwrap().2,
19205 &edits,
19206 &edit_preview,
19207 include_deletions,
19208 cx,
19209 );
19210 assertion_fn(highlighted_edits, cx)
19211 });
19212}
19213
19214#[track_caller]
19215fn assert_breakpoint(
19216 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19217 path: &Arc<Path>,
19218 expected: Vec<(u32, Breakpoint)>,
19219) {
19220 if expected.len() == 0usize {
19221 assert!(!breakpoints.contains_key(path), "{}", path.display());
19222 } else {
19223 let mut breakpoint = breakpoints
19224 .get(path)
19225 .unwrap()
19226 .into_iter()
19227 .map(|breakpoint| {
19228 (
19229 breakpoint.row,
19230 Breakpoint {
19231 message: breakpoint.message.clone(),
19232 state: breakpoint.state,
19233 condition: breakpoint.condition.clone(),
19234 hit_condition: breakpoint.hit_condition.clone(),
19235 },
19236 )
19237 })
19238 .collect::<Vec<_>>();
19239
19240 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19241
19242 assert_eq!(expected, breakpoint);
19243 }
19244}
19245
19246fn add_log_breakpoint_at_cursor(
19247 editor: &mut Editor,
19248 log_message: &str,
19249 window: &mut Window,
19250 cx: &mut Context<Editor>,
19251) {
19252 let (anchor, bp) = editor
19253 .breakpoints_at_cursors(window, cx)
19254 .first()
19255 .and_then(|(anchor, bp)| {
19256 if let Some(bp) = bp {
19257 Some((*anchor, bp.clone()))
19258 } else {
19259 None
19260 }
19261 })
19262 .unwrap_or_else(|| {
19263 let cursor_position: Point = editor.selections.newest(cx).head();
19264
19265 let breakpoint_position = editor
19266 .snapshot(window, cx)
19267 .display_snapshot
19268 .buffer_snapshot
19269 .anchor_before(Point::new(cursor_position.row, 0));
19270
19271 (breakpoint_position, Breakpoint::new_log(&log_message))
19272 });
19273
19274 editor.edit_breakpoint_at_anchor(
19275 anchor,
19276 bp,
19277 BreakpointEditAction::EditLogMessage(log_message.into()),
19278 cx,
19279 );
19280}
19281
19282#[gpui::test]
19283async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19284 init_test(cx, |_| {});
19285
19286 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19287 let fs = FakeFs::new(cx.executor());
19288 fs.insert_tree(
19289 path!("/a"),
19290 json!({
19291 "main.rs": sample_text,
19292 }),
19293 )
19294 .await;
19295 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19296 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19297 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19298
19299 let fs = FakeFs::new(cx.executor());
19300 fs.insert_tree(
19301 path!("/a"),
19302 json!({
19303 "main.rs": sample_text,
19304 }),
19305 )
19306 .await;
19307 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19308 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19309 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19310 let worktree_id = workspace
19311 .update(cx, |workspace, _window, cx| {
19312 workspace.project().update(cx, |project, cx| {
19313 project.worktrees(cx).next().unwrap().read(cx).id()
19314 })
19315 })
19316 .unwrap();
19317
19318 let buffer = project
19319 .update(cx, |project, cx| {
19320 project.open_buffer((worktree_id, "main.rs"), cx)
19321 })
19322 .await
19323 .unwrap();
19324
19325 let (editor, cx) = cx.add_window_view(|window, cx| {
19326 Editor::new(
19327 EditorMode::full(),
19328 MultiBuffer::build_from_buffer(buffer, cx),
19329 Some(project.clone()),
19330 window,
19331 cx,
19332 )
19333 });
19334
19335 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19336 let abs_path = project.read_with(cx, |project, cx| {
19337 project
19338 .absolute_path(&project_path, cx)
19339 .map(|path_buf| Arc::from(path_buf.to_owned()))
19340 .unwrap()
19341 });
19342
19343 // assert we can add breakpoint on the first line
19344 editor.update_in(cx, |editor, window, cx| {
19345 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19346 editor.move_to_end(&MoveToEnd, window, cx);
19347 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19348 });
19349
19350 let breakpoints = editor.update(cx, |editor, cx| {
19351 editor
19352 .breakpoint_store()
19353 .as_ref()
19354 .unwrap()
19355 .read(cx)
19356 .all_source_breakpoints(cx)
19357 .clone()
19358 });
19359
19360 assert_eq!(1, breakpoints.len());
19361 assert_breakpoint(
19362 &breakpoints,
19363 &abs_path,
19364 vec![
19365 (0, Breakpoint::new_standard()),
19366 (3, Breakpoint::new_standard()),
19367 ],
19368 );
19369
19370 editor.update_in(cx, |editor, window, cx| {
19371 editor.move_to_beginning(&MoveToBeginning, window, cx);
19372 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19373 });
19374
19375 let breakpoints = editor.update(cx, |editor, cx| {
19376 editor
19377 .breakpoint_store()
19378 .as_ref()
19379 .unwrap()
19380 .read(cx)
19381 .all_source_breakpoints(cx)
19382 .clone()
19383 });
19384
19385 assert_eq!(1, breakpoints.len());
19386 assert_breakpoint(
19387 &breakpoints,
19388 &abs_path,
19389 vec![(3, Breakpoint::new_standard())],
19390 );
19391
19392 editor.update_in(cx, |editor, window, cx| {
19393 editor.move_to_end(&MoveToEnd, window, cx);
19394 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19395 });
19396
19397 let breakpoints = editor.update(cx, |editor, cx| {
19398 editor
19399 .breakpoint_store()
19400 .as_ref()
19401 .unwrap()
19402 .read(cx)
19403 .all_source_breakpoints(cx)
19404 .clone()
19405 });
19406
19407 assert_eq!(0, breakpoints.len());
19408 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19409}
19410
19411#[gpui::test]
19412async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19413 init_test(cx, |_| {});
19414
19415 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19416
19417 let fs = FakeFs::new(cx.executor());
19418 fs.insert_tree(
19419 path!("/a"),
19420 json!({
19421 "main.rs": sample_text,
19422 }),
19423 )
19424 .await;
19425 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19426 let (workspace, cx) =
19427 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19428
19429 let worktree_id = workspace.update(cx, |workspace, cx| {
19430 workspace.project().update(cx, |project, cx| {
19431 project.worktrees(cx).next().unwrap().read(cx).id()
19432 })
19433 });
19434
19435 let buffer = project
19436 .update(cx, |project, cx| {
19437 project.open_buffer((worktree_id, "main.rs"), cx)
19438 })
19439 .await
19440 .unwrap();
19441
19442 let (editor, cx) = cx.add_window_view(|window, cx| {
19443 Editor::new(
19444 EditorMode::full(),
19445 MultiBuffer::build_from_buffer(buffer, cx),
19446 Some(project.clone()),
19447 window,
19448 cx,
19449 )
19450 });
19451
19452 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19453 let abs_path = project.read_with(cx, |project, cx| {
19454 project
19455 .absolute_path(&project_path, cx)
19456 .map(|path_buf| Arc::from(path_buf.to_owned()))
19457 .unwrap()
19458 });
19459
19460 editor.update_in(cx, |editor, window, cx| {
19461 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19462 });
19463
19464 let breakpoints = editor.update(cx, |editor, cx| {
19465 editor
19466 .breakpoint_store()
19467 .as_ref()
19468 .unwrap()
19469 .read(cx)
19470 .all_source_breakpoints(cx)
19471 .clone()
19472 });
19473
19474 assert_breakpoint(
19475 &breakpoints,
19476 &abs_path,
19477 vec![(0, Breakpoint::new_log("hello world"))],
19478 );
19479
19480 // Removing a log message from a log breakpoint should remove it
19481 editor.update_in(cx, |editor, window, cx| {
19482 add_log_breakpoint_at_cursor(editor, "", window, cx);
19483 });
19484
19485 let breakpoints = editor.update(cx, |editor, cx| {
19486 editor
19487 .breakpoint_store()
19488 .as_ref()
19489 .unwrap()
19490 .read(cx)
19491 .all_source_breakpoints(cx)
19492 .clone()
19493 });
19494
19495 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19496
19497 editor.update_in(cx, |editor, window, cx| {
19498 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19499 editor.move_to_end(&MoveToEnd, window, cx);
19500 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19501 // Not adding a log message to a standard breakpoint shouldn't remove it
19502 add_log_breakpoint_at_cursor(editor, "", window, cx);
19503 });
19504
19505 let breakpoints = editor.update(cx, |editor, cx| {
19506 editor
19507 .breakpoint_store()
19508 .as_ref()
19509 .unwrap()
19510 .read(cx)
19511 .all_source_breakpoints(cx)
19512 .clone()
19513 });
19514
19515 assert_breakpoint(
19516 &breakpoints,
19517 &abs_path,
19518 vec![
19519 (0, Breakpoint::new_standard()),
19520 (3, Breakpoint::new_standard()),
19521 ],
19522 );
19523
19524 editor.update_in(cx, |editor, window, cx| {
19525 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19526 });
19527
19528 let breakpoints = editor.update(cx, |editor, cx| {
19529 editor
19530 .breakpoint_store()
19531 .as_ref()
19532 .unwrap()
19533 .read(cx)
19534 .all_source_breakpoints(cx)
19535 .clone()
19536 });
19537
19538 assert_breakpoint(
19539 &breakpoints,
19540 &abs_path,
19541 vec![
19542 (0, Breakpoint::new_standard()),
19543 (3, Breakpoint::new_log("hello world")),
19544 ],
19545 );
19546
19547 editor.update_in(cx, |editor, window, cx| {
19548 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19549 });
19550
19551 let breakpoints = editor.update(cx, |editor, cx| {
19552 editor
19553 .breakpoint_store()
19554 .as_ref()
19555 .unwrap()
19556 .read(cx)
19557 .all_source_breakpoints(cx)
19558 .clone()
19559 });
19560
19561 assert_breakpoint(
19562 &breakpoints,
19563 &abs_path,
19564 vec![
19565 (0, Breakpoint::new_standard()),
19566 (3, Breakpoint::new_log("hello Earth!!")),
19567 ],
19568 );
19569}
19570
19571/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19572/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19573/// or when breakpoints were placed out of order. This tests for a regression too
19574#[gpui::test]
19575async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19576 init_test(cx, |_| {});
19577
19578 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19579 let fs = FakeFs::new(cx.executor());
19580 fs.insert_tree(
19581 path!("/a"),
19582 json!({
19583 "main.rs": sample_text,
19584 }),
19585 )
19586 .await;
19587 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19588 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19589 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19590
19591 let fs = FakeFs::new(cx.executor());
19592 fs.insert_tree(
19593 path!("/a"),
19594 json!({
19595 "main.rs": sample_text,
19596 }),
19597 )
19598 .await;
19599 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19600 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19601 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19602 let worktree_id = workspace
19603 .update(cx, |workspace, _window, cx| {
19604 workspace.project().update(cx, |project, cx| {
19605 project.worktrees(cx).next().unwrap().read(cx).id()
19606 })
19607 })
19608 .unwrap();
19609
19610 let buffer = project
19611 .update(cx, |project, cx| {
19612 project.open_buffer((worktree_id, "main.rs"), cx)
19613 })
19614 .await
19615 .unwrap();
19616
19617 let (editor, cx) = cx.add_window_view(|window, cx| {
19618 Editor::new(
19619 EditorMode::full(),
19620 MultiBuffer::build_from_buffer(buffer, cx),
19621 Some(project.clone()),
19622 window,
19623 cx,
19624 )
19625 });
19626
19627 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19628 let abs_path = project.read_with(cx, |project, cx| {
19629 project
19630 .absolute_path(&project_path, cx)
19631 .map(|path_buf| Arc::from(path_buf.to_owned()))
19632 .unwrap()
19633 });
19634
19635 // assert we can add breakpoint on the first line
19636 editor.update_in(cx, |editor, window, cx| {
19637 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19638 editor.move_to_end(&MoveToEnd, window, cx);
19639 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19640 editor.move_up(&MoveUp, window, cx);
19641 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19642 });
19643
19644 let breakpoints = editor.update(cx, |editor, cx| {
19645 editor
19646 .breakpoint_store()
19647 .as_ref()
19648 .unwrap()
19649 .read(cx)
19650 .all_source_breakpoints(cx)
19651 .clone()
19652 });
19653
19654 assert_eq!(1, breakpoints.len());
19655 assert_breakpoint(
19656 &breakpoints,
19657 &abs_path,
19658 vec![
19659 (0, Breakpoint::new_standard()),
19660 (2, Breakpoint::new_standard()),
19661 (3, Breakpoint::new_standard()),
19662 ],
19663 );
19664
19665 editor.update_in(cx, |editor, window, cx| {
19666 editor.move_to_beginning(&MoveToBeginning, window, cx);
19667 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19668 editor.move_to_end(&MoveToEnd, window, cx);
19669 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19670 // Disabling a breakpoint that doesn't exist should do nothing
19671 editor.move_up(&MoveUp, window, cx);
19672 editor.move_up(&MoveUp, window, cx);
19673 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19674 });
19675
19676 let breakpoints = editor.update(cx, |editor, cx| {
19677 editor
19678 .breakpoint_store()
19679 .as_ref()
19680 .unwrap()
19681 .read(cx)
19682 .all_source_breakpoints(cx)
19683 .clone()
19684 });
19685
19686 let disable_breakpoint = {
19687 let mut bp = Breakpoint::new_standard();
19688 bp.state = BreakpointState::Disabled;
19689 bp
19690 };
19691
19692 assert_eq!(1, breakpoints.len());
19693 assert_breakpoint(
19694 &breakpoints,
19695 &abs_path,
19696 vec![
19697 (0, disable_breakpoint.clone()),
19698 (2, Breakpoint::new_standard()),
19699 (3, disable_breakpoint.clone()),
19700 ],
19701 );
19702
19703 editor.update_in(cx, |editor, window, cx| {
19704 editor.move_to_beginning(&MoveToBeginning, window, cx);
19705 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19706 editor.move_to_end(&MoveToEnd, window, cx);
19707 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19708 editor.move_up(&MoveUp, window, cx);
19709 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19710 });
19711
19712 let breakpoints = editor.update(cx, |editor, cx| {
19713 editor
19714 .breakpoint_store()
19715 .as_ref()
19716 .unwrap()
19717 .read(cx)
19718 .all_source_breakpoints(cx)
19719 .clone()
19720 });
19721
19722 assert_eq!(1, breakpoints.len());
19723 assert_breakpoint(
19724 &breakpoints,
19725 &abs_path,
19726 vec![
19727 (0, Breakpoint::new_standard()),
19728 (2, disable_breakpoint),
19729 (3, Breakpoint::new_standard()),
19730 ],
19731 );
19732}
19733
19734#[gpui::test]
19735async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19736 init_test(cx, |_| {});
19737 let capabilities = lsp::ServerCapabilities {
19738 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19739 prepare_provider: Some(true),
19740 work_done_progress_options: Default::default(),
19741 })),
19742 ..Default::default()
19743 };
19744 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19745
19746 cx.set_state(indoc! {"
19747 struct Fˇoo {}
19748 "});
19749
19750 cx.update_editor(|editor, _, cx| {
19751 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19752 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19753 editor.highlight_background::<DocumentHighlightRead>(
19754 &[highlight_range],
19755 |c| c.editor_document_highlight_read_background,
19756 cx,
19757 );
19758 });
19759
19760 let mut prepare_rename_handler = cx
19761 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19762 move |_, _, _| async move {
19763 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19764 start: lsp::Position {
19765 line: 0,
19766 character: 7,
19767 },
19768 end: lsp::Position {
19769 line: 0,
19770 character: 10,
19771 },
19772 })))
19773 },
19774 );
19775 let prepare_rename_task = cx
19776 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19777 .expect("Prepare rename was not started");
19778 prepare_rename_handler.next().await.unwrap();
19779 prepare_rename_task.await.expect("Prepare rename failed");
19780
19781 let mut rename_handler =
19782 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19783 let edit = lsp::TextEdit {
19784 range: lsp::Range {
19785 start: lsp::Position {
19786 line: 0,
19787 character: 7,
19788 },
19789 end: lsp::Position {
19790 line: 0,
19791 character: 10,
19792 },
19793 },
19794 new_text: "FooRenamed".to_string(),
19795 };
19796 Ok(Some(lsp::WorkspaceEdit::new(
19797 // Specify the same edit twice
19798 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19799 )))
19800 });
19801 let rename_task = cx
19802 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19803 .expect("Confirm rename was not started");
19804 rename_handler.next().await.unwrap();
19805 rename_task.await.expect("Confirm rename failed");
19806 cx.run_until_parked();
19807
19808 // Despite two edits, only one is actually applied as those are identical
19809 cx.assert_editor_state(indoc! {"
19810 struct FooRenamedˇ {}
19811 "});
19812}
19813
19814#[gpui::test]
19815async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19816 init_test(cx, |_| {});
19817 // These capabilities indicate that the server does not support prepare rename.
19818 let capabilities = lsp::ServerCapabilities {
19819 rename_provider: Some(lsp::OneOf::Left(true)),
19820 ..Default::default()
19821 };
19822 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19823
19824 cx.set_state(indoc! {"
19825 struct Fˇoo {}
19826 "});
19827
19828 cx.update_editor(|editor, _window, cx| {
19829 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19830 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19831 editor.highlight_background::<DocumentHighlightRead>(
19832 &[highlight_range],
19833 |c| c.editor_document_highlight_read_background,
19834 cx,
19835 );
19836 });
19837
19838 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19839 .expect("Prepare rename was not started")
19840 .await
19841 .expect("Prepare rename failed");
19842
19843 let mut rename_handler =
19844 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19845 let edit = lsp::TextEdit {
19846 range: lsp::Range {
19847 start: lsp::Position {
19848 line: 0,
19849 character: 7,
19850 },
19851 end: lsp::Position {
19852 line: 0,
19853 character: 10,
19854 },
19855 },
19856 new_text: "FooRenamed".to_string(),
19857 };
19858 Ok(Some(lsp::WorkspaceEdit::new(
19859 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19860 )))
19861 });
19862 let rename_task = cx
19863 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19864 .expect("Confirm rename was not started");
19865 rename_handler.next().await.unwrap();
19866 rename_task.await.expect("Confirm rename failed");
19867 cx.run_until_parked();
19868
19869 // Correct range is renamed, as `surrounding_word` is used to find it.
19870 cx.assert_editor_state(indoc! {"
19871 struct FooRenamedˇ {}
19872 "});
19873}
19874
19875#[gpui::test]
19876async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19877 init_test(cx, |_| {});
19878 let mut cx = EditorTestContext::new(cx).await;
19879
19880 let language = Arc::new(
19881 Language::new(
19882 LanguageConfig::default(),
19883 Some(tree_sitter_html::LANGUAGE.into()),
19884 )
19885 .with_brackets_query(
19886 r#"
19887 ("<" @open "/>" @close)
19888 ("</" @open ">" @close)
19889 ("<" @open ">" @close)
19890 ("\"" @open "\"" @close)
19891 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19892 "#,
19893 )
19894 .unwrap(),
19895 );
19896 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19897
19898 cx.set_state(indoc! {"
19899 <span>ˇ</span>
19900 "});
19901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19902 cx.assert_editor_state(indoc! {"
19903 <span>
19904 ˇ
19905 </span>
19906 "});
19907
19908 cx.set_state(indoc! {"
19909 <span><span></span>ˇ</span>
19910 "});
19911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19912 cx.assert_editor_state(indoc! {"
19913 <span><span></span>
19914 ˇ</span>
19915 "});
19916
19917 cx.set_state(indoc! {"
19918 <span>ˇ
19919 </span>
19920 "});
19921 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19922 cx.assert_editor_state(indoc! {"
19923 <span>
19924 ˇ
19925 </span>
19926 "});
19927}
19928
19929#[gpui::test(iterations = 10)]
19930async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19931 init_test(cx, |_| {});
19932
19933 let fs = FakeFs::new(cx.executor());
19934 fs.insert_tree(
19935 path!("/dir"),
19936 json!({
19937 "a.ts": "a",
19938 }),
19939 )
19940 .await;
19941
19942 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19944 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19945
19946 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19947 language_registry.add(Arc::new(Language::new(
19948 LanguageConfig {
19949 name: "TypeScript".into(),
19950 matcher: LanguageMatcher {
19951 path_suffixes: vec!["ts".to_string()],
19952 ..Default::default()
19953 },
19954 ..Default::default()
19955 },
19956 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19957 )));
19958 let mut fake_language_servers = language_registry.register_fake_lsp(
19959 "TypeScript",
19960 FakeLspAdapter {
19961 capabilities: lsp::ServerCapabilities {
19962 code_lens_provider: Some(lsp::CodeLensOptions {
19963 resolve_provider: Some(true),
19964 }),
19965 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19966 commands: vec!["_the/command".to_string()],
19967 ..lsp::ExecuteCommandOptions::default()
19968 }),
19969 ..lsp::ServerCapabilities::default()
19970 },
19971 ..FakeLspAdapter::default()
19972 },
19973 );
19974
19975 let (buffer, _handle) = project
19976 .update(cx, |p, cx| {
19977 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19978 })
19979 .await
19980 .unwrap();
19981 cx.executor().run_until_parked();
19982
19983 let fake_server = fake_language_servers.next().await.unwrap();
19984
19985 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19986 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19987 drop(buffer_snapshot);
19988 let actions = cx
19989 .update_window(*workspace, |_, window, cx| {
19990 project.code_actions(&buffer, anchor..anchor, window, cx)
19991 })
19992 .unwrap();
19993
19994 fake_server
19995 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19996 Ok(Some(vec![
19997 lsp::CodeLens {
19998 range: lsp::Range::default(),
19999 command: Some(lsp::Command {
20000 title: "Code lens command".to_owned(),
20001 command: "_the/command".to_owned(),
20002 arguments: None,
20003 }),
20004 data: None,
20005 },
20006 lsp::CodeLens {
20007 range: lsp::Range::default(),
20008 command: Some(lsp::Command {
20009 title: "Command not in capabilities".to_owned(),
20010 command: "not in capabilities".to_owned(),
20011 arguments: None,
20012 }),
20013 data: None,
20014 },
20015 lsp::CodeLens {
20016 range: lsp::Range {
20017 start: lsp::Position {
20018 line: 1,
20019 character: 1,
20020 },
20021 end: lsp::Position {
20022 line: 1,
20023 character: 1,
20024 },
20025 },
20026 command: Some(lsp::Command {
20027 title: "Command not in range".to_owned(),
20028 command: "_the/command".to_owned(),
20029 arguments: None,
20030 }),
20031 data: None,
20032 },
20033 ]))
20034 })
20035 .next()
20036 .await;
20037
20038 let actions = actions.await.unwrap();
20039 assert_eq!(
20040 actions.len(),
20041 1,
20042 "Should have only one valid action for the 0..0 range"
20043 );
20044 let action = actions[0].clone();
20045 let apply = project.update(cx, |project, cx| {
20046 project.apply_code_action(buffer.clone(), action, true, cx)
20047 });
20048
20049 // Resolving the code action does not populate its edits. In absence of
20050 // edits, we must execute the given command.
20051 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20052 |mut lens, _| async move {
20053 let lens_command = lens.command.as_mut().expect("should have a command");
20054 assert_eq!(lens_command.title, "Code lens command");
20055 lens_command.arguments = Some(vec![json!("the-argument")]);
20056 Ok(lens)
20057 },
20058 );
20059
20060 // While executing the command, the language server sends the editor
20061 // a `workspaceEdit` request.
20062 fake_server
20063 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20064 let fake = fake_server.clone();
20065 move |params, _| {
20066 assert_eq!(params.command, "_the/command");
20067 let fake = fake.clone();
20068 async move {
20069 fake.server
20070 .request::<lsp::request::ApplyWorkspaceEdit>(
20071 lsp::ApplyWorkspaceEditParams {
20072 label: None,
20073 edit: lsp::WorkspaceEdit {
20074 changes: Some(
20075 [(
20076 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20077 vec![lsp::TextEdit {
20078 range: lsp::Range::new(
20079 lsp::Position::new(0, 0),
20080 lsp::Position::new(0, 0),
20081 ),
20082 new_text: "X".into(),
20083 }],
20084 )]
20085 .into_iter()
20086 .collect(),
20087 ),
20088 ..Default::default()
20089 },
20090 },
20091 )
20092 .await
20093 .into_response()
20094 .unwrap();
20095 Ok(Some(json!(null)))
20096 }
20097 }
20098 })
20099 .next()
20100 .await;
20101
20102 // Applying the code lens command returns a project transaction containing the edits
20103 // sent by the language server in its `workspaceEdit` request.
20104 let transaction = apply.await.unwrap();
20105 assert!(transaction.0.contains_key(&buffer));
20106 buffer.update(cx, |buffer, cx| {
20107 assert_eq!(buffer.text(), "Xa");
20108 buffer.undo(cx);
20109 assert_eq!(buffer.text(), "a");
20110 });
20111}
20112
20113#[gpui::test]
20114async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20115 init_test(cx, |_| {});
20116
20117 let fs = FakeFs::new(cx.executor());
20118 let main_text = r#"fn main() {
20119println!("1");
20120println!("2");
20121println!("3");
20122println!("4");
20123println!("5");
20124}"#;
20125 let lib_text = "mod foo {}";
20126 fs.insert_tree(
20127 path!("/a"),
20128 json!({
20129 "lib.rs": lib_text,
20130 "main.rs": main_text,
20131 }),
20132 )
20133 .await;
20134
20135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20136 let (workspace, cx) =
20137 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20138 let worktree_id = workspace.update(cx, |workspace, cx| {
20139 workspace.project().update(cx, |project, cx| {
20140 project.worktrees(cx).next().unwrap().read(cx).id()
20141 })
20142 });
20143
20144 let expected_ranges = vec![
20145 Point::new(0, 0)..Point::new(0, 0),
20146 Point::new(1, 0)..Point::new(1, 1),
20147 Point::new(2, 0)..Point::new(2, 2),
20148 Point::new(3, 0)..Point::new(3, 3),
20149 ];
20150
20151 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20152 let editor_1 = workspace
20153 .update_in(cx, |workspace, window, cx| {
20154 workspace.open_path(
20155 (worktree_id, "main.rs"),
20156 Some(pane_1.downgrade()),
20157 true,
20158 window,
20159 cx,
20160 )
20161 })
20162 .unwrap()
20163 .await
20164 .downcast::<Editor>()
20165 .unwrap();
20166 pane_1.update(cx, |pane, cx| {
20167 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20168 open_editor.update(cx, |editor, cx| {
20169 assert_eq!(
20170 editor.display_text(cx),
20171 main_text,
20172 "Original main.rs text on initial open",
20173 );
20174 assert_eq!(
20175 editor
20176 .selections
20177 .all::<Point>(cx)
20178 .into_iter()
20179 .map(|s| s.range())
20180 .collect::<Vec<_>>(),
20181 vec![Point::zero()..Point::zero()],
20182 "Default selections on initial open",
20183 );
20184 })
20185 });
20186 editor_1.update_in(cx, |editor, window, cx| {
20187 editor.change_selections(None, window, cx, |s| {
20188 s.select_ranges(expected_ranges.clone());
20189 });
20190 });
20191
20192 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20193 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20194 });
20195 let editor_2 = workspace
20196 .update_in(cx, |workspace, window, cx| {
20197 workspace.open_path(
20198 (worktree_id, "main.rs"),
20199 Some(pane_2.downgrade()),
20200 true,
20201 window,
20202 cx,
20203 )
20204 })
20205 .unwrap()
20206 .await
20207 .downcast::<Editor>()
20208 .unwrap();
20209 pane_2.update(cx, |pane, cx| {
20210 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20211 open_editor.update(cx, |editor, cx| {
20212 assert_eq!(
20213 editor.display_text(cx),
20214 main_text,
20215 "Original main.rs text on initial open in another panel",
20216 );
20217 assert_eq!(
20218 editor
20219 .selections
20220 .all::<Point>(cx)
20221 .into_iter()
20222 .map(|s| s.range())
20223 .collect::<Vec<_>>(),
20224 vec![Point::zero()..Point::zero()],
20225 "Default selections on initial open in another panel",
20226 );
20227 })
20228 });
20229
20230 editor_2.update_in(cx, |editor, window, cx| {
20231 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20232 });
20233
20234 let _other_editor_1 = workspace
20235 .update_in(cx, |workspace, window, cx| {
20236 workspace.open_path(
20237 (worktree_id, "lib.rs"),
20238 Some(pane_1.downgrade()),
20239 true,
20240 window,
20241 cx,
20242 )
20243 })
20244 .unwrap()
20245 .await
20246 .downcast::<Editor>()
20247 .unwrap();
20248 pane_1
20249 .update_in(cx, |pane, window, cx| {
20250 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20251 })
20252 .await
20253 .unwrap();
20254 drop(editor_1);
20255 pane_1.update(cx, |pane, cx| {
20256 pane.active_item()
20257 .unwrap()
20258 .downcast::<Editor>()
20259 .unwrap()
20260 .update(cx, |editor, cx| {
20261 assert_eq!(
20262 editor.display_text(cx),
20263 lib_text,
20264 "Other file should be open and active",
20265 );
20266 });
20267 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20268 });
20269
20270 let _other_editor_2 = workspace
20271 .update_in(cx, |workspace, window, cx| {
20272 workspace.open_path(
20273 (worktree_id, "lib.rs"),
20274 Some(pane_2.downgrade()),
20275 true,
20276 window,
20277 cx,
20278 )
20279 })
20280 .unwrap()
20281 .await
20282 .downcast::<Editor>()
20283 .unwrap();
20284 pane_2
20285 .update_in(cx, |pane, window, cx| {
20286 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20287 })
20288 .await
20289 .unwrap();
20290 drop(editor_2);
20291 pane_2.update(cx, |pane, cx| {
20292 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20293 open_editor.update(cx, |editor, cx| {
20294 assert_eq!(
20295 editor.display_text(cx),
20296 lib_text,
20297 "Other file should be open and active in another panel too",
20298 );
20299 });
20300 assert_eq!(
20301 pane.items().count(),
20302 1,
20303 "No other editors should be open in another pane",
20304 );
20305 });
20306
20307 let _editor_1_reopened = workspace
20308 .update_in(cx, |workspace, window, cx| {
20309 workspace.open_path(
20310 (worktree_id, "main.rs"),
20311 Some(pane_1.downgrade()),
20312 true,
20313 window,
20314 cx,
20315 )
20316 })
20317 .unwrap()
20318 .await
20319 .downcast::<Editor>()
20320 .unwrap();
20321 let _editor_2_reopened = workspace
20322 .update_in(cx, |workspace, window, cx| {
20323 workspace.open_path(
20324 (worktree_id, "main.rs"),
20325 Some(pane_2.downgrade()),
20326 true,
20327 window,
20328 cx,
20329 )
20330 })
20331 .unwrap()
20332 .await
20333 .downcast::<Editor>()
20334 .unwrap();
20335 pane_1.update(cx, |pane, cx| {
20336 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20337 open_editor.update(cx, |editor, cx| {
20338 assert_eq!(
20339 editor.display_text(cx),
20340 main_text,
20341 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20342 );
20343 assert_eq!(
20344 editor
20345 .selections
20346 .all::<Point>(cx)
20347 .into_iter()
20348 .map(|s| s.range())
20349 .collect::<Vec<_>>(),
20350 expected_ranges,
20351 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20352 );
20353 })
20354 });
20355 pane_2.update(cx, |pane, cx| {
20356 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20357 open_editor.update(cx, |editor, cx| {
20358 assert_eq!(
20359 editor.display_text(cx),
20360 r#"fn main() {
20361⋯rintln!("1");
20362⋯intln!("2");
20363⋯ntln!("3");
20364println!("4");
20365println!("5");
20366}"#,
20367 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20368 );
20369 assert_eq!(
20370 editor
20371 .selections
20372 .all::<Point>(cx)
20373 .into_iter()
20374 .map(|s| s.range())
20375 .collect::<Vec<_>>(),
20376 vec![Point::zero()..Point::zero()],
20377 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20378 );
20379 })
20380 });
20381}
20382
20383#[gpui::test]
20384async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20385 init_test(cx, |_| {});
20386
20387 let fs = FakeFs::new(cx.executor());
20388 let main_text = r#"fn main() {
20389println!("1");
20390println!("2");
20391println!("3");
20392println!("4");
20393println!("5");
20394}"#;
20395 let lib_text = "mod foo {}";
20396 fs.insert_tree(
20397 path!("/a"),
20398 json!({
20399 "lib.rs": lib_text,
20400 "main.rs": main_text,
20401 }),
20402 )
20403 .await;
20404
20405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20406 let (workspace, cx) =
20407 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20408 let worktree_id = workspace.update(cx, |workspace, cx| {
20409 workspace.project().update(cx, |project, cx| {
20410 project.worktrees(cx).next().unwrap().read(cx).id()
20411 })
20412 });
20413
20414 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20415 let editor = workspace
20416 .update_in(cx, |workspace, window, cx| {
20417 workspace.open_path(
20418 (worktree_id, "main.rs"),
20419 Some(pane.downgrade()),
20420 true,
20421 window,
20422 cx,
20423 )
20424 })
20425 .unwrap()
20426 .await
20427 .downcast::<Editor>()
20428 .unwrap();
20429 pane.update(cx, |pane, cx| {
20430 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20431 open_editor.update(cx, |editor, cx| {
20432 assert_eq!(
20433 editor.display_text(cx),
20434 main_text,
20435 "Original main.rs text on initial open",
20436 );
20437 })
20438 });
20439 editor.update_in(cx, |editor, window, cx| {
20440 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20441 });
20442
20443 cx.update_global(|store: &mut SettingsStore, cx| {
20444 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20445 s.restore_on_file_reopen = Some(false);
20446 });
20447 });
20448 editor.update_in(cx, |editor, window, cx| {
20449 editor.fold_ranges(
20450 vec![
20451 Point::new(1, 0)..Point::new(1, 1),
20452 Point::new(2, 0)..Point::new(2, 2),
20453 Point::new(3, 0)..Point::new(3, 3),
20454 ],
20455 false,
20456 window,
20457 cx,
20458 );
20459 });
20460 pane.update_in(cx, |pane, window, cx| {
20461 pane.close_all_items(&CloseAllItems::default(), window, cx)
20462 })
20463 .await
20464 .unwrap();
20465 pane.update(cx, |pane, _| {
20466 assert!(pane.active_item().is_none());
20467 });
20468 cx.update_global(|store: &mut SettingsStore, cx| {
20469 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20470 s.restore_on_file_reopen = Some(true);
20471 });
20472 });
20473
20474 let _editor_reopened = workspace
20475 .update_in(cx, |workspace, window, cx| {
20476 workspace.open_path(
20477 (worktree_id, "main.rs"),
20478 Some(pane.downgrade()),
20479 true,
20480 window,
20481 cx,
20482 )
20483 })
20484 .unwrap()
20485 .await
20486 .downcast::<Editor>()
20487 .unwrap();
20488 pane.update(cx, |pane, cx| {
20489 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20490 open_editor.update(cx, |editor, cx| {
20491 assert_eq!(
20492 editor.display_text(cx),
20493 main_text,
20494 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20495 );
20496 })
20497 });
20498}
20499
20500#[gpui::test]
20501async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20502 struct EmptyModalView {
20503 focus_handle: gpui::FocusHandle,
20504 }
20505 impl EventEmitter<DismissEvent> for EmptyModalView {}
20506 impl Render for EmptyModalView {
20507 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20508 div()
20509 }
20510 }
20511 impl Focusable for EmptyModalView {
20512 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20513 self.focus_handle.clone()
20514 }
20515 }
20516 impl workspace::ModalView for EmptyModalView {}
20517 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20518 EmptyModalView {
20519 focus_handle: cx.focus_handle(),
20520 }
20521 }
20522
20523 init_test(cx, |_| {});
20524
20525 let fs = FakeFs::new(cx.executor());
20526 let project = Project::test(fs, [], cx).await;
20527 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20528 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20529 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20530 let editor = cx.new_window_entity(|window, cx| {
20531 Editor::new(
20532 EditorMode::full(),
20533 buffer,
20534 Some(project.clone()),
20535 window,
20536 cx,
20537 )
20538 });
20539 workspace
20540 .update(cx, |workspace, window, cx| {
20541 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20542 })
20543 .unwrap();
20544 editor.update_in(cx, |editor, window, cx| {
20545 editor.open_context_menu(&OpenContextMenu, window, cx);
20546 assert!(editor.mouse_context_menu.is_some());
20547 });
20548 workspace
20549 .update(cx, |workspace, window, cx| {
20550 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20551 })
20552 .unwrap();
20553 cx.read(|cx| {
20554 assert!(editor.read(cx).mouse_context_menu.is_none());
20555 });
20556}
20557
20558#[gpui::test]
20559async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20560 init_test(cx, |_| {});
20561
20562 let fs = FakeFs::new(cx.executor());
20563 fs.insert_file(path!("/file.html"), Default::default())
20564 .await;
20565
20566 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20567
20568 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20569 let html_language = Arc::new(Language::new(
20570 LanguageConfig {
20571 name: "HTML".into(),
20572 matcher: LanguageMatcher {
20573 path_suffixes: vec!["html".to_string()],
20574 ..LanguageMatcher::default()
20575 },
20576 brackets: BracketPairConfig {
20577 pairs: vec![BracketPair {
20578 start: "<".into(),
20579 end: ">".into(),
20580 close: true,
20581 ..Default::default()
20582 }],
20583 ..Default::default()
20584 },
20585 ..Default::default()
20586 },
20587 Some(tree_sitter_html::LANGUAGE.into()),
20588 ));
20589 language_registry.add(html_language);
20590 let mut fake_servers = language_registry.register_fake_lsp(
20591 "HTML",
20592 FakeLspAdapter {
20593 capabilities: lsp::ServerCapabilities {
20594 completion_provider: Some(lsp::CompletionOptions {
20595 resolve_provider: Some(true),
20596 ..Default::default()
20597 }),
20598 ..Default::default()
20599 },
20600 ..Default::default()
20601 },
20602 );
20603
20604 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20605 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20606
20607 let worktree_id = workspace
20608 .update(cx, |workspace, _window, cx| {
20609 workspace.project().update(cx, |project, cx| {
20610 project.worktrees(cx).next().unwrap().read(cx).id()
20611 })
20612 })
20613 .unwrap();
20614 project
20615 .update(cx, |project, cx| {
20616 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20617 })
20618 .await
20619 .unwrap();
20620 let editor = workspace
20621 .update(cx, |workspace, window, cx| {
20622 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20623 })
20624 .unwrap()
20625 .await
20626 .unwrap()
20627 .downcast::<Editor>()
20628 .unwrap();
20629
20630 let fake_server = fake_servers.next().await.unwrap();
20631 editor.update_in(cx, |editor, window, cx| {
20632 editor.set_text("<ad></ad>", window, cx);
20633 editor.change_selections(None, window, cx, |selections| {
20634 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20635 });
20636 let Some((buffer, _)) = editor
20637 .buffer
20638 .read(cx)
20639 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20640 else {
20641 panic!("Failed to get buffer for selection position");
20642 };
20643 let buffer = buffer.read(cx);
20644 let buffer_id = buffer.remote_id();
20645 let opening_range =
20646 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20647 let closing_range =
20648 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20649 let mut linked_ranges = HashMap::default();
20650 linked_ranges.insert(
20651 buffer_id,
20652 vec![(opening_range.clone(), vec![closing_range.clone()])],
20653 );
20654 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20655 });
20656 let mut completion_handle =
20657 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20658 Ok(Some(lsp::CompletionResponse::Array(vec![
20659 lsp::CompletionItem {
20660 label: "head".to_string(),
20661 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20662 lsp::InsertReplaceEdit {
20663 new_text: "head".to_string(),
20664 insert: lsp::Range::new(
20665 lsp::Position::new(0, 1),
20666 lsp::Position::new(0, 3),
20667 ),
20668 replace: lsp::Range::new(
20669 lsp::Position::new(0, 1),
20670 lsp::Position::new(0, 3),
20671 ),
20672 },
20673 )),
20674 ..Default::default()
20675 },
20676 ])))
20677 });
20678 editor.update_in(cx, |editor, window, cx| {
20679 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20680 });
20681 cx.run_until_parked();
20682 completion_handle.next().await.unwrap();
20683 editor.update(cx, |editor, _| {
20684 assert!(
20685 editor.context_menu_visible(),
20686 "Completion menu should be visible"
20687 );
20688 });
20689 editor.update_in(cx, |editor, window, cx| {
20690 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20691 });
20692 cx.executor().run_until_parked();
20693 editor.update(cx, |editor, cx| {
20694 assert_eq!(editor.text(cx), "<head></head>");
20695 });
20696}
20697
20698#[gpui::test]
20699async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20700 init_test(cx, |_| {});
20701
20702 let fs = FakeFs::new(cx.executor());
20703 fs.insert_tree(
20704 path!("/root"),
20705 json!({
20706 "a": {
20707 "main.rs": "fn main() {}",
20708 },
20709 "foo": {
20710 "bar": {
20711 "external_file.rs": "pub mod external {}",
20712 }
20713 }
20714 }),
20715 )
20716 .await;
20717
20718 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20719 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20720 language_registry.add(rust_lang());
20721 let _fake_servers = language_registry.register_fake_lsp(
20722 "Rust",
20723 FakeLspAdapter {
20724 ..FakeLspAdapter::default()
20725 },
20726 );
20727 let (workspace, cx) =
20728 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20729 let worktree_id = workspace.update(cx, |workspace, cx| {
20730 workspace.project().update(cx, |project, cx| {
20731 project.worktrees(cx).next().unwrap().read(cx).id()
20732 })
20733 });
20734
20735 let assert_language_servers_count =
20736 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20737 project.update(cx, |project, cx| {
20738 let current = project
20739 .lsp_store()
20740 .read(cx)
20741 .as_local()
20742 .unwrap()
20743 .language_servers
20744 .len();
20745 assert_eq!(expected, current, "{context}");
20746 });
20747 };
20748
20749 assert_language_servers_count(
20750 0,
20751 "No servers should be running before any file is open",
20752 cx,
20753 );
20754 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20755 let main_editor = workspace
20756 .update_in(cx, |workspace, window, cx| {
20757 workspace.open_path(
20758 (worktree_id, "main.rs"),
20759 Some(pane.downgrade()),
20760 true,
20761 window,
20762 cx,
20763 )
20764 })
20765 .unwrap()
20766 .await
20767 .downcast::<Editor>()
20768 .unwrap();
20769 pane.update(cx, |pane, cx| {
20770 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20771 open_editor.update(cx, |editor, cx| {
20772 assert_eq!(
20773 editor.display_text(cx),
20774 "fn main() {}",
20775 "Original main.rs text on initial open",
20776 );
20777 });
20778 assert_eq!(open_editor, main_editor);
20779 });
20780 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20781
20782 let external_editor = workspace
20783 .update_in(cx, |workspace, window, cx| {
20784 workspace.open_abs_path(
20785 PathBuf::from("/root/foo/bar/external_file.rs"),
20786 OpenOptions::default(),
20787 window,
20788 cx,
20789 )
20790 })
20791 .await
20792 .expect("opening external file")
20793 .downcast::<Editor>()
20794 .expect("downcasted external file's open element to editor");
20795 pane.update(cx, |pane, cx| {
20796 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20797 open_editor.update(cx, |editor, cx| {
20798 assert_eq!(
20799 editor.display_text(cx),
20800 "pub mod external {}",
20801 "External file is open now",
20802 );
20803 });
20804 assert_eq!(open_editor, external_editor);
20805 });
20806 assert_language_servers_count(
20807 1,
20808 "Second, external, *.rs file should join the existing server",
20809 cx,
20810 );
20811
20812 pane.update_in(cx, |pane, window, cx| {
20813 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20814 })
20815 .await
20816 .unwrap();
20817 pane.update_in(cx, |pane, window, cx| {
20818 pane.navigate_backward(window, cx);
20819 });
20820 cx.run_until_parked();
20821 pane.update(cx, |pane, cx| {
20822 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20823 open_editor.update(cx, |editor, cx| {
20824 assert_eq!(
20825 editor.display_text(cx),
20826 "pub mod external {}",
20827 "External file is open now",
20828 );
20829 });
20830 });
20831 assert_language_servers_count(
20832 1,
20833 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20834 cx,
20835 );
20836
20837 cx.update(|_, cx| {
20838 workspace::reload(&workspace::Reload::default(), cx);
20839 });
20840 assert_language_servers_count(
20841 1,
20842 "After reloading the worktree with local and external files opened, only one project should be started",
20843 cx,
20844 );
20845}
20846
20847#[gpui::test]
20848async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20849 init_test(cx, |_| {});
20850
20851 let mut cx = EditorTestContext::new(cx).await;
20852 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20854
20855 // test cursor move to start of each line on tab
20856 // for `if`, `elif`, `else`, `while`, `with` and `for`
20857 cx.set_state(indoc! {"
20858 def main():
20859 ˇ for item in items:
20860 ˇ while item.active:
20861 ˇ if item.value > 10:
20862 ˇ continue
20863 ˇ elif item.value < 0:
20864 ˇ break
20865 ˇ else:
20866 ˇ with item.context() as ctx:
20867 ˇ yield count
20868 ˇ else:
20869 ˇ log('while else')
20870 ˇ else:
20871 ˇ log('for else')
20872 "});
20873 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20874 cx.assert_editor_state(indoc! {"
20875 def main():
20876 ˇfor item in items:
20877 ˇwhile item.active:
20878 ˇif item.value > 10:
20879 ˇcontinue
20880 ˇelif item.value < 0:
20881 ˇbreak
20882 ˇelse:
20883 ˇwith item.context() as ctx:
20884 ˇyield count
20885 ˇelse:
20886 ˇlog('while else')
20887 ˇelse:
20888 ˇlog('for else')
20889 "});
20890 // test relative indent is preserved when tab
20891 // for `if`, `elif`, `else`, `while`, `with` and `for`
20892 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20893 cx.assert_editor_state(indoc! {"
20894 def main():
20895 ˇfor item in items:
20896 ˇwhile item.active:
20897 ˇif item.value > 10:
20898 ˇcontinue
20899 ˇelif item.value < 0:
20900 ˇbreak
20901 ˇelse:
20902 ˇwith item.context() as ctx:
20903 ˇyield count
20904 ˇelse:
20905 ˇlog('while else')
20906 ˇelse:
20907 ˇlog('for else')
20908 "});
20909
20910 // test cursor move to start of each line on tab
20911 // for `try`, `except`, `else`, `finally`, `match` and `def`
20912 cx.set_state(indoc! {"
20913 def main():
20914 ˇ try:
20915 ˇ fetch()
20916 ˇ except ValueError:
20917 ˇ handle_error()
20918 ˇ else:
20919 ˇ match value:
20920 ˇ case _:
20921 ˇ finally:
20922 ˇ def status():
20923 ˇ return 0
20924 "});
20925 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20926 cx.assert_editor_state(indoc! {"
20927 def main():
20928 ˇtry:
20929 ˇfetch()
20930 ˇexcept ValueError:
20931 ˇhandle_error()
20932 ˇelse:
20933 ˇmatch value:
20934 ˇcase _:
20935 ˇfinally:
20936 ˇdef status():
20937 ˇreturn 0
20938 "});
20939 // test relative indent is preserved when tab
20940 // for `try`, `except`, `else`, `finally`, `match` and `def`
20941 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20942 cx.assert_editor_state(indoc! {"
20943 def main():
20944 ˇtry:
20945 ˇfetch()
20946 ˇexcept ValueError:
20947 ˇhandle_error()
20948 ˇelse:
20949 ˇmatch value:
20950 ˇcase _:
20951 ˇfinally:
20952 ˇdef status():
20953 ˇreturn 0
20954 "});
20955}
20956
20957#[gpui::test]
20958async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20959 init_test(cx, |_| {});
20960
20961 let mut cx = EditorTestContext::new(cx).await;
20962 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20963 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20964
20965 // test `else` auto outdents when typed inside `if` block
20966 cx.set_state(indoc! {"
20967 def main():
20968 if i == 2:
20969 return
20970 ˇ
20971 "});
20972 cx.update_editor(|editor, window, cx| {
20973 editor.handle_input("else:", window, cx);
20974 });
20975 cx.assert_editor_state(indoc! {"
20976 def main():
20977 if i == 2:
20978 return
20979 else:ˇ
20980 "});
20981
20982 // test `except` auto outdents when typed inside `try` block
20983 cx.set_state(indoc! {"
20984 def main():
20985 try:
20986 i = 2
20987 ˇ
20988 "});
20989 cx.update_editor(|editor, window, cx| {
20990 editor.handle_input("except:", window, cx);
20991 });
20992 cx.assert_editor_state(indoc! {"
20993 def main():
20994 try:
20995 i = 2
20996 except:ˇ
20997 "});
20998
20999 // test `else` auto outdents when typed inside `except` block
21000 cx.set_state(indoc! {"
21001 def main():
21002 try:
21003 i = 2
21004 except:
21005 j = 2
21006 ˇ
21007 "});
21008 cx.update_editor(|editor, window, cx| {
21009 editor.handle_input("else:", window, cx);
21010 });
21011 cx.assert_editor_state(indoc! {"
21012 def main():
21013 try:
21014 i = 2
21015 except:
21016 j = 2
21017 else:ˇ
21018 "});
21019
21020 // test `finally` auto outdents when typed inside `else` block
21021 cx.set_state(indoc! {"
21022 def main():
21023 try:
21024 i = 2
21025 except:
21026 j = 2
21027 else:
21028 k = 2
21029 ˇ
21030 "});
21031 cx.update_editor(|editor, window, cx| {
21032 editor.handle_input("finally:", window, cx);
21033 });
21034 cx.assert_editor_state(indoc! {"
21035 def main():
21036 try:
21037 i = 2
21038 except:
21039 j = 2
21040 else:
21041 k = 2
21042 finally:ˇ
21043 "});
21044
21045 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21046 // cx.set_state(indoc! {"
21047 // def main():
21048 // try:
21049 // for i in range(n):
21050 // pass
21051 // ˇ
21052 // "});
21053 // cx.update_editor(|editor, window, cx| {
21054 // editor.handle_input("except:", window, cx);
21055 // });
21056 // cx.assert_editor_state(indoc! {"
21057 // def main():
21058 // try:
21059 // for i in range(n):
21060 // pass
21061 // except:ˇ
21062 // "});
21063
21064 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21065 // cx.set_state(indoc! {"
21066 // def main():
21067 // try:
21068 // i = 2
21069 // except:
21070 // for i in range(n):
21071 // pass
21072 // ˇ
21073 // "});
21074 // cx.update_editor(|editor, window, cx| {
21075 // editor.handle_input("else:", window, cx);
21076 // });
21077 // cx.assert_editor_state(indoc! {"
21078 // def main():
21079 // try:
21080 // i = 2
21081 // except:
21082 // for i in range(n):
21083 // pass
21084 // else:ˇ
21085 // "});
21086
21087 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21088 // cx.set_state(indoc! {"
21089 // def main():
21090 // try:
21091 // i = 2
21092 // except:
21093 // j = 2
21094 // else:
21095 // for i in range(n):
21096 // pass
21097 // ˇ
21098 // "});
21099 // cx.update_editor(|editor, window, cx| {
21100 // editor.handle_input("finally:", window, cx);
21101 // });
21102 // cx.assert_editor_state(indoc! {"
21103 // def main():
21104 // try:
21105 // i = 2
21106 // except:
21107 // j = 2
21108 // else:
21109 // for i in range(n):
21110 // pass
21111 // finally:ˇ
21112 // "});
21113
21114 // test `else` stays at correct indent when typed after `for` block
21115 cx.set_state(indoc! {"
21116 def main():
21117 for i in range(10):
21118 if i == 3:
21119 break
21120 ˇ
21121 "});
21122 cx.update_editor(|editor, window, cx| {
21123 editor.handle_input("else:", window, cx);
21124 });
21125 cx.assert_editor_state(indoc! {"
21126 def main():
21127 for i in range(10):
21128 if i == 3:
21129 break
21130 else:ˇ
21131 "});
21132
21133 // test does not outdent on typing after line with square brackets
21134 cx.set_state(indoc! {"
21135 def f() -> list[str]:
21136 ˇ
21137 "});
21138 cx.update_editor(|editor, window, cx| {
21139 editor.handle_input("a", window, cx);
21140 });
21141 cx.assert_editor_state(indoc! {"
21142 def f() -> list[str]:
21143 aˇ
21144 "});
21145}
21146
21147#[gpui::test]
21148async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21149 init_test(cx, |_| {});
21150 update_test_language_settings(cx, |settings| {
21151 settings.defaults.extend_comment_on_newline = Some(false);
21152 });
21153 let mut cx = EditorTestContext::new(cx).await;
21154 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21156
21157 // test correct indent after newline on comment
21158 cx.set_state(indoc! {"
21159 # COMMENT:ˇ
21160 "});
21161 cx.update_editor(|editor, window, cx| {
21162 editor.newline(&Newline, window, cx);
21163 });
21164 cx.assert_editor_state(indoc! {"
21165 # COMMENT:
21166 ˇ
21167 "});
21168
21169 // test correct indent after newline in brackets
21170 cx.set_state(indoc! {"
21171 {ˇ}
21172 "});
21173 cx.update_editor(|editor, window, cx| {
21174 editor.newline(&Newline, window, cx);
21175 });
21176 cx.run_until_parked();
21177 cx.assert_editor_state(indoc! {"
21178 {
21179 ˇ
21180 }
21181 "});
21182
21183 cx.set_state(indoc! {"
21184 (ˇ)
21185 "});
21186 cx.update_editor(|editor, window, cx| {
21187 editor.newline(&Newline, window, cx);
21188 });
21189 cx.run_until_parked();
21190 cx.assert_editor_state(indoc! {"
21191 (
21192 ˇ
21193 )
21194 "});
21195
21196 // do not indent after empty lists or dictionaries
21197 cx.set_state(indoc! {"
21198 a = []ˇ
21199 "});
21200 cx.update_editor(|editor, window, cx| {
21201 editor.newline(&Newline, window, cx);
21202 });
21203 cx.run_until_parked();
21204 cx.assert_editor_state(indoc! {"
21205 a = []
21206 ˇ
21207 "});
21208}
21209
21210fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21211 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21212 point..point
21213}
21214
21215fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21216 let (text, ranges) = marked_text_ranges(marked_text, true);
21217 assert_eq!(editor.text(cx), text);
21218 assert_eq!(
21219 editor.selections.ranges(cx),
21220 ranges,
21221 "Assert selections are {}",
21222 marked_text
21223 );
21224}
21225
21226pub fn handle_signature_help_request(
21227 cx: &mut EditorLspTestContext,
21228 mocked_response: lsp::SignatureHelp,
21229) -> impl Future<Output = ()> + use<> {
21230 let mut request =
21231 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21232 let mocked_response = mocked_response.clone();
21233 async move { Ok(Some(mocked_response)) }
21234 });
21235
21236 async move {
21237 request.next().await;
21238 }
21239}
21240
21241#[track_caller]
21242pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21243 cx.update_editor(|editor, _, _| {
21244 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21245 let entries = menu.entries.borrow();
21246 let entries = entries
21247 .iter()
21248 .map(|entry| entry.string.as_str())
21249 .collect::<Vec<_>>();
21250 assert_eq!(entries, expected);
21251 } else {
21252 panic!("Expected completions menu");
21253 }
21254 });
21255}
21256
21257/// Handle completion request passing a marked string specifying where the completion
21258/// should be triggered from using '|' character, what range should be replaced, and what completions
21259/// should be returned using '<' and '>' to delimit the range.
21260///
21261/// Also see `handle_completion_request_with_insert_and_replace`.
21262#[track_caller]
21263pub fn handle_completion_request(
21264 marked_string: &str,
21265 completions: Vec<&'static str>,
21266 is_incomplete: bool,
21267 counter: Arc<AtomicUsize>,
21268 cx: &mut EditorLspTestContext,
21269) -> impl Future<Output = ()> {
21270 let complete_from_marker: TextRangeMarker = '|'.into();
21271 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21272 let (_, mut marked_ranges) = marked_text_ranges_by(
21273 marked_string,
21274 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21275 );
21276
21277 let complete_from_position =
21278 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21279 let replace_range =
21280 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21281
21282 let mut request =
21283 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21284 let completions = completions.clone();
21285 counter.fetch_add(1, atomic::Ordering::Release);
21286 async move {
21287 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21288 assert_eq!(
21289 params.text_document_position.position,
21290 complete_from_position
21291 );
21292 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21293 is_incomplete: is_incomplete,
21294 item_defaults: None,
21295 items: completions
21296 .iter()
21297 .map(|completion_text| lsp::CompletionItem {
21298 label: completion_text.to_string(),
21299 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21300 range: replace_range,
21301 new_text: completion_text.to_string(),
21302 })),
21303 ..Default::default()
21304 })
21305 .collect(),
21306 })))
21307 }
21308 });
21309
21310 async move {
21311 request.next().await;
21312 }
21313}
21314
21315/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21316/// given instead, which also contains an `insert` range.
21317///
21318/// This function uses markers to define ranges:
21319/// - `|` marks the cursor position
21320/// - `<>` marks the replace range
21321/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21322pub fn handle_completion_request_with_insert_and_replace(
21323 cx: &mut EditorLspTestContext,
21324 marked_string: &str,
21325 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21326 counter: Arc<AtomicUsize>,
21327) -> impl Future<Output = ()> {
21328 let complete_from_marker: TextRangeMarker = '|'.into();
21329 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21330 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21331
21332 let (_, mut marked_ranges) = marked_text_ranges_by(
21333 marked_string,
21334 vec![
21335 complete_from_marker.clone(),
21336 replace_range_marker.clone(),
21337 insert_range_marker.clone(),
21338 ],
21339 );
21340
21341 let complete_from_position =
21342 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21343 let replace_range =
21344 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21345
21346 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21347 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21348 _ => lsp::Range {
21349 start: replace_range.start,
21350 end: complete_from_position,
21351 },
21352 };
21353
21354 let mut request =
21355 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21356 let completions = completions.clone();
21357 counter.fetch_add(1, atomic::Ordering::Release);
21358 async move {
21359 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21360 assert_eq!(
21361 params.text_document_position.position, complete_from_position,
21362 "marker `|` position doesn't match",
21363 );
21364 Ok(Some(lsp::CompletionResponse::Array(
21365 completions
21366 .iter()
21367 .map(|(label, new_text)| lsp::CompletionItem {
21368 label: label.to_string(),
21369 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21370 lsp::InsertReplaceEdit {
21371 insert: insert_range,
21372 replace: replace_range,
21373 new_text: new_text.to_string(),
21374 },
21375 )),
21376 ..Default::default()
21377 })
21378 .collect(),
21379 )))
21380 }
21381 });
21382
21383 async move {
21384 request.next().await;
21385 }
21386}
21387
21388fn handle_resolve_completion_request(
21389 cx: &mut EditorLspTestContext,
21390 edits: Option<Vec<(&'static str, &'static str)>>,
21391) -> impl Future<Output = ()> {
21392 let edits = edits.map(|edits| {
21393 edits
21394 .iter()
21395 .map(|(marked_string, new_text)| {
21396 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21397 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21398 lsp::TextEdit::new(replace_range, new_text.to_string())
21399 })
21400 .collect::<Vec<_>>()
21401 });
21402
21403 let mut request =
21404 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21405 let edits = edits.clone();
21406 async move {
21407 Ok(lsp::CompletionItem {
21408 additional_text_edits: edits,
21409 ..Default::default()
21410 })
21411 }
21412 });
21413
21414 async move {
21415 request.next().await;
21416 }
21417}
21418
21419pub(crate) fn update_test_language_settings(
21420 cx: &mut TestAppContext,
21421 f: impl Fn(&mut AllLanguageSettingsContent),
21422) {
21423 cx.update(|cx| {
21424 SettingsStore::update_global(cx, |store, cx| {
21425 store.update_user_settings::<AllLanguageSettings>(cx, f);
21426 });
21427 });
21428}
21429
21430pub(crate) fn update_test_project_settings(
21431 cx: &mut TestAppContext,
21432 f: impl Fn(&mut ProjectSettings),
21433) {
21434 cx.update(|cx| {
21435 SettingsStore::update_global(cx, |store, cx| {
21436 store.update_user_settings::<ProjectSettings>(cx, f);
21437 });
21438 });
21439}
21440
21441pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21442 cx.update(|cx| {
21443 assets::Assets.load_test_fonts(cx);
21444 let store = SettingsStore::test(cx);
21445 cx.set_global(store);
21446 theme::init(theme::LoadThemes::JustBase, cx);
21447 release_channel::init(SemanticVersion::default(), cx);
21448 client::init_settings(cx);
21449 language::init(cx);
21450 Project::init_settings(cx);
21451 workspace::init_settings(cx);
21452 crate::init(cx);
21453 });
21454
21455 update_test_language_settings(cx, f);
21456}
21457
21458#[track_caller]
21459fn assert_hunk_revert(
21460 not_reverted_text_with_selections: &str,
21461 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21462 expected_reverted_text_with_selections: &str,
21463 base_text: &str,
21464 cx: &mut EditorLspTestContext,
21465) {
21466 cx.set_state(not_reverted_text_with_selections);
21467 cx.set_head_text(base_text);
21468 cx.executor().run_until_parked();
21469
21470 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21471 let snapshot = editor.snapshot(window, cx);
21472 let reverted_hunk_statuses = snapshot
21473 .buffer_snapshot
21474 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21475 .map(|hunk| hunk.status().kind)
21476 .collect::<Vec<_>>();
21477
21478 editor.git_restore(&Default::default(), window, cx);
21479 reverted_hunk_statuses
21480 });
21481 cx.executor().run_until_parked();
21482 cx.assert_editor_state(expected_reverted_text_with_selections);
21483 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21484}