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 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_toggle_case(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // If all lower case -> upper case
4319 cx.set_state(indoc! {"
4320 «hello worldˇ»
4321 "});
4322 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 «HELLO WORLDˇ»
4325 "});
4326
4327 // If all upper case -> lower case
4328 cx.set_state(indoc! {"
4329 «HELLO WORLDˇ»
4330 "});
4331 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 «hello worldˇ»
4334 "});
4335
4336 // If any upper case characters are identified -> lower case
4337 // This matches JetBrains IDEs
4338 cx.set_state(indoc! {"
4339 «hEllo worldˇ»
4340 "});
4341 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 «hello worldˇ»
4344 "});
4345}
4346
4347#[gpui::test]
4348async fn test_manipulate_text(cx: &mut TestAppContext) {
4349 init_test(cx, |_| {});
4350
4351 let mut cx = EditorTestContext::new(cx).await;
4352
4353 // Test convert_to_upper_case()
4354 cx.set_state(indoc! {"
4355 «hello worldˇ»
4356 "});
4357 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 «HELLO WORLDˇ»
4360 "});
4361
4362 // Test convert_to_lower_case()
4363 cx.set_state(indoc! {"
4364 «HELLO WORLDˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4367 cx.assert_editor_state(indoc! {"
4368 «hello worldˇ»
4369 "});
4370
4371 // Test multiple line, single selection case
4372 cx.set_state(indoc! {"
4373 «The quick brown
4374 fox jumps over
4375 the lazy dogˇ»
4376 "});
4377 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4378 cx.assert_editor_state(indoc! {"
4379 «The Quick Brown
4380 Fox Jumps Over
4381 The Lazy Dogˇ»
4382 "});
4383
4384 // Test multiple line, single selection case
4385 cx.set_state(indoc! {"
4386 «The quick brown
4387 fox jumps over
4388 the lazy dogˇ»
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «TheQuickBrown
4395 FoxJumpsOver
4396 TheLazyDogˇ»
4397 "});
4398
4399 // From here on out, test more complex cases of manipulate_text()
4400
4401 // Test no selection case - should affect words cursors are in
4402 // Cursor at beginning, middle, and end of word
4403 cx.set_state(indoc! {"
4404 ˇhello big beauˇtiful worldˇ
4405 "});
4406 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4407 cx.assert_editor_state(indoc! {"
4408 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4409 "});
4410
4411 // Test multiple selections on a single line and across multiple lines
4412 cx.set_state(indoc! {"
4413 «Theˇ» quick «brown
4414 foxˇ» jumps «overˇ»
4415 the «lazyˇ» dog
4416 "});
4417 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 «THEˇ» quick «BROWN
4420 FOXˇ» jumps «OVERˇ»
4421 the «LAZYˇ» dog
4422 "});
4423
4424 // Test case where text length grows
4425 cx.set_state(indoc! {"
4426 «tschüߡ»
4427 "});
4428 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 «TSCHÜSSˇ»
4431 "});
4432
4433 // Test to make sure we don't crash when text shrinks
4434 cx.set_state(indoc! {"
4435 aaa_bbbˇ
4436 "});
4437 cx.update_editor(|e, window, cx| {
4438 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «aaaBbbˇ»
4442 "});
4443
4444 // Test to make sure we all aware of the fact that each word can grow and shrink
4445 // Final selections should be aware of this fact
4446 cx.set_state(indoc! {"
4447 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4448 "});
4449 cx.update_editor(|e, window, cx| {
4450 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4451 });
4452 cx.assert_editor_state(indoc! {"
4453 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4454 "});
4455
4456 cx.set_state(indoc! {"
4457 «hElLo, WoRld!ˇ»
4458 "});
4459 cx.update_editor(|e, window, cx| {
4460 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4461 });
4462 cx.assert_editor_state(indoc! {"
4463 «HeLlO, wOrLD!ˇ»
4464 "});
4465}
4466
4467#[gpui::test]
4468fn test_duplicate_line(cx: &mut TestAppContext) {
4469 init_test(cx, |_| {});
4470
4471 let editor = cx.add_window(|window, cx| {
4472 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4473 build_editor(buffer, window, cx)
4474 });
4475 _ = editor.update(cx, |editor, window, cx| {
4476 editor.change_selections(None, window, cx, |s| {
4477 s.select_display_ranges([
4478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4479 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4480 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4481 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4482 ])
4483 });
4484 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4485 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4486 assert_eq!(
4487 editor.selections.display_ranges(cx),
4488 vec![
4489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4490 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4491 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4492 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4493 ]
4494 );
4495 });
4496
4497 let editor = cx.add_window(|window, cx| {
4498 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4499 build_editor(buffer, window, cx)
4500 });
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.change_selections(None, window, cx, |s| {
4503 s.select_display_ranges([
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4506 ])
4507 });
4508 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4509 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![
4513 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4514 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4515 ]
4516 );
4517 });
4518
4519 // With `move_upwards` the selections stay in place, except for
4520 // the lines inserted above them
4521 let editor = cx.add_window(|window, cx| {
4522 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4523 build_editor(buffer, window, cx)
4524 });
4525 _ = editor.update(cx, |editor, window, cx| {
4526 editor.change_selections(None, window, cx, |s| {
4527 s.select_display_ranges([
4528 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4529 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4531 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4532 ])
4533 });
4534 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4535 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4536 assert_eq!(
4537 editor.selections.display_ranges(cx),
4538 vec![
4539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4540 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4541 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4542 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4543 ]
4544 );
4545 });
4546
4547 let editor = cx.add_window(|window, cx| {
4548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4549 build_editor(buffer, window, cx)
4550 });
4551 _ = editor.update(cx, |editor, window, cx| {
4552 editor.change_selections(None, window, cx, |s| {
4553 s.select_display_ranges([
4554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4556 ])
4557 });
4558 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4559 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4565 ]
4566 );
4567 });
4568
4569 let editor = cx.add_window(|window, cx| {
4570 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4571 build_editor(buffer, window, cx)
4572 });
4573 _ = editor.update(cx, |editor, window, cx| {
4574 editor.change_selections(None, window, cx, |s| {
4575 s.select_display_ranges([
4576 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4577 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4578 ])
4579 });
4580 editor.duplicate_selection(&DuplicateSelection, window, cx);
4581 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4587 ]
4588 );
4589 });
4590}
4591
4592#[gpui::test]
4593fn test_move_line_up_down(cx: &mut TestAppContext) {
4594 init_test(cx, |_| {});
4595
4596 let editor = cx.add_window(|window, cx| {
4597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4598 build_editor(buffer, window, cx)
4599 });
4600 _ = editor.update(cx, |editor, window, cx| {
4601 editor.fold_creases(
4602 vec![
4603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4606 ],
4607 true,
4608 window,
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_display_ranges([
4613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4617 ])
4618 });
4619 assert_eq!(
4620 editor.display_text(cx),
4621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4622 );
4623
4624 editor.move_line_up(&MoveLineUp, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4636 ]
4637 );
4638 });
4639
4640 _ = editor.update(cx, |editor, window, cx| {
4641 editor.move_line_down(&MoveLineDown, window, cx);
4642 assert_eq!(
4643 editor.display_text(cx),
4644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4653 ]
4654 );
4655 });
4656
4657 _ = editor.update(cx, |editor, window, cx| {
4658 editor.move_line_down(&MoveLineDown, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4670 ]
4671 );
4672 });
4673
4674 _ = editor.update(cx, |editor, window, cx| {
4675 editor.move_line_up(&MoveLineUp, window, cx);
4676 assert_eq!(
4677 editor.display_text(cx),
4678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4679 );
4680 assert_eq!(
4681 editor.selections.display_ranges(cx),
4682 vec![
4683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4687 ]
4688 );
4689 });
4690}
4691
4692#[gpui::test]
4693fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let editor = cx.add_window(|window, cx| {
4697 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4698 build_editor(buffer, window, cx)
4699 });
4700 _ = editor.update(cx, |editor, window, cx| {
4701 let snapshot = editor.buffer.read(cx).snapshot(cx);
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 style: BlockStyle::Fixed,
4705 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4706 height: Some(1),
4707 render: Arc::new(|_| div().into_any()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 Some(Autoscroll::fit()),
4712 cx,
4713 );
4714 editor.change_selections(None, window, cx, |s| {
4715 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4716 });
4717 editor.move_line_down(&MoveLineDown, window, cx);
4718 });
4719}
4720
4721#[gpui::test]
4722async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4723 init_test(cx, |_| {});
4724
4725 let mut cx = EditorTestContext::new(cx).await;
4726 cx.set_state(
4727 &"
4728 ˇzero
4729 one
4730 two
4731 three
4732 four
4733 five
4734 "
4735 .unindent(),
4736 );
4737
4738 // Create a four-line block that replaces three lines of text.
4739 cx.update_editor(|editor, window, cx| {
4740 let snapshot = editor.snapshot(window, cx);
4741 let snapshot = &snapshot.buffer_snapshot;
4742 let placement = BlockPlacement::Replace(
4743 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4744 );
4745 editor.insert_blocks(
4746 [BlockProperties {
4747 placement,
4748 height: Some(4),
4749 style: BlockStyle::Sticky,
4750 render: Arc::new(|_| gpui::div().into_any_element()),
4751 priority: 0,
4752 render_in_minimap: true,
4753 }],
4754 None,
4755 cx,
4756 );
4757 });
4758
4759 // Move down so that the cursor touches the block.
4760 cx.update_editor(|editor, window, cx| {
4761 editor.move_down(&Default::default(), window, cx);
4762 });
4763 cx.assert_editor_state(
4764 &"
4765 zero
4766 «one
4767 two
4768 threeˇ»
4769 four
4770 five
4771 "
4772 .unindent(),
4773 );
4774
4775 // Move down past the block.
4776 cx.update_editor(|editor, window, cx| {
4777 editor.move_down(&Default::default(), window, cx);
4778 });
4779 cx.assert_editor_state(
4780 &"
4781 zero
4782 one
4783 two
4784 three
4785 ˇfour
4786 five
4787 "
4788 .unindent(),
4789 );
4790}
4791
4792#[gpui::test]
4793fn test_transpose(cx: &mut TestAppContext) {
4794 init_test(cx, |_| {});
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bac");
4802 assert_eq!(editor.selections.ranges(cx), [2..2]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bca");
4806 assert_eq!(editor.selections.ranges(cx), [3..3]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bac");
4810 assert_eq!(editor.selections.ranges(cx), [3..3]);
4811
4812 editor
4813 });
4814
4815 _ = cx.add_window(|window, cx| {
4816 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4817 editor.set_style(EditorStyle::default(), window, cx);
4818 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4819 editor.transpose(&Default::default(), window, cx);
4820 assert_eq!(editor.text(cx), "acb\nde");
4821 assert_eq!(editor.selections.ranges(cx), [3..3]);
4822
4823 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4824 editor.transpose(&Default::default(), window, cx);
4825 assert_eq!(editor.text(cx), "acbd\ne");
4826 assert_eq!(editor.selections.ranges(cx), [5..5]);
4827
4828 editor.transpose(&Default::default(), window, cx);
4829 assert_eq!(editor.text(cx), "acbde\n");
4830 assert_eq!(editor.selections.ranges(cx), [6..6]);
4831
4832 editor.transpose(&Default::default(), window, cx);
4833 assert_eq!(editor.text(cx), "acbd\ne");
4834 assert_eq!(editor.selections.ranges(cx), [6..6]);
4835
4836 editor
4837 });
4838
4839 _ = cx.add_window(|window, cx| {
4840 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4841 editor.set_style(EditorStyle::default(), window, cx);
4842 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4843 editor.transpose(&Default::default(), window, cx);
4844 assert_eq!(editor.text(cx), "bacd\ne");
4845 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4846
4847 editor.transpose(&Default::default(), window, cx);
4848 assert_eq!(editor.text(cx), "bcade\n");
4849 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4850
4851 editor.transpose(&Default::default(), window, cx);
4852 assert_eq!(editor.text(cx), "bcda\ne");
4853 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4854
4855 editor.transpose(&Default::default(), window, cx);
4856 assert_eq!(editor.text(cx), "bcade\n");
4857 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4858
4859 editor.transpose(&Default::default(), window, cx);
4860 assert_eq!(editor.text(cx), "bcaed\n");
4861 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4862
4863 editor
4864 });
4865
4866 _ = cx.add_window(|window, cx| {
4867 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4868 editor.set_style(EditorStyle::default(), window, cx);
4869 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4870 editor.transpose(&Default::default(), window, cx);
4871 assert_eq!(editor.text(cx), "🏀🍐✋");
4872 assert_eq!(editor.selections.ranges(cx), [8..8]);
4873
4874 editor.transpose(&Default::default(), window, cx);
4875 assert_eq!(editor.text(cx), "🏀✋🍐");
4876 assert_eq!(editor.selections.ranges(cx), [11..11]);
4877
4878 editor.transpose(&Default::default(), window, cx);
4879 assert_eq!(editor.text(cx), "🏀🍐✋");
4880 assert_eq!(editor.selections.ranges(cx), [11..11]);
4881
4882 editor
4883 });
4884}
4885
4886#[gpui::test]
4887async fn test_rewrap(cx: &mut TestAppContext) {
4888 init_test(cx, |settings| {
4889 settings.languages.extend([
4890 (
4891 "Markdown".into(),
4892 LanguageSettingsContent {
4893 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4894 ..Default::default()
4895 },
4896 ),
4897 (
4898 "Plain Text".into(),
4899 LanguageSettingsContent {
4900 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4901 ..Default::default()
4902 },
4903 ),
4904 ])
4905 });
4906
4907 let mut cx = EditorTestContext::new(cx).await;
4908
4909 let language_with_c_comments = Arc::new(Language::new(
4910 LanguageConfig {
4911 line_comments: vec!["// ".into()],
4912 ..LanguageConfig::default()
4913 },
4914 None,
4915 ));
4916 let language_with_pound_comments = Arc::new(Language::new(
4917 LanguageConfig {
4918 line_comments: vec!["# ".into()],
4919 ..LanguageConfig::default()
4920 },
4921 None,
4922 ));
4923 let markdown_language = Arc::new(Language::new(
4924 LanguageConfig {
4925 name: "Markdown".into(),
4926 ..LanguageConfig::default()
4927 },
4928 None,
4929 ));
4930 let language_with_doc_comments = Arc::new(Language::new(
4931 LanguageConfig {
4932 line_comments: vec!["// ".into(), "/// ".into()],
4933 ..LanguageConfig::default()
4934 },
4935 Some(tree_sitter_rust::LANGUAGE.into()),
4936 ));
4937
4938 let plaintext_language = Arc::new(Language::new(
4939 LanguageConfig {
4940 name: "Plain Text".into(),
4941 ..LanguageConfig::default()
4942 },
4943 None,
4944 ));
4945
4946 assert_rewrap(
4947 indoc! {"
4948 // ˇ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.
4949 "},
4950 indoc! {"
4951 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4952 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4953 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4954 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4955 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4956 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4957 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4958 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4959 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4960 // porttitor id. Aliquam id accumsan eros.
4961 "},
4962 language_with_c_comments.clone(),
4963 &mut cx,
4964 );
4965
4966 // Test that rewrapping works inside of a selection
4967 assert_rewrap(
4968 indoc! {"
4969 «// 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.ˇ»
4970 "},
4971 indoc! {"
4972 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4973 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4974 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4975 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4976 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4977 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4978 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4979 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4980 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4981 // porttitor id. Aliquam id accumsan eros.ˇ»
4982 "},
4983 language_with_c_comments.clone(),
4984 &mut cx,
4985 );
4986
4987 // Test that cursors that expand to the same region are collapsed.
4988 assert_rewrap(
4989 indoc! {"
4990 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4991 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4992 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4993 // ˇ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.
4994 "},
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4997 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4998 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4999 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5000 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5001 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5002 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5003 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5004 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5005 // porttitor id. Aliquam id accumsan eros.
5006 "},
5007 language_with_c_comments.clone(),
5008 &mut cx,
5009 );
5010
5011 // Test that non-contiguous selections are treated separately.
5012 assert_rewrap(
5013 indoc! {"
5014 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5015 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5016 //
5017 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5018 // ˇ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.
5019 "},
5020 indoc! {"
5021 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5022 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5023 // auctor, eu lacinia sapien scelerisque.
5024 //
5025 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5026 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5027 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5028 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5029 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5030 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5031 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5032 "},
5033 language_with_c_comments.clone(),
5034 &mut cx,
5035 );
5036
5037 // Test that different comment prefixes are supported.
5038 assert_rewrap(
5039 indoc! {"
5040 # ˇ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.
5041 "},
5042 indoc! {"
5043 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5044 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5045 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5046 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5047 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5048 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5049 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5050 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5051 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5052 # accumsan eros.
5053 "},
5054 language_with_pound_comments.clone(),
5055 &mut cx,
5056 );
5057
5058 // Test that rewrapping is ignored outside of comments in most languages.
5059 assert_rewrap(
5060 indoc! {"
5061 /// Adds two numbers.
5062 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5063 fn add(a: u32, b: u32) -> u32 {
5064 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ˇ
5065 }
5066 "},
5067 indoc! {"
5068 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5069 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5070 fn add(a: u32, b: u32) -> u32 {
5071 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ˇ
5072 }
5073 "},
5074 language_with_doc_comments.clone(),
5075 &mut cx,
5076 );
5077
5078 // Test that rewrapping works in Markdown and Plain Text languages.
5079 assert_rewrap(
5080 indoc! {"
5081 # Hello
5082
5083 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.
5084 "},
5085 indoc! {"
5086 # Hello
5087
5088 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5089 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5090 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5091 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5092 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5093 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5094 Integer sit amet scelerisque nisi.
5095 "},
5096 markdown_language,
5097 &mut cx,
5098 );
5099
5100 assert_rewrap(
5101 indoc! {"
5102 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.
5103 "},
5104 indoc! {"
5105 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5106 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5107 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5108 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5109 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5110 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5111 Integer sit amet scelerisque nisi.
5112 "},
5113 plaintext_language.clone(),
5114 &mut cx,
5115 );
5116
5117 // Test rewrapping unaligned comments in a selection.
5118 assert_rewrap(
5119 indoc! {"
5120 fn foo() {
5121 if true {
5122 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5123 // Praesent semper egestas tellus id dignissim.ˇ»
5124 do_something();
5125 } else {
5126 //
5127 }
5128 }
5129 "},
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5134 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5135 // egestas tellus id dignissim.ˇ»
5136 do_something();
5137 } else {
5138 //
5139 }
5140 }
5141 "},
5142 language_with_doc_comments.clone(),
5143 &mut cx,
5144 );
5145
5146 assert_rewrap(
5147 indoc! {"
5148 fn foo() {
5149 if true {
5150 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5151 // Praesent semper egestas tellus id dignissim.»
5152 do_something();
5153 } else {
5154 //
5155 }
5156
5157 }
5158 "},
5159 indoc! {"
5160 fn foo() {
5161 if true {
5162 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5163 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5164 // egestas tellus id dignissim.»
5165 do_something();
5166 } else {
5167 //
5168 }
5169
5170 }
5171 "},
5172 language_with_doc_comments.clone(),
5173 &mut cx,
5174 );
5175
5176 assert_rewrap(
5177 indoc! {"
5178 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5179
5180 two»
5181
5182 three
5183
5184 «ˇ\t
5185
5186 four four four four four four four four four four four four four four four four four four four four»
5187
5188 «ˇfive five five five five five five five five five five five five five five five five five five five
5189 \t»
5190 six six six six six six six six six six six six six six six six six six six six six six six six six
5191 "},
5192 indoc! {"
5193 «ˇone one one one one one one one one one one one one one one one one one one one
5194 one one one one one
5195
5196 two»
5197
5198 three
5199
5200 «ˇ\t
5201
5202 four four four four four four four four four four four four four four four four
5203 four four four four»
5204
5205 «ˇfive five five five five five five five five five five five five five five five
5206 five five five five
5207 \t»
5208 six six six six six six six six six six six six six six six six six six six six six six six six six
5209 "},
5210 plaintext_language.clone(),
5211 &mut cx,
5212 );
5213
5214 assert_rewrap(
5215 indoc! {"
5216 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5217 //ˇ
5218 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5219 //ˇ short short short
5220 int main(void) {
5221 return 17;
5222 }
5223 "},
5224 indoc! {"
5225 //ˇ long long long long long long long long long long long long long long long
5226 // long long long long long long long long long long long long long
5227 //ˇ
5228 //ˇ long long long long long long long long long long long long long long long
5229 //ˇ long long long long long long long long long long long long long short short
5230 // short
5231 int main(void) {
5232 return 17;
5233 }
5234 "},
5235 language_with_c_comments,
5236 &mut cx,
5237 );
5238
5239 #[track_caller]
5240 fn assert_rewrap(
5241 unwrapped_text: &str,
5242 wrapped_text: &str,
5243 language: Arc<Language>,
5244 cx: &mut EditorTestContext,
5245 ) {
5246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5247 cx.set_state(unwrapped_text);
5248 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5249 cx.assert_editor_state(wrapped_text);
5250 }
5251}
5252
5253#[gpui::test]
5254async fn test_hard_wrap(cx: &mut TestAppContext) {
5255 init_test(cx, |_| {});
5256 let mut cx = EditorTestContext::new(cx).await;
5257
5258 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5259 cx.update_editor(|editor, _, cx| {
5260 editor.set_hard_wrap(Some(14), cx);
5261 });
5262
5263 cx.set_state(indoc!(
5264 "
5265 one two three ˇ
5266 "
5267 ));
5268 cx.simulate_input("four");
5269 cx.run_until_parked();
5270
5271 cx.assert_editor_state(indoc!(
5272 "
5273 one two three
5274 fourˇ
5275 "
5276 ));
5277
5278 cx.update_editor(|editor, window, cx| {
5279 editor.newline(&Default::default(), window, cx);
5280 });
5281 cx.run_until_parked();
5282 cx.assert_editor_state(indoc!(
5283 "
5284 one two three
5285 four
5286 ˇ
5287 "
5288 ));
5289
5290 cx.simulate_input("five");
5291 cx.run_until_parked();
5292 cx.assert_editor_state(indoc!(
5293 "
5294 one two three
5295 four
5296 fiveˇ
5297 "
5298 ));
5299
5300 cx.update_editor(|editor, window, cx| {
5301 editor.newline(&Default::default(), window, cx);
5302 });
5303 cx.run_until_parked();
5304 cx.simulate_input("# ");
5305 cx.run_until_parked();
5306 cx.assert_editor_state(indoc!(
5307 "
5308 one two three
5309 four
5310 five
5311 # ˇ
5312 "
5313 ));
5314
5315 cx.update_editor(|editor, window, cx| {
5316 editor.newline(&Default::default(), window, cx);
5317 });
5318 cx.run_until_parked();
5319 cx.assert_editor_state(indoc!(
5320 "
5321 one two three
5322 four
5323 five
5324 #\x20
5325 #ˇ
5326 "
5327 ));
5328
5329 cx.simulate_input(" 6");
5330 cx.run_until_parked();
5331 cx.assert_editor_state(indoc!(
5332 "
5333 one two three
5334 four
5335 five
5336 #
5337 # 6ˇ
5338 "
5339 ));
5340}
5341
5342#[gpui::test]
5343async fn test_clipboard(cx: &mut TestAppContext) {
5344 init_test(cx, |_| {});
5345
5346 let mut cx = EditorTestContext::new(cx).await;
5347
5348 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5349 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5350 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5351
5352 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5353 cx.set_state("two ˇfour ˇsix ˇ");
5354 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5355 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5356
5357 // Paste again but with only two cursors. Since the number of cursors doesn't
5358 // match the number of slices in the clipboard, the entire clipboard text
5359 // is pasted at each cursor.
5360 cx.set_state("ˇtwo one✅ four three six five ˇ");
5361 cx.update_editor(|e, window, cx| {
5362 e.handle_input("( ", window, cx);
5363 e.paste(&Paste, window, cx);
5364 e.handle_input(") ", window, cx);
5365 });
5366 cx.assert_editor_state(
5367 &([
5368 "( one✅ ",
5369 "three ",
5370 "five ) ˇtwo one✅ four three six five ( one✅ ",
5371 "three ",
5372 "five ) ˇ",
5373 ]
5374 .join("\n")),
5375 );
5376
5377 // Cut with three selections, one of which is full-line.
5378 cx.set_state(indoc! {"
5379 1«2ˇ»3
5380 4ˇ567
5381 «8ˇ»9"});
5382 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 1ˇ3
5385 ˇ9"});
5386
5387 // Paste with three selections, noticing how the copied selection that was full-line
5388 // gets inserted before the second cursor.
5389 cx.set_state(indoc! {"
5390 1ˇ3
5391 9ˇ
5392 «oˇ»ne"});
5393 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5394 cx.assert_editor_state(indoc! {"
5395 12ˇ3
5396 4567
5397 9ˇ
5398 8ˇne"});
5399
5400 // Copy with a single cursor only, which writes the whole line into the clipboard.
5401 cx.set_state(indoc! {"
5402 The quick brown
5403 fox juˇmps over
5404 the lazy dog"});
5405 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5406 assert_eq!(
5407 cx.read_from_clipboard()
5408 .and_then(|item| item.text().as_deref().map(str::to_string)),
5409 Some("fox jumps over\n".to_string())
5410 );
5411
5412 // Paste with three selections, noticing how the copied full-line selection is inserted
5413 // before the empty selections but replaces the selection that is non-empty.
5414 cx.set_state(indoc! {"
5415 Tˇhe quick brown
5416 «foˇ»x jumps over
5417 tˇhe lazy dog"});
5418 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5419 cx.assert_editor_state(indoc! {"
5420 fox jumps over
5421 Tˇhe quick brown
5422 fox jumps over
5423 ˇx jumps over
5424 fox jumps over
5425 tˇhe lazy dog"});
5426}
5427
5428#[gpui::test]
5429async fn test_copy_trim(cx: &mut TestAppContext) {
5430 init_test(cx, |_| {});
5431
5432 let mut cx = EditorTestContext::new(cx).await;
5433 cx.set_state(
5434 r#" «for selection in selections.iter() {
5435 let mut start = selection.start;
5436 let mut end = selection.end;
5437 let is_entire_line = selection.is_empty();
5438 if is_entire_line {
5439 start = Point::new(start.row, 0);ˇ»
5440 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5441 }
5442 "#,
5443 );
5444 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5445 assert_eq!(
5446 cx.read_from_clipboard()
5447 .and_then(|item| item.text().as_deref().map(str::to_string)),
5448 Some(
5449 "for selection in selections.iter() {
5450 let mut start = selection.start;
5451 let mut end = selection.end;
5452 let is_entire_line = selection.is_empty();
5453 if is_entire_line {
5454 start = Point::new(start.row, 0);"
5455 .to_string()
5456 ),
5457 "Regular copying preserves all indentation selected",
5458 );
5459 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5460 assert_eq!(
5461 cx.read_from_clipboard()
5462 .and_then(|item| item.text().as_deref().map(str::to_string)),
5463 Some(
5464 "for selection in selections.iter() {
5465let mut start = selection.start;
5466let mut end = selection.end;
5467let is_entire_line = selection.is_empty();
5468if is_entire_line {
5469 start = Point::new(start.row, 0);"
5470 .to_string()
5471 ),
5472 "Copying with stripping should strip all leading whitespaces"
5473 );
5474
5475 cx.set_state(
5476 r#" « for selection in selections.iter() {
5477 let mut start = selection.start;
5478 let mut end = selection.end;
5479 let is_entire_line = selection.is_empty();
5480 if is_entire_line {
5481 start = Point::new(start.row, 0);ˇ»
5482 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5483 }
5484 "#,
5485 );
5486 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5487 assert_eq!(
5488 cx.read_from_clipboard()
5489 .and_then(|item| item.text().as_deref().map(str::to_string)),
5490 Some(
5491 " for selection in selections.iter() {
5492 let mut start = selection.start;
5493 let mut end = selection.end;
5494 let is_entire_line = selection.is_empty();
5495 if is_entire_line {
5496 start = Point::new(start.row, 0);"
5497 .to_string()
5498 ),
5499 "Regular copying preserves all indentation selected",
5500 );
5501 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5502 assert_eq!(
5503 cx.read_from_clipboard()
5504 .and_then(|item| item.text().as_deref().map(str::to_string)),
5505 Some(
5506 "for selection in selections.iter() {
5507let mut start = selection.start;
5508let mut end = selection.end;
5509let is_entire_line = selection.is_empty();
5510if is_entire_line {
5511 start = Point::new(start.row, 0);"
5512 .to_string()
5513 ),
5514 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5515 );
5516
5517 cx.set_state(
5518 r#" «ˇ for selection in selections.iter() {
5519 let mut start = selection.start;
5520 let mut end = selection.end;
5521 let is_entire_line = selection.is_empty();
5522 if is_entire_line {
5523 start = Point::new(start.row, 0);»
5524 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5525 }
5526 "#,
5527 );
5528 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5529 assert_eq!(
5530 cx.read_from_clipboard()
5531 .and_then(|item| item.text().as_deref().map(str::to_string)),
5532 Some(
5533 " for selection in selections.iter() {
5534 let mut start = selection.start;
5535 let mut end = selection.end;
5536 let is_entire_line = selection.is_empty();
5537 if is_entire_line {
5538 start = Point::new(start.row, 0);"
5539 .to_string()
5540 ),
5541 "Regular copying for reverse selection works the same",
5542 );
5543 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5544 assert_eq!(
5545 cx.read_from_clipboard()
5546 .and_then(|item| item.text().as_deref().map(str::to_string)),
5547 Some(
5548 "for selection in selections.iter() {
5549let mut start = selection.start;
5550let mut end = selection.end;
5551let is_entire_line = selection.is_empty();
5552if is_entire_line {
5553 start = Point::new(start.row, 0);"
5554 .to_string()
5555 ),
5556 "Copying with stripping for reverse selection works the same"
5557 );
5558
5559 cx.set_state(
5560 r#" for selection «in selections.iter() {
5561 let mut start = selection.start;
5562 let mut end = selection.end;
5563 let is_entire_line = selection.is_empty();
5564 if is_entire_line {
5565 start = Point::new(start.row, 0);ˇ»
5566 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5567 }
5568 "#,
5569 );
5570 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5571 assert_eq!(
5572 cx.read_from_clipboard()
5573 .and_then(|item| item.text().as_deref().map(str::to_string)),
5574 Some(
5575 "in selections.iter() {
5576 let mut start = selection.start;
5577 let mut end = selection.end;
5578 let is_entire_line = selection.is_empty();
5579 if is_entire_line {
5580 start = Point::new(start.row, 0);"
5581 .to_string()
5582 ),
5583 "When selecting past the indent, the copying works as usual",
5584 );
5585 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5586 assert_eq!(
5587 cx.read_from_clipboard()
5588 .and_then(|item| item.text().as_deref().map(str::to_string)),
5589 Some(
5590 "in selections.iter() {
5591 let mut start = selection.start;
5592 let mut end = selection.end;
5593 let is_entire_line = selection.is_empty();
5594 if is_entire_line {
5595 start = Point::new(start.row, 0);"
5596 .to_string()
5597 ),
5598 "When selecting past the indent, nothing is trimmed"
5599 );
5600
5601 cx.set_state(
5602 r#" «for selection in selections.iter() {
5603 let mut start = selection.start;
5604
5605 let mut end = selection.end;
5606 let is_entire_line = selection.is_empty();
5607 if is_entire_line {
5608 start = Point::new(start.row, 0);
5609ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5610 }
5611 "#,
5612 );
5613 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5614 assert_eq!(
5615 cx.read_from_clipboard()
5616 .and_then(|item| item.text().as_deref().map(str::to_string)),
5617 Some(
5618 "for selection in selections.iter() {
5619let mut start = selection.start;
5620
5621let mut end = selection.end;
5622let is_entire_line = selection.is_empty();
5623if is_entire_line {
5624 start = Point::new(start.row, 0);
5625"
5626 .to_string()
5627 ),
5628 "Copying with stripping should ignore empty lines"
5629 );
5630}
5631
5632#[gpui::test]
5633async fn test_paste_multiline(cx: &mut TestAppContext) {
5634 init_test(cx, |_| {});
5635
5636 let mut cx = EditorTestContext::new(cx).await;
5637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5638
5639 // Cut an indented block, without the leading whitespace.
5640 cx.set_state(indoc! {"
5641 const a: B = (
5642 c(),
5643 «d(
5644 e,
5645 f
5646 )ˇ»
5647 );
5648 "});
5649 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5650 cx.assert_editor_state(indoc! {"
5651 const a: B = (
5652 c(),
5653 ˇ
5654 );
5655 "});
5656
5657 // Paste it at the same position.
5658 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5659 cx.assert_editor_state(indoc! {"
5660 const a: B = (
5661 c(),
5662 d(
5663 e,
5664 f
5665 )ˇ
5666 );
5667 "});
5668
5669 // Paste it at a line with a lower indent level.
5670 cx.set_state(indoc! {"
5671 ˇ
5672 const a: B = (
5673 c(),
5674 );
5675 "});
5676 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5677 cx.assert_editor_state(indoc! {"
5678 d(
5679 e,
5680 f
5681 )ˇ
5682 const a: B = (
5683 c(),
5684 );
5685 "});
5686
5687 // Cut an indented block, with the leading whitespace.
5688 cx.set_state(indoc! {"
5689 const a: B = (
5690 c(),
5691 « d(
5692 e,
5693 f
5694 )
5695 ˇ»);
5696 "});
5697 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5698 cx.assert_editor_state(indoc! {"
5699 const a: B = (
5700 c(),
5701 ˇ);
5702 "});
5703
5704 // Paste it at the same position.
5705 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5706 cx.assert_editor_state(indoc! {"
5707 const a: B = (
5708 c(),
5709 d(
5710 e,
5711 f
5712 )
5713 ˇ);
5714 "});
5715
5716 // Paste it at a line with a higher indent level.
5717 cx.set_state(indoc! {"
5718 const a: B = (
5719 c(),
5720 d(
5721 e,
5722 fˇ
5723 )
5724 );
5725 "});
5726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5727 cx.assert_editor_state(indoc! {"
5728 const a: B = (
5729 c(),
5730 d(
5731 e,
5732 f d(
5733 e,
5734 f
5735 )
5736 ˇ
5737 )
5738 );
5739 "});
5740
5741 // Copy an indented block, starting mid-line
5742 cx.set_state(indoc! {"
5743 const a: B = (
5744 c(),
5745 somethin«g(
5746 e,
5747 f
5748 )ˇ»
5749 );
5750 "});
5751 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5752
5753 // Paste it on a line with a lower indent level
5754 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5755 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5756 cx.assert_editor_state(indoc! {"
5757 const a: B = (
5758 c(),
5759 something(
5760 e,
5761 f
5762 )
5763 );
5764 g(
5765 e,
5766 f
5767 )ˇ"});
5768}
5769
5770#[gpui::test]
5771async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5772 init_test(cx, |_| {});
5773
5774 cx.write_to_clipboard(ClipboardItem::new_string(
5775 " d(\n e\n );\n".into(),
5776 ));
5777
5778 let mut cx = EditorTestContext::new(cx).await;
5779 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5780
5781 cx.set_state(indoc! {"
5782 fn a() {
5783 b();
5784 if c() {
5785 ˇ
5786 }
5787 }
5788 "});
5789
5790 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5791 cx.assert_editor_state(indoc! {"
5792 fn a() {
5793 b();
5794 if c() {
5795 d(
5796 e
5797 );
5798 ˇ
5799 }
5800 }
5801 "});
5802
5803 cx.set_state(indoc! {"
5804 fn a() {
5805 b();
5806 ˇ
5807 }
5808 "});
5809
5810 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5811 cx.assert_editor_state(indoc! {"
5812 fn a() {
5813 b();
5814 d(
5815 e
5816 );
5817 ˇ
5818 }
5819 "});
5820}
5821
5822#[gpui::test]
5823fn test_select_all(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825
5826 let editor = cx.add_window(|window, cx| {
5827 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5828 build_editor(buffer, window, cx)
5829 });
5830 _ = editor.update(cx, |editor, window, cx| {
5831 editor.select_all(&SelectAll, window, cx);
5832 assert_eq!(
5833 editor.selections.display_ranges(cx),
5834 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5835 );
5836 });
5837}
5838
5839#[gpui::test]
5840fn test_select_line(cx: &mut TestAppContext) {
5841 init_test(cx, |_| {});
5842
5843 let editor = cx.add_window(|window, cx| {
5844 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5845 build_editor(buffer, window, cx)
5846 });
5847 _ = editor.update(cx, |editor, window, cx| {
5848 editor.change_selections(None, window, cx, |s| {
5849 s.select_display_ranges([
5850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5851 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5852 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5853 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5854 ])
5855 });
5856 editor.select_line(&SelectLine, window, cx);
5857 assert_eq!(
5858 editor.selections.display_ranges(cx),
5859 vec![
5860 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5861 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5862 ]
5863 );
5864 });
5865
5866 _ = editor.update(cx, |editor, window, cx| {
5867 editor.select_line(&SelectLine, window, cx);
5868 assert_eq!(
5869 editor.selections.display_ranges(cx),
5870 vec![
5871 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5872 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5873 ]
5874 );
5875 });
5876
5877 _ = editor.update(cx, |editor, window, cx| {
5878 editor.select_line(&SelectLine, window, cx);
5879 assert_eq!(
5880 editor.selections.display_ranges(cx),
5881 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5882 );
5883 });
5884}
5885
5886#[gpui::test]
5887async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 #[track_caller]
5892 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5893 cx.set_state(initial_state);
5894 cx.update_editor(|e, window, cx| {
5895 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5896 });
5897 cx.assert_editor_state(expected_state);
5898 }
5899
5900 // Selection starts and ends at the middle of lines, left-to-right
5901 test(
5902 &mut cx,
5903 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5904 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5905 );
5906 // Same thing, right-to-left
5907 test(
5908 &mut cx,
5909 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5910 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5911 );
5912
5913 // Whole buffer, left-to-right, last line *doesn't* end with newline
5914 test(
5915 &mut cx,
5916 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5917 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5918 );
5919 // Same thing, right-to-left
5920 test(
5921 &mut cx,
5922 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5923 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5924 );
5925
5926 // Whole buffer, left-to-right, last line ends with newline
5927 test(
5928 &mut cx,
5929 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5930 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5931 );
5932 // Same thing, right-to-left
5933 test(
5934 &mut cx,
5935 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5936 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5937 );
5938
5939 // Starts at the end of a line, ends at the start of another
5940 test(
5941 &mut cx,
5942 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5943 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5944 );
5945}
5946
5947#[gpui::test]
5948async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 let editor = cx.add_window(|window, cx| {
5952 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5953 build_editor(buffer, window, cx)
5954 });
5955
5956 // setup
5957 _ = editor.update(cx, |editor, window, cx| {
5958 editor.fold_creases(
5959 vec![
5960 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5961 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5963 ],
5964 true,
5965 window,
5966 cx,
5967 );
5968 assert_eq!(
5969 editor.display_text(cx),
5970 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5971 );
5972 });
5973
5974 _ = editor.update(cx, |editor, window, cx| {
5975 editor.change_selections(None, window, cx, |s| {
5976 s.select_display_ranges([
5977 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5978 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5979 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5980 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5981 ])
5982 });
5983 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5984 assert_eq!(
5985 editor.display_text(cx),
5986 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5987 );
5988 });
5989 EditorTestContext::for_editor(editor, cx)
5990 .await
5991 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5992
5993 _ = editor.update(cx, |editor, window, cx| {
5994 editor.change_selections(None, window, cx, |s| {
5995 s.select_display_ranges([
5996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5997 ])
5998 });
5999 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6000 assert_eq!(
6001 editor.display_text(cx),
6002 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6003 );
6004 assert_eq!(
6005 editor.selections.display_ranges(cx),
6006 [
6007 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6008 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6009 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6010 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6011 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6012 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6013 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6014 ]
6015 );
6016 });
6017 EditorTestContext::for_editor(editor, cx)
6018 .await
6019 .assert_editor_state(
6020 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6021 );
6022}
6023
6024#[gpui::test]
6025async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6026 init_test(cx, |_| {});
6027
6028 let mut cx = EditorTestContext::new(cx).await;
6029
6030 cx.set_state(indoc!(
6031 r#"abc
6032 defˇghi
6033
6034 jk
6035 nlmo
6036 "#
6037 ));
6038
6039 cx.update_editor(|editor, window, cx| {
6040 editor.add_selection_above(&Default::default(), window, cx);
6041 });
6042
6043 cx.assert_editor_state(indoc!(
6044 r#"abcˇ
6045 defˇghi
6046
6047 jk
6048 nlmo
6049 "#
6050 ));
6051
6052 cx.update_editor(|editor, window, cx| {
6053 editor.add_selection_above(&Default::default(), window, cx);
6054 });
6055
6056 cx.assert_editor_state(indoc!(
6057 r#"abcˇ
6058 defˇghi
6059
6060 jk
6061 nlmo
6062 "#
6063 ));
6064
6065 cx.update_editor(|editor, window, cx| {
6066 editor.add_selection_below(&Default::default(), window, cx);
6067 });
6068
6069 cx.assert_editor_state(indoc!(
6070 r#"abc
6071 defˇghi
6072
6073 jk
6074 nlmo
6075 "#
6076 ));
6077
6078 cx.update_editor(|editor, window, cx| {
6079 editor.undo_selection(&Default::default(), window, cx);
6080 });
6081
6082 cx.assert_editor_state(indoc!(
6083 r#"abcˇ
6084 defˇghi
6085
6086 jk
6087 nlmo
6088 "#
6089 ));
6090
6091 cx.update_editor(|editor, window, cx| {
6092 editor.redo_selection(&Default::default(), window, cx);
6093 });
6094
6095 cx.assert_editor_state(indoc!(
6096 r#"abc
6097 defˇghi
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ˇghi
6111 ˇ
6112 jk
6113 nlmo
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ˇghi
6124 ˇ
6125 jkˇ
6126 nlmo
6127 "#
6128 ));
6129
6130 cx.update_editor(|editor, window, cx| {
6131 editor.add_selection_below(&Default::default(), window, cx);
6132 });
6133
6134 cx.assert_editor_state(indoc!(
6135 r#"abc
6136 defˇghi
6137 ˇ
6138 jkˇ
6139 nlmˇo
6140 "#
6141 ));
6142
6143 cx.update_editor(|editor, window, cx| {
6144 editor.add_selection_below(&Default::default(), window, cx);
6145 });
6146
6147 cx.assert_editor_state(indoc!(
6148 r#"abc
6149 defˇghi
6150 ˇ
6151 jkˇ
6152 nlmˇo
6153 ˇ"#
6154 ));
6155
6156 // change selections
6157 cx.set_state(indoc!(
6158 r#"abc
6159 def«ˇg»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#"abc
6172 def«ˇg»hi
6173
6174 jk
6175 nlm«ˇo»
6176 "#
6177 ));
6178
6179 cx.update_editor(|editor, window, cx| {
6180 editor.add_selection_below(&Default::default(), window, cx);
6181 });
6182
6183 cx.assert_editor_state(indoc!(
6184 r#"abc
6185 def«ˇg»hi
6186
6187 jk
6188 nlm«ˇo»
6189 "#
6190 ));
6191
6192 cx.update_editor(|editor, window, cx| {
6193 editor.add_selection_above(&Default::default(), window, cx);
6194 });
6195
6196 cx.assert_editor_state(indoc!(
6197 r#"abc
6198 def«ˇg»hi
6199
6200 jk
6201 nlmo
6202 "#
6203 ));
6204
6205 cx.update_editor(|editor, window, cx| {
6206 editor.add_selection_above(&Default::default(), window, cx);
6207 });
6208
6209 cx.assert_editor_state(indoc!(
6210 r#"abc
6211 def«ˇg»hi
6212
6213 jk
6214 nlmo
6215 "#
6216 ));
6217
6218 // Change selections again
6219 cx.set_state(indoc!(
6220 r#"a«bc
6221 defgˇ»hi
6222
6223 jk
6224 nlmo
6225 "#
6226 ));
6227
6228 cx.update_editor(|editor, window, cx| {
6229 editor.add_selection_below(&Default::default(), window, cx);
6230 });
6231
6232 cx.assert_editor_state(indoc!(
6233 r#"a«bcˇ»
6234 d«efgˇ»hi
6235
6236 j«kˇ»
6237 nlmo
6238 "#
6239 ));
6240
6241 cx.update_editor(|editor, window, cx| {
6242 editor.add_selection_below(&Default::default(), window, cx);
6243 });
6244 cx.assert_editor_state(indoc!(
6245 r#"a«bcˇ»
6246 d«efgˇ»hi
6247
6248 j«kˇ»
6249 n«lmoˇ»
6250 "#
6251 ));
6252 cx.update_editor(|editor, window, cx| {
6253 editor.add_selection_above(&Default::default(), window, cx);
6254 });
6255
6256 cx.assert_editor_state(indoc!(
6257 r#"a«bcˇ»
6258 d«efgˇ»hi
6259
6260 j«kˇ»
6261 nlmo
6262 "#
6263 ));
6264
6265 // Change selections again
6266 cx.set_state(indoc!(
6267 r#"abc
6268 d«ˇefghi
6269
6270 jk
6271 nlm»o
6272 "#
6273 ));
6274
6275 cx.update_editor(|editor, window, cx| {
6276 editor.add_selection_above(&Default::default(), window, cx);
6277 });
6278
6279 cx.assert_editor_state(indoc!(
6280 r#"a«ˇbc»
6281 d«ˇef»ghi
6282
6283 j«ˇk»
6284 n«ˇlm»o
6285 "#
6286 ));
6287
6288 cx.update_editor(|editor, window, cx| {
6289 editor.add_selection_below(&Default::default(), window, cx);
6290 });
6291
6292 cx.assert_editor_state(indoc!(
6293 r#"abc
6294 d«ˇef»ghi
6295
6296 j«ˇk»
6297 n«ˇlm»o
6298 "#
6299 ));
6300}
6301
6302#[gpui::test]
6303async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6304 init_test(cx, |_| {});
6305 let mut cx = EditorTestContext::new(cx).await;
6306
6307 cx.set_state(indoc!(
6308 r#"line onˇe
6309 liˇne two
6310 line three
6311 line four"#
6312 ));
6313
6314 cx.update_editor(|editor, window, cx| {
6315 editor.add_selection_below(&Default::default(), window, cx);
6316 });
6317
6318 // test multiple cursors expand in the same direction
6319 cx.assert_editor_state(indoc!(
6320 r#"line onˇe
6321 liˇne twˇo
6322 liˇne three
6323 line four"#
6324 ));
6325
6326 cx.update_editor(|editor, window, cx| {
6327 editor.add_selection_below(&Default::default(), window, cx);
6328 });
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.add_selection_below(&Default::default(), window, cx);
6332 });
6333
6334 // test multiple cursors expand below overflow
6335 cx.assert_editor_state(indoc!(
6336 r#"line onˇe
6337 liˇne twˇo
6338 liˇne thˇree
6339 liˇne foˇur"#
6340 ));
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.add_selection_above(&Default::default(), window, cx);
6344 });
6345
6346 // test multiple cursors retrieves back correctly
6347 cx.assert_editor_state(indoc!(
6348 r#"line onˇe
6349 liˇne twˇo
6350 liˇne thˇree
6351 line four"#
6352 ));
6353
6354 cx.update_editor(|editor, window, cx| {
6355 editor.add_selection_above(&Default::default(), window, cx);
6356 });
6357
6358 cx.update_editor(|editor, window, cx| {
6359 editor.add_selection_above(&Default::default(), window, cx);
6360 });
6361
6362 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6363 cx.assert_editor_state(indoc!(
6364 r#"liˇne onˇe
6365 liˇne two
6366 line three
6367 line four"#
6368 ));
6369
6370 cx.update_editor(|editor, window, cx| {
6371 editor.undo_selection(&Default::default(), window, cx);
6372 });
6373
6374 // test undo
6375 cx.assert_editor_state(indoc!(
6376 r#"line onˇe
6377 liˇne twˇo
6378 line three
6379 line four"#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.redo_selection(&Default::default(), window, cx);
6384 });
6385
6386 // test redo
6387 cx.assert_editor_state(indoc!(
6388 r#"liˇne onˇe
6389 liˇne two
6390 line three
6391 line four"#
6392 ));
6393
6394 cx.set_state(indoc!(
6395 r#"abcd
6396 ef«ghˇ»
6397 ijkl
6398 «mˇ»nop"#
6399 ));
6400
6401 cx.update_editor(|editor, window, cx| {
6402 editor.add_selection_above(&Default::default(), window, cx);
6403 });
6404
6405 // test multiple selections expand in the same direction
6406 cx.assert_editor_state(indoc!(
6407 r#"ab«cdˇ»
6408 ef«ghˇ»
6409 «iˇ»jkl
6410 «mˇ»nop"#
6411 ));
6412
6413 cx.update_editor(|editor, window, cx| {
6414 editor.add_selection_above(&Default::default(), window, cx);
6415 });
6416
6417 // test multiple selection upward overflow
6418 cx.assert_editor_state(indoc!(
6419 r#"ab«cdˇ»
6420 «eˇ»f«ghˇ»
6421 «iˇ»jkl
6422 «mˇ»nop"#
6423 ));
6424
6425 cx.update_editor(|editor, window, cx| {
6426 editor.add_selection_below(&Default::default(), window, cx);
6427 });
6428
6429 // test multiple selection retrieves back correctly
6430 cx.assert_editor_state(indoc!(
6431 r#"abcd
6432 ef«ghˇ»
6433 «iˇ»jkl
6434 «mˇ»nop"#
6435 ));
6436
6437 cx.update_editor(|editor, window, cx| {
6438 editor.add_selection_below(&Default::default(), window, cx);
6439 });
6440
6441 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6442 cx.assert_editor_state(indoc!(
6443 r#"abcd
6444 ef«ghˇ»
6445 ij«klˇ»
6446 «mˇ»nop"#
6447 ));
6448
6449 cx.update_editor(|editor, window, cx| {
6450 editor.undo_selection(&Default::default(), window, cx);
6451 });
6452
6453 // test undo
6454 cx.assert_editor_state(indoc!(
6455 r#"abcd
6456 ef«ghˇ»
6457 «iˇ»jkl
6458 «mˇ»nop"#
6459 ));
6460
6461 cx.update_editor(|editor, window, cx| {
6462 editor.redo_selection(&Default::default(), window, cx);
6463 });
6464
6465 // test redo
6466 cx.assert_editor_state(indoc!(
6467 r#"abcd
6468 ef«ghˇ»
6469 ij«klˇ»
6470 «mˇ»nop"#
6471 ));
6472}
6473
6474#[gpui::test]
6475async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6476 init_test(cx, |_| {});
6477 let mut cx = EditorTestContext::new(cx).await;
6478
6479 cx.set_state(indoc!(
6480 r#"line onˇe
6481 liˇne two
6482 line three
6483 line four"#
6484 ));
6485
6486 cx.update_editor(|editor, window, cx| {
6487 editor.add_selection_below(&Default::default(), window, cx);
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 });
6491
6492 // initial state with two multi cursor groups
6493 cx.assert_editor_state(indoc!(
6494 r#"line onˇe
6495 liˇne twˇo
6496 liˇne thˇree
6497 liˇne foˇur"#
6498 ));
6499
6500 // add single cursor in middle - simulate opt click
6501 cx.update_editor(|editor, window, cx| {
6502 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6503 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6504 editor.end_selection(window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"line onˇe
6509 liˇne twˇo
6510 liˇneˇ thˇree
6511 liˇne foˇur"#
6512 ));
6513
6514 cx.update_editor(|editor, window, cx| {
6515 editor.add_selection_above(&Default::default(), window, cx);
6516 });
6517
6518 // test new added selection expands above and existing selection shrinks
6519 cx.assert_editor_state(indoc!(
6520 r#"line onˇe
6521 liˇneˇ twˇo
6522 liˇneˇ thˇree
6523 line four"#
6524 ));
6525
6526 cx.update_editor(|editor, window, cx| {
6527 editor.add_selection_above(&Default::default(), window, cx);
6528 });
6529
6530 // test new added selection expands above and existing selection shrinks
6531 cx.assert_editor_state(indoc!(
6532 r#"lineˇ onˇe
6533 liˇneˇ twˇo
6534 lineˇ three
6535 line four"#
6536 ));
6537
6538 // intial state with two selection groups
6539 cx.set_state(indoc!(
6540 r#"abcd
6541 ef«ghˇ»
6542 ijkl
6543 «mˇ»nop"#
6544 ));
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.add_selection_above(&Default::default(), window, cx);
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 });
6550
6551 cx.assert_editor_state(indoc!(
6552 r#"ab«cdˇ»
6553 «eˇ»f«ghˇ»
6554 «iˇ»jkl
6555 «mˇ»nop"#
6556 ));
6557
6558 // add single selection in middle - simulate opt drag
6559 cx.update_editor(|editor, window, cx| {
6560 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6561 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6562 editor.update_selection(
6563 DisplayPoint::new(DisplayRow(2), 4),
6564 0,
6565 gpui::Point::<f32>::default(),
6566 window,
6567 cx,
6568 );
6569 editor.end_selection(window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"ab«cdˇ»
6574 «eˇ»f«ghˇ»
6575 «iˇ»jk«lˇ»
6576 «mˇ»nop"#
6577 ));
6578
6579 cx.update_editor(|editor, window, cx| {
6580 editor.add_selection_below(&Default::default(), window, cx);
6581 });
6582
6583 // test new added selection expands below, others shrinks from above
6584 cx.assert_editor_state(indoc!(
6585 r#"abcd
6586 ef«ghˇ»
6587 «iˇ»jk«lˇ»
6588 «mˇ»no«pˇ»"#
6589 ));
6590}
6591
6592#[gpui::test]
6593async fn test_select_next(cx: &mut TestAppContext) {
6594 init_test(cx, |_| {});
6595
6596 let mut cx = EditorTestContext::new(cx).await;
6597 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6598
6599 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6600 .unwrap();
6601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6602
6603 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6604 .unwrap();
6605 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6606
6607 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6608 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6609
6610 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6611 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6612
6613 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6614 .unwrap();
6615 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6616
6617 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6618 .unwrap();
6619 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6620
6621 // Test selection direction should be preserved
6622 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6623
6624 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6625 .unwrap();
6626 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6627}
6628
6629#[gpui::test]
6630async fn test_select_all_matches(cx: &mut TestAppContext) {
6631 init_test(cx, |_| {});
6632
6633 let mut cx = EditorTestContext::new(cx).await;
6634
6635 // Test caret-only selections
6636 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6637 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6638 .unwrap();
6639 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6640
6641 // Test left-to-right selections
6642 cx.set_state("abc\n«abcˇ»\nabc");
6643 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6644 .unwrap();
6645 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6646
6647 // Test right-to-left selections
6648 cx.set_state("abc\n«ˇabc»\nabc");
6649 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6650 .unwrap();
6651 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6652
6653 // Test selecting whitespace with caret selection
6654 cx.set_state("abc\nˇ abc\nabc");
6655 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6656 .unwrap();
6657 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6658
6659 // Test selecting whitespace with left-to-right selection
6660 cx.set_state("abc\n«ˇ »abc\nabc");
6661 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6662 .unwrap();
6663 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6664
6665 // Test no matches with right-to-left selection
6666 cx.set_state("abc\n« ˇ»abc\nabc");
6667 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6668 .unwrap();
6669 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6670}
6671
6672#[gpui::test]
6673async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6674 init_test(cx, |_| {});
6675
6676 let mut cx = EditorTestContext::new(cx).await;
6677
6678 let large_body_1 = "\nd".repeat(200);
6679 let large_body_2 = "\ne".repeat(200);
6680
6681 cx.set_state(&format!(
6682 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6683 ));
6684 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6685 let scroll_position = editor.scroll_position(cx);
6686 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6687 scroll_position
6688 });
6689
6690 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6691 .unwrap();
6692 cx.assert_editor_state(&format!(
6693 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6694 ));
6695 let scroll_position_after_selection =
6696 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6697 assert_eq!(
6698 initial_scroll_position, scroll_position_after_selection,
6699 "Scroll position should not change after selecting all matches"
6700 );
6701}
6702
6703#[gpui::test]
6704async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6705 init_test(cx, |_| {});
6706
6707 let mut cx = EditorLspTestContext::new_rust(
6708 lsp::ServerCapabilities {
6709 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6710 ..Default::default()
6711 },
6712 cx,
6713 )
6714 .await;
6715
6716 cx.set_state(indoc! {"
6717 line 1
6718 line 2
6719 linˇe 3
6720 line 4
6721 line 5
6722 "});
6723
6724 // Make an edit
6725 cx.update_editor(|editor, window, cx| {
6726 editor.handle_input("X", window, cx);
6727 });
6728
6729 // Move cursor to a different position
6730 cx.update_editor(|editor, window, cx| {
6731 editor.change_selections(None, window, cx, |s| {
6732 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6733 });
6734 });
6735
6736 cx.assert_editor_state(indoc! {"
6737 line 1
6738 line 2
6739 linXe 3
6740 line 4
6741 liˇne 5
6742 "});
6743
6744 cx.lsp
6745 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6746 Ok(Some(vec![lsp::TextEdit::new(
6747 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6748 "PREFIX ".to_string(),
6749 )]))
6750 });
6751
6752 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6753 .unwrap()
6754 .await
6755 .unwrap();
6756
6757 cx.assert_editor_state(indoc! {"
6758 PREFIX line 1
6759 line 2
6760 linXe 3
6761 line 4
6762 liˇne 5
6763 "});
6764
6765 // Undo formatting
6766 cx.update_editor(|editor, window, cx| {
6767 editor.undo(&Default::default(), window, cx);
6768 });
6769
6770 // Verify cursor moved back to position after edit
6771 cx.assert_editor_state(indoc! {"
6772 line 1
6773 line 2
6774 linXˇe 3
6775 line 4
6776 line 5
6777 "});
6778}
6779
6780#[gpui::test]
6781async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6782 init_test(cx, |_| {});
6783
6784 let mut cx = EditorTestContext::new(cx).await;
6785
6786 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6787 cx.update_editor(|editor, window, cx| {
6788 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6789 });
6790
6791 cx.set_state(indoc! {"
6792 line 1
6793 line 2
6794 linˇe 3
6795 line 4
6796 line 5
6797 line 6
6798 line 7
6799 line 8
6800 line 9
6801 line 10
6802 "});
6803
6804 let snapshot = cx.buffer_snapshot();
6805 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6806
6807 cx.update(|_, cx| {
6808 provider.update(cx, |provider, _| {
6809 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6810 id: None,
6811 edits: vec![(edit_position..edit_position, "X".into())],
6812 edit_preview: None,
6813 }))
6814 })
6815 });
6816
6817 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6818 cx.update_editor(|editor, window, cx| {
6819 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6820 });
6821
6822 cx.assert_editor_state(indoc! {"
6823 line 1
6824 line 2
6825 lineXˇ 3
6826 line 4
6827 line 5
6828 line 6
6829 line 7
6830 line 8
6831 line 9
6832 line 10
6833 "});
6834
6835 cx.update_editor(|editor, window, cx| {
6836 editor.change_selections(None, window, cx, |s| {
6837 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6838 });
6839 });
6840
6841 cx.assert_editor_state(indoc! {"
6842 line 1
6843 line 2
6844 lineX 3
6845 line 4
6846 line 5
6847 line 6
6848 line 7
6849 line 8
6850 line 9
6851 liˇne 10
6852 "});
6853
6854 cx.update_editor(|editor, window, cx| {
6855 editor.undo(&Default::default(), window, cx);
6856 });
6857
6858 cx.assert_editor_state(indoc! {"
6859 line 1
6860 line 2
6861 lineˇ 3
6862 line 4
6863 line 5
6864 line 6
6865 line 7
6866 line 8
6867 line 9
6868 line 10
6869 "});
6870}
6871
6872#[gpui::test]
6873async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6874 init_test(cx, |_| {});
6875
6876 let mut cx = EditorTestContext::new(cx).await;
6877 cx.set_state(
6878 r#"let foo = 2;
6879lˇet foo = 2;
6880let fooˇ = 2;
6881let foo = 2;
6882let foo = ˇ2;"#,
6883 );
6884
6885 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6886 .unwrap();
6887 cx.assert_editor_state(
6888 r#"let foo = 2;
6889«letˇ» foo = 2;
6890let «fooˇ» = 2;
6891let foo = 2;
6892let foo = «2ˇ»;"#,
6893 );
6894
6895 // noop for multiple selections with different contents
6896 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6897 .unwrap();
6898 cx.assert_editor_state(
6899 r#"let foo = 2;
6900«letˇ» foo = 2;
6901let «fooˇ» = 2;
6902let foo = 2;
6903let foo = «2ˇ»;"#,
6904 );
6905
6906 // Test last selection direction should be preserved
6907 cx.set_state(
6908 r#"let foo = 2;
6909let foo = 2;
6910let «fooˇ» = 2;
6911let «ˇfoo» = 2;
6912let foo = 2;"#,
6913 );
6914
6915 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6916 .unwrap();
6917 cx.assert_editor_state(
6918 r#"let foo = 2;
6919let foo = 2;
6920let «fooˇ» = 2;
6921let «ˇfoo» = 2;
6922let «ˇfoo» = 2;"#,
6923 );
6924}
6925
6926#[gpui::test]
6927async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6928 init_test(cx, |_| {});
6929
6930 let mut cx =
6931 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6932
6933 cx.assert_editor_state(indoc! {"
6934 ˇbbb
6935 ccc
6936
6937 bbb
6938 ccc
6939 "});
6940 cx.dispatch_action(SelectPrevious::default());
6941 cx.assert_editor_state(indoc! {"
6942 «bbbˇ»
6943 ccc
6944
6945 bbb
6946 ccc
6947 "});
6948 cx.dispatch_action(SelectPrevious::default());
6949 cx.assert_editor_state(indoc! {"
6950 «bbbˇ»
6951 ccc
6952
6953 «bbbˇ»
6954 ccc
6955 "});
6956}
6957
6958#[gpui::test]
6959async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6960 init_test(cx, |_| {});
6961
6962 let mut cx = EditorTestContext::new(cx).await;
6963 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6964
6965 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6966 .unwrap();
6967 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6968
6969 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6970 .unwrap();
6971 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6972
6973 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6974 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6975
6976 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6977 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6978
6979 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6980 .unwrap();
6981 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6982
6983 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6984 .unwrap();
6985 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6986}
6987
6988#[gpui::test]
6989async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6990 init_test(cx, |_| {});
6991
6992 let mut cx = EditorTestContext::new(cx).await;
6993 cx.set_state("aˇ");
6994
6995 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6996 .unwrap();
6997 cx.assert_editor_state("«aˇ»");
6998 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6999 .unwrap();
7000 cx.assert_editor_state("«aˇ»");
7001}
7002
7003#[gpui::test]
7004async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorTestContext::new(cx).await;
7008 cx.set_state(
7009 r#"let foo = 2;
7010lˇet foo = 2;
7011let fooˇ = 2;
7012let foo = 2;
7013let foo = ˇ2;"#,
7014 );
7015
7016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7017 .unwrap();
7018 cx.assert_editor_state(
7019 r#"let foo = 2;
7020«letˇ» foo = 2;
7021let «fooˇ» = 2;
7022let foo = 2;
7023let foo = «2ˇ»;"#,
7024 );
7025
7026 // noop for multiple selections with different contents
7027 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7028 .unwrap();
7029 cx.assert_editor_state(
7030 r#"let foo = 2;
7031«letˇ» foo = 2;
7032let «fooˇ» = 2;
7033let foo = 2;
7034let foo = «2ˇ»;"#,
7035 );
7036}
7037
7038#[gpui::test]
7039async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7040 init_test(cx, |_| {});
7041
7042 let mut cx = EditorTestContext::new(cx).await;
7043 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7044
7045 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7046 .unwrap();
7047 // selection direction is preserved
7048 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7049
7050 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7051 .unwrap();
7052 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7053
7054 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7055 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7056
7057 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7058 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7059
7060 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7061 .unwrap();
7062 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7063
7064 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7067}
7068
7069#[gpui::test]
7070async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7071 init_test(cx, |_| {});
7072
7073 let language = Arc::new(Language::new(
7074 LanguageConfig::default(),
7075 Some(tree_sitter_rust::LANGUAGE.into()),
7076 ));
7077
7078 let text = r#"
7079 use mod1::mod2::{mod3, mod4};
7080
7081 fn fn_1(param1: bool, param2: &str) {
7082 let var1 = "text";
7083 }
7084 "#
7085 .unindent();
7086
7087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7089 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7090
7091 editor
7092 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7093 .await;
7094
7095 editor.update_in(cx, |editor, window, cx| {
7096 editor.change_selections(None, window, cx, |s| {
7097 s.select_display_ranges([
7098 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7099 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7100 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7101 ]);
7102 });
7103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7104 });
7105 editor.update(cx, |editor, cx| {
7106 assert_text_with_selections(
7107 editor,
7108 indoc! {r#"
7109 use mod1::mod2::{mod3, «mod4ˇ»};
7110
7111 fn fn_1«ˇ(param1: bool, param2: &str)» {
7112 let var1 = "«ˇtext»";
7113 }
7114 "#},
7115 cx,
7116 );
7117 });
7118
7119 editor.update_in(cx, |editor, window, cx| {
7120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7121 });
7122 editor.update(cx, |editor, cx| {
7123 assert_text_with_selections(
7124 editor,
7125 indoc! {r#"
7126 use mod1::mod2::«{mod3, mod4}ˇ»;
7127
7128 «ˇfn fn_1(param1: bool, param2: &str) {
7129 let var1 = "text";
7130 }»
7131 "#},
7132 cx,
7133 );
7134 });
7135
7136 editor.update_in(cx, |editor, window, cx| {
7137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7138 });
7139 assert_eq!(
7140 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7141 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7142 );
7143
7144 // Trying to expand the selected syntax node one more time has no effect.
7145 editor.update_in(cx, |editor, window, cx| {
7146 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7147 });
7148 assert_eq!(
7149 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7150 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7151 );
7152
7153 editor.update_in(cx, |editor, window, cx| {
7154 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7155 });
7156 editor.update(cx, |editor, cx| {
7157 assert_text_with_selections(
7158 editor,
7159 indoc! {r#"
7160 use mod1::mod2::«{mod3, mod4}ˇ»;
7161
7162 «ˇfn fn_1(param1: bool, param2: &str) {
7163 let var1 = "text";
7164 }»
7165 "#},
7166 cx,
7167 );
7168 });
7169
7170 editor.update_in(cx, |editor, window, cx| {
7171 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7172 });
7173 editor.update(cx, |editor, cx| {
7174 assert_text_with_selections(
7175 editor,
7176 indoc! {r#"
7177 use mod1::mod2::{mod3, «mod4ˇ»};
7178
7179 fn fn_1«ˇ(param1: bool, param2: &str)» {
7180 let var1 = "«ˇtext»";
7181 }
7182 "#},
7183 cx,
7184 );
7185 });
7186
7187 editor.update_in(cx, |editor, window, cx| {
7188 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7189 });
7190 editor.update(cx, |editor, cx| {
7191 assert_text_with_selections(
7192 editor,
7193 indoc! {r#"
7194 use mod1::mod2::{mod3, mo«ˇ»d4};
7195
7196 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7197 let var1 = "te«ˇ»xt";
7198 }
7199 "#},
7200 cx,
7201 );
7202 });
7203
7204 // Trying to shrink the selected syntax node one more time has no effect.
7205 editor.update_in(cx, |editor, window, cx| {
7206 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7207 });
7208 editor.update_in(cx, |editor, _, cx| {
7209 assert_text_with_selections(
7210 editor,
7211 indoc! {r#"
7212 use mod1::mod2::{mod3, mo«ˇ»d4};
7213
7214 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7215 let var1 = "te«ˇ»xt";
7216 }
7217 "#},
7218 cx,
7219 );
7220 });
7221
7222 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7223 // a fold.
7224 editor.update_in(cx, |editor, window, cx| {
7225 editor.fold_creases(
7226 vec![
7227 Crease::simple(
7228 Point::new(0, 21)..Point::new(0, 24),
7229 FoldPlaceholder::test(),
7230 ),
7231 Crease::simple(
7232 Point::new(3, 20)..Point::new(3, 22),
7233 FoldPlaceholder::test(),
7234 ),
7235 ],
7236 true,
7237 window,
7238 cx,
7239 );
7240 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7241 });
7242 editor.update(cx, |editor, cx| {
7243 assert_text_with_selections(
7244 editor,
7245 indoc! {r#"
7246 use mod1::mod2::«{mod3, mod4}ˇ»;
7247
7248 fn fn_1«ˇ(param1: bool, param2: &str)» {
7249 let var1 = "«ˇtext»";
7250 }
7251 "#},
7252 cx,
7253 );
7254 });
7255}
7256
7257#[gpui::test]
7258async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7259 init_test(cx, |_| {});
7260
7261 let language = Arc::new(Language::new(
7262 LanguageConfig::default(),
7263 Some(tree_sitter_rust::LANGUAGE.into()),
7264 ));
7265
7266 let text = "let a = 2;";
7267
7268 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7269 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7270 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7271
7272 editor
7273 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7274 .await;
7275
7276 // Test case 1: Cursor at end of word
7277 editor.update_in(cx, |editor, window, cx| {
7278 editor.change_selections(None, window, cx, |s| {
7279 s.select_display_ranges([
7280 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7281 ]);
7282 });
7283 });
7284 editor.update(cx, |editor, cx| {
7285 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7286 });
7287 editor.update_in(cx, |editor, window, cx| {
7288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7289 });
7290 editor.update(cx, |editor, cx| {
7291 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7292 });
7293 editor.update_in(cx, |editor, window, cx| {
7294 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7295 });
7296 editor.update(cx, |editor, cx| {
7297 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7298 });
7299
7300 // Test case 2: Cursor at end of statement
7301 editor.update_in(cx, |editor, window, cx| {
7302 editor.change_selections(None, window, cx, |s| {
7303 s.select_display_ranges([
7304 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7305 ]);
7306 });
7307 });
7308 editor.update(cx, |editor, cx| {
7309 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7310 });
7311 editor.update_in(cx, |editor, window, cx| {
7312 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7313 });
7314 editor.update(cx, |editor, cx| {
7315 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7316 });
7317}
7318
7319#[gpui::test]
7320async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7321 init_test(cx, |_| {});
7322
7323 let language = Arc::new(Language::new(
7324 LanguageConfig::default(),
7325 Some(tree_sitter_rust::LANGUAGE.into()),
7326 ));
7327
7328 let text = r#"
7329 use mod1::mod2::{mod3, mod4};
7330
7331 fn fn_1(param1: bool, param2: &str) {
7332 let var1 = "hello world";
7333 }
7334 "#
7335 .unindent();
7336
7337 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7339 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7340
7341 editor
7342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7343 .await;
7344
7345 // Test 1: Cursor on a letter of a string word
7346 editor.update_in(cx, |editor, window, cx| {
7347 editor.change_selections(None, window, cx, |s| {
7348 s.select_display_ranges([
7349 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7350 ]);
7351 });
7352 });
7353 editor.update_in(cx, |editor, window, cx| {
7354 assert_text_with_selections(
7355 editor,
7356 indoc! {r#"
7357 use mod1::mod2::{mod3, mod4};
7358
7359 fn fn_1(param1: bool, param2: &str) {
7360 let var1 = "hˇello world";
7361 }
7362 "#},
7363 cx,
7364 );
7365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7366 assert_text_with_selections(
7367 editor,
7368 indoc! {r#"
7369 use mod1::mod2::{mod3, mod4};
7370
7371 fn fn_1(param1: bool, param2: &str) {
7372 let var1 = "«ˇhello» world";
7373 }
7374 "#},
7375 cx,
7376 );
7377 });
7378
7379 // Test 2: Partial selection within a word
7380 editor.update_in(cx, |editor, window, cx| {
7381 editor.change_selections(None, window, cx, |s| {
7382 s.select_display_ranges([
7383 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7384 ]);
7385 });
7386 });
7387 editor.update_in(cx, |editor, window, cx| {
7388 assert_text_with_selections(
7389 editor,
7390 indoc! {r#"
7391 use mod1::mod2::{mod3, mod4};
7392
7393 fn fn_1(param1: bool, param2: &str) {
7394 let var1 = "h«elˇ»lo world";
7395 }
7396 "#},
7397 cx,
7398 );
7399 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7400 assert_text_with_selections(
7401 editor,
7402 indoc! {r#"
7403 use mod1::mod2::{mod3, mod4};
7404
7405 fn fn_1(param1: bool, param2: &str) {
7406 let var1 = "«ˇhello» world";
7407 }
7408 "#},
7409 cx,
7410 );
7411 });
7412
7413 // Test 3: Complete word already selected
7414 editor.update_in(cx, |editor, window, cx| {
7415 editor.change_selections(None, window, cx, |s| {
7416 s.select_display_ranges([
7417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7418 ]);
7419 });
7420 });
7421 editor.update_in(cx, |editor, window, cx| {
7422 assert_text_with_selections(
7423 editor,
7424 indoc! {r#"
7425 use mod1::mod2::{mod3, mod4};
7426
7427 fn fn_1(param1: bool, param2: &str) {
7428 let var1 = "«helloˇ» world";
7429 }
7430 "#},
7431 cx,
7432 );
7433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7434 assert_text_with_selections(
7435 editor,
7436 indoc! {r#"
7437 use mod1::mod2::{mod3, mod4};
7438
7439 fn fn_1(param1: bool, param2: &str) {
7440 let var1 = "«hello worldˇ»";
7441 }
7442 "#},
7443 cx,
7444 );
7445 });
7446
7447 // Test 4: Selection spanning across words
7448 editor.update_in(cx, |editor, window, cx| {
7449 editor.change_selections(None, window, cx, |s| {
7450 s.select_display_ranges([
7451 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7452 ]);
7453 });
7454 });
7455 editor.update_in(cx, |editor, window, cx| {
7456 assert_text_with_selections(
7457 editor,
7458 indoc! {r#"
7459 use mod1::mod2::{mod3, mod4};
7460
7461 fn fn_1(param1: bool, param2: &str) {
7462 let var1 = "hel«lo woˇ»rld";
7463 }
7464 "#},
7465 cx,
7466 );
7467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7468 assert_text_with_selections(
7469 editor,
7470 indoc! {r#"
7471 use mod1::mod2::{mod3, mod4};
7472
7473 fn fn_1(param1: bool, param2: &str) {
7474 let var1 = "«ˇhello world»";
7475 }
7476 "#},
7477 cx,
7478 );
7479 });
7480
7481 // Test 5: Expansion beyond string
7482 editor.update_in(cx, |editor, window, cx| {
7483 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 assert_text_with_selections(
7486 editor,
7487 indoc! {r#"
7488 use mod1::mod2::{mod3, mod4};
7489
7490 fn fn_1(param1: bool, param2: &str) {
7491 «ˇlet var1 = "hello world";»
7492 }
7493 "#},
7494 cx,
7495 );
7496 });
7497}
7498
7499#[gpui::test]
7500async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7501 init_test(cx, |_| {});
7502
7503 let base_text = r#"
7504 impl A {
7505 // this is an uncommitted comment
7506
7507 fn b() {
7508 c();
7509 }
7510
7511 // this is another uncommitted comment
7512
7513 fn d() {
7514 // e
7515 // f
7516 }
7517 }
7518
7519 fn g() {
7520 // h
7521 }
7522 "#
7523 .unindent();
7524
7525 let text = r#"
7526 ˇimpl A {
7527
7528 fn b() {
7529 c();
7530 }
7531
7532 fn d() {
7533 // e
7534 // f
7535 }
7536 }
7537
7538 fn g() {
7539 // h
7540 }
7541 "#
7542 .unindent();
7543
7544 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7545 cx.set_state(&text);
7546 cx.set_head_text(&base_text);
7547 cx.update_editor(|editor, window, cx| {
7548 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7549 });
7550
7551 cx.assert_state_with_diff(
7552 "
7553 ˇimpl A {
7554 - // this is an uncommitted comment
7555
7556 fn b() {
7557 c();
7558 }
7559
7560 - // this is another uncommitted comment
7561 -
7562 fn d() {
7563 // e
7564 // f
7565 }
7566 }
7567
7568 fn g() {
7569 // h
7570 }
7571 "
7572 .unindent(),
7573 );
7574
7575 let expected_display_text = "
7576 impl A {
7577 // this is an uncommitted comment
7578
7579 fn b() {
7580 ⋯
7581 }
7582
7583 // this is another uncommitted comment
7584
7585 fn d() {
7586 ⋯
7587 }
7588 }
7589
7590 fn g() {
7591 ⋯
7592 }
7593 "
7594 .unindent();
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7598 assert_eq!(editor.display_text(cx), expected_display_text);
7599 });
7600}
7601
7602#[gpui::test]
7603async fn test_autoindent(cx: &mut TestAppContext) {
7604 init_test(cx, |_| {});
7605
7606 let language = Arc::new(
7607 Language::new(
7608 LanguageConfig {
7609 brackets: BracketPairConfig {
7610 pairs: vec![
7611 BracketPair {
7612 start: "{".to_string(),
7613 end: "}".to_string(),
7614 close: false,
7615 surround: false,
7616 newline: true,
7617 },
7618 BracketPair {
7619 start: "(".to_string(),
7620 end: ")".to_string(),
7621 close: false,
7622 surround: false,
7623 newline: true,
7624 },
7625 ],
7626 ..Default::default()
7627 },
7628 ..Default::default()
7629 },
7630 Some(tree_sitter_rust::LANGUAGE.into()),
7631 )
7632 .with_indents_query(
7633 r#"
7634 (_ "(" ")" @end) @indent
7635 (_ "{" "}" @end) @indent
7636 "#,
7637 )
7638 .unwrap(),
7639 );
7640
7641 let text = "fn a() {}";
7642
7643 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7645 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7646 editor
7647 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7648 .await;
7649
7650 editor.update_in(cx, |editor, window, cx| {
7651 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7652 editor.newline(&Newline, window, cx);
7653 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7654 assert_eq!(
7655 editor.selections.ranges(cx),
7656 &[
7657 Point::new(1, 4)..Point::new(1, 4),
7658 Point::new(3, 4)..Point::new(3, 4),
7659 Point::new(5, 0)..Point::new(5, 0)
7660 ]
7661 );
7662 });
7663}
7664
7665#[gpui::test]
7666async fn test_autoindent_selections(cx: &mut TestAppContext) {
7667 init_test(cx, |_| {});
7668
7669 {
7670 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7671 cx.set_state(indoc! {"
7672 impl A {
7673
7674 fn b() {}
7675
7676 «fn c() {
7677
7678 }ˇ»
7679 }
7680 "});
7681
7682 cx.update_editor(|editor, window, cx| {
7683 editor.autoindent(&Default::default(), window, cx);
7684 });
7685
7686 cx.assert_editor_state(indoc! {"
7687 impl A {
7688
7689 fn b() {}
7690
7691 «fn c() {
7692
7693 }ˇ»
7694 }
7695 "});
7696 }
7697
7698 {
7699 let mut cx = EditorTestContext::new_multibuffer(
7700 cx,
7701 [indoc! { "
7702 impl A {
7703 «
7704 // a
7705 fn b(){}
7706 »
7707 «
7708 }
7709 fn c(){}
7710 »
7711 "}],
7712 );
7713
7714 let buffer = cx.update_editor(|editor, _, cx| {
7715 let buffer = editor.buffer().update(cx, |buffer, _| {
7716 buffer.all_buffers().iter().next().unwrap().clone()
7717 });
7718 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7719 buffer
7720 });
7721
7722 cx.run_until_parked();
7723 cx.update_editor(|editor, window, cx| {
7724 editor.select_all(&Default::default(), window, cx);
7725 editor.autoindent(&Default::default(), window, cx)
7726 });
7727 cx.run_until_parked();
7728
7729 cx.update(|_, cx| {
7730 assert_eq!(
7731 buffer.read(cx).text(),
7732 indoc! { "
7733 impl A {
7734
7735 // a
7736 fn b(){}
7737
7738
7739 }
7740 fn c(){}
7741
7742 " }
7743 )
7744 });
7745 }
7746}
7747
7748#[gpui::test]
7749async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let mut cx = EditorTestContext::new(cx).await;
7753
7754 let language = Arc::new(Language::new(
7755 LanguageConfig {
7756 brackets: BracketPairConfig {
7757 pairs: vec![
7758 BracketPair {
7759 start: "{".to_string(),
7760 end: "}".to_string(),
7761 close: true,
7762 surround: true,
7763 newline: true,
7764 },
7765 BracketPair {
7766 start: "(".to_string(),
7767 end: ")".to_string(),
7768 close: true,
7769 surround: true,
7770 newline: true,
7771 },
7772 BracketPair {
7773 start: "/*".to_string(),
7774 end: " */".to_string(),
7775 close: true,
7776 surround: true,
7777 newline: true,
7778 },
7779 BracketPair {
7780 start: "[".to_string(),
7781 end: "]".to_string(),
7782 close: false,
7783 surround: false,
7784 newline: true,
7785 },
7786 BracketPair {
7787 start: "\"".to_string(),
7788 end: "\"".to_string(),
7789 close: true,
7790 surround: true,
7791 newline: false,
7792 },
7793 BracketPair {
7794 start: "<".to_string(),
7795 end: ">".to_string(),
7796 close: false,
7797 surround: true,
7798 newline: true,
7799 },
7800 ],
7801 ..Default::default()
7802 },
7803 autoclose_before: "})]".to_string(),
7804 ..Default::default()
7805 },
7806 Some(tree_sitter_rust::LANGUAGE.into()),
7807 ));
7808
7809 cx.language_registry().add(language.clone());
7810 cx.update_buffer(|buffer, cx| {
7811 buffer.set_language(Some(language), cx);
7812 });
7813
7814 cx.set_state(
7815 &r#"
7816 🏀ˇ
7817 εˇ
7818 ❤️ˇ
7819 "#
7820 .unindent(),
7821 );
7822
7823 // autoclose multiple nested brackets at multiple cursors
7824 cx.update_editor(|editor, window, cx| {
7825 editor.handle_input("{", window, cx);
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 });
7829 cx.assert_editor_state(
7830 &"
7831 🏀{{{ˇ}}}
7832 ε{{{ˇ}}}
7833 ❤️{{{ˇ}}}
7834 "
7835 .unindent(),
7836 );
7837
7838 // insert a different closing bracket
7839 cx.update_editor(|editor, window, cx| {
7840 editor.handle_input(")", window, cx);
7841 });
7842 cx.assert_editor_state(
7843 &"
7844 🏀{{{)ˇ}}}
7845 ε{{{)ˇ}}}
7846 ❤️{{{)ˇ}}}
7847 "
7848 .unindent(),
7849 );
7850
7851 // skip over the auto-closed brackets when typing a closing bracket
7852 cx.update_editor(|editor, window, cx| {
7853 editor.move_right(&MoveRight, window, cx);
7854 editor.handle_input("}", window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 });
7858 cx.assert_editor_state(
7859 &"
7860 🏀{{{)}}}}ˇ
7861 ε{{{)}}}}ˇ
7862 ❤️{{{)}}}}ˇ
7863 "
7864 .unindent(),
7865 );
7866
7867 // autoclose multi-character pairs
7868 cx.set_state(
7869 &"
7870 ˇ
7871 ˇ
7872 "
7873 .unindent(),
7874 );
7875 cx.update_editor(|editor, window, cx| {
7876 editor.handle_input("/", window, cx);
7877 editor.handle_input("*", window, cx);
7878 });
7879 cx.assert_editor_state(
7880 &"
7881 /*ˇ */
7882 /*ˇ */
7883 "
7884 .unindent(),
7885 );
7886
7887 // one cursor autocloses a multi-character pair, one cursor
7888 // does not autoclose.
7889 cx.set_state(
7890 &"
7891 /ˇ
7892 ˇ
7893 "
7894 .unindent(),
7895 );
7896 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7897 cx.assert_editor_state(
7898 &"
7899 /*ˇ */
7900 *ˇ
7901 "
7902 .unindent(),
7903 );
7904
7905 // Don't autoclose if the next character isn't whitespace and isn't
7906 // listed in the language's "autoclose_before" section.
7907 cx.set_state("ˇa b");
7908 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7909 cx.assert_editor_state("{ˇa b");
7910
7911 // Don't autoclose if `close` is false for the bracket pair
7912 cx.set_state("ˇ");
7913 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7914 cx.assert_editor_state("[ˇ");
7915
7916 // Surround with brackets if text is selected
7917 cx.set_state("«aˇ» b");
7918 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7919 cx.assert_editor_state("{«aˇ»} b");
7920
7921 // Autoclose when not immediately after a word character
7922 cx.set_state("a ˇ");
7923 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7924 cx.assert_editor_state("a \"ˇ\"");
7925
7926 // Autoclose pair where the start and end characters are the same
7927 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7928 cx.assert_editor_state("a \"\"ˇ");
7929
7930 // Don't autoclose when immediately after a word character
7931 cx.set_state("aˇ");
7932 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7933 cx.assert_editor_state("a\"ˇ");
7934
7935 // Do autoclose when after a non-word character
7936 cx.set_state("{ˇ");
7937 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7938 cx.assert_editor_state("{\"ˇ\"");
7939
7940 // Non identical pairs autoclose regardless of preceding character
7941 cx.set_state("aˇ");
7942 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7943 cx.assert_editor_state("a{ˇ}");
7944
7945 // Don't autoclose pair if autoclose is disabled
7946 cx.set_state("ˇ");
7947 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7948 cx.assert_editor_state("<ˇ");
7949
7950 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7951 cx.set_state("«aˇ» b");
7952 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7953 cx.assert_editor_state("<«aˇ»> b");
7954}
7955
7956#[gpui::test]
7957async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7958 init_test(cx, |settings| {
7959 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7960 });
7961
7962 let mut cx = EditorTestContext::new(cx).await;
7963
7964 let language = Arc::new(Language::new(
7965 LanguageConfig {
7966 brackets: BracketPairConfig {
7967 pairs: vec![
7968 BracketPair {
7969 start: "{".to_string(),
7970 end: "}".to_string(),
7971 close: true,
7972 surround: true,
7973 newline: true,
7974 },
7975 BracketPair {
7976 start: "(".to_string(),
7977 end: ")".to_string(),
7978 close: true,
7979 surround: true,
7980 newline: true,
7981 },
7982 BracketPair {
7983 start: "[".to_string(),
7984 end: "]".to_string(),
7985 close: false,
7986 surround: false,
7987 newline: true,
7988 },
7989 ],
7990 ..Default::default()
7991 },
7992 autoclose_before: "})]".to_string(),
7993 ..Default::default()
7994 },
7995 Some(tree_sitter_rust::LANGUAGE.into()),
7996 ));
7997
7998 cx.language_registry().add(language.clone());
7999 cx.update_buffer(|buffer, cx| {
8000 buffer.set_language(Some(language), cx);
8001 });
8002
8003 cx.set_state(
8004 &"
8005 ˇ
8006 ˇ
8007 ˇ
8008 "
8009 .unindent(),
8010 );
8011
8012 // ensure only matching closing brackets are skipped over
8013 cx.update_editor(|editor, window, cx| {
8014 editor.handle_input("}", window, cx);
8015 editor.move_left(&MoveLeft, window, cx);
8016 editor.handle_input(")", window, cx);
8017 editor.move_left(&MoveLeft, window, cx);
8018 });
8019 cx.assert_editor_state(
8020 &"
8021 ˇ)}
8022 ˇ)}
8023 ˇ)}
8024 "
8025 .unindent(),
8026 );
8027
8028 // skip-over closing brackets at multiple cursors
8029 cx.update_editor(|editor, window, cx| {
8030 editor.handle_input(")", window, cx);
8031 editor.handle_input("}", window, cx);
8032 });
8033 cx.assert_editor_state(
8034 &"
8035 )}ˇ
8036 )}ˇ
8037 )}ˇ
8038 "
8039 .unindent(),
8040 );
8041
8042 // ignore non-close brackets
8043 cx.update_editor(|editor, window, cx| {
8044 editor.handle_input("]", window, cx);
8045 editor.move_left(&MoveLeft, window, cx);
8046 editor.handle_input("]", window, cx);
8047 });
8048 cx.assert_editor_state(
8049 &"
8050 )}]ˇ]
8051 )}]ˇ]
8052 )}]ˇ]
8053 "
8054 .unindent(),
8055 );
8056}
8057
8058#[gpui::test]
8059async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let mut cx = EditorTestContext::new(cx).await;
8063
8064 let html_language = Arc::new(
8065 Language::new(
8066 LanguageConfig {
8067 name: "HTML".into(),
8068 brackets: BracketPairConfig {
8069 pairs: vec![
8070 BracketPair {
8071 start: "<".into(),
8072 end: ">".into(),
8073 close: true,
8074 ..Default::default()
8075 },
8076 BracketPair {
8077 start: "{".into(),
8078 end: "}".into(),
8079 close: true,
8080 ..Default::default()
8081 },
8082 BracketPair {
8083 start: "(".into(),
8084 end: ")".into(),
8085 close: true,
8086 ..Default::default()
8087 },
8088 ],
8089 ..Default::default()
8090 },
8091 autoclose_before: "})]>".into(),
8092 ..Default::default()
8093 },
8094 Some(tree_sitter_html::LANGUAGE.into()),
8095 )
8096 .with_injection_query(
8097 r#"
8098 (script_element
8099 (raw_text) @injection.content
8100 (#set! injection.language "javascript"))
8101 "#,
8102 )
8103 .unwrap(),
8104 );
8105
8106 let javascript_language = Arc::new(Language::new(
8107 LanguageConfig {
8108 name: "JavaScript".into(),
8109 brackets: BracketPairConfig {
8110 pairs: vec![
8111 BracketPair {
8112 start: "/*".into(),
8113 end: " */".into(),
8114 close: true,
8115 ..Default::default()
8116 },
8117 BracketPair {
8118 start: "{".into(),
8119 end: "}".into(),
8120 close: true,
8121 ..Default::default()
8122 },
8123 BracketPair {
8124 start: "(".into(),
8125 end: ")".into(),
8126 close: true,
8127 ..Default::default()
8128 },
8129 ],
8130 ..Default::default()
8131 },
8132 autoclose_before: "})]>".into(),
8133 ..Default::default()
8134 },
8135 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8136 ));
8137
8138 cx.language_registry().add(html_language.clone());
8139 cx.language_registry().add(javascript_language.clone());
8140
8141 cx.update_buffer(|buffer, cx| {
8142 buffer.set_language(Some(html_language), cx);
8143 });
8144
8145 cx.set_state(
8146 &r#"
8147 <body>ˇ
8148 <script>
8149 var x = 1;ˇ
8150 </script>
8151 </body>ˇ
8152 "#
8153 .unindent(),
8154 );
8155
8156 // Precondition: different languages are active at different locations.
8157 cx.update_editor(|editor, window, cx| {
8158 let snapshot = editor.snapshot(window, cx);
8159 let cursors = editor.selections.ranges::<usize>(cx);
8160 let languages = cursors
8161 .iter()
8162 .map(|c| snapshot.language_at(c.start).unwrap().name())
8163 .collect::<Vec<_>>();
8164 assert_eq!(
8165 languages,
8166 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8167 );
8168 });
8169
8170 // Angle brackets autoclose in HTML, but not JavaScript.
8171 cx.update_editor(|editor, window, cx| {
8172 editor.handle_input("<", window, cx);
8173 editor.handle_input("a", window, cx);
8174 });
8175 cx.assert_editor_state(
8176 &r#"
8177 <body><aˇ>
8178 <script>
8179 var x = 1;<aˇ
8180 </script>
8181 </body><aˇ>
8182 "#
8183 .unindent(),
8184 );
8185
8186 // Curly braces and parens autoclose in both HTML and JavaScript.
8187 cx.update_editor(|editor, window, cx| {
8188 editor.handle_input(" b=", window, cx);
8189 editor.handle_input("{", window, cx);
8190 editor.handle_input("c", window, cx);
8191 editor.handle_input("(", window, cx);
8192 });
8193 cx.assert_editor_state(
8194 &r#"
8195 <body><a b={c(ˇ)}>
8196 <script>
8197 var x = 1;<a b={c(ˇ)}
8198 </script>
8199 </body><a b={c(ˇ)}>
8200 "#
8201 .unindent(),
8202 );
8203
8204 // Brackets that were already autoclosed are skipped.
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input(")", window, cx);
8207 editor.handle_input("d", window, cx);
8208 editor.handle_input("}", window, cx);
8209 });
8210 cx.assert_editor_state(
8211 &r#"
8212 <body><a b={c()d}ˇ>
8213 <script>
8214 var x = 1;<a b={c()d}ˇ
8215 </script>
8216 </body><a b={c()d}ˇ>
8217 "#
8218 .unindent(),
8219 );
8220 cx.update_editor(|editor, window, cx| {
8221 editor.handle_input(">", window, cx);
8222 });
8223 cx.assert_editor_state(
8224 &r#"
8225 <body><a b={c()d}>ˇ
8226 <script>
8227 var x = 1;<a b={c()d}>ˇ
8228 </script>
8229 </body><a b={c()d}>ˇ
8230 "#
8231 .unindent(),
8232 );
8233
8234 // Reset
8235 cx.set_state(
8236 &r#"
8237 <body>ˇ
8238 <script>
8239 var x = 1;ˇ
8240 </script>
8241 </body>ˇ
8242 "#
8243 .unindent(),
8244 );
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.handle_input("<", window, cx);
8248 });
8249 cx.assert_editor_state(
8250 &r#"
8251 <body><ˇ>
8252 <script>
8253 var x = 1;<ˇ
8254 </script>
8255 </body><ˇ>
8256 "#
8257 .unindent(),
8258 );
8259
8260 // When backspacing, the closing angle brackets are removed.
8261 cx.update_editor(|editor, window, cx| {
8262 editor.backspace(&Backspace, window, cx);
8263 });
8264 cx.assert_editor_state(
8265 &r#"
8266 <body>ˇ
8267 <script>
8268 var x = 1;ˇ
8269 </script>
8270 </body>ˇ
8271 "#
8272 .unindent(),
8273 );
8274
8275 // Block comments autoclose in JavaScript, but not HTML.
8276 cx.update_editor(|editor, window, cx| {
8277 editor.handle_input("/", window, cx);
8278 editor.handle_input("*", window, cx);
8279 });
8280 cx.assert_editor_state(
8281 &r#"
8282 <body>/*ˇ
8283 <script>
8284 var x = 1;/*ˇ */
8285 </script>
8286 </body>/*ˇ
8287 "#
8288 .unindent(),
8289 );
8290}
8291
8292#[gpui::test]
8293async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8294 init_test(cx, |_| {});
8295
8296 let mut cx = EditorTestContext::new(cx).await;
8297
8298 let rust_language = Arc::new(
8299 Language::new(
8300 LanguageConfig {
8301 name: "Rust".into(),
8302 brackets: serde_json::from_value(json!([
8303 { "start": "{", "end": "}", "close": true, "newline": true },
8304 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8305 ]))
8306 .unwrap(),
8307 autoclose_before: "})]>".into(),
8308 ..Default::default()
8309 },
8310 Some(tree_sitter_rust::LANGUAGE.into()),
8311 )
8312 .with_override_query("(string_literal) @string")
8313 .unwrap(),
8314 );
8315
8316 cx.language_registry().add(rust_language.clone());
8317 cx.update_buffer(|buffer, cx| {
8318 buffer.set_language(Some(rust_language), cx);
8319 });
8320
8321 cx.set_state(
8322 &r#"
8323 let x = ˇ
8324 "#
8325 .unindent(),
8326 );
8327
8328 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8329 cx.update_editor(|editor, window, cx| {
8330 editor.handle_input("\"", window, cx);
8331 });
8332 cx.assert_editor_state(
8333 &r#"
8334 let x = "ˇ"
8335 "#
8336 .unindent(),
8337 );
8338
8339 // Inserting another quotation mark. The cursor moves across the existing
8340 // automatically-inserted quotation mark.
8341 cx.update_editor(|editor, window, cx| {
8342 editor.handle_input("\"", window, cx);
8343 });
8344 cx.assert_editor_state(
8345 &r#"
8346 let x = ""ˇ
8347 "#
8348 .unindent(),
8349 );
8350
8351 // Reset
8352 cx.set_state(
8353 &r#"
8354 let x = ˇ
8355 "#
8356 .unindent(),
8357 );
8358
8359 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8360 cx.update_editor(|editor, window, cx| {
8361 editor.handle_input("\"", window, cx);
8362 editor.handle_input(" ", window, cx);
8363 editor.move_left(&Default::default(), window, cx);
8364 editor.handle_input("\\", window, cx);
8365 editor.handle_input("\"", window, cx);
8366 });
8367 cx.assert_editor_state(
8368 &r#"
8369 let x = "\"ˇ "
8370 "#
8371 .unindent(),
8372 );
8373
8374 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8375 // mark. Nothing is inserted.
8376 cx.update_editor(|editor, window, cx| {
8377 editor.move_right(&Default::default(), window, cx);
8378 editor.handle_input("\"", window, cx);
8379 });
8380 cx.assert_editor_state(
8381 &r#"
8382 let x = "\" "ˇ
8383 "#
8384 .unindent(),
8385 );
8386}
8387
8388#[gpui::test]
8389async fn test_surround_with_pair(cx: &mut TestAppContext) {
8390 init_test(cx, |_| {});
8391
8392 let language = Arc::new(Language::new(
8393 LanguageConfig {
8394 brackets: BracketPairConfig {
8395 pairs: vec![
8396 BracketPair {
8397 start: "{".to_string(),
8398 end: "}".to_string(),
8399 close: true,
8400 surround: true,
8401 newline: true,
8402 },
8403 BracketPair {
8404 start: "/* ".to_string(),
8405 end: "*/".to_string(),
8406 close: true,
8407 surround: true,
8408 ..Default::default()
8409 },
8410 ],
8411 ..Default::default()
8412 },
8413 ..Default::default()
8414 },
8415 Some(tree_sitter_rust::LANGUAGE.into()),
8416 ));
8417
8418 let text = r#"
8419 a
8420 b
8421 c
8422 "#
8423 .unindent();
8424
8425 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8426 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8428 editor
8429 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8430 .await;
8431
8432 editor.update_in(cx, |editor, window, cx| {
8433 editor.change_selections(None, window, cx, |s| {
8434 s.select_display_ranges([
8435 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8437 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8438 ])
8439 });
8440
8441 editor.handle_input("{", window, cx);
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 assert_eq!(
8445 editor.text(cx),
8446 "
8447 {{{a}}}
8448 {{{b}}}
8449 {{{c}}}
8450 "
8451 .unindent()
8452 );
8453 assert_eq!(
8454 editor.selections.display_ranges(cx),
8455 [
8456 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8457 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8458 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8459 ]
8460 );
8461
8462 editor.undo(&Undo, window, cx);
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 assert_eq!(
8466 editor.text(cx),
8467 "
8468 a
8469 b
8470 c
8471 "
8472 .unindent()
8473 );
8474 assert_eq!(
8475 editor.selections.display_ranges(cx),
8476 [
8477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8478 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8479 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8480 ]
8481 );
8482
8483 // Ensure inserting the first character of a multi-byte bracket pair
8484 // doesn't surround the selections with the bracket.
8485 editor.handle_input("/", window, cx);
8486 assert_eq!(
8487 editor.text(cx),
8488 "
8489 /
8490 /
8491 /
8492 "
8493 .unindent()
8494 );
8495 assert_eq!(
8496 editor.selections.display_ranges(cx),
8497 [
8498 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8499 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8501 ]
8502 );
8503
8504 editor.undo(&Undo, window, cx);
8505 assert_eq!(
8506 editor.text(cx),
8507 "
8508 a
8509 b
8510 c
8511 "
8512 .unindent()
8513 );
8514 assert_eq!(
8515 editor.selections.display_ranges(cx),
8516 [
8517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8518 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8519 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8520 ]
8521 );
8522
8523 // Ensure inserting the last character of a multi-byte bracket pair
8524 // doesn't surround the selections with the bracket.
8525 editor.handle_input("*", window, cx);
8526 assert_eq!(
8527 editor.text(cx),
8528 "
8529 *
8530 *
8531 *
8532 "
8533 .unindent()
8534 );
8535 assert_eq!(
8536 editor.selections.display_ranges(cx),
8537 [
8538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8539 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8540 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8541 ]
8542 );
8543 });
8544}
8545
8546#[gpui::test]
8547async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8548 init_test(cx, |_| {});
8549
8550 let language = Arc::new(Language::new(
8551 LanguageConfig {
8552 brackets: BracketPairConfig {
8553 pairs: vec![BracketPair {
8554 start: "{".to_string(),
8555 end: "}".to_string(),
8556 close: true,
8557 surround: true,
8558 newline: true,
8559 }],
8560 ..Default::default()
8561 },
8562 autoclose_before: "}".to_string(),
8563 ..Default::default()
8564 },
8565 Some(tree_sitter_rust::LANGUAGE.into()),
8566 ));
8567
8568 let text = r#"
8569 a
8570 b
8571 c
8572 "#
8573 .unindent();
8574
8575 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8578 editor
8579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8580 .await;
8581
8582 editor.update_in(cx, |editor, window, cx| {
8583 editor.change_selections(None, window, cx, |s| {
8584 s.select_ranges([
8585 Point::new(0, 1)..Point::new(0, 1),
8586 Point::new(1, 1)..Point::new(1, 1),
8587 Point::new(2, 1)..Point::new(2, 1),
8588 ])
8589 });
8590
8591 editor.handle_input("{", window, cx);
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("_", window, cx);
8594 assert_eq!(
8595 editor.text(cx),
8596 "
8597 a{{_}}
8598 b{{_}}
8599 c{{_}}
8600 "
8601 .unindent()
8602 );
8603 assert_eq!(
8604 editor.selections.ranges::<Point>(cx),
8605 [
8606 Point::new(0, 4)..Point::new(0, 4),
8607 Point::new(1, 4)..Point::new(1, 4),
8608 Point::new(2, 4)..Point::new(2, 4)
8609 ]
8610 );
8611
8612 editor.backspace(&Default::default(), window, cx);
8613 editor.backspace(&Default::default(), window, cx);
8614 assert_eq!(
8615 editor.text(cx),
8616 "
8617 a{}
8618 b{}
8619 c{}
8620 "
8621 .unindent()
8622 );
8623 assert_eq!(
8624 editor.selections.ranges::<Point>(cx),
8625 [
8626 Point::new(0, 2)..Point::new(0, 2),
8627 Point::new(1, 2)..Point::new(1, 2),
8628 Point::new(2, 2)..Point::new(2, 2)
8629 ]
8630 );
8631
8632 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8633 assert_eq!(
8634 editor.text(cx),
8635 "
8636 a
8637 b
8638 c
8639 "
8640 .unindent()
8641 );
8642 assert_eq!(
8643 editor.selections.ranges::<Point>(cx),
8644 [
8645 Point::new(0, 1)..Point::new(0, 1),
8646 Point::new(1, 1)..Point::new(1, 1),
8647 Point::new(2, 1)..Point::new(2, 1)
8648 ]
8649 );
8650 });
8651}
8652
8653#[gpui::test]
8654async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8655 init_test(cx, |settings| {
8656 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8657 });
8658
8659 let mut cx = EditorTestContext::new(cx).await;
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig {
8663 brackets: BracketPairConfig {
8664 pairs: vec![
8665 BracketPair {
8666 start: "{".to_string(),
8667 end: "}".to_string(),
8668 close: true,
8669 surround: true,
8670 newline: true,
8671 },
8672 BracketPair {
8673 start: "(".to_string(),
8674 end: ")".to_string(),
8675 close: true,
8676 surround: true,
8677 newline: true,
8678 },
8679 BracketPair {
8680 start: "[".to_string(),
8681 end: "]".to_string(),
8682 close: false,
8683 surround: true,
8684 newline: true,
8685 },
8686 ],
8687 ..Default::default()
8688 },
8689 autoclose_before: "})]".to_string(),
8690 ..Default::default()
8691 },
8692 Some(tree_sitter_rust::LANGUAGE.into()),
8693 ));
8694
8695 cx.language_registry().add(language.clone());
8696 cx.update_buffer(|buffer, cx| {
8697 buffer.set_language(Some(language), cx);
8698 });
8699
8700 cx.set_state(
8701 &"
8702 {(ˇ)}
8703 [[ˇ]]
8704 {(ˇ)}
8705 "
8706 .unindent(),
8707 );
8708
8709 cx.update_editor(|editor, window, cx| {
8710 editor.backspace(&Default::default(), window, cx);
8711 editor.backspace(&Default::default(), window, cx);
8712 });
8713
8714 cx.assert_editor_state(
8715 &"
8716 ˇ
8717 ˇ]]
8718 ˇ
8719 "
8720 .unindent(),
8721 );
8722
8723 cx.update_editor(|editor, window, cx| {
8724 editor.handle_input("{", window, cx);
8725 editor.handle_input("{", window, cx);
8726 editor.move_right(&MoveRight, window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_left(&MoveLeft, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.backspace(&Default::default(), window, cx);
8731 });
8732
8733 cx.assert_editor_state(
8734 &"
8735 {ˇ}
8736 {ˇ}]]
8737 {ˇ}
8738 "
8739 .unindent(),
8740 );
8741
8742 cx.update_editor(|editor, window, cx| {
8743 editor.backspace(&Default::default(), window, cx);
8744 });
8745
8746 cx.assert_editor_state(
8747 &"
8748 ˇ
8749 ˇ]]
8750 ˇ
8751 "
8752 .unindent(),
8753 );
8754}
8755
8756#[gpui::test]
8757async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8758 init_test(cx, |_| {});
8759
8760 let language = Arc::new(Language::new(
8761 LanguageConfig::default(),
8762 Some(tree_sitter_rust::LANGUAGE.into()),
8763 ));
8764
8765 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8766 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8767 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8768 editor
8769 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8770 .await;
8771
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.set_auto_replace_emoji_shortcode(true);
8774
8775 editor.handle_input("Hello ", window, cx);
8776 editor.handle_input(":wave", window, cx);
8777 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8778
8779 editor.handle_input(":", window, cx);
8780 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8781
8782 editor.handle_input(" :smile", window, cx);
8783 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8784
8785 editor.handle_input(":", window, cx);
8786 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8787
8788 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8789 editor.handle_input(":wave", window, cx);
8790 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8791
8792 editor.handle_input(":", window, cx);
8793 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8794
8795 editor.handle_input(":1", window, cx);
8796 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8797
8798 editor.handle_input(":", window, cx);
8799 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8800
8801 // Ensure shortcode does not get replaced when it is part of a word
8802 editor.handle_input(" Test:wave", window, cx);
8803 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8804
8805 editor.handle_input(":", window, cx);
8806 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8807
8808 editor.set_auto_replace_emoji_shortcode(false);
8809
8810 // Ensure shortcode does not get replaced when auto replace is off
8811 editor.handle_input(" :wave", window, cx);
8812 assert_eq!(
8813 editor.text(cx),
8814 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8815 );
8816
8817 editor.handle_input(":", window, cx);
8818 assert_eq!(
8819 editor.text(cx),
8820 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8821 );
8822 });
8823}
8824
8825#[gpui::test]
8826async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8827 init_test(cx, |_| {});
8828
8829 let (text, insertion_ranges) = marked_text_ranges(
8830 indoc! {"
8831 ˇ
8832 "},
8833 false,
8834 );
8835
8836 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8837 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8838
8839 _ = editor.update_in(cx, |editor, window, cx| {
8840 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8841
8842 editor
8843 .insert_snippet(&insertion_ranges, snippet, window, cx)
8844 .unwrap();
8845
8846 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8847 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8848 assert_eq!(editor.text(cx), expected_text);
8849 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8850 }
8851
8852 assert(
8853 editor,
8854 cx,
8855 indoc! {"
8856 type «» =•
8857 "},
8858 );
8859
8860 assert!(editor.context_menu_visible(), "There should be a matches");
8861 });
8862}
8863
8864#[gpui::test]
8865async fn test_snippets(cx: &mut TestAppContext) {
8866 init_test(cx, |_| {});
8867
8868 let mut cx = EditorTestContext::new(cx).await;
8869
8870 cx.set_state(indoc! {"
8871 a.ˇ b
8872 a.ˇ b
8873 a.ˇ b
8874 "});
8875
8876 cx.update_editor(|editor, window, cx| {
8877 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8878 let insertion_ranges = editor
8879 .selections
8880 .all(cx)
8881 .iter()
8882 .map(|s| s.range().clone())
8883 .collect::<Vec<_>>();
8884 editor
8885 .insert_snippet(&insertion_ranges, snippet, window, cx)
8886 .unwrap();
8887 });
8888
8889 cx.assert_editor_state(indoc! {"
8890 a.f(«oneˇ», two, «threeˇ») b
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 "});
8894
8895 // Can't move earlier than the first tab stop
8896 cx.update_editor(|editor, window, cx| {
8897 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8898 });
8899 cx.assert_editor_state(indoc! {"
8900 a.f(«oneˇ», two, «threeˇ») b
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 "});
8904
8905 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8906 cx.assert_editor_state(indoc! {"
8907 a.f(one, «twoˇ», three) b
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 "});
8911
8912 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8913 cx.assert_editor_state(indoc! {"
8914 a.f(«oneˇ», two, «threeˇ») b
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 "});
8918
8919 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8920 cx.assert_editor_state(indoc! {"
8921 a.f(one, «twoˇ», three) b
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 "});
8925 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8926 cx.assert_editor_state(indoc! {"
8927 a.f(one, two, three)ˇ b
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 "});
8931
8932 // As soon as the last tab stop is reached, snippet state is gone
8933 cx.update_editor(|editor, window, cx| {
8934 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8935 });
8936 cx.assert_editor_state(indoc! {"
8937 a.f(one, two, three)ˇ b
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 "});
8941}
8942
8943#[gpui::test]
8944async fn test_snippet_indentation(cx: &mut TestAppContext) {
8945 init_test(cx, |_| {});
8946
8947 let mut cx = EditorTestContext::new(cx).await;
8948
8949 cx.update_editor(|editor, window, cx| {
8950 let snippet = Snippet::parse(indoc! {"
8951 /*
8952 * Multiline comment with leading indentation
8953 *
8954 * $1
8955 */
8956 $0"})
8957 .unwrap();
8958 let insertion_ranges = editor
8959 .selections
8960 .all(cx)
8961 .iter()
8962 .map(|s| s.range().clone())
8963 .collect::<Vec<_>>();
8964 editor
8965 .insert_snippet(&insertion_ranges, snippet, window, cx)
8966 .unwrap();
8967 });
8968
8969 cx.assert_editor_state(indoc! {"
8970 /*
8971 * Multiline comment with leading indentation
8972 *
8973 * ˇ
8974 */
8975 "});
8976
8977 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8978 cx.assert_editor_state(indoc! {"
8979 /*
8980 * Multiline comment with leading indentation
8981 *
8982 *•
8983 */
8984 ˇ"});
8985}
8986
8987#[gpui::test]
8988async fn test_document_format_during_save(cx: &mut TestAppContext) {
8989 init_test(cx, |_| {});
8990
8991 let fs = FakeFs::new(cx.executor());
8992 fs.insert_file(path!("/file.rs"), Default::default()).await;
8993
8994 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8995
8996 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8997 language_registry.add(rust_lang());
8998 let mut fake_servers = language_registry.register_fake_lsp(
8999 "Rust",
9000 FakeLspAdapter {
9001 capabilities: lsp::ServerCapabilities {
9002 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9003 ..Default::default()
9004 },
9005 ..Default::default()
9006 },
9007 );
9008
9009 let buffer = project
9010 .update(cx, |project, cx| {
9011 project.open_local_buffer(path!("/file.rs"), cx)
9012 })
9013 .await
9014 .unwrap();
9015
9016 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9017 let (editor, cx) = cx.add_window_view(|window, cx| {
9018 build_editor_with_project(project.clone(), buffer, window, cx)
9019 });
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.set_text("one\ntwo\nthree\n", window, cx)
9022 });
9023 assert!(cx.read(|cx| editor.is_dirty(cx)));
9024
9025 cx.executor().start_waiting();
9026 let fake_server = fake_servers.next().await.unwrap();
9027
9028 {
9029 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9030 move |params, _| async move {
9031 assert_eq!(
9032 params.text_document.uri,
9033 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9034 );
9035 assert_eq!(params.options.tab_size, 4);
9036 Ok(Some(vec![lsp::TextEdit::new(
9037 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9038 ", ".to_string(),
9039 )]))
9040 },
9041 );
9042 let save = editor
9043 .update_in(cx, |editor, window, cx| {
9044 editor.save(true, project.clone(), window, cx)
9045 })
9046 .unwrap();
9047 cx.executor().start_waiting();
9048 save.await;
9049
9050 assert_eq!(
9051 editor.update(cx, |editor, cx| editor.text(cx)),
9052 "one, two\nthree\n"
9053 );
9054 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9055 }
9056
9057 {
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.set_text("one\ntwo\nthree\n", window, cx)
9060 });
9061 assert!(cx.read(|cx| editor.is_dirty(cx)));
9062
9063 // Ensure we can still save even if formatting hangs.
9064 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9065 move |params, _| async move {
9066 assert_eq!(
9067 params.text_document.uri,
9068 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9069 );
9070 futures::future::pending::<()>().await;
9071 unreachable!()
9072 },
9073 );
9074 let save = editor
9075 .update_in(cx, |editor, window, cx| {
9076 editor.save(true, project.clone(), window, cx)
9077 })
9078 .unwrap();
9079 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9080 cx.executor().start_waiting();
9081 save.await;
9082 assert_eq!(
9083 editor.update(cx, |editor, cx| editor.text(cx)),
9084 "one\ntwo\nthree\n"
9085 );
9086 }
9087
9088 // For non-dirty buffer, no formatting request should be sent
9089 {
9090 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9091
9092 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9093 panic!("Should not be invoked on non-dirty buffer");
9094 });
9095 let save = editor
9096 .update_in(cx, |editor, window, cx| {
9097 editor.save(true, project.clone(), window, cx)
9098 })
9099 .unwrap();
9100 cx.executor().start_waiting();
9101 save.await;
9102 }
9103
9104 // Set rust language override and assert overridden tabsize is sent to language server
9105 update_test_language_settings(cx, |settings| {
9106 settings.languages.insert(
9107 "Rust".into(),
9108 LanguageSettingsContent {
9109 tab_size: NonZeroU32::new(8),
9110 ..Default::default()
9111 },
9112 );
9113 });
9114
9115 {
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.set_text("somehting_new\n", window, cx)
9118 });
9119 assert!(cx.read(|cx| editor.is_dirty(cx)));
9120 let _formatting_request_signal = fake_server
9121 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9122 assert_eq!(
9123 params.text_document.uri,
9124 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9125 );
9126 assert_eq!(params.options.tab_size, 8);
9127 Ok(Some(vec![]))
9128 });
9129 let save = editor
9130 .update_in(cx, |editor, window, cx| {
9131 editor.save(true, project.clone(), window, cx)
9132 })
9133 .unwrap();
9134 cx.executor().start_waiting();
9135 save.await;
9136 }
9137}
9138
9139#[gpui::test]
9140async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9141 init_test(cx, |_| {});
9142
9143 let cols = 4;
9144 let rows = 10;
9145 let sample_text_1 = sample_text(rows, cols, 'a');
9146 assert_eq!(
9147 sample_text_1,
9148 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9149 );
9150 let sample_text_2 = sample_text(rows, cols, 'l');
9151 assert_eq!(
9152 sample_text_2,
9153 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9154 );
9155 let sample_text_3 = sample_text(rows, cols, 'v');
9156 assert_eq!(
9157 sample_text_3,
9158 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9159 );
9160
9161 let fs = FakeFs::new(cx.executor());
9162 fs.insert_tree(
9163 path!("/a"),
9164 json!({
9165 "main.rs": sample_text_1,
9166 "other.rs": sample_text_2,
9167 "lib.rs": sample_text_3,
9168 }),
9169 )
9170 .await;
9171
9172 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9174 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9175
9176 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9177 language_registry.add(rust_lang());
9178 let mut fake_servers = language_registry.register_fake_lsp(
9179 "Rust",
9180 FakeLspAdapter {
9181 capabilities: lsp::ServerCapabilities {
9182 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9183 ..Default::default()
9184 },
9185 ..Default::default()
9186 },
9187 );
9188
9189 let worktree = project.update(cx, |project, cx| {
9190 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9191 assert_eq!(worktrees.len(), 1);
9192 worktrees.pop().unwrap()
9193 });
9194 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9195
9196 let buffer_1 = project
9197 .update(cx, |project, cx| {
9198 project.open_buffer((worktree_id, "main.rs"), cx)
9199 })
9200 .await
9201 .unwrap();
9202 let buffer_2 = project
9203 .update(cx, |project, cx| {
9204 project.open_buffer((worktree_id, "other.rs"), cx)
9205 })
9206 .await
9207 .unwrap();
9208 let buffer_3 = project
9209 .update(cx, |project, cx| {
9210 project.open_buffer((worktree_id, "lib.rs"), cx)
9211 })
9212 .await
9213 .unwrap();
9214
9215 let multi_buffer = cx.new(|cx| {
9216 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9217 multi_buffer.push_excerpts(
9218 buffer_1.clone(),
9219 [
9220 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9221 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9222 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9223 ],
9224 cx,
9225 );
9226 multi_buffer.push_excerpts(
9227 buffer_2.clone(),
9228 [
9229 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9230 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9231 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9232 ],
9233 cx,
9234 );
9235 multi_buffer.push_excerpts(
9236 buffer_3.clone(),
9237 [
9238 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9239 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9240 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9241 ],
9242 cx,
9243 );
9244 multi_buffer
9245 });
9246 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9247 Editor::new(
9248 EditorMode::full(),
9249 multi_buffer,
9250 Some(project.clone()),
9251 window,
9252 cx,
9253 )
9254 });
9255
9256 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9257 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9258 s.select_ranges(Some(1..2))
9259 });
9260 editor.insert("|one|two|three|", window, cx);
9261 });
9262 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9263 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9264 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9265 s.select_ranges(Some(60..70))
9266 });
9267 editor.insert("|four|five|six|", window, cx);
9268 });
9269 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9270
9271 // First two buffers should be edited, but not the third one.
9272 assert_eq!(
9273 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9274 "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}",
9275 );
9276 buffer_1.update(cx, |buffer, _| {
9277 assert!(buffer.is_dirty());
9278 assert_eq!(
9279 buffer.text(),
9280 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9281 )
9282 });
9283 buffer_2.update(cx, |buffer, _| {
9284 assert!(buffer.is_dirty());
9285 assert_eq!(
9286 buffer.text(),
9287 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9288 )
9289 });
9290 buffer_3.update(cx, |buffer, _| {
9291 assert!(!buffer.is_dirty());
9292 assert_eq!(buffer.text(), sample_text_3,)
9293 });
9294 cx.executor().run_until_parked();
9295
9296 cx.executor().start_waiting();
9297 let save = multi_buffer_editor
9298 .update_in(cx, |editor, window, cx| {
9299 editor.save(true, project.clone(), window, cx)
9300 })
9301 .unwrap();
9302
9303 let fake_server = fake_servers.next().await.unwrap();
9304 fake_server
9305 .server
9306 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9307 Ok(Some(vec![lsp::TextEdit::new(
9308 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9309 format!("[{} formatted]", params.text_document.uri),
9310 )]))
9311 })
9312 .detach();
9313 save.await;
9314
9315 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9316 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9317 assert_eq!(
9318 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9319 uri!(
9320 "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}"
9321 ),
9322 );
9323 buffer_1.update(cx, |buffer, _| {
9324 assert!(!buffer.is_dirty());
9325 assert_eq!(
9326 buffer.text(),
9327 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9328 )
9329 });
9330 buffer_2.update(cx, |buffer, _| {
9331 assert!(!buffer.is_dirty());
9332 assert_eq!(
9333 buffer.text(),
9334 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9335 )
9336 });
9337 buffer_3.update(cx, |buffer, _| {
9338 assert!(!buffer.is_dirty());
9339 assert_eq!(buffer.text(), sample_text_3,)
9340 });
9341}
9342
9343#[gpui::test]
9344async fn test_range_format_during_save(cx: &mut TestAppContext) {
9345 init_test(cx, |_| {});
9346
9347 let fs = FakeFs::new(cx.executor());
9348 fs.insert_file(path!("/file.rs"), Default::default()).await;
9349
9350 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9351
9352 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9353 language_registry.add(rust_lang());
9354 let mut fake_servers = language_registry.register_fake_lsp(
9355 "Rust",
9356 FakeLspAdapter {
9357 capabilities: lsp::ServerCapabilities {
9358 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9359 ..Default::default()
9360 },
9361 ..Default::default()
9362 },
9363 );
9364
9365 let buffer = project
9366 .update(cx, |project, cx| {
9367 project.open_local_buffer(path!("/file.rs"), cx)
9368 })
9369 .await
9370 .unwrap();
9371
9372 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9373 let (editor, cx) = cx.add_window_view(|window, cx| {
9374 build_editor_with_project(project.clone(), buffer, window, cx)
9375 });
9376 editor.update_in(cx, |editor, window, cx| {
9377 editor.set_text("one\ntwo\nthree\n", window, cx)
9378 });
9379 assert!(cx.read(|cx| editor.is_dirty(cx)));
9380
9381 cx.executor().start_waiting();
9382 let fake_server = fake_servers.next().await.unwrap();
9383
9384 let save = editor
9385 .update_in(cx, |editor, window, cx| {
9386 editor.save(true, project.clone(), window, cx)
9387 })
9388 .unwrap();
9389 fake_server
9390 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9391 assert_eq!(
9392 params.text_document.uri,
9393 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9394 );
9395 assert_eq!(params.options.tab_size, 4);
9396 Ok(Some(vec![lsp::TextEdit::new(
9397 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9398 ", ".to_string(),
9399 )]))
9400 })
9401 .next()
9402 .await;
9403 cx.executor().start_waiting();
9404 save.await;
9405 assert_eq!(
9406 editor.update(cx, |editor, cx| editor.text(cx)),
9407 "one, two\nthree\n"
9408 );
9409 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9410
9411 editor.update_in(cx, |editor, window, cx| {
9412 editor.set_text("one\ntwo\nthree\n", window, cx)
9413 });
9414 assert!(cx.read(|cx| editor.is_dirty(cx)));
9415
9416 // Ensure we can still save even if formatting hangs.
9417 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9418 move |params, _| async move {
9419 assert_eq!(
9420 params.text_document.uri,
9421 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9422 );
9423 futures::future::pending::<()>().await;
9424 unreachable!()
9425 },
9426 );
9427 let save = editor
9428 .update_in(cx, |editor, window, cx| {
9429 editor.save(true, project.clone(), window, cx)
9430 })
9431 .unwrap();
9432 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9433 cx.executor().start_waiting();
9434 save.await;
9435 assert_eq!(
9436 editor.update(cx, |editor, cx| editor.text(cx)),
9437 "one\ntwo\nthree\n"
9438 );
9439 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9440
9441 // For non-dirty buffer, no formatting request should be sent
9442 let save = editor
9443 .update_in(cx, |editor, window, cx| {
9444 editor.save(true, project.clone(), window, cx)
9445 })
9446 .unwrap();
9447 let _pending_format_request = fake_server
9448 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9449 panic!("Should not be invoked on non-dirty buffer");
9450 })
9451 .next();
9452 cx.executor().start_waiting();
9453 save.await;
9454
9455 // Set Rust language override and assert overridden tabsize is sent to language server
9456 update_test_language_settings(cx, |settings| {
9457 settings.languages.insert(
9458 "Rust".into(),
9459 LanguageSettingsContent {
9460 tab_size: NonZeroU32::new(8),
9461 ..Default::default()
9462 },
9463 );
9464 });
9465
9466 editor.update_in(cx, |editor, window, cx| {
9467 editor.set_text("somehting_new\n", window, cx)
9468 });
9469 assert!(cx.read(|cx| editor.is_dirty(cx)));
9470 let save = editor
9471 .update_in(cx, |editor, window, cx| {
9472 editor.save(true, project.clone(), window, cx)
9473 })
9474 .unwrap();
9475 fake_server
9476 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9477 assert_eq!(
9478 params.text_document.uri,
9479 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9480 );
9481 assert_eq!(params.options.tab_size, 8);
9482 Ok(Some(Vec::new()))
9483 })
9484 .next()
9485 .await;
9486 save.await;
9487}
9488
9489#[gpui::test]
9490async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9491 init_test(cx, |settings| {
9492 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9493 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9494 ))
9495 });
9496
9497 let fs = FakeFs::new(cx.executor());
9498 fs.insert_file(path!("/file.rs"), Default::default()).await;
9499
9500 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9501
9502 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9503 language_registry.add(Arc::new(Language::new(
9504 LanguageConfig {
9505 name: "Rust".into(),
9506 matcher: LanguageMatcher {
9507 path_suffixes: vec!["rs".to_string()],
9508 ..Default::default()
9509 },
9510 ..LanguageConfig::default()
9511 },
9512 Some(tree_sitter_rust::LANGUAGE.into()),
9513 )));
9514 update_test_language_settings(cx, |settings| {
9515 // Enable Prettier formatting for the same buffer, and ensure
9516 // LSP is called instead of Prettier.
9517 settings.defaults.prettier = Some(PrettierSettings {
9518 allowed: true,
9519 ..PrettierSettings::default()
9520 });
9521 });
9522 let mut fake_servers = language_registry.register_fake_lsp(
9523 "Rust",
9524 FakeLspAdapter {
9525 capabilities: lsp::ServerCapabilities {
9526 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9527 ..Default::default()
9528 },
9529 ..Default::default()
9530 },
9531 );
9532
9533 let buffer = project
9534 .update(cx, |project, cx| {
9535 project.open_local_buffer(path!("/file.rs"), cx)
9536 })
9537 .await
9538 .unwrap();
9539
9540 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9541 let (editor, cx) = cx.add_window_view(|window, cx| {
9542 build_editor_with_project(project.clone(), buffer, window, cx)
9543 });
9544 editor.update_in(cx, |editor, window, cx| {
9545 editor.set_text("one\ntwo\nthree\n", window, cx)
9546 });
9547
9548 cx.executor().start_waiting();
9549 let fake_server = fake_servers.next().await.unwrap();
9550
9551 let format = editor
9552 .update_in(cx, |editor, window, cx| {
9553 editor.perform_format(
9554 project.clone(),
9555 FormatTrigger::Manual,
9556 FormatTarget::Buffers,
9557 window,
9558 cx,
9559 )
9560 })
9561 .unwrap();
9562 fake_server
9563 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9564 assert_eq!(
9565 params.text_document.uri,
9566 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9567 );
9568 assert_eq!(params.options.tab_size, 4);
9569 Ok(Some(vec![lsp::TextEdit::new(
9570 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9571 ", ".to_string(),
9572 )]))
9573 })
9574 .next()
9575 .await;
9576 cx.executor().start_waiting();
9577 format.await;
9578 assert_eq!(
9579 editor.update(cx, |editor, cx| editor.text(cx)),
9580 "one, two\nthree\n"
9581 );
9582
9583 editor.update_in(cx, |editor, window, cx| {
9584 editor.set_text("one\ntwo\nthree\n", window, cx)
9585 });
9586 // Ensure we don't lock if formatting hangs.
9587 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9588 move |params, _| async move {
9589 assert_eq!(
9590 params.text_document.uri,
9591 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9592 );
9593 futures::future::pending::<()>().await;
9594 unreachable!()
9595 },
9596 );
9597 let format = editor
9598 .update_in(cx, |editor, window, cx| {
9599 editor.perform_format(
9600 project,
9601 FormatTrigger::Manual,
9602 FormatTarget::Buffers,
9603 window,
9604 cx,
9605 )
9606 })
9607 .unwrap();
9608 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9609 cx.executor().start_waiting();
9610 format.await;
9611 assert_eq!(
9612 editor.update(cx, |editor, cx| editor.text(cx)),
9613 "one\ntwo\nthree\n"
9614 );
9615}
9616
9617#[gpui::test]
9618async fn test_multiple_formatters(cx: &mut TestAppContext) {
9619 init_test(cx, |settings| {
9620 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9621 settings.defaults.formatter =
9622 Some(language_settings::SelectedFormatter::List(FormatterList(
9623 vec![
9624 Formatter::LanguageServer { name: None },
9625 Formatter::CodeActions(
9626 [
9627 ("code-action-1".into(), true),
9628 ("code-action-2".into(), true),
9629 ]
9630 .into_iter()
9631 .collect(),
9632 ),
9633 ]
9634 .into(),
9635 )))
9636 });
9637
9638 let fs = FakeFs::new(cx.executor());
9639 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9640 .await;
9641
9642 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9643 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9644 language_registry.add(rust_lang());
9645
9646 let mut fake_servers = language_registry.register_fake_lsp(
9647 "Rust",
9648 FakeLspAdapter {
9649 capabilities: lsp::ServerCapabilities {
9650 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9651 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9652 commands: vec!["the-command-for-code-action-1".into()],
9653 ..Default::default()
9654 }),
9655 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9656 ..Default::default()
9657 },
9658 ..Default::default()
9659 },
9660 );
9661
9662 let buffer = project
9663 .update(cx, |project, cx| {
9664 project.open_local_buffer(path!("/file.rs"), cx)
9665 })
9666 .await
9667 .unwrap();
9668
9669 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9670 let (editor, cx) = cx.add_window_view(|window, cx| {
9671 build_editor_with_project(project.clone(), buffer, window, cx)
9672 });
9673
9674 cx.executor().start_waiting();
9675
9676 let fake_server = fake_servers.next().await.unwrap();
9677 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9678 move |_params, _| async move {
9679 Ok(Some(vec![lsp::TextEdit::new(
9680 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9681 "applied-formatting\n".to_string(),
9682 )]))
9683 },
9684 );
9685 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9686 move |params, _| async move {
9687 assert_eq!(
9688 params.context.only,
9689 Some(vec!["code-action-1".into(), "code-action-2".into()])
9690 );
9691 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9692 Ok(Some(vec![
9693 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9694 kind: Some("code-action-1".into()),
9695 edit: Some(lsp::WorkspaceEdit::new(
9696 [(
9697 uri.clone(),
9698 vec![lsp::TextEdit::new(
9699 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9700 "applied-code-action-1-edit\n".to_string(),
9701 )],
9702 )]
9703 .into_iter()
9704 .collect(),
9705 )),
9706 command: Some(lsp::Command {
9707 command: "the-command-for-code-action-1".into(),
9708 ..Default::default()
9709 }),
9710 ..Default::default()
9711 }),
9712 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9713 kind: Some("code-action-2".into()),
9714 edit: Some(lsp::WorkspaceEdit::new(
9715 [(
9716 uri.clone(),
9717 vec![lsp::TextEdit::new(
9718 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9719 "applied-code-action-2-edit\n".to_string(),
9720 )],
9721 )]
9722 .into_iter()
9723 .collect(),
9724 )),
9725 ..Default::default()
9726 }),
9727 ]))
9728 },
9729 );
9730
9731 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9732 move |params, _| async move { Ok(params) }
9733 });
9734
9735 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9736 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9737 let fake = fake_server.clone();
9738 let lock = command_lock.clone();
9739 move |params, _| {
9740 assert_eq!(params.command, "the-command-for-code-action-1");
9741 let fake = fake.clone();
9742 let lock = lock.clone();
9743 async move {
9744 lock.lock().await;
9745 fake.server
9746 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9747 label: None,
9748 edit: lsp::WorkspaceEdit {
9749 changes: Some(
9750 [(
9751 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9752 vec![lsp::TextEdit {
9753 range: lsp::Range::new(
9754 lsp::Position::new(0, 0),
9755 lsp::Position::new(0, 0),
9756 ),
9757 new_text: "applied-code-action-1-command\n".into(),
9758 }],
9759 )]
9760 .into_iter()
9761 .collect(),
9762 ),
9763 ..Default::default()
9764 },
9765 })
9766 .await
9767 .into_response()
9768 .unwrap();
9769 Ok(Some(json!(null)))
9770 }
9771 }
9772 });
9773
9774 cx.executor().start_waiting();
9775 editor
9776 .update_in(cx, |editor, window, cx| {
9777 editor.perform_format(
9778 project.clone(),
9779 FormatTrigger::Manual,
9780 FormatTarget::Buffers,
9781 window,
9782 cx,
9783 )
9784 })
9785 .unwrap()
9786 .await;
9787 editor.update(cx, |editor, cx| {
9788 assert_eq!(
9789 editor.text(cx),
9790 r#"
9791 applied-code-action-2-edit
9792 applied-code-action-1-command
9793 applied-code-action-1-edit
9794 applied-formatting
9795 one
9796 two
9797 three
9798 "#
9799 .unindent()
9800 );
9801 });
9802
9803 editor.update_in(cx, |editor, window, cx| {
9804 editor.undo(&Default::default(), window, cx);
9805 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9806 });
9807
9808 // Perform a manual edit while waiting for an LSP command
9809 // that's being run as part of a formatting code action.
9810 let lock_guard = command_lock.lock().await;
9811 let format = editor
9812 .update_in(cx, |editor, window, cx| {
9813 editor.perform_format(
9814 project.clone(),
9815 FormatTrigger::Manual,
9816 FormatTarget::Buffers,
9817 window,
9818 cx,
9819 )
9820 })
9821 .unwrap();
9822 cx.run_until_parked();
9823 editor.update(cx, |editor, cx| {
9824 assert_eq!(
9825 editor.text(cx),
9826 r#"
9827 applied-code-action-1-edit
9828 applied-formatting
9829 one
9830 two
9831 three
9832 "#
9833 .unindent()
9834 );
9835
9836 editor.buffer.update(cx, |buffer, cx| {
9837 let ix = buffer.len(cx);
9838 buffer.edit([(ix..ix, "edited\n")], None, cx);
9839 });
9840 });
9841
9842 // Allow the LSP command to proceed. Because the buffer was edited,
9843 // the second code action will not be run.
9844 drop(lock_guard);
9845 format.await;
9846 editor.update_in(cx, |editor, window, cx| {
9847 assert_eq!(
9848 editor.text(cx),
9849 r#"
9850 applied-code-action-1-command
9851 applied-code-action-1-edit
9852 applied-formatting
9853 one
9854 two
9855 three
9856 edited
9857 "#
9858 .unindent()
9859 );
9860
9861 // The manual edit is undone first, because it is the last thing the user did
9862 // (even though the command completed afterwards).
9863 editor.undo(&Default::default(), window, cx);
9864 assert_eq!(
9865 editor.text(cx),
9866 r#"
9867 applied-code-action-1-command
9868 applied-code-action-1-edit
9869 applied-formatting
9870 one
9871 two
9872 three
9873 "#
9874 .unindent()
9875 );
9876
9877 // All the formatting (including the command, which completed after the manual edit)
9878 // is undone together.
9879 editor.undo(&Default::default(), window, cx);
9880 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9881 });
9882}
9883
9884#[gpui::test]
9885async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9886 init_test(cx, |settings| {
9887 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9888 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9889 ))
9890 });
9891
9892 let fs = FakeFs::new(cx.executor());
9893 fs.insert_file(path!("/file.ts"), Default::default()).await;
9894
9895 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9896
9897 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9898 language_registry.add(Arc::new(Language::new(
9899 LanguageConfig {
9900 name: "TypeScript".into(),
9901 matcher: LanguageMatcher {
9902 path_suffixes: vec!["ts".to_string()],
9903 ..Default::default()
9904 },
9905 ..LanguageConfig::default()
9906 },
9907 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9908 )));
9909 update_test_language_settings(cx, |settings| {
9910 settings.defaults.prettier = Some(PrettierSettings {
9911 allowed: true,
9912 ..PrettierSettings::default()
9913 });
9914 });
9915 let mut fake_servers = language_registry.register_fake_lsp(
9916 "TypeScript",
9917 FakeLspAdapter {
9918 capabilities: lsp::ServerCapabilities {
9919 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9920 ..Default::default()
9921 },
9922 ..Default::default()
9923 },
9924 );
9925
9926 let buffer = project
9927 .update(cx, |project, cx| {
9928 project.open_local_buffer(path!("/file.ts"), cx)
9929 })
9930 .await
9931 .unwrap();
9932
9933 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9934 let (editor, cx) = cx.add_window_view(|window, cx| {
9935 build_editor_with_project(project.clone(), buffer, window, cx)
9936 });
9937 editor.update_in(cx, |editor, window, cx| {
9938 editor.set_text(
9939 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9940 window,
9941 cx,
9942 )
9943 });
9944
9945 cx.executor().start_waiting();
9946 let fake_server = fake_servers.next().await.unwrap();
9947
9948 let format = editor
9949 .update_in(cx, |editor, window, cx| {
9950 editor.perform_code_action_kind(
9951 project.clone(),
9952 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9953 window,
9954 cx,
9955 )
9956 })
9957 .unwrap();
9958 fake_server
9959 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9960 assert_eq!(
9961 params.text_document.uri,
9962 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9963 );
9964 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9965 lsp::CodeAction {
9966 title: "Organize Imports".to_string(),
9967 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9968 edit: Some(lsp::WorkspaceEdit {
9969 changes: Some(
9970 [(
9971 params.text_document.uri.clone(),
9972 vec![lsp::TextEdit::new(
9973 lsp::Range::new(
9974 lsp::Position::new(1, 0),
9975 lsp::Position::new(2, 0),
9976 ),
9977 "".to_string(),
9978 )],
9979 )]
9980 .into_iter()
9981 .collect(),
9982 ),
9983 ..Default::default()
9984 }),
9985 ..Default::default()
9986 },
9987 )]))
9988 })
9989 .next()
9990 .await;
9991 cx.executor().start_waiting();
9992 format.await;
9993 assert_eq!(
9994 editor.update(cx, |editor, cx| editor.text(cx)),
9995 "import { a } from 'module';\n\nconst x = a;\n"
9996 );
9997
9998 editor.update_in(cx, |editor, window, cx| {
9999 editor.set_text(
10000 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10001 window,
10002 cx,
10003 )
10004 });
10005 // Ensure we don't lock if code action hangs.
10006 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10007 move |params, _| async move {
10008 assert_eq!(
10009 params.text_document.uri,
10010 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10011 );
10012 futures::future::pending::<()>().await;
10013 unreachable!()
10014 },
10015 );
10016 let format = editor
10017 .update_in(cx, |editor, window, cx| {
10018 editor.perform_code_action_kind(
10019 project,
10020 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10021 window,
10022 cx,
10023 )
10024 })
10025 .unwrap();
10026 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10027 cx.executor().start_waiting();
10028 format.await;
10029 assert_eq!(
10030 editor.update(cx, |editor, cx| editor.text(cx)),
10031 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10032 );
10033}
10034
10035#[gpui::test]
10036async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10037 init_test(cx, |_| {});
10038
10039 let mut cx = EditorLspTestContext::new_rust(
10040 lsp::ServerCapabilities {
10041 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10042 ..Default::default()
10043 },
10044 cx,
10045 )
10046 .await;
10047
10048 cx.set_state(indoc! {"
10049 one.twoˇ
10050 "});
10051
10052 // The format request takes a long time. When it completes, it inserts
10053 // a newline and an indent before the `.`
10054 cx.lsp
10055 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10056 let executor = cx.background_executor().clone();
10057 async move {
10058 executor.timer(Duration::from_millis(100)).await;
10059 Ok(Some(vec![lsp::TextEdit {
10060 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10061 new_text: "\n ".into(),
10062 }]))
10063 }
10064 });
10065
10066 // Submit a format request.
10067 let format_1 = cx
10068 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10069 .unwrap();
10070 cx.executor().run_until_parked();
10071
10072 // Submit a second format request.
10073 let format_2 = cx
10074 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10075 .unwrap();
10076 cx.executor().run_until_parked();
10077
10078 // Wait for both format requests to complete
10079 cx.executor().advance_clock(Duration::from_millis(200));
10080 cx.executor().start_waiting();
10081 format_1.await.unwrap();
10082 cx.executor().start_waiting();
10083 format_2.await.unwrap();
10084
10085 // The formatting edits only happens once.
10086 cx.assert_editor_state(indoc! {"
10087 one
10088 .twoˇ
10089 "});
10090}
10091
10092#[gpui::test]
10093async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10094 init_test(cx, |settings| {
10095 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10096 });
10097
10098 let mut cx = EditorLspTestContext::new_rust(
10099 lsp::ServerCapabilities {
10100 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10101 ..Default::default()
10102 },
10103 cx,
10104 )
10105 .await;
10106
10107 // Set up a buffer white some trailing whitespace and no trailing newline.
10108 cx.set_state(
10109 &[
10110 "one ", //
10111 "twoˇ", //
10112 "three ", //
10113 "four", //
10114 ]
10115 .join("\n"),
10116 );
10117
10118 // Submit a format request.
10119 let format = cx
10120 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10121 .unwrap();
10122
10123 // Record which buffer changes have been sent to the language server
10124 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10125 cx.lsp
10126 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10127 let buffer_changes = buffer_changes.clone();
10128 move |params, _| {
10129 buffer_changes.lock().extend(
10130 params
10131 .content_changes
10132 .into_iter()
10133 .map(|e| (e.range.unwrap(), e.text)),
10134 );
10135 }
10136 });
10137
10138 // Handle formatting requests to the language server.
10139 cx.lsp
10140 .set_request_handler::<lsp::request::Formatting, _, _>({
10141 let buffer_changes = buffer_changes.clone();
10142 move |_, _| {
10143 // When formatting is requested, trailing whitespace has already been stripped,
10144 // and the trailing newline has already been added.
10145 assert_eq!(
10146 &buffer_changes.lock()[1..],
10147 &[
10148 (
10149 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10150 "".into()
10151 ),
10152 (
10153 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10154 "".into()
10155 ),
10156 (
10157 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10158 "\n".into()
10159 ),
10160 ]
10161 );
10162
10163 // Insert blank lines between each line of the buffer.
10164 async move {
10165 Ok(Some(vec![
10166 lsp::TextEdit {
10167 range: lsp::Range::new(
10168 lsp::Position::new(1, 0),
10169 lsp::Position::new(1, 0),
10170 ),
10171 new_text: "\n".into(),
10172 },
10173 lsp::TextEdit {
10174 range: lsp::Range::new(
10175 lsp::Position::new(2, 0),
10176 lsp::Position::new(2, 0),
10177 ),
10178 new_text: "\n".into(),
10179 },
10180 ]))
10181 }
10182 }
10183 });
10184
10185 // After formatting the buffer, the trailing whitespace is stripped,
10186 // a newline is appended, and the edits provided by the language server
10187 // have been applied.
10188 format.await.unwrap();
10189 cx.assert_editor_state(
10190 &[
10191 "one", //
10192 "", //
10193 "twoˇ", //
10194 "", //
10195 "three", //
10196 "four", //
10197 "", //
10198 ]
10199 .join("\n"),
10200 );
10201
10202 // Undoing the formatting undoes the trailing whitespace removal, the
10203 // trailing newline, and the LSP edits.
10204 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10205 cx.assert_editor_state(
10206 &[
10207 "one ", //
10208 "twoˇ", //
10209 "three ", //
10210 "four", //
10211 ]
10212 .join("\n"),
10213 );
10214}
10215
10216#[gpui::test]
10217async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10218 cx: &mut TestAppContext,
10219) {
10220 init_test(cx, |_| {});
10221
10222 cx.update(|cx| {
10223 cx.update_global::<SettingsStore, _>(|settings, cx| {
10224 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10225 settings.auto_signature_help = Some(true);
10226 });
10227 });
10228 });
10229
10230 let mut cx = EditorLspTestContext::new_rust(
10231 lsp::ServerCapabilities {
10232 signature_help_provider: Some(lsp::SignatureHelpOptions {
10233 ..Default::default()
10234 }),
10235 ..Default::default()
10236 },
10237 cx,
10238 )
10239 .await;
10240
10241 let language = Language::new(
10242 LanguageConfig {
10243 name: "Rust".into(),
10244 brackets: BracketPairConfig {
10245 pairs: vec![
10246 BracketPair {
10247 start: "{".to_string(),
10248 end: "}".to_string(),
10249 close: true,
10250 surround: true,
10251 newline: true,
10252 },
10253 BracketPair {
10254 start: "(".to_string(),
10255 end: ")".to_string(),
10256 close: true,
10257 surround: true,
10258 newline: true,
10259 },
10260 BracketPair {
10261 start: "/*".to_string(),
10262 end: " */".to_string(),
10263 close: true,
10264 surround: true,
10265 newline: true,
10266 },
10267 BracketPair {
10268 start: "[".to_string(),
10269 end: "]".to_string(),
10270 close: false,
10271 surround: false,
10272 newline: true,
10273 },
10274 BracketPair {
10275 start: "\"".to_string(),
10276 end: "\"".to_string(),
10277 close: true,
10278 surround: true,
10279 newline: false,
10280 },
10281 BracketPair {
10282 start: "<".to_string(),
10283 end: ">".to_string(),
10284 close: false,
10285 surround: true,
10286 newline: true,
10287 },
10288 ],
10289 ..Default::default()
10290 },
10291 autoclose_before: "})]".to_string(),
10292 ..Default::default()
10293 },
10294 Some(tree_sitter_rust::LANGUAGE.into()),
10295 );
10296 let language = Arc::new(language);
10297
10298 cx.language_registry().add(language.clone());
10299 cx.update_buffer(|buffer, cx| {
10300 buffer.set_language(Some(language), cx);
10301 });
10302
10303 cx.set_state(
10304 &r#"
10305 fn main() {
10306 sampleˇ
10307 }
10308 "#
10309 .unindent(),
10310 );
10311
10312 cx.update_editor(|editor, window, cx| {
10313 editor.handle_input("(", window, cx);
10314 });
10315 cx.assert_editor_state(
10316 &"
10317 fn main() {
10318 sample(ˇ)
10319 }
10320 "
10321 .unindent(),
10322 );
10323
10324 let mocked_response = lsp::SignatureHelp {
10325 signatures: vec![lsp::SignatureInformation {
10326 label: "fn sample(param1: u8, param2: u8)".to_string(),
10327 documentation: None,
10328 parameters: Some(vec![
10329 lsp::ParameterInformation {
10330 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10331 documentation: None,
10332 },
10333 lsp::ParameterInformation {
10334 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10335 documentation: None,
10336 },
10337 ]),
10338 active_parameter: None,
10339 }],
10340 active_signature: Some(0),
10341 active_parameter: Some(0),
10342 };
10343 handle_signature_help_request(&mut cx, mocked_response).await;
10344
10345 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10346 .await;
10347
10348 cx.editor(|editor, _, _| {
10349 let signature_help_state = editor.signature_help_state.popover().cloned();
10350 assert_eq!(
10351 signature_help_state.unwrap().label,
10352 "param1: u8, param2: u8"
10353 );
10354 });
10355}
10356
10357#[gpui::test]
10358async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10359 init_test(cx, |_| {});
10360
10361 cx.update(|cx| {
10362 cx.update_global::<SettingsStore, _>(|settings, cx| {
10363 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10364 settings.auto_signature_help = Some(false);
10365 settings.show_signature_help_after_edits = Some(false);
10366 });
10367 });
10368 });
10369
10370 let mut cx = EditorLspTestContext::new_rust(
10371 lsp::ServerCapabilities {
10372 signature_help_provider: Some(lsp::SignatureHelpOptions {
10373 ..Default::default()
10374 }),
10375 ..Default::default()
10376 },
10377 cx,
10378 )
10379 .await;
10380
10381 let language = Language::new(
10382 LanguageConfig {
10383 name: "Rust".into(),
10384 brackets: BracketPairConfig {
10385 pairs: vec![
10386 BracketPair {
10387 start: "{".to_string(),
10388 end: "}".to_string(),
10389 close: true,
10390 surround: true,
10391 newline: true,
10392 },
10393 BracketPair {
10394 start: "(".to_string(),
10395 end: ")".to_string(),
10396 close: true,
10397 surround: true,
10398 newline: true,
10399 },
10400 BracketPair {
10401 start: "/*".to_string(),
10402 end: " */".to_string(),
10403 close: true,
10404 surround: true,
10405 newline: true,
10406 },
10407 BracketPair {
10408 start: "[".to_string(),
10409 end: "]".to_string(),
10410 close: false,
10411 surround: false,
10412 newline: true,
10413 },
10414 BracketPair {
10415 start: "\"".to_string(),
10416 end: "\"".to_string(),
10417 close: true,
10418 surround: true,
10419 newline: false,
10420 },
10421 BracketPair {
10422 start: "<".to_string(),
10423 end: ">".to_string(),
10424 close: false,
10425 surround: true,
10426 newline: true,
10427 },
10428 ],
10429 ..Default::default()
10430 },
10431 autoclose_before: "})]".to_string(),
10432 ..Default::default()
10433 },
10434 Some(tree_sitter_rust::LANGUAGE.into()),
10435 );
10436 let language = Arc::new(language);
10437
10438 cx.language_registry().add(language.clone());
10439 cx.update_buffer(|buffer, cx| {
10440 buffer.set_language(Some(language), cx);
10441 });
10442
10443 // Ensure that signature_help is not called when no signature help is enabled.
10444 cx.set_state(
10445 &r#"
10446 fn main() {
10447 sampleˇ
10448 }
10449 "#
10450 .unindent(),
10451 );
10452 cx.update_editor(|editor, window, cx| {
10453 editor.handle_input("(", window, cx);
10454 });
10455 cx.assert_editor_state(
10456 &"
10457 fn main() {
10458 sample(ˇ)
10459 }
10460 "
10461 .unindent(),
10462 );
10463 cx.editor(|editor, _, _| {
10464 assert!(editor.signature_help_state.task().is_none());
10465 });
10466
10467 let mocked_response = lsp::SignatureHelp {
10468 signatures: vec![lsp::SignatureInformation {
10469 label: "fn sample(param1: u8, param2: u8)".to_string(),
10470 documentation: None,
10471 parameters: Some(vec![
10472 lsp::ParameterInformation {
10473 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10474 documentation: None,
10475 },
10476 lsp::ParameterInformation {
10477 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10478 documentation: None,
10479 },
10480 ]),
10481 active_parameter: None,
10482 }],
10483 active_signature: Some(0),
10484 active_parameter: Some(0),
10485 };
10486
10487 // Ensure that signature_help is called when enabled afte edits
10488 cx.update(|_, cx| {
10489 cx.update_global::<SettingsStore, _>(|settings, cx| {
10490 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10491 settings.auto_signature_help = Some(false);
10492 settings.show_signature_help_after_edits = Some(true);
10493 });
10494 });
10495 });
10496 cx.set_state(
10497 &r#"
10498 fn main() {
10499 sampleˇ
10500 }
10501 "#
10502 .unindent(),
10503 );
10504 cx.update_editor(|editor, window, cx| {
10505 editor.handle_input("(", window, cx);
10506 });
10507 cx.assert_editor_state(
10508 &"
10509 fn main() {
10510 sample(ˇ)
10511 }
10512 "
10513 .unindent(),
10514 );
10515 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10516 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10517 .await;
10518 cx.update_editor(|editor, _, _| {
10519 let signature_help_state = editor.signature_help_state.popover().cloned();
10520 assert!(signature_help_state.is_some());
10521 assert_eq!(
10522 signature_help_state.unwrap().label,
10523 "param1: u8, param2: u8"
10524 );
10525 editor.signature_help_state = SignatureHelpState::default();
10526 });
10527
10528 // Ensure that signature_help is called when auto signature help override is enabled
10529 cx.update(|_, cx| {
10530 cx.update_global::<SettingsStore, _>(|settings, cx| {
10531 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10532 settings.auto_signature_help = Some(true);
10533 settings.show_signature_help_after_edits = Some(false);
10534 });
10535 });
10536 });
10537 cx.set_state(
10538 &r#"
10539 fn main() {
10540 sampleˇ
10541 }
10542 "#
10543 .unindent(),
10544 );
10545 cx.update_editor(|editor, window, cx| {
10546 editor.handle_input("(", window, cx);
10547 });
10548 cx.assert_editor_state(
10549 &"
10550 fn main() {
10551 sample(ˇ)
10552 }
10553 "
10554 .unindent(),
10555 );
10556 handle_signature_help_request(&mut cx, mocked_response).await;
10557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10558 .await;
10559 cx.editor(|editor, _, _| {
10560 let signature_help_state = editor.signature_help_state.popover().cloned();
10561 assert!(signature_help_state.is_some());
10562 assert_eq!(
10563 signature_help_state.unwrap().label,
10564 "param1: u8, param2: u8"
10565 );
10566 });
10567}
10568
10569#[gpui::test]
10570async fn test_signature_help(cx: &mut TestAppContext) {
10571 init_test(cx, |_| {});
10572 cx.update(|cx| {
10573 cx.update_global::<SettingsStore, _>(|settings, cx| {
10574 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10575 settings.auto_signature_help = Some(true);
10576 });
10577 });
10578 });
10579
10580 let mut cx = EditorLspTestContext::new_rust(
10581 lsp::ServerCapabilities {
10582 signature_help_provider: Some(lsp::SignatureHelpOptions {
10583 ..Default::default()
10584 }),
10585 ..Default::default()
10586 },
10587 cx,
10588 )
10589 .await;
10590
10591 // A test that directly calls `show_signature_help`
10592 cx.update_editor(|editor, window, cx| {
10593 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10594 });
10595
10596 let mocked_response = lsp::SignatureHelp {
10597 signatures: vec![lsp::SignatureInformation {
10598 label: "fn sample(param1: u8, param2: u8)".to_string(),
10599 documentation: None,
10600 parameters: Some(vec![
10601 lsp::ParameterInformation {
10602 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10603 documentation: None,
10604 },
10605 lsp::ParameterInformation {
10606 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10607 documentation: None,
10608 },
10609 ]),
10610 active_parameter: None,
10611 }],
10612 active_signature: Some(0),
10613 active_parameter: Some(0),
10614 };
10615 handle_signature_help_request(&mut cx, mocked_response).await;
10616
10617 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10618 .await;
10619
10620 cx.editor(|editor, _, _| {
10621 let signature_help_state = editor.signature_help_state.popover().cloned();
10622 assert!(signature_help_state.is_some());
10623 assert_eq!(
10624 signature_help_state.unwrap().label,
10625 "param1: u8, param2: u8"
10626 );
10627 });
10628
10629 // When exiting outside from inside the brackets, `signature_help` is closed.
10630 cx.set_state(indoc! {"
10631 fn main() {
10632 sample(ˇ);
10633 }
10634
10635 fn sample(param1: u8, param2: u8) {}
10636 "});
10637
10638 cx.update_editor(|editor, window, cx| {
10639 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10640 });
10641
10642 let mocked_response = lsp::SignatureHelp {
10643 signatures: Vec::new(),
10644 active_signature: None,
10645 active_parameter: None,
10646 };
10647 handle_signature_help_request(&mut cx, mocked_response).await;
10648
10649 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10650 .await;
10651
10652 cx.editor(|editor, _, _| {
10653 assert!(!editor.signature_help_state.is_shown());
10654 });
10655
10656 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10657 cx.set_state(indoc! {"
10658 fn main() {
10659 sample(ˇ);
10660 }
10661
10662 fn sample(param1: u8, param2: u8) {}
10663 "});
10664
10665 let mocked_response = lsp::SignatureHelp {
10666 signatures: vec![lsp::SignatureInformation {
10667 label: "fn sample(param1: u8, param2: u8)".to_string(),
10668 documentation: None,
10669 parameters: Some(vec![
10670 lsp::ParameterInformation {
10671 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10672 documentation: None,
10673 },
10674 lsp::ParameterInformation {
10675 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10676 documentation: None,
10677 },
10678 ]),
10679 active_parameter: None,
10680 }],
10681 active_signature: Some(0),
10682 active_parameter: Some(0),
10683 };
10684 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10685 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10686 .await;
10687 cx.editor(|editor, _, _| {
10688 assert!(editor.signature_help_state.is_shown());
10689 });
10690
10691 // Restore the popover with more parameter input
10692 cx.set_state(indoc! {"
10693 fn main() {
10694 sample(param1, param2ˇ);
10695 }
10696
10697 fn sample(param1: u8, param2: u8) {}
10698 "});
10699
10700 let mocked_response = lsp::SignatureHelp {
10701 signatures: vec![lsp::SignatureInformation {
10702 label: "fn sample(param1: u8, param2: u8)".to_string(),
10703 documentation: None,
10704 parameters: Some(vec![
10705 lsp::ParameterInformation {
10706 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10707 documentation: None,
10708 },
10709 lsp::ParameterInformation {
10710 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10711 documentation: None,
10712 },
10713 ]),
10714 active_parameter: None,
10715 }],
10716 active_signature: Some(0),
10717 active_parameter: Some(1),
10718 };
10719 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10720 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10721 .await;
10722
10723 // When selecting a range, the popover is gone.
10724 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10725 cx.update_editor(|editor, window, cx| {
10726 editor.change_selections(None, window, cx, |s| {
10727 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10728 })
10729 });
10730 cx.assert_editor_state(indoc! {"
10731 fn main() {
10732 sample(param1, «ˇparam2»);
10733 }
10734
10735 fn sample(param1: u8, param2: u8) {}
10736 "});
10737 cx.editor(|editor, _, _| {
10738 assert!(!editor.signature_help_state.is_shown());
10739 });
10740
10741 // When unselecting again, the popover is back if within the brackets.
10742 cx.update_editor(|editor, window, cx| {
10743 editor.change_selections(None, window, cx, |s| {
10744 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10745 })
10746 });
10747 cx.assert_editor_state(indoc! {"
10748 fn main() {
10749 sample(param1, ˇparam2);
10750 }
10751
10752 fn sample(param1: u8, param2: u8) {}
10753 "});
10754 handle_signature_help_request(&mut cx, mocked_response).await;
10755 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10756 .await;
10757 cx.editor(|editor, _, _| {
10758 assert!(editor.signature_help_state.is_shown());
10759 });
10760
10761 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10762 cx.update_editor(|editor, window, cx| {
10763 editor.change_selections(None, window, cx, |s| {
10764 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10765 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10766 })
10767 });
10768 cx.assert_editor_state(indoc! {"
10769 fn main() {
10770 sample(param1, ˇparam2);
10771 }
10772
10773 fn sample(param1: u8, param2: u8) {}
10774 "});
10775
10776 let mocked_response = lsp::SignatureHelp {
10777 signatures: vec![lsp::SignatureInformation {
10778 label: "fn sample(param1: u8, param2: u8)".to_string(),
10779 documentation: None,
10780 parameters: Some(vec![
10781 lsp::ParameterInformation {
10782 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10783 documentation: None,
10784 },
10785 lsp::ParameterInformation {
10786 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10787 documentation: None,
10788 },
10789 ]),
10790 active_parameter: None,
10791 }],
10792 active_signature: Some(0),
10793 active_parameter: Some(1),
10794 };
10795 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10796 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10797 .await;
10798 cx.update_editor(|editor, _, cx| {
10799 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10800 });
10801 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10802 .await;
10803 cx.update_editor(|editor, window, cx| {
10804 editor.change_selections(None, window, cx, |s| {
10805 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10806 })
10807 });
10808 cx.assert_editor_state(indoc! {"
10809 fn main() {
10810 sample(param1, «ˇparam2»);
10811 }
10812
10813 fn sample(param1: u8, param2: u8) {}
10814 "});
10815 cx.update_editor(|editor, window, cx| {
10816 editor.change_selections(None, window, cx, |s| {
10817 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10818 })
10819 });
10820 cx.assert_editor_state(indoc! {"
10821 fn main() {
10822 sample(param1, ˇparam2);
10823 }
10824
10825 fn sample(param1: u8, param2: u8) {}
10826 "});
10827 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10828 .await;
10829}
10830
10831#[gpui::test]
10832async fn test_completion_mode(cx: &mut TestAppContext) {
10833 init_test(cx, |_| {});
10834 let mut cx = EditorLspTestContext::new_rust(
10835 lsp::ServerCapabilities {
10836 completion_provider: Some(lsp::CompletionOptions {
10837 resolve_provider: Some(true),
10838 ..Default::default()
10839 }),
10840 ..Default::default()
10841 },
10842 cx,
10843 )
10844 .await;
10845
10846 struct Run {
10847 run_description: &'static str,
10848 initial_state: String,
10849 buffer_marked_text: String,
10850 completion_label: &'static str,
10851 completion_text: &'static str,
10852 expected_with_insert_mode: String,
10853 expected_with_replace_mode: String,
10854 expected_with_replace_subsequence_mode: String,
10855 expected_with_replace_suffix_mode: String,
10856 }
10857
10858 let runs = [
10859 Run {
10860 run_description: "Start of word matches completion text",
10861 initial_state: "before ediˇ after".into(),
10862 buffer_marked_text: "before <edi|> after".into(),
10863 completion_label: "editor",
10864 completion_text: "editor",
10865 expected_with_insert_mode: "before editorˇ after".into(),
10866 expected_with_replace_mode: "before editorˇ after".into(),
10867 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10868 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10869 },
10870 Run {
10871 run_description: "Accept same text at the middle of the word",
10872 initial_state: "before ediˇtor after".into(),
10873 buffer_marked_text: "before <edi|tor> after".into(),
10874 completion_label: "editor",
10875 completion_text: "editor",
10876 expected_with_insert_mode: "before editorˇtor after".into(),
10877 expected_with_replace_mode: "before editorˇ after".into(),
10878 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10879 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10880 },
10881 Run {
10882 run_description: "End of word matches completion text -- cursor at end",
10883 initial_state: "before torˇ after".into(),
10884 buffer_marked_text: "before <tor|> after".into(),
10885 completion_label: "editor",
10886 completion_text: "editor",
10887 expected_with_insert_mode: "before editorˇ after".into(),
10888 expected_with_replace_mode: "before editorˇ after".into(),
10889 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10890 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10891 },
10892 Run {
10893 run_description: "End of word matches completion text -- cursor at start",
10894 initial_state: "before ˇtor after".into(),
10895 buffer_marked_text: "before <|tor> after".into(),
10896 completion_label: "editor",
10897 completion_text: "editor",
10898 expected_with_insert_mode: "before editorˇtor after".into(),
10899 expected_with_replace_mode: "before editorˇ after".into(),
10900 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10901 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10902 },
10903 Run {
10904 run_description: "Prepend text containing whitespace",
10905 initial_state: "pˇfield: bool".into(),
10906 buffer_marked_text: "<p|field>: bool".into(),
10907 completion_label: "pub ",
10908 completion_text: "pub ",
10909 expected_with_insert_mode: "pub ˇfield: bool".into(),
10910 expected_with_replace_mode: "pub ˇ: bool".into(),
10911 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10912 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10913 },
10914 Run {
10915 run_description: "Add element to start of list",
10916 initial_state: "[element_ˇelement_2]".into(),
10917 buffer_marked_text: "[<element_|element_2>]".into(),
10918 completion_label: "element_1",
10919 completion_text: "element_1",
10920 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10921 expected_with_replace_mode: "[element_1ˇ]".into(),
10922 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10923 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10924 },
10925 Run {
10926 run_description: "Add element to start of list -- first and second elements are equal",
10927 initial_state: "[elˇelement]".into(),
10928 buffer_marked_text: "[<el|element>]".into(),
10929 completion_label: "element",
10930 completion_text: "element",
10931 expected_with_insert_mode: "[elementˇelement]".into(),
10932 expected_with_replace_mode: "[elementˇ]".into(),
10933 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10934 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10935 },
10936 Run {
10937 run_description: "Ends with matching suffix",
10938 initial_state: "SubˇError".into(),
10939 buffer_marked_text: "<Sub|Error>".into(),
10940 completion_label: "SubscriptionError",
10941 completion_text: "SubscriptionError",
10942 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10943 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10944 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10945 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10946 },
10947 Run {
10948 run_description: "Suffix is a subsequence -- contiguous",
10949 initial_state: "SubˇErr".into(),
10950 buffer_marked_text: "<Sub|Err>".into(),
10951 completion_label: "SubscriptionError",
10952 completion_text: "SubscriptionError",
10953 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10954 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10955 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10956 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10957 },
10958 Run {
10959 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10960 initial_state: "Suˇscrirr".into(),
10961 buffer_marked_text: "<Su|scrirr>".into(),
10962 completion_label: "SubscriptionError",
10963 completion_text: "SubscriptionError",
10964 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10965 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10966 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10967 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10968 },
10969 Run {
10970 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10971 initial_state: "foo(indˇix)".into(),
10972 buffer_marked_text: "foo(<ind|ix>)".into(),
10973 completion_label: "node_index",
10974 completion_text: "node_index",
10975 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10976 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10977 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10978 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10979 },
10980 Run {
10981 run_description: "Replace range ends before cursor - should extend to cursor",
10982 initial_state: "before editˇo after".into(),
10983 buffer_marked_text: "before <{ed}>it|o after".into(),
10984 completion_label: "editor",
10985 completion_text: "editor",
10986 expected_with_insert_mode: "before editorˇo after".into(),
10987 expected_with_replace_mode: "before editorˇo after".into(),
10988 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10989 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10990 },
10991 Run {
10992 run_description: "Uses label for suffix matching",
10993 initial_state: "before ediˇtor after".into(),
10994 buffer_marked_text: "before <edi|tor> after".into(),
10995 completion_label: "editor",
10996 completion_text: "editor()",
10997 expected_with_insert_mode: "before editor()ˇtor after".into(),
10998 expected_with_replace_mode: "before editor()ˇ after".into(),
10999 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11000 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11001 },
11002 Run {
11003 run_description: "Case insensitive subsequence and suffix matching",
11004 initial_state: "before EDiˇtoR after".into(),
11005 buffer_marked_text: "before <EDi|toR> after".into(),
11006 completion_label: "editor",
11007 completion_text: "editor",
11008 expected_with_insert_mode: "before editorˇtoR after".into(),
11009 expected_with_replace_mode: "before editorˇ after".into(),
11010 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11011 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11012 },
11013 ];
11014
11015 for run in runs {
11016 let run_variations = [
11017 (LspInsertMode::Insert, run.expected_with_insert_mode),
11018 (LspInsertMode::Replace, run.expected_with_replace_mode),
11019 (
11020 LspInsertMode::ReplaceSubsequence,
11021 run.expected_with_replace_subsequence_mode,
11022 ),
11023 (
11024 LspInsertMode::ReplaceSuffix,
11025 run.expected_with_replace_suffix_mode,
11026 ),
11027 ];
11028
11029 for (lsp_insert_mode, expected_text) in run_variations {
11030 eprintln!(
11031 "run = {:?}, mode = {lsp_insert_mode:.?}",
11032 run.run_description,
11033 );
11034
11035 update_test_language_settings(&mut cx, |settings| {
11036 settings.defaults.completions = Some(CompletionSettings {
11037 lsp_insert_mode,
11038 words: WordsCompletionMode::Disabled,
11039 lsp: true,
11040 lsp_fetch_timeout_ms: 0,
11041 });
11042 });
11043
11044 cx.set_state(&run.initial_state);
11045 cx.update_editor(|editor, window, cx| {
11046 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11047 });
11048
11049 let counter = Arc::new(AtomicUsize::new(0));
11050 handle_completion_request_with_insert_and_replace(
11051 &mut cx,
11052 &run.buffer_marked_text,
11053 vec![(run.completion_label, run.completion_text)],
11054 counter.clone(),
11055 )
11056 .await;
11057 cx.condition(|editor, _| editor.context_menu_visible())
11058 .await;
11059 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11060
11061 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11062 editor
11063 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11064 .unwrap()
11065 });
11066 cx.assert_editor_state(&expected_text);
11067 handle_resolve_completion_request(&mut cx, None).await;
11068 apply_additional_edits.await.unwrap();
11069 }
11070 }
11071}
11072
11073#[gpui::test]
11074async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11075 init_test(cx, |_| {});
11076 let mut cx = EditorLspTestContext::new_rust(
11077 lsp::ServerCapabilities {
11078 completion_provider: Some(lsp::CompletionOptions {
11079 resolve_provider: Some(true),
11080 ..Default::default()
11081 }),
11082 ..Default::default()
11083 },
11084 cx,
11085 )
11086 .await;
11087
11088 let initial_state = "SubˇError";
11089 let buffer_marked_text = "<Sub|Error>";
11090 let completion_text = "SubscriptionError";
11091 let expected_with_insert_mode = "SubscriptionErrorˇError";
11092 let expected_with_replace_mode = "SubscriptionErrorˇ";
11093
11094 update_test_language_settings(&mut cx, |settings| {
11095 settings.defaults.completions = Some(CompletionSettings {
11096 words: WordsCompletionMode::Disabled,
11097 // set the opposite here to ensure that the action is overriding the default behavior
11098 lsp_insert_mode: LspInsertMode::Insert,
11099 lsp: true,
11100 lsp_fetch_timeout_ms: 0,
11101 });
11102 });
11103
11104 cx.set_state(initial_state);
11105 cx.update_editor(|editor, window, cx| {
11106 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11107 });
11108
11109 let counter = Arc::new(AtomicUsize::new(0));
11110 handle_completion_request_with_insert_and_replace(
11111 &mut cx,
11112 &buffer_marked_text,
11113 vec![(completion_text, completion_text)],
11114 counter.clone(),
11115 )
11116 .await;
11117 cx.condition(|editor, _| editor.context_menu_visible())
11118 .await;
11119 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11120
11121 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11122 editor
11123 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11124 .unwrap()
11125 });
11126 cx.assert_editor_state(&expected_with_replace_mode);
11127 handle_resolve_completion_request(&mut cx, None).await;
11128 apply_additional_edits.await.unwrap();
11129
11130 update_test_language_settings(&mut cx, |settings| {
11131 settings.defaults.completions = Some(CompletionSettings {
11132 words: WordsCompletionMode::Disabled,
11133 // set the opposite here to ensure that the action is overriding the default behavior
11134 lsp_insert_mode: LspInsertMode::Replace,
11135 lsp: true,
11136 lsp_fetch_timeout_ms: 0,
11137 });
11138 });
11139
11140 cx.set_state(initial_state);
11141 cx.update_editor(|editor, window, cx| {
11142 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11143 });
11144 handle_completion_request_with_insert_and_replace(
11145 &mut cx,
11146 &buffer_marked_text,
11147 vec![(completion_text, completion_text)],
11148 counter.clone(),
11149 )
11150 .await;
11151 cx.condition(|editor, _| editor.context_menu_visible())
11152 .await;
11153 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11154
11155 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11156 editor
11157 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11158 .unwrap()
11159 });
11160 cx.assert_editor_state(&expected_with_insert_mode);
11161 handle_resolve_completion_request(&mut cx, None).await;
11162 apply_additional_edits.await.unwrap();
11163}
11164
11165#[gpui::test]
11166async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11167 init_test(cx, |_| {});
11168 let mut cx = EditorLspTestContext::new_rust(
11169 lsp::ServerCapabilities {
11170 completion_provider: Some(lsp::CompletionOptions {
11171 resolve_provider: Some(true),
11172 ..Default::default()
11173 }),
11174 ..Default::default()
11175 },
11176 cx,
11177 )
11178 .await;
11179
11180 // scenario: surrounding text matches completion text
11181 let completion_text = "to_offset";
11182 let initial_state = indoc! {"
11183 1. buf.to_offˇsuffix
11184 2. buf.to_offˇsuf
11185 3. buf.to_offˇfix
11186 4. buf.to_offˇ
11187 5. into_offˇensive
11188 6. ˇsuffix
11189 7. let ˇ //
11190 8. aaˇzz
11191 9. buf.to_off«zzzzzˇ»suffix
11192 10. buf.«ˇzzzzz»suffix
11193 11. to_off«ˇzzzzz»
11194
11195 buf.to_offˇsuffix // newest cursor
11196 "};
11197 let completion_marked_buffer = indoc! {"
11198 1. buf.to_offsuffix
11199 2. buf.to_offsuf
11200 3. buf.to_offfix
11201 4. buf.to_off
11202 5. into_offensive
11203 6. suffix
11204 7. let //
11205 8. aazz
11206 9. buf.to_offzzzzzsuffix
11207 10. buf.zzzzzsuffix
11208 11. to_offzzzzz
11209
11210 buf.<to_off|suffix> // newest cursor
11211 "};
11212 let expected = indoc! {"
11213 1. buf.to_offsetˇ
11214 2. buf.to_offsetˇsuf
11215 3. buf.to_offsetˇfix
11216 4. buf.to_offsetˇ
11217 5. into_offsetˇensive
11218 6. to_offsetˇsuffix
11219 7. let to_offsetˇ //
11220 8. aato_offsetˇzz
11221 9. buf.to_offsetˇ
11222 10. buf.to_offsetˇsuffix
11223 11. to_offsetˇ
11224
11225 buf.to_offsetˇ // newest cursor
11226 "};
11227 cx.set_state(initial_state);
11228 cx.update_editor(|editor, window, cx| {
11229 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11230 });
11231 handle_completion_request_with_insert_and_replace(
11232 &mut cx,
11233 completion_marked_buffer,
11234 vec![(completion_text, completion_text)],
11235 Arc::new(AtomicUsize::new(0)),
11236 )
11237 .await;
11238 cx.condition(|editor, _| editor.context_menu_visible())
11239 .await;
11240 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11241 editor
11242 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11243 .unwrap()
11244 });
11245 cx.assert_editor_state(expected);
11246 handle_resolve_completion_request(&mut cx, None).await;
11247 apply_additional_edits.await.unwrap();
11248
11249 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11250 let completion_text = "foo_and_bar";
11251 let initial_state = indoc! {"
11252 1. ooanbˇ
11253 2. zooanbˇ
11254 3. ooanbˇz
11255 4. zooanbˇz
11256 5. ooanˇ
11257 6. oanbˇ
11258
11259 ooanbˇ
11260 "};
11261 let completion_marked_buffer = indoc! {"
11262 1. ooanb
11263 2. zooanb
11264 3. ooanbz
11265 4. zooanbz
11266 5. ooan
11267 6. oanb
11268
11269 <ooanb|>
11270 "};
11271 let expected = indoc! {"
11272 1. foo_and_barˇ
11273 2. zfoo_and_barˇ
11274 3. foo_and_barˇz
11275 4. zfoo_and_barˇz
11276 5. ooanfoo_and_barˇ
11277 6. oanbfoo_and_barˇ
11278
11279 foo_and_barˇ
11280 "};
11281 cx.set_state(initial_state);
11282 cx.update_editor(|editor, window, cx| {
11283 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11284 });
11285 handle_completion_request_with_insert_and_replace(
11286 &mut cx,
11287 completion_marked_buffer,
11288 vec![(completion_text, completion_text)],
11289 Arc::new(AtomicUsize::new(0)),
11290 )
11291 .await;
11292 cx.condition(|editor, _| editor.context_menu_visible())
11293 .await;
11294 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11295 editor
11296 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11297 .unwrap()
11298 });
11299 cx.assert_editor_state(expected);
11300 handle_resolve_completion_request(&mut cx, None).await;
11301 apply_additional_edits.await.unwrap();
11302
11303 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11304 // (expects the same as if it was inserted at the end)
11305 let completion_text = "foo_and_bar";
11306 let initial_state = indoc! {"
11307 1. ooˇanb
11308 2. zooˇanb
11309 3. ooˇanbz
11310 4. zooˇanbz
11311
11312 ooˇanb
11313 "};
11314 let completion_marked_buffer = indoc! {"
11315 1. ooanb
11316 2. zooanb
11317 3. ooanbz
11318 4. zooanbz
11319
11320 <oo|anb>
11321 "};
11322 let expected = indoc! {"
11323 1. foo_and_barˇ
11324 2. zfoo_and_barˇ
11325 3. foo_and_barˇz
11326 4. zfoo_and_barˇz
11327
11328 foo_and_barˇ
11329 "};
11330 cx.set_state(initial_state);
11331 cx.update_editor(|editor, window, cx| {
11332 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11333 });
11334 handle_completion_request_with_insert_and_replace(
11335 &mut cx,
11336 completion_marked_buffer,
11337 vec![(completion_text, completion_text)],
11338 Arc::new(AtomicUsize::new(0)),
11339 )
11340 .await;
11341 cx.condition(|editor, _| editor.context_menu_visible())
11342 .await;
11343 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11344 editor
11345 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11346 .unwrap()
11347 });
11348 cx.assert_editor_state(expected);
11349 handle_resolve_completion_request(&mut cx, None).await;
11350 apply_additional_edits.await.unwrap();
11351}
11352
11353// This used to crash
11354#[gpui::test]
11355async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11356 init_test(cx, |_| {});
11357
11358 let buffer_text = indoc! {"
11359 fn main() {
11360 10.satu;
11361
11362 //
11363 // separate cursors so they open in different excerpts (manually reproducible)
11364 //
11365
11366 10.satu20;
11367 }
11368 "};
11369 let multibuffer_text_with_selections = indoc! {"
11370 fn main() {
11371 10.satuˇ;
11372
11373 //
11374
11375 //
11376
11377 10.satuˇ20;
11378 }
11379 "};
11380 let expected_multibuffer = indoc! {"
11381 fn main() {
11382 10.saturating_sub()ˇ;
11383
11384 //
11385
11386 //
11387
11388 10.saturating_sub()ˇ;
11389 }
11390 "};
11391
11392 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11393 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11394
11395 let fs = FakeFs::new(cx.executor());
11396 fs.insert_tree(
11397 path!("/a"),
11398 json!({
11399 "main.rs": buffer_text,
11400 }),
11401 )
11402 .await;
11403
11404 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11405 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11406 language_registry.add(rust_lang());
11407 let mut fake_servers = language_registry.register_fake_lsp(
11408 "Rust",
11409 FakeLspAdapter {
11410 capabilities: lsp::ServerCapabilities {
11411 completion_provider: Some(lsp::CompletionOptions {
11412 resolve_provider: None,
11413 ..lsp::CompletionOptions::default()
11414 }),
11415 ..lsp::ServerCapabilities::default()
11416 },
11417 ..FakeLspAdapter::default()
11418 },
11419 );
11420 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11421 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11422 let buffer = project
11423 .update(cx, |project, cx| {
11424 project.open_local_buffer(path!("/a/main.rs"), cx)
11425 })
11426 .await
11427 .unwrap();
11428
11429 let multi_buffer = cx.new(|cx| {
11430 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11431 multi_buffer.push_excerpts(
11432 buffer.clone(),
11433 [ExcerptRange::new(0..first_excerpt_end)],
11434 cx,
11435 );
11436 multi_buffer.push_excerpts(
11437 buffer.clone(),
11438 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11439 cx,
11440 );
11441 multi_buffer
11442 });
11443
11444 let editor = workspace
11445 .update(cx, |_, window, cx| {
11446 cx.new(|cx| {
11447 Editor::new(
11448 EditorMode::Full {
11449 scale_ui_elements_with_buffer_font_size: false,
11450 show_active_line_background: false,
11451 sized_by_content: false,
11452 },
11453 multi_buffer.clone(),
11454 Some(project.clone()),
11455 window,
11456 cx,
11457 )
11458 })
11459 })
11460 .unwrap();
11461
11462 let pane = workspace
11463 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11464 .unwrap();
11465 pane.update_in(cx, |pane, window, cx| {
11466 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11467 });
11468
11469 let fake_server = fake_servers.next().await.unwrap();
11470
11471 editor.update_in(cx, |editor, window, cx| {
11472 editor.change_selections(None, window, cx, |s| {
11473 s.select_ranges([
11474 Point::new(1, 11)..Point::new(1, 11),
11475 Point::new(7, 11)..Point::new(7, 11),
11476 ])
11477 });
11478
11479 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11480 });
11481
11482 editor.update_in(cx, |editor, window, cx| {
11483 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11484 });
11485
11486 fake_server
11487 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11488 let completion_item = lsp::CompletionItem {
11489 label: "saturating_sub()".into(),
11490 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11491 lsp::InsertReplaceEdit {
11492 new_text: "saturating_sub()".to_owned(),
11493 insert: lsp::Range::new(
11494 lsp::Position::new(7, 7),
11495 lsp::Position::new(7, 11),
11496 ),
11497 replace: lsp::Range::new(
11498 lsp::Position::new(7, 7),
11499 lsp::Position::new(7, 13),
11500 ),
11501 },
11502 )),
11503 ..lsp::CompletionItem::default()
11504 };
11505
11506 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11507 })
11508 .next()
11509 .await
11510 .unwrap();
11511
11512 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11513 .await;
11514
11515 editor
11516 .update_in(cx, |editor, window, cx| {
11517 editor
11518 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11519 .unwrap()
11520 })
11521 .await
11522 .unwrap();
11523
11524 editor.update(cx, |editor, cx| {
11525 assert_text_with_selections(editor, expected_multibuffer, cx);
11526 })
11527}
11528
11529#[gpui::test]
11530async fn test_completion(cx: &mut TestAppContext) {
11531 init_test(cx, |_| {});
11532
11533 let mut cx = EditorLspTestContext::new_rust(
11534 lsp::ServerCapabilities {
11535 completion_provider: Some(lsp::CompletionOptions {
11536 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11537 resolve_provider: Some(true),
11538 ..Default::default()
11539 }),
11540 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11541 ..Default::default()
11542 },
11543 cx,
11544 )
11545 .await;
11546 let counter = Arc::new(AtomicUsize::new(0));
11547
11548 cx.set_state(indoc! {"
11549 oneˇ
11550 two
11551 three
11552 "});
11553 cx.simulate_keystroke(".");
11554 handle_completion_request(
11555 indoc! {"
11556 one.|<>
11557 two
11558 three
11559 "},
11560 vec!["first_completion", "second_completion"],
11561 true,
11562 counter.clone(),
11563 &mut cx,
11564 )
11565 .await;
11566 cx.condition(|editor, _| editor.context_menu_visible())
11567 .await;
11568 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11569
11570 let _handler = handle_signature_help_request(
11571 &mut cx,
11572 lsp::SignatureHelp {
11573 signatures: vec![lsp::SignatureInformation {
11574 label: "test signature".to_string(),
11575 documentation: None,
11576 parameters: Some(vec![lsp::ParameterInformation {
11577 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11578 documentation: None,
11579 }]),
11580 active_parameter: None,
11581 }],
11582 active_signature: None,
11583 active_parameter: None,
11584 },
11585 );
11586 cx.update_editor(|editor, window, cx| {
11587 assert!(
11588 !editor.signature_help_state.is_shown(),
11589 "No signature help was called for"
11590 );
11591 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11592 });
11593 cx.run_until_parked();
11594 cx.update_editor(|editor, _, _| {
11595 assert!(
11596 !editor.signature_help_state.is_shown(),
11597 "No signature help should be shown when completions menu is open"
11598 );
11599 });
11600
11601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11602 editor.context_menu_next(&Default::default(), window, cx);
11603 editor
11604 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11605 .unwrap()
11606 });
11607 cx.assert_editor_state(indoc! {"
11608 one.second_completionˇ
11609 two
11610 three
11611 "});
11612
11613 handle_resolve_completion_request(
11614 &mut cx,
11615 Some(vec![
11616 (
11617 //This overlaps with the primary completion edit which is
11618 //misbehavior from the LSP spec, test that we filter it out
11619 indoc! {"
11620 one.second_ˇcompletion
11621 two
11622 threeˇ
11623 "},
11624 "overlapping additional edit",
11625 ),
11626 (
11627 indoc! {"
11628 one.second_completion
11629 two
11630 threeˇ
11631 "},
11632 "\nadditional edit",
11633 ),
11634 ]),
11635 )
11636 .await;
11637 apply_additional_edits.await.unwrap();
11638 cx.assert_editor_state(indoc! {"
11639 one.second_completionˇ
11640 two
11641 three
11642 additional edit
11643 "});
11644
11645 cx.set_state(indoc! {"
11646 one.second_completion
11647 twoˇ
11648 threeˇ
11649 additional edit
11650 "});
11651 cx.simulate_keystroke(" ");
11652 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11653 cx.simulate_keystroke("s");
11654 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11655
11656 cx.assert_editor_state(indoc! {"
11657 one.second_completion
11658 two sˇ
11659 three sˇ
11660 additional edit
11661 "});
11662 handle_completion_request(
11663 indoc! {"
11664 one.second_completion
11665 two s
11666 three <s|>
11667 additional edit
11668 "},
11669 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11670 true,
11671 counter.clone(),
11672 &mut cx,
11673 )
11674 .await;
11675 cx.condition(|editor, _| editor.context_menu_visible())
11676 .await;
11677 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11678
11679 cx.simulate_keystroke("i");
11680
11681 handle_completion_request(
11682 indoc! {"
11683 one.second_completion
11684 two si
11685 three <si|>
11686 additional edit
11687 "},
11688 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11689 true,
11690 counter.clone(),
11691 &mut cx,
11692 )
11693 .await;
11694 cx.condition(|editor, _| editor.context_menu_visible())
11695 .await;
11696 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11697
11698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11699 editor
11700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11701 .unwrap()
11702 });
11703 cx.assert_editor_state(indoc! {"
11704 one.second_completion
11705 two sixth_completionˇ
11706 three sixth_completionˇ
11707 additional edit
11708 "});
11709
11710 apply_additional_edits.await.unwrap();
11711
11712 update_test_language_settings(&mut cx, |settings| {
11713 settings.defaults.show_completions_on_input = Some(false);
11714 });
11715 cx.set_state("editorˇ");
11716 cx.simulate_keystroke(".");
11717 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11718 cx.simulate_keystrokes("c l o");
11719 cx.assert_editor_state("editor.cloˇ");
11720 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11721 cx.update_editor(|editor, window, cx| {
11722 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11723 });
11724 handle_completion_request(
11725 "editor.<clo|>",
11726 vec!["close", "clobber"],
11727 true,
11728 counter.clone(),
11729 &mut cx,
11730 )
11731 .await;
11732 cx.condition(|editor, _| editor.context_menu_visible())
11733 .await;
11734 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11735
11736 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11737 editor
11738 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11739 .unwrap()
11740 });
11741 cx.assert_editor_state("editor.closeˇ");
11742 handle_resolve_completion_request(&mut cx, None).await;
11743 apply_additional_edits.await.unwrap();
11744}
11745
11746#[gpui::test]
11747async fn test_completion_reuse(cx: &mut TestAppContext) {
11748 init_test(cx, |_| {});
11749
11750 let mut cx = EditorLspTestContext::new_rust(
11751 lsp::ServerCapabilities {
11752 completion_provider: Some(lsp::CompletionOptions {
11753 trigger_characters: Some(vec![".".to_string()]),
11754 ..Default::default()
11755 }),
11756 ..Default::default()
11757 },
11758 cx,
11759 )
11760 .await;
11761
11762 let counter = Arc::new(AtomicUsize::new(0));
11763 cx.set_state("objˇ");
11764 cx.simulate_keystroke(".");
11765
11766 // Initial completion request returns complete results
11767 let is_incomplete = false;
11768 handle_completion_request(
11769 "obj.|<>",
11770 vec!["a", "ab", "abc"],
11771 is_incomplete,
11772 counter.clone(),
11773 &mut cx,
11774 )
11775 .await;
11776 cx.run_until_parked();
11777 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11778 cx.assert_editor_state("obj.ˇ");
11779 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11780
11781 // Type "a" - filters existing completions
11782 cx.simulate_keystroke("a");
11783 cx.run_until_parked();
11784 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11785 cx.assert_editor_state("obj.aˇ");
11786 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11787
11788 // Type "b" - filters existing completions
11789 cx.simulate_keystroke("b");
11790 cx.run_until_parked();
11791 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11792 cx.assert_editor_state("obj.abˇ");
11793 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11794
11795 // Type "c" - filters existing completions
11796 cx.simulate_keystroke("c");
11797 cx.run_until_parked();
11798 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11799 cx.assert_editor_state("obj.abcˇ");
11800 check_displayed_completions(vec!["abc"], &mut cx);
11801
11802 // Backspace to delete "c" - filters existing completions
11803 cx.update_editor(|editor, window, cx| {
11804 editor.backspace(&Backspace, window, cx);
11805 });
11806 cx.run_until_parked();
11807 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11808 cx.assert_editor_state("obj.abˇ");
11809 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11810
11811 // Moving cursor to the left dismisses menu.
11812 cx.update_editor(|editor, window, cx| {
11813 editor.move_left(&MoveLeft, window, cx);
11814 });
11815 cx.run_until_parked();
11816 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11817 cx.assert_editor_state("obj.aˇb");
11818 cx.update_editor(|editor, _, _| {
11819 assert_eq!(editor.context_menu_visible(), false);
11820 });
11821
11822 // Type "b" - new request
11823 cx.simulate_keystroke("b");
11824 let is_incomplete = false;
11825 handle_completion_request(
11826 "obj.<ab|>a",
11827 vec!["ab", "abc"],
11828 is_incomplete,
11829 counter.clone(),
11830 &mut cx,
11831 )
11832 .await;
11833 cx.run_until_parked();
11834 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11835 cx.assert_editor_state("obj.abˇb");
11836 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11837
11838 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11839 cx.update_editor(|editor, window, cx| {
11840 editor.backspace(&Backspace, window, cx);
11841 });
11842 let is_incomplete = false;
11843 handle_completion_request(
11844 "obj.<a|>b",
11845 vec!["a", "ab", "abc"],
11846 is_incomplete,
11847 counter.clone(),
11848 &mut cx,
11849 )
11850 .await;
11851 cx.run_until_parked();
11852 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11853 cx.assert_editor_state("obj.aˇb");
11854 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11855
11856 // Backspace to delete "a" - dismisses menu.
11857 cx.update_editor(|editor, window, cx| {
11858 editor.backspace(&Backspace, window, cx);
11859 });
11860 cx.run_until_parked();
11861 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11862 cx.assert_editor_state("obj.ˇb");
11863 cx.update_editor(|editor, _, _| {
11864 assert_eq!(editor.context_menu_visible(), false);
11865 });
11866}
11867
11868#[gpui::test]
11869async fn test_word_completion(cx: &mut TestAppContext) {
11870 let lsp_fetch_timeout_ms = 10;
11871 init_test(cx, |language_settings| {
11872 language_settings.defaults.completions = Some(CompletionSettings {
11873 words: WordsCompletionMode::Fallback,
11874 lsp: true,
11875 lsp_fetch_timeout_ms: 10,
11876 lsp_insert_mode: LspInsertMode::Insert,
11877 });
11878 });
11879
11880 let mut cx = EditorLspTestContext::new_rust(
11881 lsp::ServerCapabilities {
11882 completion_provider: Some(lsp::CompletionOptions {
11883 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11884 ..lsp::CompletionOptions::default()
11885 }),
11886 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11887 ..lsp::ServerCapabilities::default()
11888 },
11889 cx,
11890 )
11891 .await;
11892
11893 let throttle_completions = Arc::new(AtomicBool::new(false));
11894
11895 let lsp_throttle_completions = throttle_completions.clone();
11896 let _completion_requests_handler =
11897 cx.lsp
11898 .server
11899 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11900 let lsp_throttle_completions = lsp_throttle_completions.clone();
11901 let cx = cx.clone();
11902 async move {
11903 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11904 cx.background_executor()
11905 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11906 .await;
11907 }
11908 Ok(Some(lsp::CompletionResponse::Array(vec![
11909 lsp::CompletionItem {
11910 label: "first".into(),
11911 ..lsp::CompletionItem::default()
11912 },
11913 lsp::CompletionItem {
11914 label: "last".into(),
11915 ..lsp::CompletionItem::default()
11916 },
11917 ])))
11918 }
11919 });
11920
11921 cx.set_state(indoc! {"
11922 oneˇ
11923 two
11924 three
11925 "});
11926 cx.simulate_keystroke(".");
11927 cx.executor().run_until_parked();
11928 cx.condition(|editor, _| editor.context_menu_visible())
11929 .await;
11930 cx.update_editor(|editor, window, cx| {
11931 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11932 {
11933 assert_eq!(
11934 completion_menu_entries(&menu),
11935 &["first", "last"],
11936 "When LSP server is fast to reply, no fallback word completions are used"
11937 );
11938 } else {
11939 panic!("expected completion menu to be open");
11940 }
11941 editor.cancel(&Cancel, window, cx);
11942 });
11943 cx.executor().run_until_parked();
11944 cx.condition(|editor, _| !editor.context_menu_visible())
11945 .await;
11946
11947 throttle_completions.store(true, atomic::Ordering::Release);
11948 cx.simulate_keystroke(".");
11949 cx.executor()
11950 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11951 cx.executor().run_until_parked();
11952 cx.condition(|editor, _| editor.context_menu_visible())
11953 .await;
11954 cx.update_editor(|editor, _, _| {
11955 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11956 {
11957 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11958 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11959 } else {
11960 panic!("expected completion menu to be open");
11961 }
11962 });
11963}
11964
11965#[gpui::test]
11966async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11967 init_test(cx, |language_settings| {
11968 language_settings.defaults.completions = Some(CompletionSettings {
11969 words: WordsCompletionMode::Enabled,
11970 lsp: true,
11971 lsp_fetch_timeout_ms: 0,
11972 lsp_insert_mode: LspInsertMode::Insert,
11973 });
11974 });
11975
11976 let mut cx = EditorLspTestContext::new_rust(
11977 lsp::ServerCapabilities {
11978 completion_provider: Some(lsp::CompletionOptions {
11979 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11980 ..lsp::CompletionOptions::default()
11981 }),
11982 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11983 ..lsp::ServerCapabilities::default()
11984 },
11985 cx,
11986 )
11987 .await;
11988
11989 let _completion_requests_handler =
11990 cx.lsp
11991 .server
11992 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11993 Ok(Some(lsp::CompletionResponse::Array(vec![
11994 lsp::CompletionItem {
11995 label: "first".into(),
11996 ..lsp::CompletionItem::default()
11997 },
11998 lsp::CompletionItem {
11999 label: "last".into(),
12000 ..lsp::CompletionItem::default()
12001 },
12002 ])))
12003 });
12004
12005 cx.set_state(indoc! {"ˇ
12006 first
12007 last
12008 second
12009 "});
12010 cx.simulate_keystroke(".");
12011 cx.executor().run_until_parked();
12012 cx.condition(|editor, _| editor.context_menu_visible())
12013 .await;
12014 cx.update_editor(|editor, _, _| {
12015 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12016 {
12017 assert_eq!(
12018 completion_menu_entries(&menu),
12019 &["first", "last", "second"],
12020 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12021 );
12022 } else {
12023 panic!("expected completion menu to be open");
12024 }
12025 });
12026}
12027
12028#[gpui::test]
12029async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12030 init_test(cx, |language_settings| {
12031 language_settings.defaults.completions = Some(CompletionSettings {
12032 words: WordsCompletionMode::Disabled,
12033 lsp: true,
12034 lsp_fetch_timeout_ms: 0,
12035 lsp_insert_mode: LspInsertMode::Insert,
12036 });
12037 });
12038
12039 let mut cx = EditorLspTestContext::new_rust(
12040 lsp::ServerCapabilities {
12041 completion_provider: Some(lsp::CompletionOptions {
12042 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12043 ..lsp::CompletionOptions::default()
12044 }),
12045 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12046 ..lsp::ServerCapabilities::default()
12047 },
12048 cx,
12049 )
12050 .await;
12051
12052 let _completion_requests_handler =
12053 cx.lsp
12054 .server
12055 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12056 panic!("LSP completions should not be queried when dealing with word completions")
12057 });
12058
12059 cx.set_state(indoc! {"ˇ
12060 first
12061 last
12062 second
12063 "});
12064 cx.update_editor(|editor, window, cx| {
12065 editor.show_word_completions(&ShowWordCompletions, window, cx);
12066 });
12067 cx.executor().run_until_parked();
12068 cx.condition(|editor, _| editor.context_menu_visible())
12069 .await;
12070 cx.update_editor(|editor, _, _| {
12071 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12072 {
12073 assert_eq!(
12074 completion_menu_entries(&menu),
12075 &["first", "last", "second"],
12076 "`ShowWordCompletions` action should show word completions"
12077 );
12078 } else {
12079 panic!("expected completion menu to be open");
12080 }
12081 });
12082
12083 cx.simulate_keystroke("l");
12084 cx.executor().run_until_parked();
12085 cx.condition(|editor, _| editor.context_menu_visible())
12086 .await;
12087 cx.update_editor(|editor, _, _| {
12088 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12089 {
12090 assert_eq!(
12091 completion_menu_entries(&menu),
12092 &["last"],
12093 "After showing word completions, further editing should filter them and not query the LSP"
12094 );
12095 } else {
12096 panic!("expected completion menu to be open");
12097 }
12098 });
12099}
12100
12101#[gpui::test]
12102async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12103 init_test(cx, |language_settings| {
12104 language_settings.defaults.completions = Some(CompletionSettings {
12105 words: WordsCompletionMode::Fallback,
12106 lsp: false,
12107 lsp_fetch_timeout_ms: 0,
12108 lsp_insert_mode: LspInsertMode::Insert,
12109 });
12110 });
12111
12112 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12113
12114 cx.set_state(indoc! {"ˇ
12115 0_usize
12116 let
12117 33
12118 4.5f32
12119 "});
12120 cx.update_editor(|editor, window, cx| {
12121 editor.show_completions(&ShowCompletions::default(), window, cx);
12122 });
12123 cx.executor().run_until_parked();
12124 cx.condition(|editor, _| editor.context_menu_visible())
12125 .await;
12126 cx.update_editor(|editor, window, cx| {
12127 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12128 {
12129 assert_eq!(
12130 completion_menu_entries(&menu),
12131 &["let"],
12132 "With no digits in the completion query, no digits should be in the word completions"
12133 );
12134 } else {
12135 panic!("expected completion menu to be open");
12136 }
12137 editor.cancel(&Cancel, window, cx);
12138 });
12139
12140 cx.set_state(indoc! {"3ˇ
12141 0_usize
12142 let
12143 3
12144 33.35f32
12145 "});
12146 cx.update_editor(|editor, window, cx| {
12147 editor.show_completions(&ShowCompletions::default(), window, cx);
12148 });
12149 cx.executor().run_until_parked();
12150 cx.condition(|editor, _| editor.context_menu_visible())
12151 .await;
12152 cx.update_editor(|editor, _, _| {
12153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12154 {
12155 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12156 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12157 } else {
12158 panic!("expected completion menu to be open");
12159 }
12160 });
12161}
12162
12163fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12164 let position = || lsp::Position {
12165 line: params.text_document_position.position.line,
12166 character: params.text_document_position.position.character,
12167 };
12168 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12169 range: lsp::Range {
12170 start: position(),
12171 end: position(),
12172 },
12173 new_text: text.to_string(),
12174 }))
12175}
12176
12177#[gpui::test]
12178async fn test_multiline_completion(cx: &mut TestAppContext) {
12179 init_test(cx, |_| {});
12180
12181 let fs = FakeFs::new(cx.executor());
12182 fs.insert_tree(
12183 path!("/a"),
12184 json!({
12185 "main.ts": "a",
12186 }),
12187 )
12188 .await;
12189
12190 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12191 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12192 let typescript_language = Arc::new(Language::new(
12193 LanguageConfig {
12194 name: "TypeScript".into(),
12195 matcher: LanguageMatcher {
12196 path_suffixes: vec!["ts".to_string()],
12197 ..LanguageMatcher::default()
12198 },
12199 line_comments: vec!["// ".into()],
12200 ..LanguageConfig::default()
12201 },
12202 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12203 ));
12204 language_registry.add(typescript_language.clone());
12205 let mut fake_servers = language_registry.register_fake_lsp(
12206 "TypeScript",
12207 FakeLspAdapter {
12208 capabilities: lsp::ServerCapabilities {
12209 completion_provider: Some(lsp::CompletionOptions {
12210 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12211 ..lsp::CompletionOptions::default()
12212 }),
12213 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12214 ..lsp::ServerCapabilities::default()
12215 },
12216 // Emulate vtsls label generation
12217 label_for_completion: Some(Box::new(|item, _| {
12218 let text = if let Some(description) = item
12219 .label_details
12220 .as_ref()
12221 .and_then(|label_details| label_details.description.as_ref())
12222 {
12223 format!("{} {}", item.label, description)
12224 } else if let Some(detail) = &item.detail {
12225 format!("{} {}", item.label, detail)
12226 } else {
12227 item.label.clone()
12228 };
12229 let len = text.len();
12230 Some(language::CodeLabel {
12231 text,
12232 runs: Vec::new(),
12233 filter_range: 0..len,
12234 })
12235 })),
12236 ..FakeLspAdapter::default()
12237 },
12238 );
12239 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12240 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12241 let worktree_id = workspace
12242 .update(cx, |workspace, _window, cx| {
12243 workspace.project().update(cx, |project, cx| {
12244 project.worktrees(cx).next().unwrap().read(cx).id()
12245 })
12246 })
12247 .unwrap();
12248 let _buffer = project
12249 .update(cx, |project, cx| {
12250 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12251 })
12252 .await
12253 .unwrap();
12254 let editor = workspace
12255 .update(cx, |workspace, window, cx| {
12256 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12257 })
12258 .unwrap()
12259 .await
12260 .unwrap()
12261 .downcast::<Editor>()
12262 .unwrap();
12263 let fake_server = fake_servers.next().await.unwrap();
12264
12265 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12266 let multiline_label_2 = "a\nb\nc\n";
12267 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12268 let multiline_description = "d\ne\nf\n";
12269 let multiline_detail_2 = "g\nh\ni\n";
12270
12271 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12272 move |params, _| async move {
12273 Ok(Some(lsp::CompletionResponse::Array(vec![
12274 lsp::CompletionItem {
12275 label: multiline_label.to_string(),
12276 text_edit: gen_text_edit(¶ms, "new_text_1"),
12277 ..lsp::CompletionItem::default()
12278 },
12279 lsp::CompletionItem {
12280 label: "single line label 1".to_string(),
12281 detail: Some(multiline_detail.to_string()),
12282 text_edit: gen_text_edit(¶ms, "new_text_2"),
12283 ..lsp::CompletionItem::default()
12284 },
12285 lsp::CompletionItem {
12286 label: "single line label 2".to_string(),
12287 label_details: Some(lsp::CompletionItemLabelDetails {
12288 description: Some(multiline_description.to_string()),
12289 detail: None,
12290 }),
12291 text_edit: gen_text_edit(¶ms, "new_text_2"),
12292 ..lsp::CompletionItem::default()
12293 },
12294 lsp::CompletionItem {
12295 label: multiline_label_2.to_string(),
12296 detail: Some(multiline_detail_2.to_string()),
12297 text_edit: gen_text_edit(¶ms, "new_text_3"),
12298 ..lsp::CompletionItem::default()
12299 },
12300 lsp::CompletionItem {
12301 label: "Label with many spaces and \t but without newlines".to_string(),
12302 detail: Some(
12303 "Details with many spaces and \t but without newlines".to_string(),
12304 ),
12305 text_edit: gen_text_edit(¶ms, "new_text_4"),
12306 ..lsp::CompletionItem::default()
12307 },
12308 ])))
12309 },
12310 );
12311
12312 editor.update_in(cx, |editor, window, cx| {
12313 cx.focus_self(window);
12314 editor.move_to_end(&MoveToEnd, window, cx);
12315 editor.handle_input(".", window, cx);
12316 });
12317 cx.run_until_parked();
12318 completion_handle.next().await.unwrap();
12319
12320 editor.update(cx, |editor, _| {
12321 assert!(editor.context_menu_visible());
12322 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12323 {
12324 let completion_labels = menu
12325 .completions
12326 .borrow()
12327 .iter()
12328 .map(|c| c.label.text.clone())
12329 .collect::<Vec<_>>();
12330 assert_eq!(
12331 completion_labels,
12332 &[
12333 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12334 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12335 "single line label 2 d e f ",
12336 "a b c g h i ",
12337 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12338 ],
12339 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12340 );
12341
12342 for completion in menu
12343 .completions
12344 .borrow()
12345 .iter() {
12346 assert_eq!(
12347 completion.label.filter_range,
12348 0..completion.label.text.len(),
12349 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12350 );
12351 }
12352 } else {
12353 panic!("expected completion menu to be open");
12354 }
12355 });
12356}
12357
12358#[gpui::test]
12359async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12360 init_test(cx, |_| {});
12361 let mut cx = EditorLspTestContext::new_rust(
12362 lsp::ServerCapabilities {
12363 completion_provider: Some(lsp::CompletionOptions {
12364 trigger_characters: Some(vec![".".to_string()]),
12365 ..Default::default()
12366 }),
12367 ..Default::default()
12368 },
12369 cx,
12370 )
12371 .await;
12372 cx.lsp
12373 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12374 Ok(Some(lsp::CompletionResponse::Array(vec![
12375 lsp::CompletionItem {
12376 label: "first".into(),
12377 ..Default::default()
12378 },
12379 lsp::CompletionItem {
12380 label: "last".into(),
12381 ..Default::default()
12382 },
12383 ])))
12384 });
12385 cx.set_state("variableˇ");
12386 cx.simulate_keystroke(".");
12387 cx.executor().run_until_parked();
12388
12389 cx.update_editor(|editor, _, _| {
12390 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12391 {
12392 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12393 } else {
12394 panic!("expected completion menu to be open");
12395 }
12396 });
12397
12398 cx.update_editor(|editor, window, cx| {
12399 editor.move_page_down(&MovePageDown::default(), window, cx);
12400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12401 {
12402 assert!(
12403 menu.selected_item == 1,
12404 "expected PageDown to select the last item from the context menu"
12405 );
12406 } else {
12407 panic!("expected completion menu to stay open after PageDown");
12408 }
12409 });
12410
12411 cx.update_editor(|editor, window, cx| {
12412 editor.move_page_up(&MovePageUp::default(), window, cx);
12413 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12414 {
12415 assert!(
12416 menu.selected_item == 0,
12417 "expected PageUp to select the first item from the context menu"
12418 );
12419 } else {
12420 panic!("expected completion menu to stay open after PageUp");
12421 }
12422 });
12423}
12424
12425#[gpui::test]
12426async fn test_as_is_completions(cx: &mut TestAppContext) {
12427 init_test(cx, |_| {});
12428 let mut cx = EditorLspTestContext::new_rust(
12429 lsp::ServerCapabilities {
12430 completion_provider: Some(lsp::CompletionOptions {
12431 ..Default::default()
12432 }),
12433 ..Default::default()
12434 },
12435 cx,
12436 )
12437 .await;
12438 cx.lsp
12439 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12440 Ok(Some(lsp::CompletionResponse::Array(vec![
12441 lsp::CompletionItem {
12442 label: "unsafe".into(),
12443 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12444 range: lsp::Range {
12445 start: lsp::Position {
12446 line: 1,
12447 character: 2,
12448 },
12449 end: lsp::Position {
12450 line: 1,
12451 character: 3,
12452 },
12453 },
12454 new_text: "unsafe".to_string(),
12455 })),
12456 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12457 ..Default::default()
12458 },
12459 ])))
12460 });
12461 cx.set_state("fn a() {}\n nˇ");
12462 cx.executor().run_until_parked();
12463 cx.update_editor(|editor, window, cx| {
12464 editor.show_completions(
12465 &ShowCompletions {
12466 trigger: Some("\n".into()),
12467 },
12468 window,
12469 cx,
12470 );
12471 });
12472 cx.executor().run_until_parked();
12473
12474 cx.update_editor(|editor, window, cx| {
12475 editor.confirm_completion(&Default::default(), window, cx)
12476 });
12477 cx.executor().run_until_parked();
12478 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12479}
12480
12481#[gpui::test]
12482async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12483 init_test(cx, |_| {});
12484
12485 let mut cx = EditorLspTestContext::new_rust(
12486 lsp::ServerCapabilities {
12487 completion_provider: Some(lsp::CompletionOptions {
12488 trigger_characters: Some(vec![".".to_string()]),
12489 resolve_provider: Some(true),
12490 ..Default::default()
12491 }),
12492 ..Default::default()
12493 },
12494 cx,
12495 )
12496 .await;
12497
12498 cx.set_state("fn main() { let a = 2ˇ; }");
12499 cx.simulate_keystroke(".");
12500 let completion_item = lsp::CompletionItem {
12501 label: "Some".into(),
12502 kind: Some(lsp::CompletionItemKind::SNIPPET),
12503 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12504 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12505 kind: lsp::MarkupKind::Markdown,
12506 value: "```rust\nSome(2)\n```".to_string(),
12507 })),
12508 deprecated: Some(false),
12509 sort_text: Some("Some".to_string()),
12510 filter_text: Some("Some".to_string()),
12511 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12512 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12513 range: lsp::Range {
12514 start: lsp::Position {
12515 line: 0,
12516 character: 22,
12517 },
12518 end: lsp::Position {
12519 line: 0,
12520 character: 22,
12521 },
12522 },
12523 new_text: "Some(2)".to_string(),
12524 })),
12525 additional_text_edits: Some(vec![lsp::TextEdit {
12526 range: lsp::Range {
12527 start: lsp::Position {
12528 line: 0,
12529 character: 20,
12530 },
12531 end: lsp::Position {
12532 line: 0,
12533 character: 22,
12534 },
12535 },
12536 new_text: "".to_string(),
12537 }]),
12538 ..Default::default()
12539 };
12540
12541 let closure_completion_item = completion_item.clone();
12542 let counter = Arc::new(AtomicUsize::new(0));
12543 let counter_clone = counter.clone();
12544 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12545 let task_completion_item = closure_completion_item.clone();
12546 counter_clone.fetch_add(1, atomic::Ordering::Release);
12547 async move {
12548 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12549 is_incomplete: true,
12550 item_defaults: None,
12551 items: vec![task_completion_item],
12552 })))
12553 }
12554 });
12555
12556 cx.condition(|editor, _| editor.context_menu_visible())
12557 .await;
12558 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12559 assert!(request.next().await.is_some());
12560 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12561
12562 cx.simulate_keystrokes("S o m");
12563 cx.condition(|editor, _| editor.context_menu_visible())
12564 .await;
12565 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12566 assert!(request.next().await.is_some());
12567 assert!(request.next().await.is_some());
12568 assert!(request.next().await.is_some());
12569 request.close();
12570 assert!(request.next().await.is_none());
12571 assert_eq!(
12572 counter.load(atomic::Ordering::Acquire),
12573 4,
12574 "With the completions menu open, only one LSP request should happen per input"
12575 );
12576}
12577
12578#[gpui::test]
12579async fn test_toggle_comment(cx: &mut TestAppContext) {
12580 init_test(cx, |_| {});
12581 let mut cx = EditorTestContext::new(cx).await;
12582 let language = Arc::new(Language::new(
12583 LanguageConfig {
12584 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12585 ..Default::default()
12586 },
12587 Some(tree_sitter_rust::LANGUAGE.into()),
12588 ));
12589 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12590
12591 // If multiple selections intersect a line, the line is only toggled once.
12592 cx.set_state(indoc! {"
12593 fn a() {
12594 «//b();
12595 ˇ»// «c();
12596 //ˇ» d();
12597 }
12598 "});
12599
12600 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12601
12602 cx.assert_editor_state(indoc! {"
12603 fn a() {
12604 «b();
12605 c();
12606 ˇ» d();
12607 }
12608 "});
12609
12610 // The comment prefix is inserted at the same column for every line in a
12611 // selection.
12612 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12613
12614 cx.assert_editor_state(indoc! {"
12615 fn a() {
12616 // «b();
12617 // c();
12618 ˇ»// d();
12619 }
12620 "});
12621
12622 // If a selection ends at the beginning of a line, that line is not toggled.
12623 cx.set_selections_state(indoc! {"
12624 fn a() {
12625 // b();
12626 «// c();
12627 ˇ» // d();
12628 }
12629 "});
12630
12631 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12632
12633 cx.assert_editor_state(indoc! {"
12634 fn a() {
12635 // b();
12636 «c();
12637 ˇ» // d();
12638 }
12639 "});
12640
12641 // If a selection span a single line and is empty, the line is toggled.
12642 cx.set_state(indoc! {"
12643 fn a() {
12644 a();
12645 b();
12646 ˇ
12647 }
12648 "});
12649
12650 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12651
12652 cx.assert_editor_state(indoc! {"
12653 fn a() {
12654 a();
12655 b();
12656 //•ˇ
12657 }
12658 "});
12659
12660 // If a selection span multiple lines, empty lines are not toggled.
12661 cx.set_state(indoc! {"
12662 fn a() {
12663 «a();
12664
12665 c();ˇ»
12666 }
12667 "});
12668
12669 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12670
12671 cx.assert_editor_state(indoc! {"
12672 fn a() {
12673 // «a();
12674
12675 // c();ˇ»
12676 }
12677 "});
12678
12679 // If a selection includes multiple comment prefixes, all lines are uncommented.
12680 cx.set_state(indoc! {"
12681 fn a() {
12682 «// a();
12683 /// b();
12684 //! c();ˇ»
12685 }
12686 "});
12687
12688 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12689
12690 cx.assert_editor_state(indoc! {"
12691 fn a() {
12692 «a();
12693 b();
12694 c();ˇ»
12695 }
12696 "});
12697}
12698
12699#[gpui::test]
12700async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12701 init_test(cx, |_| {});
12702 let mut cx = EditorTestContext::new(cx).await;
12703 let language = Arc::new(Language::new(
12704 LanguageConfig {
12705 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12706 ..Default::default()
12707 },
12708 Some(tree_sitter_rust::LANGUAGE.into()),
12709 ));
12710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12711
12712 let toggle_comments = &ToggleComments {
12713 advance_downwards: false,
12714 ignore_indent: true,
12715 };
12716
12717 // If multiple selections intersect a line, the line is only toggled once.
12718 cx.set_state(indoc! {"
12719 fn a() {
12720 // «b();
12721 // c();
12722 // ˇ» d();
12723 }
12724 "});
12725
12726 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12727
12728 cx.assert_editor_state(indoc! {"
12729 fn a() {
12730 «b();
12731 c();
12732 ˇ» d();
12733 }
12734 "});
12735
12736 // The comment prefix is inserted at the beginning of each line
12737 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12738
12739 cx.assert_editor_state(indoc! {"
12740 fn a() {
12741 // «b();
12742 // c();
12743 // ˇ» d();
12744 }
12745 "});
12746
12747 // If a selection ends at the beginning of a line, that line is not toggled.
12748 cx.set_selections_state(indoc! {"
12749 fn a() {
12750 // b();
12751 // «c();
12752 ˇ»// d();
12753 }
12754 "});
12755
12756 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12757
12758 cx.assert_editor_state(indoc! {"
12759 fn a() {
12760 // b();
12761 «c();
12762 ˇ»// d();
12763 }
12764 "});
12765
12766 // If a selection span a single line and is empty, the line is toggled.
12767 cx.set_state(indoc! {"
12768 fn a() {
12769 a();
12770 b();
12771 ˇ
12772 }
12773 "});
12774
12775 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12776
12777 cx.assert_editor_state(indoc! {"
12778 fn a() {
12779 a();
12780 b();
12781 //ˇ
12782 }
12783 "});
12784
12785 // If a selection span multiple lines, empty lines are not toggled.
12786 cx.set_state(indoc! {"
12787 fn a() {
12788 «a();
12789
12790 c();ˇ»
12791 }
12792 "});
12793
12794 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12795
12796 cx.assert_editor_state(indoc! {"
12797 fn a() {
12798 // «a();
12799
12800 // c();ˇ»
12801 }
12802 "});
12803
12804 // If a selection includes multiple comment prefixes, all lines are uncommented.
12805 cx.set_state(indoc! {"
12806 fn a() {
12807 // «a();
12808 /// b();
12809 //! c();ˇ»
12810 }
12811 "});
12812
12813 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12814
12815 cx.assert_editor_state(indoc! {"
12816 fn a() {
12817 «a();
12818 b();
12819 c();ˇ»
12820 }
12821 "});
12822}
12823
12824#[gpui::test]
12825async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12826 init_test(cx, |_| {});
12827
12828 let language = Arc::new(Language::new(
12829 LanguageConfig {
12830 line_comments: vec!["// ".into()],
12831 ..Default::default()
12832 },
12833 Some(tree_sitter_rust::LANGUAGE.into()),
12834 ));
12835
12836 let mut cx = EditorTestContext::new(cx).await;
12837
12838 cx.language_registry().add(language.clone());
12839 cx.update_buffer(|buffer, cx| {
12840 buffer.set_language(Some(language), cx);
12841 });
12842
12843 let toggle_comments = &ToggleComments {
12844 advance_downwards: true,
12845 ignore_indent: false,
12846 };
12847
12848 // Single cursor on one line -> advance
12849 // Cursor moves horizontally 3 characters as well on non-blank line
12850 cx.set_state(indoc!(
12851 "fn a() {
12852 ˇdog();
12853 cat();
12854 }"
12855 ));
12856 cx.update_editor(|editor, window, cx| {
12857 editor.toggle_comments(toggle_comments, window, cx);
12858 });
12859 cx.assert_editor_state(indoc!(
12860 "fn a() {
12861 // dog();
12862 catˇ();
12863 }"
12864 ));
12865
12866 // Single selection on one line -> don't advance
12867 cx.set_state(indoc!(
12868 "fn a() {
12869 «dog()ˇ»;
12870 cat();
12871 }"
12872 ));
12873 cx.update_editor(|editor, window, cx| {
12874 editor.toggle_comments(toggle_comments, window, cx);
12875 });
12876 cx.assert_editor_state(indoc!(
12877 "fn a() {
12878 // «dog()ˇ»;
12879 cat();
12880 }"
12881 ));
12882
12883 // Multiple cursors on one line -> advance
12884 cx.set_state(indoc!(
12885 "fn a() {
12886 ˇdˇog();
12887 cat();
12888 }"
12889 ));
12890 cx.update_editor(|editor, window, cx| {
12891 editor.toggle_comments(toggle_comments, window, cx);
12892 });
12893 cx.assert_editor_state(indoc!(
12894 "fn a() {
12895 // dog();
12896 catˇ(ˇ);
12897 }"
12898 ));
12899
12900 // Multiple cursors on one line, with selection -> don't advance
12901 cx.set_state(indoc!(
12902 "fn a() {
12903 ˇdˇog«()ˇ»;
12904 cat();
12905 }"
12906 ));
12907 cx.update_editor(|editor, window, cx| {
12908 editor.toggle_comments(toggle_comments, window, cx);
12909 });
12910 cx.assert_editor_state(indoc!(
12911 "fn a() {
12912 // ˇdˇog«()ˇ»;
12913 cat();
12914 }"
12915 ));
12916
12917 // Single cursor on one line -> advance
12918 // Cursor moves to column 0 on blank line
12919 cx.set_state(indoc!(
12920 "fn a() {
12921 ˇdog();
12922
12923 cat();
12924 }"
12925 ));
12926 cx.update_editor(|editor, window, cx| {
12927 editor.toggle_comments(toggle_comments, window, cx);
12928 });
12929 cx.assert_editor_state(indoc!(
12930 "fn a() {
12931 // dog();
12932 ˇ
12933 cat();
12934 }"
12935 ));
12936
12937 // Single cursor on one line -> advance
12938 // Cursor starts and ends at column 0
12939 cx.set_state(indoc!(
12940 "fn a() {
12941 ˇ dog();
12942 cat();
12943 }"
12944 ));
12945 cx.update_editor(|editor, window, cx| {
12946 editor.toggle_comments(toggle_comments, window, cx);
12947 });
12948 cx.assert_editor_state(indoc!(
12949 "fn a() {
12950 // dog();
12951 ˇ cat();
12952 }"
12953 ));
12954}
12955
12956#[gpui::test]
12957async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12958 init_test(cx, |_| {});
12959
12960 let mut cx = EditorTestContext::new(cx).await;
12961
12962 let html_language = Arc::new(
12963 Language::new(
12964 LanguageConfig {
12965 name: "HTML".into(),
12966 block_comment: Some(("<!-- ".into(), " -->".into())),
12967 ..Default::default()
12968 },
12969 Some(tree_sitter_html::LANGUAGE.into()),
12970 )
12971 .with_injection_query(
12972 r#"
12973 (script_element
12974 (raw_text) @injection.content
12975 (#set! injection.language "javascript"))
12976 "#,
12977 )
12978 .unwrap(),
12979 );
12980
12981 let javascript_language = Arc::new(Language::new(
12982 LanguageConfig {
12983 name: "JavaScript".into(),
12984 line_comments: vec!["// ".into()],
12985 ..Default::default()
12986 },
12987 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12988 ));
12989
12990 cx.language_registry().add(html_language.clone());
12991 cx.language_registry().add(javascript_language.clone());
12992 cx.update_buffer(|buffer, cx| {
12993 buffer.set_language(Some(html_language), cx);
12994 });
12995
12996 // Toggle comments for empty selections
12997 cx.set_state(
12998 &r#"
12999 <p>A</p>ˇ
13000 <p>B</p>ˇ
13001 <p>C</p>ˇ
13002 "#
13003 .unindent(),
13004 );
13005 cx.update_editor(|editor, window, cx| {
13006 editor.toggle_comments(&ToggleComments::default(), window, cx)
13007 });
13008 cx.assert_editor_state(
13009 &r#"
13010 <!-- <p>A</p>ˇ -->
13011 <!-- <p>B</p>ˇ -->
13012 <!-- <p>C</p>ˇ -->
13013 "#
13014 .unindent(),
13015 );
13016 cx.update_editor(|editor, window, cx| {
13017 editor.toggle_comments(&ToggleComments::default(), window, cx)
13018 });
13019 cx.assert_editor_state(
13020 &r#"
13021 <p>A</p>ˇ
13022 <p>B</p>ˇ
13023 <p>C</p>ˇ
13024 "#
13025 .unindent(),
13026 );
13027
13028 // Toggle comments for mixture of empty and non-empty selections, where
13029 // multiple selections occupy a given line.
13030 cx.set_state(
13031 &r#"
13032 <p>A«</p>
13033 <p>ˇ»B</p>ˇ
13034 <p>C«</p>
13035 <p>ˇ»D</p>ˇ
13036 "#
13037 .unindent(),
13038 );
13039
13040 cx.update_editor(|editor, window, cx| {
13041 editor.toggle_comments(&ToggleComments::default(), window, cx)
13042 });
13043 cx.assert_editor_state(
13044 &r#"
13045 <!-- <p>A«</p>
13046 <p>ˇ»B</p>ˇ -->
13047 <!-- <p>C«</p>
13048 <p>ˇ»D</p>ˇ -->
13049 "#
13050 .unindent(),
13051 );
13052 cx.update_editor(|editor, window, cx| {
13053 editor.toggle_comments(&ToggleComments::default(), window, cx)
13054 });
13055 cx.assert_editor_state(
13056 &r#"
13057 <p>A«</p>
13058 <p>ˇ»B</p>ˇ
13059 <p>C«</p>
13060 <p>ˇ»D</p>ˇ
13061 "#
13062 .unindent(),
13063 );
13064
13065 // Toggle comments when different languages are active for different
13066 // selections.
13067 cx.set_state(
13068 &r#"
13069 ˇ<script>
13070 ˇvar x = new Y();
13071 ˇ</script>
13072 "#
13073 .unindent(),
13074 );
13075 cx.executor().run_until_parked();
13076 cx.update_editor(|editor, window, cx| {
13077 editor.toggle_comments(&ToggleComments::default(), window, cx)
13078 });
13079 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13080 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13081 cx.assert_editor_state(
13082 &r#"
13083 <!-- ˇ<script> -->
13084 // ˇvar x = new Y();
13085 <!-- ˇ</script> -->
13086 "#
13087 .unindent(),
13088 );
13089}
13090
13091#[gpui::test]
13092fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13093 init_test(cx, |_| {});
13094
13095 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13096 let multibuffer = cx.new(|cx| {
13097 let mut multibuffer = MultiBuffer::new(ReadWrite);
13098 multibuffer.push_excerpts(
13099 buffer.clone(),
13100 [
13101 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13102 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13103 ],
13104 cx,
13105 );
13106 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13107 multibuffer
13108 });
13109
13110 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13111 editor.update_in(cx, |editor, window, cx| {
13112 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13113 editor.change_selections(None, window, cx, |s| {
13114 s.select_ranges([
13115 Point::new(0, 0)..Point::new(0, 0),
13116 Point::new(1, 0)..Point::new(1, 0),
13117 ])
13118 });
13119
13120 editor.handle_input("X", window, cx);
13121 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13122 assert_eq!(
13123 editor.selections.ranges(cx),
13124 [
13125 Point::new(0, 1)..Point::new(0, 1),
13126 Point::new(1, 1)..Point::new(1, 1),
13127 ]
13128 );
13129
13130 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13131 editor.change_selections(None, window, cx, |s| {
13132 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13133 });
13134 editor.backspace(&Default::default(), window, cx);
13135 assert_eq!(editor.text(cx), "Xa\nbbb");
13136 assert_eq!(
13137 editor.selections.ranges(cx),
13138 [Point::new(1, 0)..Point::new(1, 0)]
13139 );
13140
13141 editor.change_selections(None, window, cx, |s| {
13142 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13143 });
13144 editor.backspace(&Default::default(), window, cx);
13145 assert_eq!(editor.text(cx), "X\nbb");
13146 assert_eq!(
13147 editor.selections.ranges(cx),
13148 [Point::new(0, 1)..Point::new(0, 1)]
13149 );
13150 });
13151}
13152
13153#[gpui::test]
13154fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13155 init_test(cx, |_| {});
13156
13157 let markers = vec![('[', ']').into(), ('(', ')').into()];
13158 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13159 indoc! {"
13160 [aaaa
13161 (bbbb]
13162 cccc)",
13163 },
13164 markers.clone(),
13165 );
13166 let excerpt_ranges = markers.into_iter().map(|marker| {
13167 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13168 ExcerptRange::new(context.clone())
13169 });
13170 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13171 let multibuffer = cx.new(|cx| {
13172 let mut multibuffer = MultiBuffer::new(ReadWrite);
13173 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13174 multibuffer
13175 });
13176
13177 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13178 editor.update_in(cx, |editor, window, cx| {
13179 let (expected_text, selection_ranges) = marked_text_ranges(
13180 indoc! {"
13181 aaaa
13182 bˇbbb
13183 bˇbbˇb
13184 cccc"
13185 },
13186 true,
13187 );
13188 assert_eq!(editor.text(cx), expected_text);
13189 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13190
13191 editor.handle_input("X", window, cx);
13192
13193 let (expected_text, expected_selections) = marked_text_ranges(
13194 indoc! {"
13195 aaaa
13196 bXˇbbXb
13197 bXˇbbXˇb
13198 cccc"
13199 },
13200 false,
13201 );
13202 assert_eq!(editor.text(cx), expected_text);
13203 assert_eq!(editor.selections.ranges(cx), expected_selections);
13204
13205 editor.newline(&Newline, window, cx);
13206 let (expected_text, expected_selections) = marked_text_ranges(
13207 indoc! {"
13208 aaaa
13209 bX
13210 ˇbbX
13211 b
13212 bX
13213 ˇbbX
13214 ˇb
13215 cccc"
13216 },
13217 false,
13218 );
13219 assert_eq!(editor.text(cx), expected_text);
13220 assert_eq!(editor.selections.ranges(cx), expected_selections);
13221 });
13222}
13223
13224#[gpui::test]
13225fn test_refresh_selections(cx: &mut TestAppContext) {
13226 init_test(cx, |_| {});
13227
13228 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13229 let mut excerpt1_id = None;
13230 let multibuffer = cx.new(|cx| {
13231 let mut multibuffer = MultiBuffer::new(ReadWrite);
13232 excerpt1_id = multibuffer
13233 .push_excerpts(
13234 buffer.clone(),
13235 [
13236 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13237 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13238 ],
13239 cx,
13240 )
13241 .into_iter()
13242 .next();
13243 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13244 multibuffer
13245 });
13246
13247 let editor = cx.add_window(|window, cx| {
13248 let mut editor = build_editor(multibuffer.clone(), window, cx);
13249 let snapshot = editor.snapshot(window, cx);
13250 editor.change_selections(None, window, cx, |s| {
13251 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13252 });
13253 editor.begin_selection(
13254 Point::new(2, 1).to_display_point(&snapshot),
13255 true,
13256 1,
13257 window,
13258 cx,
13259 );
13260 assert_eq!(
13261 editor.selections.ranges(cx),
13262 [
13263 Point::new(1, 3)..Point::new(1, 3),
13264 Point::new(2, 1)..Point::new(2, 1),
13265 ]
13266 );
13267 editor
13268 });
13269
13270 // Refreshing selections is a no-op when excerpts haven't changed.
13271 _ = editor.update(cx, |editor, window, cx| {
13272 editor.change_selections(None, window, cx, |s| s.refresh());
13273 assert_eq!(
13274 editor.selections.ranges(cx),
13275 [
13276 Point::new(1, 3)..Point::new(1, 3),
13277 Point::new(2, 1)..Point::new(2, 1),
13278 ]
13279 );
13280 });
13281
13282 multibuffer.update(cx, |multibuffer, cx| {
13283 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13284 });
13285 _ = editor.update(cx, |editor, window, cx| {
13286 // Removing an excerpt causes the first selection to become degenerate.
13287 assert_eq!(
13288 editor.selections.ranges(cx),
13289 [
13290 Point::new(0, 0)..Point::new(0, 0),
13291 Point::new(0, 1)..Point::new(0, 1)
13292 ]
13293 );
13294
13295 // Refreshing selections will relocate the first selection to the original buffer
13296 // location.
13297 editor.change_selections(None, window, cx, |s| s.refresh());
13298 assert_eq!(
13299 editor.selections.ranges(cx),
13300 [
13301 Point::new(0, 1)..Point::new(0, 1),
13302 Point::new(0, 3)..Point::new(0, 3)
13303 ]
13304 );
13305 assert!(editor.selections.pending_anchor().is_some());
13306 });
13307}
13308
13309#[gpui::test]
13310fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13311 init_test(cx, |_| {});
13312
13313 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13314 let mut excerpt1_id = None;
13315 let multibuffer = cx.new(|cx| {
13316 let mut multibuffer = MultiBuffer::new(ReadWrite);
13317 excerpt1_id = multibuffer
13318 .push_excerpts(
13319 buffer.clone(),
13320 [
13321 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13322 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13323 ],
13324 cx,
13325 )
13326 .into_iter()
13327 .next();
13328 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13329 multibuffer
13330 });
13331
13332 let editor = cx.add_window(|window, cx| {
13333 let mut editor = build_editor(multibuffer.clone(), window, cx);
13334 let snapshot = editor.snapshot(window, cx);
13335 editor.begin_selection(
13336 Point::new(1, 3).to_display_point(&snapshot),
13337 false,
13338 1,
13339 window,
13340 cx,
13341 );
13342 assert_eq!(
13343 editor.selections.ranges(cx),
13344 [Point::new(1, 3)..Point::new(1, 3)]
13345 );
13346 editor
13347 });
13348
13349 multibuffer.update(cx, |multibuffer, cx| {
13350 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13351 });
13352 _ = editor.update(cx, |editor, window, cx| {
13353 assert_eq!(
13354 editor.selections.ranges(cx),
13355 [Point::new(0, 0)..Point::new(0, 0)]
13356 );
13357
13358 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13359 editor.change_selections(None, window, cx, |s| s.refresh());
13360 assert_eq!(
13361 editor.selections.ranges(cx),
13362 [Point::new(0, 3)..Point::new(0, 3)]
13363 );
13364 assert!(editor.selections.pending_anchor().is_some());
13365 });
13366}
13367
13368#[gpui::test]
13369async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13370 init_test(cx, |_| {});
13371
13372 let language = Arc::new(
13373 Language::new(
13374 LanguageConfig {
13375 brackets: BracketPairConfig {
13376 pairs: vec![
13377 BracketPair {
13378 start: "{".to_string(),
13379 end: "}".to_string(),
13380 close: true,
13381 surround: true,
13382 newline: true,
13383 },
13384 BracketPair {
13385 start: "/* ".to_string(),
13386 end: " */".to_string(),
13387 close: true,
13388 surround: true,
13389 newline: true,
13390 },
13391 ],
13392 ..Default::default()
13393 },
13394 ..Default::default()
13395 },
13396 Some(tree_sitter_rust::LANGUAGE.into()),
13397 )
13398 .with_indents_query("")
13399 .unwrap(),
13400 );
13401
13402 let text = concat!(
13403 "{ }\n", //
13404 " x\n", //
13405 " /* */\n", //
13406 "x\n", //
13407 "{{} }\n", //
13408 );
13409
13410 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13411 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13412 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13413 editor
13414 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13415 .await;
13416
13417 editor.update_in(cx, |editor, window, cx| {
13418 editor.change_selections(None, window, cx, |s| {
13419 s.select_display_ranges([
13420 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13421 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13422 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13423 ])
13424 });
13425 editor.newline(&Newline, window, cx);
13426
13427 assert_eq!(
13428 editor.buffer().read(cx).read(cx).text(),
13429 concat!(
13430 "{ \n", // Suppress rustfmt
13431 "\n", //
13432 "}\n", //
13433 " x\n", //
13434 " /* \n", //
13435 " \n", //
13436 " */\n", //
13437 "x\n", //
13438 "{{} \n", //
13439 "}\n", //
13440 )
13441 );
13442 });
13443}
13444
13445#[gpui::test]
13446fn test_highlighted_ranges(cx: &mut TestAppContext) {
13447 init_test(cx, |_| {});
13448
13449 let editor = cx.add_window(|window, cx| {
13450 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13451 build_editor(buffer.clone(), window, cx)
13452 });
13453
13454 _ = editor.update(cx, |editor, window, cx| {
13455 struct Type1;
13456 struct Type2;
13457
13458 let buffer = editor.buffer.read(cx).snapshot(cx);
13459
13460 let anchor_range =
13461 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13462
13463 editor.highlight_background::<Type1>(
13464 &[
13465 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13466 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13467 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13468 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13469 ],
13470 |_| Hsla::red(),
13471 cx,
13472 );
13473 editor.highlight_background::<Type2>(
13474 &[
13475 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13476 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13477 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13478 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13479 ],
13480 |_| Hsla::green(),
13481 cx,
13482 );
13483
13484 let snapshot = editor.snapshot(window, cx);
13485 let mut highlighted_ranges = editor.background_highlights_in_range(
13486 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13487 &snapshot,
13488 cx.theme().colors(),
13489 );
13490 // Enforce a consistent ordering based on color without relying on the ordering of the
13491 // highlight's `TypeId` which is non-executor.
13492 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13493 assert_eq!(
13494 highlighted_ranges,
13495 &[
13496 (
13497 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13498 Hsla::red(),
13499 ),
13500 (
13501 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13502 Hsla::red(),
13503 ),
13504 (
13505 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13506 Hsla::green(),
13507 ),
13508 (
13509 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13510 Hsla::green(),
13511 ),
13512 ]
13513 );
13514 assert_eq!(
13515 editor.background_highlights_in_range(
13516 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13517 &snapshot,
13518 cx.theme().colors(),
13519 ),
13520 &[(
13521 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13522 Hsla::red(),
13523 )]
13524 );
13525 });
13526}
13527
13528#[gpui::test]
13529async fn test_following(cx: &mut TestAppContext) {
13530 init_test(cx, |_| {});
13531
13532 let fs = FakeFs::new(cx.executor());
13533 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13534
13535 let buffer = project.update(cx, |project, cx| {
13536 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13537 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13538 });
13539 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13540 let follower = cx.update(|cx| {
13541 cx.open_window(
13542 WindowOptions {
13543 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13544 gpui::Point::new(px(0.), px(0.)),
13545 gpui::Point::new(px(10.), px(80.)),
13546 ))),
13547 ..Default::default()
13548 },
13549 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13550 )
13551 .unwrap()
13552 });
13553
13554 let is_still_following = Rc::new(RefCell::new(true));
13555 let follower_edit_event_count = Rc::new(RefCell::new(0));
13556 let pending_update = Rc::new(RefCell::new(None));
13557 let leader_entity = leader.root(cx).unwrap();
13558 let follower_entity = follower.root(cx).unwrap();
13559 _ = follower.update(cx, {
13560 let update = pending_update.clone();
13561 let is_still_following = is_still_following.clone();
13562 let follower_edit_event_count = follower_edit_event_count.clone();
13563 |_, window, cx| {
13564 cx.subscribe_in(
13565 &leader_entity,
13566 window,
13567 move |_, leader, event, window, cx| {
13568 leader.read(cx).add_event_to_update_proto(
13569 event,
13570 &mut update.borrow_mut(),
13571 window,
13572 cx,
13573 );
13574 },
13575 )
13576 .detach();
13577
13578 cx.subscribe_in(
13579 &follower_entity,
13580 window,
13581 move |_, _, event: &EditorEvent, _window, _cx| {
13582 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13583 *is_still_following.borrow_mut() = false;
13584 }
13585
13586 if let EditorEvent::BufferEdited = event {
13587 *follower_edit_event_count.borrow_mut() += 1;
13588 }
13589 },
13590 )
13591 .detach();
13592 }
13593 });
13594
13595 // Update the selections only
13596 _ = leader.update(cx, |leader, window, cx| {
13597 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13598 });
13599 follower
13600 .update(cx, |follower, window, cx| {
13601 follower.apply_update_proto(
13602 &project,
13603 pending_update.borrow_mut().take().unwrap(),
13604 window,
13605 cx,
13606 )
13607 })
13608 .unwrap()
13609 .await
13610 .unwrap();
13611 _ = follower.update(cx, |follower, _, cx| {
13612 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13613 });
13614 assert!(*is_still_following.borrow());
13615 assert_eq!(*follower_edit_event_count.borrow(), 0);
13616
13617 // Update the scroll position only
13618 _ = leader.update(cx, |leader, window, cx| {
13619 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13620 });
13621 follower
13622 .update(cx, |follower, window, cx| {
13623 follower.apply_update_proto(
13624 &project,
13625 pending_update.borrow_mut().take().unwrap(),
13626 window,
13627 cx,
13628 )
13629 })
13630 .unwrap()
13631 .await
13632 .unwrap();
13633 assert_eq!(
13634 follower
13635 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13636 .unwrap(),
13637 gpui::Point::new(1.5, 3.5)
13638 );
13639 assert!(*is_still_following.borrow());
13640 assert_eq!(*follower_edit_event_count.borrow(), 0);
13641
13642 // Update the selections and scroll position. The follower's scroll position is updated
13643 // via autoscroll, not via the leader's exact scroll position.
13644 _ = leader.update(cx, |leader, window, cx| {
13645 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13646 leader.request_autoscroll(Autoscroll::newest(), cx);
13647 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13648 });
13649 follower
13650 .update(cx, |follower, window, cx| {
13651 follower.apply_update_proto(
13652 &project,
13653 pending_update.borrow_mut().take().unwrap(),
13654 window,
13655 cx,
13656 )
13657 })
13658 .unwrap()
13659 .await
13660 .unwrap();
13661 _ = follower.update(cx, |follower, _, cx| {
13662 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13663 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13664 });
13665 assert!(*is_still_following.borrow());
13666
13667 // Creating a pending selection that precedes another selection
13668 _ = leader.update(cx, |leader, window, cx| {
13669 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13670 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13671 });
13672 follower
13673 .update(cx, |follower, window, cx| {
13674 follower.apply_update_proto(
13675 &project,
13676 pending_update.borrow_mut().take().unwrap(),
13677 window,
13678 cx,
13679 )
13680 })
13681 .unwrap()
13682 .await
13683 .unwrap();
13684 _ = follower.update(cx, |follower, _, cx| {
13685 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13686 });
13687 assert!(*is_still_following.borrow());
13688
13689 // Extend the pending selection so that it surrounds another selection
13690 _ = leader.update(cx, |leader, window, cx| {
13691 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13692 });
13693 follower
13694 .update(cx, |follower, window, cx| {
13695 follower.apply_update_proto(
13696 &project,
13697 pending_update.borrow_mut().take().unwrap(),
13698 window,
13699 cx,
13700 )
13701 })
13702 .unwrap()
13703 .await
13704 .unwrap();
13705 _ = follower.update(cx, |follower, _, cx| {
13706 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13707 });
13708
13709 // Scrolling locally breaks the follow
13710 _ = follower.update(cx, |follower, window, cx| {
13711 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13712 follower.set_scroll_anchor(
13713 ScrollAnchor {
13714 anchor: top_anchor,
13715 offset: gpui::Point::new(0.0, 0.5),
13716 },
13717 window,
13718 cx,
13719 );
13720 });
13721 assert!(!(*is_still_following.borrow()));
13722}
13723
13724#[gpui::test]
13725async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13726 init_test(cx, |_| {});
13727
13728 let fs = FakeFs::new(cx.executor());
13729 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13730 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13731 let pane = workspace
13732 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13733 .unwrap();
13734
13735 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13736
13737 let leader = pane.update_in(cx, |_, window, cx| {
13738 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13739 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13740 });
13741
13742 // Start following the editor when it has no excerpts.
13743 let mut state_message =
13744 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13745 let workspace_entity = workspace.root(cx).unwrap();
13746 let follower_1 = cx
13747 .update_window(*workspace.deref(), |_, window, cx| {
13748 Editor::from_state_proto(
13749 workspace_entity,
13750 ViewId {
13751 creator: CollaboratorId::PeerId(PeerId::default()),
13752 id: 0,
13753 },
13754 &mut state_message,
13755 window,
13756 cx,
13757 )
13758 })
13759 .unwrap()
13760 .unwrap()
13761 .await
13762 .unwrap();
13763
13764 let update_message = Rc::new(RefCell::new(None));
13765 follower_1.update_in(cx, {
13766 let update = update_message.clone();
13767 |_, window, cx| {
13768 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13769 leader.read(cx).add_event_to_update_proto(
13770 event,
13771 &mut update.borrow_mut(),
13772 window,
13773 cx,
13774 );
13775 })
13776 .detach();
13777 }
13778 });
13779
13780 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13781 (
13782 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13783 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13784 )
13785 });
13786
13787 // Insert some excerpts.
13788 leader.update(cx, |leader, cx| {
13789 leader.buffer.update(cx, |multibuffer, cx| {
13790 multibuffer.set_excerpts_for_path(
13791 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13792 buffer_1.clone(),
13793 vec![
13794 Point::row_range(0..3),
13795 Point::row_range(1..6),
13796 Point::row_range(12..15),
13797 ],
13798 0,
13799 cx,
13800 );
13801 multibuffer.set_excerpts_for_path(
13802 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13803 buffer_2.clone(),
13804 vec![Point::row_range(0..6), Point::row_range(8..12)],
13805 0,
13806 cx,
13807 );
13808 });
13809 });
13810
13811 // Apply the update of adding the excerpts.
13812 follower_1
13813 .update_in(cx, |follower, window, cx| {
13814 follower.apply_update_proto(
13815 &project,
13816 update_message.borrow().clone().unwrap(),
13817 window,
13818 cx,
13819 )
13820 })
13821 .await
13822 .unwrap();
13823 assert_eq!(
13824 follower_1.update(cx, |editor, cx| editor.text(cx)),
13825 leader.update(cx, |editor, cx| editor.text(cx))
13826 );
13827 update_message.borrow_mut().take();
13828
13829 // Start following separately after it already has excerpts.
13830 let mut state_message =
13831 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13832 let workspace_entity = workspace.root(cx).unwrap();
13833 let follower_2 = cx
13834 .update_window(*workspace.deref(), |_, window, cx| {
13835 Editor::from_state_proto(
13836 workspace_entity,
13837 ViewId {
13838 creator: CollaboratorId::PeerId(PeerId::default()),
13839 id: 0,
13840 },
13841 &mut state_message,
13842 window,
13843 cx,
13844 )
13845 })
13846 .unwrap()
13847 .unwrap()
13848 .await
13849 .unwrap();
13850 assert_eq!(
13851 follower_2.update(cx, |editor, cx| editor.text(cx)),
13852 leader.update(cx, |editor, cx| editor.text(cx))
13853 );
13854
13855 // Remove some excerpts.
13856 leader.update(cx, |leader, cx| {
13857 leader.buffer.update(cx, |multibuffer, cx| {
13858 let excerpt_ids = multibuffer.excerpt_ids();
13859 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13860 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13861 });
13862 });
13863
13864 // Apply the update of removing the excerpts.
13865 follower_1
13866 .update_in(cx, |follower, window, cx| {
13867 follower.apply_update_proto(
13868 &project,
13869 update_message.borrow().clone().unwrap(),
13870 window,
13871 cx,
13872 )
13873 })
13874 .await
13875 .unwrap();
13876 follower_2
13877 .update_in(cx, |follower, window, cx| {
13878 follower.apply_update_proto(
13879 &project,
13880 update_message.borrow().clone().unwrap(),
13881 window,
13882 cx,
13883 )
13884 })
13885 .await
13886 .unwrap();
13887 update_message.borrow_mut().take();
13888 assert_eq!(
13889 follower_1.update(cx, |editor, cx| editor.text(cx)),
13890 leader.update(cx, |editor, cx| editor.text(cx))
13891 );
13892}
13893
13894#[gpui::test]
13895async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13896 init_test(cx, |_| {});
13897
13898 let mut cx = EditorTestContext::new(cx).await;
13899 let lsp_store =
13900 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13901
13902 cx.set_state(indoc! {"
13903 ˇfn func(abc def: i32) -> u32 {
13904 }
13905 "});
13906
13907 cx.update(|_, cx| {
13908 lsp_store.update(cx, |lsp_store, cx| {
13909 lsp_store
13910 .update_diagnostics(
13911 LanguageServerId(0),
13912 lsp::PublishDiagnosticsParams {
13913 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13914 version: None,
13915 diagnostics: vec![
13916 lsp::Diagnostic {
13917 range: lsp::Range::new(
13918 lsp::Position::new(0, 11),
13919 lsp::Position::new(0, 12),
13920 ),
13921 severity: Some(lsp::DiagnosticSeverity::ERROR),
13922 ..Default::default()
13923 },
13924 lsp::Diagnostic {
13925 range: lsp::Range::new(
13926 lsp::Position::new(0, 12),
13927 lsp::Position::new(0, 15),
13928 ),
13929 severity: Some(lsp::DiagnosticSeverity::ERROR),
13930 ..Default::default()
13931 },
13932 lsp::Diagnostic {
13933 range: lsp::Range::new(
13934 lsp::Position::new(0, 25),
13935 lsp::Position::new(0, 28),
13936 ),
13937 severity: Some(lsp::DiagnosticSeverity::ERROR),
13938 ..Default::default()
13939 },
13940 ],
13941 },
13942 None,
13943 DiagnosticSourceKind::Pushed,
13944 &[],
13945 cx,
13946 )
13947 .unwrap()
13948 });
13949 });
13950
13951 executor.run_until_parked();
13952
13953 cx.update_editor(|editor, window, cx| {
13954 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13955 });
13956
13957 cx.assert_editor_state(indoc! {"
13958 fn func(abc def: i32) -> ˇu32 {
13959 }
13960 "});
13961
13962 cx.update_editor(|editor, window, cx| {
13963 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13964 });
13965
13966 cx.assert_editor_state(indoc! {"
13967 fn func(abc ˇdef: i32) -> u32 {
13968 }
13969 "});
13970
13971 cx.update_editor(|editor, window, cx| {
13972 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13973 });
13974
13975 cx.assert_editor_state(indoc! {"
13976 fn func(abcˇ def: i32) -> u32 {
13977 }
13978 "});
13979
13980 cx.update_editor(|editor, window, cx| {
13981 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13982 });
13983
13984 cx.assert_editor_state(indoc! {"
13985 fn func(abc def: i32) -> ˇu32 {
13986 }
13987 "});
13988}
13989
13990#[gpui::test]
13991async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13992 init_test(cx, |_| {});
13993
13994 let mut cx = EditorTestContext::new(cx).await;
13995
13996 let diff_base = r#"
13997 use some::mod;
13998
13999 const A: u32 = 42;
14000
14001 fn main() {
14002 println!("hello");
14003
14004 println!("world");
14005 }
14006 "#
14007 .unindent();
14008
14009 // Edits are modified, removed, modified, added
14010 cx.set_state(
14011 &r#"
14012 use some::modified;
14013
14014 ˇ
14015 fn main() {
14016 println!("hello there");
14017
14018 println!("around the");
14019 println!("world");
14020 }
14021 "#
14022 .unindent(),
14023 );
14024
14025 cx.set_head_text(&diff_base);
14026 executor.run_until_parked();
14027
14028 cx.update_editor(|editor, window, cx| {
14029 //Wrap around the bottom of the buffer
14030 for _ in 0..3 {
14031 editor.go_to_next_hunk(&GoToHunk, window, cx);
14032 }
14033 });
14034
14035 cx.assert_editor_state(
14036 &r#"
14037 ˇuse some::modified;
14038
14039
14040 fn main() {
14041 println!("hello there");
14042
14043 println!("around the");
14044 println!("world");
14045 }
14046 "#
14047 .unindent(),
14048 );
14049
14050 cx.update_editor(|editor, window, cx| {
14051 //Wrap around the top of the buffer
14052 for _ in 0..2 {
14053 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14054 }
14055 });
14056
14057 cx.assert_editor_state(
14058 &r#"
14059 use some::modified;
14060
14061
14062 fn main() {
14063 ˇ println!("hello there");
14064
14065 println!("around the");
14066 println!("world");
14067 }
14068 "#
14069 .unindent(),
14070 );
14071
14072 cx.update_editor(|editor, window, cx| {
14073 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14074 });
14075
14076 cx.assert_editor_state(
14077 &r#"
14078 use some::modified;
14079
14080 ˇ
14081 fn main() {
14082 println!("hello there");
14083
14084 println!("around the");
14085 println!("world");
14086 }
14087 "#
14088 .unindent(),
14089 );
14090
14091 cx.update_editor(|editor, window, cx| {
14092 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14093 });
14094
14095 cx.assert_editor_state(
14096 &r#"
14097 ˇuse some::modified;
14098
14099
14100 fn main() {
14101 println!("hello there");
14102
14103 println!("around the");
14104 println!("world");
14105 }
14106 "#
14107 .unindent(),
14108 );
14109
14110 cx.update_editor(|editor, window, cx| {
14111 for _ in 0..2 {
14112 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14113 }
14114 });
14115
14116 cx.assert_editor_state(
14117 &r#"
14118 use some::modified;
14119
14120
14121 fn main() {
14122 ˇ println!("hello there");
14123
14124 println!("around the");
14125 println!("world");
14126 }
14127 "#
14128 .unindent(),
14129 );
14130
14131 cx.update_editor(|editor, window, cx| {
14132 editor.fold(&Fold, window, cx);
14133 });
14134
14135 cx.update_editor(|editor, window, cx| {
14136 editor.go_to_next_hunk(&GoToHunk, window, cx);
14137 });
14138
14139 cx.assert_editor_state(
14140 &r#"
14141 ˇuse some::modified;
14142
14143
14144 fn main() {
14145 println!("hello there");
14146
14147 println!("around the");
14148 println!("world");
14149 }
14150 "#
14151 .unindent(),
14152 );
14153}
14154
14155#[test]
14156fn test_split_words() {
14157 fn split(text: &str) -> Vec<&str> {
14158 split_words(text).collect()
14159 }
14160
14161 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14162 assert_eq!(split("hello_world"), &["hello_", "world"]);
14163 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14164 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14165 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14166 assert_eq!(split("helloworld"), &["helloworld"]);
14167
14168 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14169}
14170
14171#[gpui::test]
14172async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14173 init_test(cx, |_| {});
14174
14175 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14176 let mut assert = |before, after| {
14177 let _state_context = cx.set_state(before);
14178 cx.run_until_parked();
14179 cx.update_editor(|editor, window, cx| {
14180 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14181 });
14182 cx.run_until_parked();
14183 cx.assert_editor_state(after);
14184 };
14185
14186 // Outside bracket jumps to outside of matching bracket
14187 assert("console.logˇ(var);", "console.log(var)ˇ;");
14188 assert("console.log(var)ˇ;", "console.logˇ(var);");
14189
14190 // Inside bracket jumps to inside of matching bracket
14191 assert("console.log(ˇvar);", "console.log(varˇ);");
14192 assert("console.log(varˇ);", "console.log(ˇvar);");
14193
14194 // When outside a bracket and inside, favor jumping to the inside bracket
14195 assert(
14196 "console.log('foo', [1, 2, 3]ˇ);",
14197 "console.log(ˇ'foo', [1, 2, 3]);",
14198 );
14199 assert(
14200 "console.log(ˇ'foo', [1, 2, 3]);",
14201 "console.log('foo', [1, 2, 3]ˇ);",
14202 );
14203
14204 // Bias forward if two options are equally likely
14205 assert(
14206 "let result = curried_fun()ˇ();",
14207 "let result = curried_fun()()ˇ;",
14208 );
14209
14210 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14211 assert(
14212 indoc! {"
14213 function test() {
14214 console.log('test')ˇ
14215 }"},
14216 indoc! {"
14217 function test() {
14218 console.logˇ('test')
14219 }"},
14220 );
14221}
14222
14223#[gpui::test]
14224async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14225 init_test(cx, |_| {});
14226
14227 let fs = FakeFs::new(cx.executor());
14228 fs.insert_tree(
14229 path!("/a"),
14230 json!({
14231 "main.rs": "fn main() { let a = 5; }",
14232 "other.rs": "// Test file",
14233 }),
14234 )
14235 .await;
14236 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14237
14238 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14239 language_registry.add(Arc::new(Language::new(
14240 LanguageConfig {
14241 name: "Rust".into(),
14242 matcher: LanguageMatcher {
14243 path_suffixes: vec!["rs".to_string()],
14244 ..Default::default()
14245 },
14246 brackets: BracketPairConfig {
14247 pairs: vec![BracketPair {
14248 start: "{".to_string(),
14249 end: "}".to_string(),
14250 close: true,
14251 surround: true,
14252 newline: true,
14253 }],
14254 disabled_scopes_by_bracket_ix: Vec::new(),
14255 },
14256 ..Default::default()
14257 },
14258 Some(tree_sitter_rust::LANGUAGE.into()),
14259 )));
14260 let mut fake_servers = language_registry.register_fake_lsp(
14261 "Rust",
14262 FakeLspAdapter {
14263 capabilities: lsp::ServerCapabilities {
14264 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14265 first_trigger_character: "{".to_string(),
14266 more_trigger_character: None,
14267 }),
14268 ..Default::default()
14269 },
14270 ..Default::default()
14271 },
14272 );
14273
14274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14275
14276 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14277
14278 let worktree_id = workspace
14279 .update(cx, |workspace, _, cx| {
14280 workspace.project().update(cx, |project, cx| {
14281 project.worktrees(cx).next().unwrap().read(cx).id()
14282 })
14283 })
14284 .unwrap();
14285
14286 let buffer = project
14287 .update(cx, |project, cx| {
14288 project.open_local_buffer(path!("/a/main.rs"), cx)
14289 })
14290 .await
14291 .unwrap();
14292 let editor_handle = workspace
14293 .update(cx, |workspace, window, cx| {
14294 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14295 })
14296 .unwrap()
14297 .await
14298 .unwrap()
14299 .downcast::<Editor>()
14300 .unwrap();
14301
14302 cx.executor().start_waiting();
14303 let fake_server = fake_servers.next().await.unwrap();
14304
14305 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14306 |params, _| async move {
14307 assert_eq!(
14308 params.text_document_position.text_document.uri,
14309 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14310 );
14311 assert_eq!(
14312 params.text_document_position.position,
14313 lsp::Position::new(0, 21),
14314 );
14315
14316 Ok(Some(vec![lsp::TextEdit {
14317 new_text: "]".to_string(),
14318 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14319 }]))
14320 },
14321 );
14322
14323 editor_handle.update_in(cx, |editor, window, cx| {
14324 window.focus(&editor.focus_handle(cx));
14325 editor.change_selections(None, window, cx, |s| {
14326 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14327 });
14328 editor.handle_input("{", window, cx);
14329 });
14330
14331 cx.executor().run_until_parked();
14332
14333 buffer.update(cx, |buffer, _| {
14334 assert_eq!(
14335 buffer.text(),
14336 "fn main() { let a = {5}; }",
14337 "No extra braces from on type formatting should appear in the buffer"
14338 )
14339 });
14340}
14341
14342#[gpui::test]
14343async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14344 init_test(cx, |_| {});
14345
14346 let fs = FakeFs::new(cx.executor());
14347 fs.insert_tree(
14348 path!("/a"),
14349 json!({
14350 "main.rs": "fn main() { let a = 5; }",
14351 "other.rs": "// Test file",
14352 }),
14353 )
14354 .await;
14355
14356 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14357
14358 let server_restarts = Arc::new(AtomicUsize::new(0));
14359 let closure_restarts = Arc::clone(&server_restarts);
14360 let language_server_name = "test language server";
14361 let language_name: LanguageName = "Rust".into();
14362
14363 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14364 language_registry.add(Arc::new(Language::new(
14365 LanguageConfig {
14366 name: language_name.clone(),
14367 matcher: LanguageMatcher {
14368 path_suffixes: vec!["rs".to_string()],
14369 ..Default::default()
14370 },
14371 ..Default::default()
14372 },
14373 Some(tree_sitter_rust::LANGUAGE.into()),
14374 )));
14375 let mut fake_servers = language_registry.register_fake_lsp(
14376 "Rust",
14377 FakeLspAdapter {
14378 name: language_server_name,
14379 initialization_options: Some(json!({
14380 "testOptionValue": true
14381 })),
14382 initializer: Some(Box::new(move |fake_server| {
14383 let task_restarts = Arc::clone(&closure_restarts);
14384 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14385 task_restarts.fetch_add(1, atomic::Ordering::Release);
14386 futures::future::ready(Ok(()))
14387 });
14388 })),
14389 ..Default::default()
14390 },
14391 );
14392
14393 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14394 let _buffer = project
14395 .update(cx, |project, cx| {
14396 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14397 })
14398 .await
14399 .unwrap();
14400 let _fake_server = fake_servers.next().await.unwrap();
14401 update_test_language_settings(cx, |language_settings| {
14402 language_settings.languages.insert(
14403 language_name.clone(),
14404 LanguageSettingsContent {
14405 tab_size: NonZeroU32::new(8),
14406 ..Default::default()
14407 },
14408 );
14409 });
14410 cx.executor().run_until_parked();
14411 assert_eq!(
14412 server_restarts.load(atomic::Ordering::Acquire),
14413 0,
14414 "Should not restart LSP server on an unrelated change"
14415 );
14416
14417 update_test_project_settings(cx, |project_settings| {
14418 project_settings.lsp.insert(
14419 "Some other server name".into(),
14420 LspSettings {
14421 binary: None,
14422 settings: None,
14423 initialization_options: Some(json!({
14424 "some other init value": false
14425 })),
14426 enable_lsp_tasks: false,
14427 },
14428 );
14429 });
14430 cx.executor().run_until_parked();
14431 assert_eq!(
14432 server_restarts.load(atomic::Ordering::Acquire),
14433 0,
14434 "Should not restart LSP server on an unrelated LSP settings change"
14435 );
14436
14437 update_test_project_settings(cx, |project_settings| {
14438 project_settings.lsp.insert(
14439 language_server_name.into(),
14440 LspSettings {
14441 binary: None,
14442 settings: None,
14443 initialization_options: Some(json!({
14444 "anotherInitValue": false
14445 })),
14446 enable_lsp_tasks: false,
14447 },
14448 );
14449 });
14450 cx.executor().run_until_parked();
14451 assert_eq!(
14452 server_restarts.load(atomic::Ordering::Acquire),
14453 1,
14454 "Should restart LSP server on a related LSP settings change"
14455 );
14456
14457 update_test_project_settings(cx, |project_settings| {
14458 project_settings.lsp.insert(
14459 language_server_name.into(),
14460 LspSettings {
14461 binary: None,
14462 settings: None,
14463 initialization_options: Some(json!({
14464 "anotherInitValue": false
14465 })),
14466 enable_lsp_tasks: false,
14467 },
14468 );
14469 });
14470 cx.executor().run_until_parked();
14471 assert_eq!(
14472 server_restarts.load(atomic::Ordering::Acquire),
14473 1,
14474 "Should not restart LSP server on a related LSP settings change that is the same"
14475 );
14476
14477 update_test_project_settings(cx, |project_settings| {
14478 project_settings.lsp.insert(
14479 language_server_name.into(),
14480 LspSettings {
14481 binary: None,
14482 settings: None,
14483 initialization_options: None,
14484 enable_lsp_tasks: false,
14485 },
14486 );
14487 });
14488 cx.executor().run_until_parked();
14489 assert_eq!(
14490 server_restarts.load(atomic::Ordering::Acquire),
14491 2,
14492 "Should restart LSP server on another related LSP settings change"
14493 );
14494}
14495
14496#[gpui::test]
14497async fn test_completions_with_additional_edits(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 let completion_item = lsp::CompletionItem {
14516 label: "some".into(),
14517 kind: Some(lsp::CompletionItemKind::SNIPPET),
14518 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14519 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14520 kind: lsp::MarkupKind::Markdown,
14521 value: "```rust\nSome(2)\n```".to_string(),
14522 })),
14523 deprecated: Some(false),
14524 sort_text: Some("fffffff2".to_string()),
14525 filter_text: Some("some".to_string()),
14526 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14527 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14528 range: lsp::Range {
14529 start: lsp::Position {
14530 line: 0,
14531 character: 22,
14532 },
14533 end: lsp::Position {
14534 line: 0,
14535 character: 22,
14536 },
14537 },
14538 new_text: "Some(2)".to_string(),
14539 })),
14540 additional_text_edits: Some(vec![lsp::TextEdit {
14541 range: lsp::Range {
14542 start: lsp::Position {
14543 line: 0,
14544 character: 20,
14545 },
14546 end: lsp::Position {
14547 line: 0,
14548 character: 22,
14549 },
14550 },
14551 new_text: "".to_string(),
14552 }]),
14553 ..Default::default()
14554 };
14555
14556 let closure_completion_item = completion_item.clone();
14557 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14558 let task_completion_item = closure_completion_item.clone();
14559 async move {
14560 Ok(Some(lsp::CompletionResponse::Array(vec![
14561 task_completion_item,
14562 ])))
14563 }
14564 });
14565
14566 request.next().await;
14567
14568 cx.condition(|editor, _| editor.context_menu_visible())
14569 .await;
14570 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14571 editor
14572 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14573 .unwrap()
14574 });
14575 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14576
14577 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14578 let task_completion_item = completion_item.clone();
14579 async move { Ok(task_completion_item) }
14580 })
14581 .next()
14582 .await
14583 .unwrap();
14584 apply_additional_edits.await.unwrap();
14585 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14586}
14587
14588#[gpui::test]
14589async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14590 init_test(cx, |_| {});
14591
14592 let mut cx = EditorLspTestContext::new_rust(
14593 lsp::ServerCapabilities {
14594 completion_provider: Some(lsp::CompletionOptions {
14595 trigger_characters: Some(vec![".".to_string()]),
14596 resolve_provider: Some(true),
14597 ..Default::default()
14598 }),
14599 ..Default::default()
14600 },
14601 cx,
14602 )
14603 .await;
14604
14605 cx.set_state("fn main() { let a = 2ˇ; }");
14606 cx.simulate_keystroke(".");
14607
14608 let item1 = lsp::CompletionItem {
14609 label: "method id()".to_string(),
14610 filter_text: Some("id".to_string()),
14611 detail: None,
14612 documentation: None,
14613 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14614 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14615 new_text: ".id".to_string(),
14616 })),
14617 ..lsp::CompletionItem::default()
14618 };
14619
14620 let item2 = lsp::CompletionItem {
14621 label: "other".to_string(),
14622 filter_text: Some("other".to_string()),
14623 detail: None,
14624 documentation: None,
14625 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14626 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14627 new_text: ".other".to_string(),
14628 })),
14629 ..lsp::CompletionItem::default()
14630 };
14631
14632 let item1 = item1.clone();
14633 cx.set_request_handler::<lsp::request::Completion, _, _>({
14634 let item1 = item1.clone();
14635 move |_, _, _| {
14636 let item1 = item1.clone();
14637 let item2 = item2.clone();
14638 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14639 }
14640 })
14641 .next()
14642 .await;
14643
14644 cx.condition(|editor, _| editor.context_menu_visible())
14645 .await;
14646 cx.update_editor(|editor, _, _| {
14647 let context_menu = editor.context_menu.borrow_mut();
14648 let context_menu = context_menu
14649 .as_ref()
14650 .expect("Should have the context menu deployed");
14651 match context_menu {
14652 CodeContextMenu::Completions(completions_menu) => {
14653 let completions = completions_menu.completions.borrow_mut();
14654 assert_eq!(
14655 completions
14656 .iter()
14657 .map(|completion| &completion.label.text)
14658 .collect::<Vec<_>>(),
14659 vec!["method id()", "other"]
14660 )
14661 }
14662 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14663 }
14664 });
14665
14666 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14667 let item1 = item1.clone();
14668 move |_, item_to_resolve, _| {
14669 let item1 = item1.clone();
14670 async move {
14671 if item1 == item_to_resolve {
14672 Ok(lsp::CompletionItem {
14673 label: "method id()".to_string(),
14674 filter_text: Some("id".to_string()),
14675 detail: Some("Now resolved!".to_string()),
14676 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14677 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14678 range: lsp::Range::new(
14679 lsp::Position::new(0, 22),
14680 lsp::Position::new(0, 22),
14681 ),
14682 new_text: ".id".to_string(),
14683 })),
14684 ..lsp::CompletionItem::default()
14685 })
14686 } else {
14687 Ok(item_to_resolve)
14688 }
14689 }
14690 }
14691 })
14692 .next()
14693 .await
14694 .unwrap();
14695 cx.run_until_parked();
14696
14697 cx.update_editor(|editor, window, cx| {
14698 editor.context_menu_next(&Default::default(), window, cx);
14699 });
14700
14701 cx.update_editor(|editor, _, _| {
14702 let context_menu = editor.context_menu.borrow_mut();
14703 let context_menu = context_menu
14704 .as_ref()
14705 .expect("Should have the context menu deployed");
14706 match context_menu {
14707 CodeContextMenu::Completions(completions_menu) => {
14708 let completions = completions_menu.completions.borrow_mut();
14709 assert_eq!(
14710 completions
14711 .iter()
14712 .map(|completion| &completion.label.text)
14713 .collect::<Vec<_>>(),
14714 vec!["method id() Now resolved!", "other"],
14715 "Should update first completion label, but not second as the filter text did not match."
14716 );
14717 }
14718 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14719 }
14720 });
14721}
14722
14723#[gpui::test]
14724async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14725 init_test(cx, |_| {});
14726 let mut cx = EditorLspTestContext::new_rust(
14727 lsp::ServerCapabilities {
14728 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14729 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14730 completion_provider: Some(lsp::CompletionOptions {
14731 resolve_provider: Some(true),
14732 ..Default::default()
14733 }),
14734 ..Default::default()
14735 },
14736 cx,
14737 )
14738 .await;
14739 cx.set_state(indoc! {"
14740 struct TestStruct {
14741 field: i32
14742 }
14743
14744 fn mainˇ() {
14745 let unused_var = 42;
14746 let test_struct = TestStruct { field: 42 };
14747 }
14748 "});
14749 let symbol_range = cx.lsp_range(indoc! {"
14750 struct TestStruct {
14751 field: i32
14752 }
14753
14754 «fn main»() {
14755 let unused_var = 42;
14756 let test_struct = TestStruct { field: 42 };
14757 }
14758 "});
14759 let mut hover_requests =
14760 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14761 Ok(Some(lsp::Hover {
14762 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14763 kind: lsp::MarkupKind::Markdown,
14764 value: "Function documentation".to_string(),
14765 }),
14766 range: Some(symbol_range),
14767 }))
14768 });
14769
14770 // Case 1: Test that code action menu hide hover popover
14771 cx.dispatch_action(Hover);
14772 hover_requests.next().await;
14773 cx.condition(|editor, _| editor.hover_state.visible()).await;
14774 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14775 move |_, _, _| async move {
14776 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14777 lsp::CodeAction {
14778 title: "Remove unused variable".to_string(),
14779 kind: Some(CodeActionKind::QUICKFIX),
14780 edit: Some(lsp::WorkspaceEdit {
14781 changes: Some(
14782 [(
14783 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14784 vec![lsp::TextEdit {
14785 range: lsp::Range::new(
14786 lsp::Position::new(5, 4),
14787 lsp::Position::new(5, 27),
14788 ),
14789 new_text: "".to_string(),
14790 }],
14791 )]
14792 .into_iter()
14793 .collect(),
14794 ),
14795 ..Default::default()
14796 }),
14797 ..Default::default()
14798 },
14799 )]))
14800 },
14801 );
14802 cx.update_editor(|editor, window, cx| {
14803 editor.toggle_code_actions(
14804 &ToggleCodeActions {
14805 deployed_from: None,
14806 quick_launch: false,
14807 },
14808 window,
14809 cx,
14810 );
14811 });
14812 code_action_requests.next().await;
14813 cx.run_until_parked();
14814 cx.condition(|editor, _| editor.context_menu_visible())
14815 .await;
14816 cx.update_editor(|editor, _, _| {
14817 assert!(
14818 !editor.hover_state.visible(),
14819 "Hover popover should be hidden when code action menu is shown"
14820 );
14821 // Hide code actions
14822 editor.context_menu.take();
14823 });
14824
14825 // Case 2: Test that code completions hide hover popover
14826 cx.dispatch_action(Hover);
14827 hover_requests.next().await;
14828 cx.condition(|editor, _| editor.hover_state.visible()).await;
14829 let counter = Arc::new(AtomicUsize::new(0));
14830 let mut completion_requests =
14831 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14832 let counter = counter.clone();
14833 async move {
14834 counter.fetch_add(1, atomic::Ordering::Release);
14835 Ok(Some(lsp::CompletionResponse::Array(vec![
14836 lsp::CompletionItem {
14837 label: "main".into(),
14838 kind: Some(lsp::CompletionItemKind::FUNCTION),
14839 detail: Some("() -> ()".to_string()),
14840 ..Default::default()
14841 },
14842 lsp::CompletionItem {
14843 label: "TestStruct".into(),
14844 kind: Some(lsp::CompletionItemKind::STRUCT),
14845 detail: Some("struct TestStruct".to_string()),
14846 ..Default::default()
14847 },
14848 ])))
14849 }
14850 });
14851 cx.update_editor(|editor, window, cx| {
14852 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14853 });
14854 completion_requests.next().await;
14855 cx.condition(|editor, _| editor.context_menu_visible())
14856 .await;
14857 cx.update_editor(|editor, _, _| {
14858 assert!(
14859 !editor.hover_state.visible(),
14860 "Hover popover should be hidden when completion menu is shown"
14861 );
14862 });
14863}
14864
14865#[gpui::test]
14866async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14867 init_test(cx, |_| {});
14868
14869 let mut cx = EditorLspTestContext::new_rust(
14870 lsp::ServerCapabilities {
14871 completion_provider: Some(lsp::CompletionOptions {
14872 trigger_characters: Some(vec![".".to_string()]),
14873 resolve_provider: Some(true),
14874 ..Default::default()
14875 }),
14876 ..Default::default()
14877 },
14878 cx,
14879 )
14880 .await;
14881
14882 cx.set_state("fn main() { let a = 2ˇ; }");
14883 cx.simulate_keystroke(".");
14884
14885 let unresolved_item_1 = lsp::CompletionItem {
14886 label: "id".to_string(),
14887 filter_text: Some("id".to_string()),
14888 detail: None,
14889 documentation: None,
14890 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14891 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14892 new_text: ".id".to_string(),
14893 })),
14894 ..lsp::CompletionItem::default()
14895 };
14896 let resolved_item_1 = lsp::CompletionItem {
14897 additional_text_edits: Some(vec![lsp::TextEdit {
14898 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14899 new_text: "!!".to_string(),
14900 }]),
14901 ..unresolved_item_1.clone()
14902 };
14903 let unresolved_item_2 = lsp::CompletionItem {
14904 label: "other".to_string(),
14905 filter_text: Some("other".to_string()),
14906 detail: None,
14907 documentation: None,
14908 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14909 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14910 new_text: ".other".to_string(),
14911 })),
14912 ..lsp::CompletionItem::default()
14913 };
14914 let resolved_item_2 = lsp::CompletionItem {
14915 additional_text_edits: Some(vec![lsp::TextEdit {
14916 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14917 new_text: "??".to_string(),
14918 }]),
14919 ..unresolved_item_2.clone()
14920 };
14921
14922 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14923 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14924 cx.lsp
14925 .server
14926 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14927 let unresolved_item_1 = unresolved_item_1.clone();
14928 let resolved_item_1 = resolved_item_1.clone();
14929 let unresolved_item_2 = unresolved_item_2.clone();
14930 let resolved_item_2 = resolved_item_2.clone();
14931 let resolve_requests_1 = resolve_requests_1.clone();
14932 let resolve_requests_2 = resolve_requests_2.clone();
14933 move |unresolved_request, _| {
14934 let unresolved_item_1 = unresolved_item_1.clone();
14935 let resolved_item_1 = resolved_item_1.clone();
14936 let unresolved_item_2 = unresolved_item_2.clone();
14937 let resolved_item_2 = resolved_item_2.clone();
14938 let resolve_requests_1 = resolve_requests_1.clone();
14939 let resolve_requests_2 = resolve_requests_2.clone();
14940 async move {
14941 if unresolved_request == unresolved_item_1 {
14942 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14943 Ok(resolved_item_1.clone())
14944 } else if unresolved_request == unresolved_item_2 {
14945 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14946 Ok(resolved_item_2.clone())
14947 } else {
14948 panic!("Unexpected completion item {unresolved_request:?}")
14949 }
14950 }
14951 }
14952 })
14953 .detach();
14954
14955 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14956 let unresolved_item_1 = unresolved_item_1.clone();
14957 let unresolved_item_2 = unresolved_item_2.clone();
14958 async move {
14959 Ok(Some(lsp::CompletionResponse::Array(vec![
14960 unresolved_item_1,
14961 unresolved_item_2,
14962 ])))
14963 }
14964 })
14965 .next()
14966 .await;
14967
14968 cx.condition(|editor, _| editor.context_menu_visible())
14969 .await;
14970 cx.update_editor(|editor, _, _| {
14971 let context_menu = editor.context_menu.borrow_mut();
14972 let context_menu = context_menu
14973 .as_ref()
14974 .expect("Should have the context menu deployed");
14975 match context_menu {
14976 CodeContextMenu::Completions(completions_menu) => {
14977 let completions = completions_menu.completions.borrow_mut();
14978 assert_eq!(
14979 completions
14980 .iter()
14981 .map(|completion| &completion.label.text)
14982 .collect::<Vec<_>>(),
14983 vec!["id", "other"]
14984 )
14985 }
14986 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14987 }
14988 });
14989 cx.run_until_parked();
14990
14991 cx.update_editor(|editor, window, cx| {
14992 editor.context_menu_next(&ContextMenuNext, window, cx);
14993 });
14994 cx.run_until_parked();
14995 cx.update_editor(|editor, window, cx| {
14996 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14997 });
14998 cx.run_until_parked();
14999 cx.update_editor(|editor, window, cx| {
15000 editor.context_menu_next(&ContextMenuNext, window, cx);
15001 });
15002 cx.run_until_parked();
15003 cx.update_editor(|editor, window, cx| {
15004 editor
15005 .compose_completion(&ComposeCompletion::default(), window, cx)
15006 .expect("No task returned")
15007 })
15008 .await
15009 .expect("Completion failed");
15010 cx.run_until_parked();
15011
15012 cx.update_editor(|editor, _, cx| {
15013 assert_eq!(
15014 resolve_requests_1.load(atomic::Ordering::Acquire),
15015 1,
15016 "Should always resolve once despite multiple selections"
15017 );
15018 assert_eq!(
15019 resolve_requests_2.load(atomic::Ordering::Acquire),
15020 1,
15021 "Should always resolve once after multiple selections and applying the completion"
15022 );
15023 assert_eq!(
15024 editor.text(cx),
15025 "fn main() { let a = ??.other; }",
15026 "Should use resolved data when applying the completion"
15027 );
15028 });
15029}
15030
15031#[gpui::test]
15032async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15033 init_test(cx, |_| {});
15034
15035 let item_0 = lsp::CompletionItem {
15036 label: "abs".into(),
15037 insert_text: Some("abs".into()),
15038 data: Some(json!({ "very": "special"})),
15039 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15040 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15041 lsp::InsertReplaceEdit {
15042 new_text: "abs".to_string(),
15043 insert: lsp::Range::default(),
15044 replace: lsp::Range::default(),
15045 },
15046 )),
15047 ..lsp::CompletionItem::default()
15048 };
15049 let items = iter::once(item_0.clone())
15050 .chain((11..51).map(|i| lsp::CompletionItem {
15051 label: format!("item_{}", i),
15052 insert_text: Some(format!("item_{}", i)),
15053 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15054 ..lsp::CompletionItem::default()
15055 }))
15056 .collect::<Vec<_>>();
15057
15058 let default_commit_characters = vec!["?".to_string()];
15059 let default_data = json!({ "default": "data"});
15060 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15061 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15062 let default_edit_range = lsp::Range {
15063 start: lsp::Position {
15064 line: 0,
15065 character: 5,
15066 },
15067 end: lsp::Position {
15068 line: 0,
15069 character: 5,
15070 },
15071 };
15072
15073 let mut cx = EditorLspTestContext::new_rust(
15074 lsp::ServerCapabilities {
15075 completion_provider: Some(lsp::CompletionOptions {
15076 trigger_characters: Some(vec![".".to_string()]),
15077 resolve_provider: Some(true),
15078 ..Default::default()
15079 }),
15080 ..Default::default()
15081 },
15082 cx,
15083 )
15084 .await;
15085
15086 cx.set_state("fn main() { let a = 2ˇ; }");
15087 cx.simulate_keystroke(".");
15088
15089 let completion_data = default_data.clone();
15090 let completion_characters = default_commit_characters.clone();
15091 let completion_items = items.clone();
15092 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15093 let default_data = completion_data.clone();
15094 let default_commit_characters = completion_characters.clone();
15095 let items = completion_items.clone();
15096 async move {
15097 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15098 items,
15099 item_defaults: Some(lsp::CompletionListItemDefaults {
15100 data: Some(default_data.clone()),
15101 commit_characters: Some(default_commit_characters.clone()),
15102 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15103 default_edit_range,
15104 )),
15105 insert_text_format: Some(default_insert_text_format),
15106 insert_text_mode: Some(default_insert_text_mode),
15107 }),
15108 ..lsp::CompletionList::default()
15109 })))
15110 }
15111 })
15112 .next()
15113 .await;
15114
15115 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15116 cx.lsp
15117 .server
15118 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15119 let closure_resolved_items = resolved_items.clone();
15120 move |item_to_resolve, _| {
15121 let closure_resolved_items = closure_resolved_items.clone();
15122 async move {
15123 closure_resolved_items.lock().push(item_to_resolve.clone());
15124 Ok(item_to_resolve)
15125 }
15126 }
15127 })
15128 .detach();
15129
15130 cx.condition(|editor, _| editor.context_menu_visible())
15131 .await;
15132 cx.run_until_parked();
15133 cx.update_editor(|editor, _, _| {
15134 let menu = editor.context_menu.borrow_mut();
15135 match menu.as_ref().expect("should have the completions menu") {
15136 CodeContextMenu::Completions(completions_menu) => {
15137 assert_eq!(
15138 completions_menu
15139 .entries
15140 .borrow()
15141 .iter()
15142 .map(|mat| mat.string.clone())
15143 .collect::<Vec<String>>(),
15144 items
15145 .iter()
15146 .map(|completion| completion.label.clone())
15147 .collect::<Vec<String>>()
15148 );
15149 }
15150 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15151 }
15152 });
15153 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15154 // with 4 from the end.
15155 assert_eq!(
15156 *resolved_items.lock(),
15157 [&items[0..16], &items[items.len() - 4..items.len()]]
15158 .concat()
15159 .iter()
15160 .cloned()
15161 .map(|mut item| {
15162 if item.data.is_none() {
15163 item.data = Some(default_data.clone());
15164 }
15165 item
15166 })
15167 .collect::<Vec<lsp::CompletionItem>>(),
15168 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15169 );
15170 resolved_items.lock().clear();
15171
15172 cx.update_editor(|editor, window, cx| {
15173 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15174 });
15175 cx.run_until_parked();
15176 // Completions that have already been resolved are skipped.
15177 assert_eq!(
15178 *resolved_items.lock(),
15179 items[items.len() - 16..items.len() - 4]
15180 .iter()
15181 .cloned()
15182 .map(|mut item| {
15183 if item.data.is_none() {
15184 item.data = Some(default_data.clone());
15185 }
15186 item
15187 })
15188 .collect::<Vec<lsp::CompletionItem>>()
15189 );
15190 resolved_items.lock().clear();
15191}
15192
15193#[gpui::test]
15194async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15195 init_test(cx, |_| {});
15196
15197 let mut cx = EditorLspTestContext::new(
15198 Language::new(
15199 LanguageConfig {
15200 matcher: LanguageMatcher {
15201 path_suffixes: vec!["jsx".into()],
15202 ..Default::default()
15203 },
15204 overrides: [(
15205 "element".into(),
15206 LanguageConfigOverride {
15207 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15208 ..Default::default()
15209 },
15210 )]
15211 .into_iter()
15212 .collect(),
15213 ..Default::default()
15214 },
15215 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15216 )
15217 .with_override_query("(jsx_self_closing_element) @element")
15218 .unwrap(),
15219 lsp::ServerCapabilities {
15220 completion_provider: Some(lsp::CompletionOptions {
15221 trigger_characters: Some(vec![":".to_string()]),
15222 ..Default::default()
15223 }),
15224 ..Default::default()
15225 },
15226 cx,
15227 )
15228 .await;
15229
15230 cx.lsp
15231 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15232 Ok(Some(lsp::CompletionResponse::Array(vec![
15233 lsp::CompletionItem {
15234 label: "bg-blue".into(),
15235 ..Default::default()
15236 },
15237 lsp::CompletionItem {
15238 label: "bg-red".into(),
15239 ..Default::default()
15240 },
15241 lsp::CompletionItem {
15242 label: "bg-yellow".into(),
15243 ..Default::default()
15244 },
15245 ])))
15246 });
15247
15248 cx.set_state(r#"<p class="bgˇ" />"#);
15249
15250 // Trigger completion when typing a dash, because the dash is an extra
15251 // word character in the 'element' scope, which contains the cursor.
15252 cx.simulate_keystroke("-");
15253 cx.executor().run_until_parked();
15254 cx.update_editor(|editor, _, _| {
15255 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15256 {
15257 assert_eq!(
15258 completion_menu_entries(&menu),
15259 &["bg-red", "bg-blue", "bg-yellow"]
15260 );
15261 } else {
15262 panic!("expected completion menu to be open");
15263 }
15264 });
15265
15266 cx.simulate_keystroke("l");
15267 cx.executor().run_until_parked();
15268 cx.update_editor(|editor, _, _| {
15269 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15270 {
15271 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15272 } else {
15273 panic!("expected completion menu to be open");
15274 }
15275 });
15276
15277 // When filtering completions, consider the character after the '-' to
15278 // be the start of a subword.
15279 cx.set_state(r#"<p class="yelˇ" />"#);
15280 cx.simulate_keystroke("l");
15281 cx.executor().run_until_parked();
15282 cx.update_editor(|editor, _, _| {
15283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15284 {
15285 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15286 } else {
15287 panic!("expected completion menu to be open");
15288 }
15289 });
15290}
15291
15292fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15293 let entries = menu.entries.borrow();
15294 entries.iter().map(|mat| mat.string.clone()).collect()
15295}
15296
15297#[gpui::test]
15298async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15299 init_test(cx, |settings| {
15300 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15301 FormatterList(vec![Formatter::Prettier].into()),
15302 ))
15303 });
15304
15305 let fs = FakeFs::new(cx.executor());
15306 fs.insert_file(path!("/file.ts"), Default::default()).await;
15307
15308 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15310
15311 language_registry.add(Arc::new(Language::new(
15312 LanguageConfig {
15313 name: "TypeScript".into(),
15314 matcher: LanguageMatcher {
15315 path_suffixes: vec!["ts".to_string()],
15316 ..Default::default()
15317 },
15318 ..Default::default()
15319 },
15320 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15321 )));
15322 update_test_language_settings(cx, |settings| {
15323 settings.defaults.prettier = Some(PrettierSettings {
15324 allowed: true,
15325 ..PrettierSettings::default()
15326 });
15327 });
15328
15329 let test_plugin = "test_plugin";
15330 let _ = language_registry.register_fake_lsp(
15331 "TypeScript",
15332 FakeLspAdapter {
15333 prettier_plugins: vec![test_plugin],
15334 ..Default::default()
15335 },
15336 );
15337
15338 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15339 let buffer = project
15340 .update(cx, |project, cx| {
15341 project.open_local_buffer(path!("/file.ts"), cx)
15342 })
15343 .await
15344 .unwrap();
15345
15346 let buffer_text = "one\ntwo\nthree\n";
15347 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15348 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15349 editor.update_in(cx, |editor, window, cx| {
15350 editor.set_text(buffer_text, window, cx)
15351 });
15352
15353 editor
15354 .update_in(cx, |editor, window, cx| {
15355 editor.perform_format(
15356 project.clone(),
15357 FormatTrigger::Manual,
15358 FormatTarget::Buffers,
15359 window,
15360 cx,
15361 )
15362 })
15363 .unwrap()
15364 .await;
15365 assert_eq!(
15366 editor.update(cx, |editor, cx| editor.text(cx)),
15367 buffer_text.to_string() + prettier_format_suffix,
15368 "Test prettier formatting was not applied to the original buffer text",
15369 );
15370
15371 update_test_language_settings(cx, |settings| {
15372 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15373 });
15374 let format = editor.update_in(cx, |editor, window, cx| {
15375 editor.perform_format(
15376 project.clone(),
15377 FormatTrigger::Manual,
15378 FormatTarget::Buffers,
15379 window,
15380 cx,
15381 )
15382 });
15383 format.await.unwrap();
15384 assert_eq!(
15385 editor.update(cx, |editor, cx| editor.text(cx)),
15386 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15387 "Autoformatting (via test prettier) was not applied to the original buffer text",
15388 );
15389}
15390
15391#[gpui::test]
15392async fn test_addition_reverts(cx: &mut TestAppContext) {
15393 init_test(cx, |_| {});
15394 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15395 let base_text = indoc! {r#"
15396 struct Row;
15397 struct Row1;
15398 struct Row2;
15399
15400 struct Row4;
15401 struct Row5;
15402 struct Row6;
15403
15404 struct Row8;
15405 struct Row9;
15406 struct Row10;"#};
15407
15408 // When addition hunks are not adjacent to carets, no hunk revert is performed
15409 assert_hunk_revert(
15410 indoc! {r#"struct Row;
15411 struct Row1;
15412 struct Row1.1;
15413 struct Row1.2;
15414 struct Row2;ˇ
15415
15416 struct Row4;
15417 struct Row5;
15418 struct Row6;
15419
15420 struct Row8;
15421 ˇstruct Row9;
15422 struct Row9.1;
15423 struct Row9.2;
15424 struct Row9.3;
15425 struct Row10;"#},
15426 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15427 indoc! {r#"struct Row;
15428 struct Row1;
15429 struct Row1.1;
15430 struct Row1.2;
15431 struct Row2;ˇ
15432
15433 struct Row4;
15434 struct Row5;
15435 struct Row6;
15436
15437 struct Row8;
15438 ˇstruct Row9;
15439 struct Row9.1;
15440 struct Row9.2;
15441 struct Row9.3;
15442 struct Row10;"#},
15443 base_text,
15444 &mut cx,
15445 );
15446 // Same for selections
15447 assert_hunk_revert(
15448 indoc! {r#"struct Row;
15449 struct Row1;
15450 struct Row2;
15451 struct Row2.1;
15452 struct Row2.2;
15453 «ˇ
15454 struct Row4;
15455 struct» Row5;
15456 «struct Row6;
15457 ˇ»
15458 struct Row9.1;
15459 struct Row9.2;
15460 struct Row9.3;
15461 struct Row8;
15462 struct Row9;
15463 struct Row10;"#},
15464 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15465 indoc! {r#"struct Row;
15466 struct Row1;
15467 struct Row2;
15468 struct Row2.1;
15469 struct Row2.2;
15470 «ˇ
15471 struct Row4;
15472 struct» Row5;
15473 «struct Row6;
15474 ˇ»
15475 struct Row9.1;
15476 struct Row9.2;
15477 struct Row9.3;
15478 struct Row8;
15479 struct Row9;
15480 struct Row10;"#},
15481 base_text,
15482 &mut cx,
15483 );
15484
15485 // When carets and selections intersect the addition hunks, those are reverted.
15486 // Adjacent carets got merged.
15487 assert_hunk_revert(
15488 indoc! {r#"struct Row;
15489 ˇ// something on the top
15490 struct Row1;
15491 struct Row2;
15492 struct Roˇw3.1;
15493 struct Row2.2;
15494 struct Row2.3;ˇ
15495
15496 struct Row4;
15497 struct ˇRow5.1;
15498 struct Row5.2;
15499 struct «Rowˇ»5.3;
15500 struct Row5;
15501 struct Row6;
15502 ˇ
15503 struct Row9.1;
15504 struct «Rowˇ»9.2;
15505 struct «ˇRow»9.3;
15506 struct Row8;
15507 struct Row9;
15508 «ˇ// something on bottom»
15509 struct Row10;"#},
15510 vec![
15511 DiffHunkStatusKind::Added,
15512 DiffHunkStatusKind::Added,
15513 DiffHunkStatusKind::Added,
15514 DiffHunkStatusKind::Added,
15515 DiffHunkStatusKind::Added,
15516 ],
15517 indoc! {r#"struct Row;
15518 ˇstruct Row1;
15519 struct Row2;
15520 ˇ
15521 struct Row4;
15522 ˇstruct Row5;
15523 struct Row6;
15524 ˇ
15525 ˇstruct Row8;
15526 struct Row9;
15527 ˇstruct Row10;"#},
15528 base_text,
15529 &mut cx,
15530 );
15531}
15532
15533#[gpui::test]
15534async fn test_modification_reverts(cx: &mut TestAppContext) {
15535 init_test(cx, |_| {});
15536 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15537 let base_text = indoc! {r#"
15538 struct Row;
15539 struct Row1;
15540 struct Row2;
15541
15542 struct Row4;
15543 struct Row5;
15544 struct Row6;
15545
15546 struct Row8;
15547 struct Row9;
15548 struct Row10;"#};
15549
15550 // Modification hunks behave the same as the addition ones.
15551 assert_hunk_revert(
15552 indoc! {r#"struct Row;
15553 struct Row1;
15554 struct Row33;
15555 ˇ
15556 struct Row4;
15557 struct Row5;
15558 struct Row6;
15559 ˇ
15560 struct Row99;
15561 struct Row9;
15562 struct Row10;"#},
15563 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15564 indoc! {r#"struct Row;
15565 struct Row1;
15566 struct Row33;
15567 ˇ
15568 struct Row4;
15569 struct Row5;
15570 struct Row6;
15571 ˇ
15572 struct Row99;
15573 struct Row9;
15574 struct Row10;"#},
15575 base_text,
15576 &mut cx,
15577 );
15578 assert_hunk_revert(
15579 indoc! {r#"struct Row;
15580 struct Row1;
15581 struct Row33;
15582 «ˇ
15583 struct Row4;
15584 struct» Row5;
15585 «struct Row6;
15586 ˇ»
15587 struct Row99;
15588 struct Row9;
15589 struct Row10;"#},
15590 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15591 indoc! {r#"struct Row;
15592 struct Row1;
15593 struct Row33;
15594 «ˇ
15595 struct Row4;
15596 struct» Row5;
15597 «struct Row6;
15598 ˇ»
15599 struct Row99;
15600 struct Row9;
15601 struct Row10;"#},
15602 base_text,
15603 &mut cx,
15604 );
15605
15606 assert_hunk_revert(
15607 indoc! {r#"ˇstruct Row1.1;
15608 struct Row1;
15609 «ˇstr»uct Row22;
15610
15611 struct ˇRow44;
15612 struct Row5;
15613 struct «Rˇ»ow66;ˇ
15614
15615 «struˇ»ct Row88;
15616 struct Row9;
15617 struct Row1011;ˇ"#},
15618 vec![
15619 DiffHunkStatusKind::Modified,
15620 DiffHunkStatusKind::Modified,
15621 DiffHunkStatusKind::Modified,
15622 DiffHunkStatusKind::Modified,
15623 DiffHunkStatusKind::Modified,
15624 DiffHunkStatusKind::Modified,
15625 ],
15626 indoc! {r#"struct Row;
15627 ˇstruct Row1;
15628 struct Row2;
15629 ˇ
15630 struct Row4;
15631 ˇstruct Row5;
15632 struct Row6;
15633 ˇ
15634 struct Row8;
15635 ˇstruct Row9;
15636 struct Row10;ˇ"#},
15637 base_text,
15638 &mut cx,
15639 );
15640}
15641
15642#[gpui::test]
15643async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15644 init_test(cx, |_| {});
15645 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15646 let base_text = indoc! {r#"
15647 one
15648
15649 two
15650 three
15651 "#};
15652
15653 cx.set_head_text(base_text);
15654 cx.set_state("\nˇ\n");
15655 cx.executor().run_until_parked();
15656 cx.update_editor(|editor, _window, cx| {
15657 editor.expand_selected_diff_hunks(cx);
15658 });
15659 cx.executor().run_until_parked();
15660 cx.update_editor(|editor, window, cx| {
15661 editor.backspace(&Default::default(), window, cx);
15662 });
15663 cx.run_until_parked();
15664 cx.assert_state_with_diff(
15665 indoc! {r#"
15666
15667 - two
15668 - threeˇ
15669 +
15670 "#}
15671 .to_string(),
15672 );
15673}
15674
15675#[gpui::test]
15676async fn test_deletion_reverts(cx: &mut TestAppContext) {
15677 init_test(cx, |_| {});
15678 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15679 let base_text = indoc! {r#"struct Row;
15680struct Row1;
15681struct Row2;
15682
15683struct Row4;
15684struct Row5;
15685struct Row6;
15686
15687struct Row8;
15688struct Row9;
15689struct Row10;"#};
15690
15691 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15692 assert_hunk_revert(
15693 indoc! {r#"struct Row;
15694 struct Row2;
15695
15696 ˇstruct Row4;
15697 struct Row5;
15698 struct Row6;
15699 ˇ
15700 struct Row8;
15701 struct Row10;"#},
15702 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15703 indoc! {r#"struct Row;
15704 struct Row2;
15705
15706 ˇstruct Row4;
15707 struct Row5;
15708 struct Row6;
15709 ˇ
15710 struct Row8;
15711 struct Row10;"#},
15712 base_text,
15713 &mut cx,
15714 );
15715 assert_hunk_revert(
15716 indoc! {r#"struct Row;
15717 struct Row2;
15718
15719 «ˇstruct Row4;
15720 struct» Row5;
15721 «struct Row6;
15722 ˇ»
15723 struct Row8;
15724 struct Row10;"#},
15725 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15726 indoc! {r#"struct Row;
15727 struct Row2;
15728
15729 «ˇstruct Row4;
15730 struct» Row5;
15731 «struct Row6;
15732 ˇ»
15733 struct Row8;
15734 struct Row10;"#},
15735 base_text,
15736 &mut cx,
15737 );
15738
15739 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15740 assert_hunk_revert(
15741 indoc! {r#"struct Row;
15742 ˇstruct Row2;
15743
15744 struct Row4;
15745 struct Row5;
15746 struct Row6;
15747
15748 struct Row8;ˇ
15749 struct Row10;"#},
15750 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15751 indoc! {r#"struct Row;
15752 struct Row1;
15753 ˇstruct Row2;
15754
15755 struct Row4;
15756 struct Row5;
15757 struct Row6;
15758
15759 struct Row8;ˇ
15760 struct Row9;
15761 struct Row10;"#},
15762 base_text,
15763 &mut cx,
15764 );
15765 assert_hunk_revert(
15766 indoc! {r#"struct Row;
15767 struct Row2«ˇ;
15768 struct Row4;
15769 struct» Row5;
15770 «struct Row6;
15771
15772 struct Row8;ˇ»
15773 struct Row10;"#},
15774 vec![
15775 DiffHunkStatusKind::Deleted,
15776 DiffHunkStatusKind::Deleted,
15777 DiffHunkStatusKind::Deleted,
15778 ],
15779 indoc! {r#"struct Row;
15780 struct Row1;
15781 struct Row2«ˇ;
15782
15783 struct Row4;
15784 struct» Row5;
15785 «struct Row6;
15786
15787 struct Row8;ˇ»
15788 struct Row9;
15789 struct Row10;"#},
15790 base_text,
15791 &mut cx,
15792 );
15793}
15794
15795#[gpui::test]
15796async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15797 init_test(cx, |_| {});
15798
15799 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15800 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15801 let base_text_3 =
15802 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15803
15804 let text_1 = edit_first_char_of_every_line(base_text_1);
15805 let text_2 = edit_first_char_of_every_line(base_text_2);
15806 let text_3 = edit_first_char_of_every_line(base_text_3);
15807
15808 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15809 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15810 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15811
15812 let multibuffer = cx.new(|cx| {
15813 let mut multibuffer = MultiBuffer::new(ReadWrite);
15814 multibuffer.push_excerpts(
15815 buffer_1.clone(),
15816 [
15817 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15818 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15819 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15820 ],
15821 cx,
15822 );
15823 multibuffer.push_excerpts(
15824 buffer_2.clone(),
15825 [
15826 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15827 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15828 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15829 ],
15830 cx,
15831 );
15832 multibuffer.push_excerpts(
15833 buffer_3.clone(),
15834 [
15835 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15836 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15837 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15838 ],
15839 cx,
15840 );
15841 multibuffer
15842 });
15843
15844 let fs = FakeFs::new(cx.executor());
15845 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15846 let (editor, cx) = cx
15847 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15848 editor.update_in(cx, |editor, _window, cx| {
15849 for (buffer, diff_base) in [
15850 (buffer_1.clone(), base_text_1),
15851 (buffer_2.clone(), base_text_2),
15852 (buffer_3.clone(), base_text_3),
15853 ] {
15854 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15855 editor
15856 .buffer
15857 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15858 }
15859 });
15860 cx.executor().run_until_parked();
15861
15862 editor.update_in(cx, |editor, window, cx| {
15863 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}");
15864 editor.select_all(&SelectAll, window, cx);
15865 editor.git_restore(&Default::default(), window, cx);
15866 });
15867 cx.executor().run_until_parked();
15868
15869 // When all ranges are selected, all buffer hunks are reverted.
15870 editor.update(cx, |editor, cx| {
15871 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");
15872 });
15873 buffer_1.update(cx, |buffer, _| {
15874 assert_eq!(buffer.text(), base_text_1);
15875 });
15876 buffer_2.update(cx, |buffer, _| {
15877 assert_eq!(buffer.text(), base_text_2);
15878 });
15879 buffer_3.update(cx, |buffer, _| {
15880 assert_eq!(buffer.text(), base_text_3);
15881 });
15882
15883 editor.update_in(cx, |editor, window, cx| {
15884 editor.undo(&Default::default(), window, cx);
15885 });
15886
15887 editor.update_in(cx, |editor, window, cx| {
15888 editor.change_selections(None, window, cx, |s| {
15889 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15890 });
15891 editor.git_restore(&Default::default(), window, cx);
15892 });
15893
15894 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15895 // but not affect buffer_2 and its related excerpts.
15896 editor.update(cx, |editor, cx| {
15897 assert_eq!(
15898 editor.text(cx),
15899 "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}"
15900 );
15901 });
15902 buffer_1.update(cx, |buffer, _| {
15903 assert_eq!(buffer.text(), base_text_1);
15904 });
15905 buffer_2.update(cx, |buffer, _| {
15906 assert_eq!(
15907 buffer.text(),
15908 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15909 );
15910 });
15911 buffer_3.update(cx, |buffer, _| {
15912 assert_eq!(
15913 buffer.text(),
15914 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15915 );
15916 });
15917
15918 fn edit_first_char_of_every_line(text: &str) -> String {
15919 text.split('\n')
15920 .map(|line| format!("X{}", &line[1..]))
15921 .collect::<Vec<_>>()
15922 .join("\n")
15923 }
15924}
15925
15926#[gpui::test]
15927async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15928 init_test(cx, |_| {});
15929
15930 let cols = 4;
15931 let rows = 10;
15932 let sample_text_1 = sample_text(rows, cols, 'a');
15933 assert_eq!(
15934 sample_text_1,
15935 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15936 );
15937 let sample_text_2 = sample_text(rows, cols, 'l');
15938 assert_eq!(
15939 sample_text_2,
15940 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15941 );
15942 let sample_text_3 = sample_text(rows, cols, 'v');
15943 assert_eq!(
15944 sample_text_3,
15945 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15946 );
15947
15948 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15949 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15950 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15951
15952 let multi_buffer = cx.new(|cx| {
15953 let mut multibuffer = MultiBuffer::new(ReadWrite);
15954 multibuffer.push_excerpts(
15955 buffer_1.clone(),
15956 [
15957 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15958 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15959 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15960 ],
15961 cx,
15962 );
15963 multibuffer.push_excerpts(
15964 buffer_2.clone(),
15965 [
15966 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15967 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15968 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15969 ],
15970 cx,
15971 );
15972 multibuffer.push_excerpts(
15973 buffer_3.clone(),
15974 [
15975 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15976 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15977 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15978 ],
15979 cx,
15980 );
15981 multibuffer
15982 });
15983
15984 let fs = FakeFs::new(cx.executor());
15985 fs.insert_tree(
15986 "/a",
15987 json!({
15988 "main.rs": sample_text_1,
15989 "other.rs": sample_text_2,
15990 "lib.rs": sample_text_3,
15991 }),
15992 )
15993 .await;
15994 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15995 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15996 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15997 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15998 Editor::new(
15999 EditorMode::full(),
16000 multi_buffer,
16001 Some(project.clone()),
16002 window,
16003 cx,
16004 )
16005 });
16006 let multibuffer_item_id = workspace
16007 .update(cx, |workspace, window, cx| {
16008 assert!(
16009 workspace.active_item(cx).is_none(),
16010 "active item should be None before the first item is added"
16011 );
16012 workspace.add_item_to_active_pane(
16013 Box::new(multi_buffer_editor.clone()),
16014 None,
16015 true,
16016 window,
16017 cx,
16018 );
16019 let active_item = workspace
16020 .active_item(cx)
16021 .expect("should have an active item after adding the multi buffer");
16022 assert!(
16023 !active_item.is_singleton(cx),
16024 "A multi buffer was expected to active after adding"
16025 );
16026 active_item.item_id()
16027 })
16028 .unwrap();
16029 cx.executor().run_until_parked();
16030
16031 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16032 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16033 s.select_ranges(Some(1..2))
16034 });
16035 editor.open_excerpts(&OpenExcerpts, window, cx);
16036 });
16037 cx.executor().run_until_parked();
16038 let first_item_id = workspace
16039 .update(cx, |workspace, window, cx| {
16040 let active_item = workspace
16041 .active_item(cx)
16042 .expect("should have an active item after navigating into the 1st buffer");
16043 let first_item_id = active_item.item_id();
16044 assert_ne!(
16045 first_item_id, multibuffer_item_id,
16046 "Should navigate into the 1st buffer and activate it"
16047 );
16048 assert!(
16049 active_item.is_singleton(cx),
16050 "New active item should be a singleton buffer"
16051 );
16052 assert_eq!(
16053 active_item
16054 .act_as::<Editor>(cx)
16055 .expect("should have navigated into an editor for the 1st buffer")
16056 .read(cx)
16057 .text(cx),
16058 sample_text_1
16059 );
16060
16061 workspace
16062 .go_back(workspace.active_pane().downgrade(), window, cx)
16063 .detach_and_log_err(cx);
16064
16065 first_item_id
16066 })
16067 .unwrap();
16068 cx.executor().run_until_parked();
16069 workspace
16070 .update(cx, |workspace, _, cx| {
16071 let active_item = workspace
16072 .active_item(cx)
16073 .expect("should have an active item after navigating back");
16074 assert_eq!(
16075 active_item.item_id(),
16076 multibuffer_item_id,
16077 "Should navigate back to the multi buffer"
16078 );
16079 assert!(!active_item.is_singleton(cx));
16080 })
16081 .unwrap();
16082
16083 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16084 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16085 s.select_ranges(Some(39..40))
16086 });
16087 editor.open_excerpts(&OpenExcerpts, window, cx);
16088 });
16089 cx.executor().run_until_parked();
16090 let second_item_id = workspace
16091 .update(cx, |workspace, window, cx| {
16092 let active_item = workspace
16093 .active_item(cx)
16094 .expect("should have an active item after navigating into the 2nd buffer");
16095 let second_item_id = active_item.item_id();
16096 assert_ne!(
16097 second_item_id, multibuffer_item_id,
16098 "Should navigate away from the multibuffer"
16099 );
16100 assert_ne!(
16101 second_item_id, first_item_id,
16102 "Should navigate into the 2nd buffer and activate it"
16103 );
16104 assert!(
16105 active_item.is_singleton(cx),
16106 "New active item should be a singleton buffer"
16107 );
16108 assert_eq!(
16109 active_item
16110 .act_as::<Editor>(cx)
16111 .expect("should have navigated into an editor")
16112 .read(cx)
16113 .text(cx),
16114 sample_text_2
16115 );
16116
16117 workspace
16118 .go_back(workspace.active_pane().downgrade(), window, cx)
16119 .detach_and_log_err(cx);
16120
16121 second_item_id
16122 })
16123 .unwrap();
16124 cx.executor().run_until_parked();
16125 workspace
16126 .update(cx, |workspace, _, cx| {
16127 let active_item = workspace
16128 .active_item(cx)
16129 .expect("should have an active item after navigating back from the 2nd buffer");
16130 assert_eq!(
16131 active_item.item_id(),
16132 multibuffer_item_id,
16133 "Should navigate back from the 2nd buffer to the multi buffer"
16134 );
16135 assert!(!active_item.is_singleton(cx));
16136 })
16137 .unwrap();
16138
16139 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16140 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16141 s.select_ranges(Some(70..70))
16142 });
16143 editor.open_excerpts(&OpenExcerpts, window, cx);
16144 });
16145 cx.executor().run_until_parked();
16146 workspace
16147 .update(cx, |workspace, window, cx| {
16148 let active_item = workspace
16149 .active_item(cx)
16150 .expect("should have an active item after navigating into the 3rd buffer");
16151 let third_item_id = active_item.item_id();
16152 assert_ne!(
16153 third_item_id, multibuffer_item_id,
16154 "Should navigate into the 3rd buffer and activate it"
16155 );
16156 assert_ne!(third_item_id, first_item_id);
16157 assert_ne!(third_item_id, second_item_id);
16158 assert!(
16159 active_item.is_singleton(cx),
16160 "New active item should be a singleton buffer"
16161 );
16162 assert_eq!(
16163 active_item
16164 .act_as::<Editor>(cx)
16165 .expect("should have navigated into an editor")
16166 .read(cx)
16167 .text(cx),
16168 sample_text_3
16169 );
16170
16171 workspace
16172 .go_back(workspace.active_pane().downgrade(), window, cx)
16173 .detach_and_log_err(cx);
16174 })
16175 .unwrap();
16176 cx.executor().run_until_parked();
16177 workspace
16178 .update(cx, |workspace, _, cx| {
16179 let active_item = workspace
16180 .active_item(cx)
16181 .expect("should have an active item after navigating back from the 3rd buffer");
16182 assert_eq!(
16183 active_item.item_id(),
16184 multibuffer_item_id,
16185 "Should navigate back from the 3rd buffer to the multi buffer"
16186 );
16187 assert!(!active_item.is_singleton(cx));
16188 })
16189 .unwrap();
16190}
16191
16192#[gpui::test]
16193async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16194 init_test(cx, |_| {});
16195
16196 let mut cx = EditorTestContext::new(cx).await;
16197
16198 let diff_base = r#"
16199 use some::mod;
16200
16201 const A: u32 = 42;
16202
16203 fn main() {
16204 println!("hello");
16205
16206 println!("world");
16207 }
16208 "#
16209 .unindent();
16210
16211 cx.set_state(
16212 &r#"
16213 use some::modified;
16214
16215 ˇ
16216 fn main() {
16217 println!("hello there");
16218
16219 println!("around the");
16220 println!("world");
16221 }
16222 "#
16223 .unindent(),
16224 );
16225
16226 cx.set_head_text(&diff_base);
16227 executor.run_until_parked();
16228
16229 cx.update_editor(|editor, window, cx| {
16230 editor.go_to_next_hunk(&GoToHunk, window, cx);
16231 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16232 });
16233 executor.run_until_parked();
16234 cx.assert_state_with_diff(
16235 r#"
16236 use some::modified;
16237
16238
16239 fn main() {
16240 - println!("hello");
16241 + ˇ println!("hello there");
16242
16243 println!("around the");
16244 println!("world");
16245 }
16246 "#
16247 .unindent(),
16248 );
16249
16250 cx.update_editor(|editor, window, cx| {
16251 for _ in 0..2 {
16252 editor.go_to_next_hunk(&GoToHunk, window, cx);
16253 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16254 }
16255 });
16256 executor.run_until_parked();
16257 cx.assert_state_with_diff(
16258 r#"
16259 - use some::mod;
16260 + ˇuse some::modified;
16261
16262
16263 fn main() {
16264 - println!("hello");
16265 + println!("hello there");
16266
16267 + println!("around the");
16268 println!("world");
16269 }
16270 "#
16271 .unindent(),
16272 );
16273
16274 cx.update_editor(|editor, window, cx| {
16275 editor.go_to_next_hunk(&GoToHunk, window, cx);
16276 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16277 });
16278 executor.run_until_parked();
16279 cx.assert_state_with_diff(
16280 r#"
16281 - use some::mod;
16282 + use some::modified;
16283
16284 - const A: u32 = 42;
16285 ˇ
16286 fn main() {
16287 - println!("hello");
16288 + println!("hello there");
16289
16290 + println!("around the");
16291 println!("world");
16292 }
16293 "#
16294 .unindent(),
16295 );
16296
16297 cx.update_editor(|editor, window, cx| {
16298 editor.cancel(&Cancel, window, cx);
16299 });
16300
16301 cx.assert_state_with_diff(
16302 r#"
16303 use some::modified;
16304
16305 ˇ
16306 fn main() {
16307 println!("hello there");
16308
16309 println!("around the");
16310 println!("world");
16311 }
16312 "#
16313 .unindent(),
16314 );
16315}
16316
16317#[gpui::test]
16318async fn test_diff_base_change_with_expanded_diff_hunks(
16319 executor: BackgroundExecutor,
16320 cx: &mut TestAppContext,
16321) {
16322 init_test(cx, |_| {});
16323
16324 let mut cx = EditorTestContext::new(cx).await;
16325
16326 let diff_base = r#"
16327 use some::mod1;
16328 use some::mod2;
16329
16330 const A: u32 = 42;
16331 const B: u32 = 42;
16332 const C: u32 = 42;
16333
16334 fn main() {
16335 println!("hello");
16336
16337 println!("world");
16338 }
16339 "#
16340 .unindent();
16341
16342 cx.set_state(
16343 &r#"
16344 use some::mod2;
16345
16346 const A: u32 = 42;
16347 const C: u32 = 42;
16348
16349 fn main(ˇ) {
16350 //println!("hello");
16351
16352 println!("world");
16353 //
16354 //
16355 }
16356 "#
16357 .unindent(),
16358 );
16359
16360 cx.set_head_text(&diff_base);
16361 executor.run_until_parked();
16362
16363 cx.update_editor(|editor, window, cx| {
16364 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16365 });
16366 executor.run_until_parked();
16367 cx.assert_state_with_diff(
16368 r#"
16369 - use some::mod1;
16370 use some::mod2;
16371
16372 const A: u32 = 42;
16373 - const B: u32 = 42;
16374 const C: u32 = 42;
16375
16376 fn main(ˇ) {
16377 - println!("hello");
16378 + //println!("hello");
16379
16380 println!("world");
16381 + //
16382 + //
16383 }
16384 "#
16385 .unindent(),
16386 );
16387
16388 cx.set_head_text("new diff base!");
16389 executor.run_until_parked();
16390 cx.assert_state_with_diff(
16391 r#"
16392 - new diff base!
16393 + use some::mod2;
16394 +
16395 + const A: u32 = 42;
16396 + const C: u32 = 42;
16397 +
16398 + fn main(ˇ) {
16399 + //println!("hello");
16400 +
16401 + println!("world");
16402 + //
16403 + //
16404 + }
16405 "#
16406 .unindent(),
16407 );
16408}
16409
16410#[gpui::test]
16411async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16412 init_test(cx, |_| {});
16413
16414 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16415 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16416 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16417 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16418 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16419 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16420
16421 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16422 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16423 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16424
16425 let multi_buffer = cx.new(|cx| {
16426 let mut multibuffer = MultiBuffer::new(ReadWrite);
16427 multibuffer.push_excerpts(
16428 buffer_1.clone(),
16429 [
16430 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16431 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16432 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16433 ],
16434 cx,
16435 );
16436 multibuffer.push_excerpts(
16437 buffer_2.clone(),
16438 [
16439 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16440 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16441 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16442 ],
16443 cx,
16444 );
16445 multibuffer.push_excerpts(
16446 buffer_3.clone(),
16447 [
16448 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16449 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16450 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16451 ],
16452 cx,
16453 );
16454 multibuffer
16455 });
16456
16457 let editor =
16458 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16459 editor
16460 .update(cx, |editor, _window, cx| {
16461 for (buffer, diff_base) in [
16462 (buffer_1.clone(), file_1_old),
16463 (buffer_2.clone(), file_2_old),
16464 (buffer_3.clone(), file_3_old),
16465 ] {
16466 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16467 editor
16468 .buffer
16469 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16470 }
16471 })
16472 .unwrap();
16473
16474 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16475 cx.run_until_parked();
16476
16477 cx.assert_editor_state(
16478 &"
16479 ˇaaa
16480 ccc
16481 ddd
16482
16483 ggg
16484 hhh
16485
16486
16487 lll
16488 mmm
16489 NNN
16490
16491 qqq
16492 rrr
16493
16494 uuu
16495 111
16496 222
16497 333
16498
16499 666
16500 777
16501
16502 000
16503 !!!"
16504 .unindent(),
16505 );
16506
16507 cx.update_editor(|editor, window, cx| {
16508 editor.select_all(&SelectAll, window, cx);
16509 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16510 });
16511 cx.executor().run_until_parked();
16512
16513 cx.assert_state_with_diff(
16514 "
16515 «aaa
16516 - bbb
16517 ccc
16518 ddd
16519
16520 ggg
16521 hhh
16522
16523
16524 lll
16525 mmm
16526 - nnn
16527 + NNN
16528
16529 qqq
16530 rrr
16531
16532 uuu
16533 111
16534 222
16535 333
16536
16537 + 666
16538 777
16539
16540 000
16541 !!!ˇ»"
16542 .unindent(),
16543 );
16544}
16545
16546#[gpui::test]
16547async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16548 init_test(cx, |_| {});
16549
16550 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16551 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16552
16553 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16554 let multi_buffer = cx.new(|cx| {
16555 let mut multibuffer = MultiBuffer::new(ReadWrite);
16556 multibuffer.push_excerpts(
16557 buffer.clone(),
16558 [
16559 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16560 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16561 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16562 ],
16563 cx,
16564 );
16565 multibuffer
16566 });
16567
16568 let editor =
16569 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16570 editor
16571 .update(cx, |editor, _window, cx| {
16572 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16573 editor
16574 .buffer
16575 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16576 })
16577 .unwrap();
16578
16579 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16580 cx.run_until_parked();
16581
16582 cx.update_editor(|editor, window, cx| {
16583 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16584 });
16585 cx.executor().run_until_parked();
16586
16587 // When the start of a hunk coincides with the start of its excerpt,
16588 // the hunk is expanded. When the start of a a hunk is earlier than
16589 // the start of its excerpt, the hunk is not expanded.
16590 cx.assert_state_with_diff(
16591 "
16592 ˇaaa
16593 - bbb
16594 + BBB
16595
16596 - ddd
16597 - eee
16598 + DDD
16599 + EEE
16600 fff
16601
16602 iii
16603 "
16604 .unindent(),
16605 );
16606}
16607
16608#[gpui::test]
16609async fn test_edits_around_expanded_insertion_hunks(
16610 executor: BackgroundExecutor,
16611 cx: &mut TestAppContext,
16612) {
16613 init_test(cx, |_| {});
16614
16615 let mut cx = EditorTestContext::new(cx).await;
16616
16617 let diff_base = r#"
16618 use some::mod1;
16619 use some::mod2;
16620
16621 const A: u32 = 42;
16622
16623 fn main() {
16624 println!("hello");
16625
16626 println!("world");
16627 }
16628 "#
16629 .unindent();
16630 executor.run_until_parked();
16631 cx.set_state(
16632 &r#"
16633 use some::mod1;
16634 use some::mod2;
16635
16636 const A: u32 = 42;
16637 const B: u32 = 42;
16638 const C: u32 = 42;
16639 ˇ
16640
16641 fn main() {
16642 println!("hello");
16643
16644 println!("world");
16645 }
16646 "#
16647 .unindent(),
16648 );
16649
16650 cx.set_head_text(&diff_base);
16651 executor.run_until_parked();
16652
16653 cx.update_editor(|editor, window, cx| {
16654 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16655 });
16656 executor.run_until_parked();
16657
16658 cx.assert_state_with_diff(
16659 r#"
16660 use some::mod1;
16661 use some::mod2;
16662
16663 const A: u32 = 42;
16664 + const B: u32 = 42;
16665 + const C: u32 = 42;
16666 + ˇ
16667
16668 fn main() {
16669 println!("hello");
16670
16671 println!("world");
16672 }
16673 "#
16674 .unindent(),
16675 );
16676
16677 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16678 executor.run_until_parked();
16679
16680 cx.assert_state_with_diff(
16681 r#"
16682 use some::mod1;
16683 use some::mod2;
16684
16685 const A: u32 = 42;
16686 + const B: u32 = 42;
16687 + const C: u32 = 42;
16688 + const D: u32 = 42;
16689 + ˇ
16690
16691 fn main() {
16692 println!("hello");
16693
16694 println!("world");
16695 }
16696 "#
16697 .unindent(),
16698 );
16699
16700 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16701 executor.run_until_parked();
16702
16703 cx.assert_state_with_diff(
16704 r#"
16705 use some::mod1;
16706 use some::mod2;
16707
16708 const A: u32 = 42;
16709 + const B: u32 = 42;
16710 + const C: u32 = 42;
16711 + const D: u32 = 42;
16712 + const E: u32 = 42;
16713 + ˇ
16714
16715 fn main() {
16716 println!("hello");
16717
16718 println!("world");
16719 }
16720 "#
16721 .unindent(),
16722 );
16723
16724 cx.update_editor(|editor, window, cx| {
16725 editor.delete_line(&DeleteLine, window, cx);
16726 });
16727 executor.run_until_parked();
16728
16729 cx.assert_state_with_diff(
16730 r#"
16731 use some::mod1;
16732 use some::mod2;
16733
16734 const A: u32 = 42;
16735 + const B: u32 = 42;
16736 + const C: u32 = 42;
16737 + const D: u32 = 42;
16738 + const E: u32 = 42;
16739 ˇ
16740 fn main() {
16741 println!("hello");
16742
16743 println!("world");
16744 }
16745 "#
16746 .unindent(),
16747 );
16748
16749 cx.update_editor(|editor, window, cx| {
16750 editor.move_up(&MoveUp, window, cx);
16751 editor.delete_line(&DeleteLine, window, cx);
16752 editor.move_up(&MoveUp, window, cx);
16753 editor.delete_line(&DeleteLine, window, cx);
16754 editor.move_up(&MoveUp, window, cx);
16755 editor.delete_line(&DeleteLine, window, cx);
16756 });
16757 executor.run_until_parked();
16758 cx.assert_state_with_diff(
16759 r#"
16760 use some::mod1;
16761 use some::mod2;
16762
16763 const A: u32 = 42;
16764 + const B: u32 = 42;
16765 ˇ
16766 fn main() {
16767 println!("hello");
16768
16769 println!("world");
16770 }
16771 "#
16772 .unindent(),
16773 );
16774
16775 cx.update_editor(|editor, window, cx| {
16776 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16777 editor.delete_line(&DeleteLine, window, cx);
16778 });
16779 executor.run_until_parked();
16780 cx.assert_state_with_diff(
16781 r#"
16782 ˇ
16783 fn main() {
16784 println!("hello");
16785
16786 println!("world");
16787 }
16788 "#
16789 .unindent(),
16790 );
16791}
16792
16793#[gpui::test]
16794async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16795 init_test(cx, |_| {});
16796
16797 let mut cx = EditorTestContext::new(cx).await;
16798 cx.set_head_text(indoc! { "
16799 one
16800 two
16801 three
16802 four
16803 five
16804 "
16805 });
16806 cx.set_state(indoc! { "
16807 one
16808 ˇthree
16809 five
16810 "});
16811 cx.run_until_parked();
16812 cx.update_editor(|editor, window, cx| {
16813 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16814 });
16815 cx.assert_state_with_diff(
16816 indoc! { "
16817 one
16818 - two
16819 ˇthree
16820 - four
16821 five
16822 "}
16823 .to_string(),
16824 );
16825 cx.update_editor(|editor, window, cx| {
16826 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16827 });
16828
16829 cx.assert_state_with_diff(
16830 indoc! { "
16831 one
16832 ˇthree
16833 five
16834 "}
16835 .to_string(),
16836 );
16837
16838 cx.set_state(indoc! { "
16839 one
16840 ˇTWO
16841 three
16842 four
16843 five
16844 "});
16845 cx.run_until_parked();
16846 cx.update_editor(|editor, window, cx| {
16847 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16848 });
16849
16850 cx.assert_state_with_diff(
16851 indoc! { "
16852 one
16853 - two
16854 + ˇTWO
16855 three
16856 four
16857 five
16858 "}
16859 .to_string(),
16860 );
16861 cx.update_editor(|editor, window, cx| {
16862 editor.move_up(&Default::default(), window, cx);
16863 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16864 });
16865 cx.assert_state_with_diff(
16866 indoc! { "
16867 one
16868 ˇTWO
16869 three
16870 four
16871 five
16872 "}
16873 .to_string(),
16874 );
16875}
16876
16877#[gpui::test]
16878async fn test_edits_around_expanded_deletion_hunks(
16879 executor: BackgroundExecutor,
16880 cx: &mut TestAppContext,
16881) {
16882 init_test(cx, |_| {});
16883
16884 let mut cx = EditorTestContext::new(cx).await;
16885
16886 let diff_base = r#"
16887 use some::mod1;
16888 use some::mod2;
16889
16890 const A: u32 = 42;
16891 const B: u32 = 42;
16892 const C: u32 = 42;
16893
16894
16895 fn main() {
16896 println!("hello");
16897
16898 println!("world");
16899 }
16900 "#
16901 .unindent();
16902 executor.run_until_parked();
16903 cx.set_state(
16904 &r#"
16905 use some::mod1;
16906 use some::mod2;
16907
16908 ˇconst B: u32 = 42;
16909 const C: u32 = 42;
16910
16911
16912 fn main() {
16913 println!("hello");
16914
16915 println!("world");
16916 }
16917 "#
16918 .unindent(),
16919 );
16920
16921 cx.set_head_text(&diff_base);
16922 executor.run_until_parked();
16923
16924 cx.update_editor(|editor, window, cx| {
16925 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16926 });
16927 executor.run_until_parked();
16928
16929 cx.assert_state_with_diff(
16930 r#"
16931 use some::mod1;
16932 use some::mod2;
16933
16934 - const A: u32 = 42;
16935 ˇconst B: u32 = 42;
16936 const C: u32 = 42;
16937
16938
16939 fn main() {
16940 println!("hello");
16941
16942 println!("world");
16943 }
16944 "#
16945 .unindent(),
16946 );
16947
16948 cx.update_editor(|editor, window, cx| {
16949 editor.delete_line(&DeleteLine, window, cx);
16950 });
16951 executor.run_until_parked();
16952 cx.assert_state_with_diff(
16953 r#"
16954 use some::mod1;
16955 use some::mod2;
16956
16957 - const A: u32 = 42;
16958 - const B: u32 = 42;
16959 ˇconst C: u32 = 42;
16960
16961
16962 fn main() {
16963 println!("hello");
16964
16965 println!("world");
16966 }
16967 "#
16968 .unindent(),
16969 );
16970
16971 cx.update_editor(|editor, window, cx| {
16972 editor.delete_line(&DeleteLine, window, cx);
16973 });
16974 executor.run_until_parked();
16975 cx.assert_state_with_diff(
16976 r#"
16977 use some::mod1;
16978 use some::mod2;
16979
16980 - const A: u32 = 42;
16981 - const B: u32 = 42;
16982 - const C: u32 = 42;
16983 ˇ
16984
16985 fn main() {
16986 println!("hello");
16987
16988 println!("world");
16989 }
16990 "#
16991 .unindent(),
16992 );
16993
16994 cx.update_editor(|editor, window, cx| {
16995 editor.handle_input("replacement", window, cx);
16996 });
16997 executor.run_until_parked();
16998 cx.assert_state_with_diff(
16999 r#"
17000 use some::mod1;
17001 use some::mod2;
17002
17003 - const A: u32 = 42;
17004 - const B: u32 = 42;
17005 - const C: u32 = 42;
17006 -
17007 + replacementˇ
17008
17009 fn main() {
17010 println!("hello");
17011
17012 println!("world");
17013 }
17014 "#
17015 .unindent(),
17016 );
17017}
17018
17019#[gpui::test]
17020async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17021 init_test(cx, |_| {});
17022
17023 let mut cx = EditorTestContext::new(cx).await;
17024
17025 let base_text = r#"
17026 one
17027 two
17028 three
17029 four
17030 five
17031 "#
17032 .unindent();
17033 executor.run_until_parked();
17034 cx.set_state(
17035 &r#"
17036 one
17037 two
17038 fˇour
17039 five
17040 "#
17041 .unindent(),
17042 );
17043
17044 cx.set_head_text(&base_text);
17045 executor.run_until_parked();
17046
17047 cx.update_editor(|editor, window, cx| {
17048 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17049 });
17050 executor.run_until_parked();
17051
17052 cx.assert_state_with_diff(
17053 r#"
17054 one
17055 two
17056 - three
17057 fˇour
17058 five
17059 "#
17060 .unindent(),
17061 );
17062
17063 cx.update_editor(|editor, window, cx| {
17064 editor.backspace(&Backspace, window, cx);
17065 editor.backspace(&Backspace, window, cx);
17066 });
17067 executor.run_until_parked();
17068 cx.assert_state_with_diff(
17069 r#"
17070 one
17071 two
17072 - threeˇ
17073 - four
17074 + our
17075 five
17076 "#
17077 .unindent(),
17078 );
17079}
17080
17081#[gpui::test]
17082async fn test_edit_after_expanded_modification_hunk(
17083 executor: BackgroundExecutor,
17084 cx: &mut TestAppContext,
17085) {
17086 init_test(cx, |_| {});
17087
17088 let mut cx = EditorTestContext::new(cx).await;
17089
17090 let diff_base = r#"
17091 use some::mod1;
17092 use some::mod2;
17093
17094 const A: u32 = 42;
17095 const B: u32 = 42;
17096 const C: u32 = 42;
17097 const D: u32 = 42;
17098
17099
17100 fn main() {
17101 println!("hello");
17102
17103 println!("world");
17104 }"#
17105 .unindent();
17106
17107 cx.set_state(
17108 &r#"
17109 use some::mod1;
17110 use some::mod2;
17111
17112 const A: u32 = 42;
17113 const B: u32 = 42;
17114 const C: u32 = 43ˇ
17115 const D: u32 = 42;
17116
17117
17118 fn main() {
17119 println!("hello");
17120
17121 println!("world");
17122 }"#
17123 .unindent(),
17124 );
17125
17126 cx.set_head_text(&diff_base);
17127 executor.run_until_parked();
17128 cx.update_editor(|editor, window, cx| {
17129 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17130 });
17131 executor.run_until_parked();
17132
17133 cx.assert_state_with_diff(
17134 r#"
17135 use some::mod1;
17136 use some::mod2;
17137
17138 const A: u32 = 42;
17139 const B: u32 = 42;
17140 - const C: u32 = 42;
17141 + const C: u32 = 43ˇ
17142 const D: u32 = 42;
17143
17144
17145 fn main() {
17146 println!("hello");
17147
17148 println!("world");
17149 }"#
17150 .unindent(),
17151 );
17152
17153 cx.update_editor(|editor, window, cx| {
17154 editor.handle_input("\nnew_line\n", window, cx);
17155 });
17156 executor.run_until_parked();
17157
17158 cx.assert_state_with_diff(
17159 r#"
17160 use some::mod1;
17161 use some::mod2;
17162
17163 const A: u32 = 42;
17164 const B: u32 = 42;
17165 - const C: u32 = 42;
17166 + const C: u32 = 43
17167 + new_line
17168 + ˇ
17169 const D: u32 = 42;
17170
17171
17172 fn main() {
17173 println!("hello");
17174
17175 println!("world");
17176 }"#
17177 .unindent(),
17178 );
17179}
17180
17181#[gpui::test]
17182async fn test_stage_and_unstage_added_file_hunk(
17183 executor: BackgroundExecutor,
17184 cx: &mut TestAppContext,
17185) {
17186 init_test(cx, |_| {});
17187
17188 let mut cx = EditorTestContext::new(cx).await;
17189 cx.update_editor(|editor, _, cx| {
17190 editor.set_expand_all_diff_hunks(cx);
17191 });
17192
17193 let working_copy = r#"
17194 ˇfn main() {
17195 println!("hello, world!");
17196 }
17197 "#
17198 .unindent();
17199
17200 cx.set_state(&working_copy);
17201 executor.run_until_parked();
17202
17203 cx.assert_state_with_diff(
17204 r#"
17205 + ˇfn main() {
17206 + println!("hello, world!");
17207 + }
17208 "#
17209 .unindent(),
17210 );
17211 cx.assert_index_text(None);
17212
17213 cx.update_editor(|editor, window, cx| {
17214 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17215 });
17216 executor.run_until_parked();
17217 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17218 cx.assert_state_with_diff(
17219 r#"
17220 + ˇfn main() {
17221 + println!("hello, world!");
17222 + }
17223 "#
17224 .unindent(),
17225 );
17226
17227 cx.update_editor(|editor, window, cx| {
17228 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17229 });
17230 executor.run_until_parked();
17231 cx.assert_index_text(None);
17232}
17233
17234async fn setup_indent_guides_editor(
17235 text: &str,
17236 cx: &mut TestAppContext,
17237) -> (BufferId, EditorTestContext) {
17238 init_test(cx, |_| {});
17239
17240 let mut cx = EditorTestContext::new(cx).await;
17241
17242 let buffer_id = cx.update_editor(|editor, window, cx| {
17243 editor.set_text(text, window, cx);
17244 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17245
17246 buffer_ids[0]
17247 });
17248
17249 (buffer_id, cx)
17250}
17251
17252fn assert_indent_guides(
17253 range: Range<u32>,
17254 expected: Vec<IndentGuide>,
17255 active_indices: Option<Vec<usize>>,
17256 cx: &mut EditorTestContext,
17257) {
17258 let indent_guides = cx.update_editor(|editor, window, cx| {
17259 let snapshot = editor.snapshot(window, cx).display_snapshot;
17260 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17261 editor,
17262 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17263 true,
17264 &snapshot,
17265 cx,
17266 );
17267
17268 indent_guides.sort_by(|a, b| {
17269 a.depth.cmp(&b.depth).then(
17270 a.start_row
17271 .cmp(&b.start_row)
17272 .then(a.end_row.cmp(&b.end_row)),
17273 )
17274 });
17275 indent_guides
17276 });
17277
17278 if let Some(expected) = active_indices {
17279 let active_indices = cx.update_editor(|editor, window, cx| {
17280 let snapshot = editor.snapshot(window, cx).display_snapshot;
17281 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17282 });
17283
17284 assert_eq!(
17285 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17286 expected,
17287 "Active indent guide indices do not match"
17288 );
17289 }
17290
17291 assert_eq!(indent_guides, expected, "Indent guides do not match");
17292}
17293
17294fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17295 IndentGuide {
17296 buffer_id,
17297 start_row: MultiBufferRow(start_row),
17298 end_row: MultiBufferRow(end_row),
17299 depth,
17300 tab_size: 4,
17301 settings: IndentGuideSettings {
17302 enabled: true,
17303 line_width: 1,
17304 active_line_width: 1,
17305 ..Default::default()
17306 },
17307 }
17308}
17309
17310#[gpui::test]
17311async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17312 let (buffer_id, mut cx) = setup_indent_guides_editor(
17313 &"
17314 fn main() {
17315 let a = 1;
17316 }"
17317 .unindent(),
17318 cx,
17319 )
17320 .await;
17321
17322 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17323}
17324
17325#[gpui::test]
17326async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17327 let (buffer_id, mut cx) = setup_indent_guides_editor(
17328 &"
17329 fn main() {
17330 let a = 1;
17331 let b = 2;
17332 }"
17333 .unindent(),
17334 cx,
17335 )
17336 .await;
17337
17338 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17339}
17340
17341#[gpui::test]
17342async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17343 let (buffer_id, mut cx) = setup_indent_guides_editor(
17344 &"
17345 fn main() {
17346 let a = 1;
17347 if a == 3 {
17348 let b = 2;
17349 } else {
17350 let c = 3;
17351 }
17352 }"
17353 .unindent(),
17354 cx,
17355 )
17356 .await;
17357
17358 assert_indent_guides(
17359 0..8,
17360 vec![
17361 indent_guide(buffer_id, 1, 6, 0),
17362 indent_guide(buffer_id, 3, 3, 1),
17363 indent_guide(buffer_id, 5, 5, 1),
17364 ],
17365 None,
17366 &mut cx,
17367 );
17368}
17369
17370#[gpui::test]
17371async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17372 let (buffer_id, mut cx) = setup_indent_guides_editor(
17373 &"
17374 fn main() {
17375 let a = 1;
17376 let b = 2;
17377 let c = 3;
17378 }"
17379 .unindent(),
17380 cx,
17381 )
17382 .await;
17383
17384 assert_indent_guides(
17385 0..5,
17386 vec![
17387 indent_guide(buffer_id, 1, 3, 0),
17388 indent_guide(buffer_id, 2, 2, 1),
17389 ],
17390 None,
17391 &mut cx,
17392 );
17393}
17394
17395#[gpui::test]
17396async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17397 let (buffer_id, mut cx) = setup_indent_guides_editor(
17398 &"
17399 fn main() {
17400 let a = 1;
17401
17402 let c = 3;
17403 }"
17404 .unindent(),
17405 cx,
17406 )
17407 .await;
17408
17409 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17410}
17411
17412#[gpui::test]
17413async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17414 let (buffer_id, mut cx) = setup_indent_guides_editor(
17415 &"
17416 fn main() {
17417 let a = 1;
17418
17419 let c = 3;
17420
17421 if a == 3 {
17422 let b = 2;
17423 } else {
17424 let c = 3;
17425 }
17426 }"
17427 .unindent(),
17428 cx,
17429 )
17430 .await;
17431
17432 assert_indent_guides(
17433 0..11,
17434 vec![
17435 indent_guide(buffer_id, 1, 9, 0),
17436 indent_guide(buffer_id, 6, 6, 1),
17437 indent_guide(buffer_id, 8, 8, 1),
17438 ],
17439 None,
17440 &mut cx,
17441 );
17442}
17443
17444#[gpui::test]
17445async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17446 let (buffer_id, mut cx) = setup_indent_guides_editor(
17447 &"
17448 fn main() {
17449 let a = 1;
17450
17451 let c = 3;
17452
17453 if a == 3 {
17454 let b = 2;
17455 } else {
17456 let c = 3;
17457 }
17458 }"
17459 .unindent(),
17460 cx,
17461 )
17462 .await;
17463
17464 assert_indent_guides(
17465 1..11,
17466 vec![
17467 indent_guide(buffer_id, 1, 9, 0),
17468 indent_guide(buffer_id, 6, 6, 1),
17469 indent_guide(buffer_id, 8, 8, 1),
17470 ],
17471 None,
17472 &mut cx,
17473 );
17474}
17475
17476#[gpui::test]
17477async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17478 let (buffer_id, mut cx) = setup_indent_guides_editor(
17479 &"
17480 fn main() {
17481 let a = 1;
17482
17483 let c = 3;
17484
17485 if a == 3 {
17486 let b = 2;
17487 } else {
17488 let c = 3;
17489 }
17490 }"
17491 .unindent(),
17492 cx,
17493 )
17494 .await;
17495
17496 assert_indent_guides(
17497 1..10,
17498 vec![
17499 indent_guide(buffer_id, 1, 9, 0),
17500 indent_guide(buffer_id, 6, 6, 1),
17501 indent_guide(buffer_id, 8, 8, 1),
17502 ],
17503 None,
17504 &mut cx,
17505 );
17506}
17507
17508#[gpui::test]
17509async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17510 let (buffer_id, mut cx) = setup_indent_guides_editor(
17511 &"
17512 fn main() {
17513 if a {
17514 b(
17515 c,
17516 d,
17517 )
17518 } else {
17519 e(
17520 f
17521 )
17522 }
17523 }"
17524 .unindent(),
17525 cx,
17526 )
17527 .await;
17528
17529 assert_indent_guides(
17530 0..11,
17531 vec![
17532 indent_guide(buffer_id, 1, 10, 0),
17533 indent_guide(buffer_id, 2, 5, 1),
17534 indent_guide(buffer_id, 7, 9, 1),
17535 indent_guide(buffer_id, 3, 4, 2),
17536 indent_guide(buffer_id, 8, 8, 2),
17537 ],
17538 None,
17539 &mut cx,
17540 );
17541
17542 cx.update_editor(|editor, window, cx| {
17543 editor.fold_at(MultiBufferRow(2), window, cx);
17544 assert_eq!(
17545 editor.display_text(cx),
17546 "
17547 fn main() {
17548 if a {
17549 b(⋯
17550 )
17551 } else {
17552 e(
17553 f
17554 )
17555 }
17556 }"
17557 .unindent()
17558 );
17559 });
17560
17561 assert_indent_guides(
17562 0..11,
17563 vec![
17564 indent_guide(buffer_id, 1, 10, 0),
17565 indent_guide(buffer_id, 2, 5, 1),
17566 indent_guide(buffer_id, 7, 9, 1),
17567 indent_guide(buffer_id, 8, 8, 2),
17568 ],
17569 None,
17570 &mut cx,
17571 );
17572}
17573
17574#[gpui::test]
17575async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17576 let (buffer_id, mut cx) = setup_indent_guides_editor(
17577 &"
17578 block1
17579 block2
17580 block3
17581 block4
17582 block2
17583 block1
17584 block1"
17585 .unindent(),
17586 cx,
17587 )
17588 .await;
17589
17590 assert_indent_guides(
17591 1..10,
17592 vec![
17593 indent_guide(buffer_id, 1, 4, 0),
17594 indent_guide(buffer_id, 2, 3, 1),
17595 indent_guide(buffer_id, 3, 3, 2),
17596 ],
17597 None,
17598 &mut cx,
17599 );
17600}
17601
17602#[gpui::test]
17603async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17604 let (buffer_id, mut cx) = setup_indent_guides_editor(
17605 &"
17606 block1
17607 block2
17608 block3
17609
17610 block1
17611 block1"
17612 .unindent(),
17613 cx,
17614 )
17615 .await;
17616
17617 assert_indent_guides(
17618 0..6,
17619 vec![
17620 indent_guide(buffer_id, 1, 2, 0),
17621 indent_guide(buffer_id, 2, 2, 1),
17622 ],
17623 None,
17624 &mut cx,
17625 );
17626}
17627
17628#[gpui::test]
17629async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17630 let (buffer_id, mut cx) = setup_indent_guides_editor(
17631 &"
17632 function component() {
17633 \treturn (
17634 \t\t\t
17635 \t\t<div>
17636 \t\t\t<abc></abc>
17637 \t\t</div>
17638 \t)
17639 }"
17640 .unindent(),
17641 cx,
17642 )
17643 .await;
17644
17645 assert_indent_guides(
17646 0..8,
17647 vec![
17648 indent_guide(buffer_id, 1, 6, 0),
17649 indent_guide(buffer_id, 2, 5, 1),
17650 indent_guide(buffer_id, 4, 4, 2),
17651 ],
17652 None,
17653 &mut cx,
17654 );
17655}
17656
17657#[gpui::test]
17658async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17659 let (buffer_id, mut cx) = setup_indent_guides_editor(
17660 &"
17661 function component() {
17662 \treturn (
17663 \t
17664 \t\t<div>
17665 \t\t\t<abc></abc>
17666 \t\t</div>
17667 \t)
17668 }"
17669 .unindent(),
17670 cx,
17671 )
17672 .await;
17673
17674 assert_indent_guides(
17675 0..8,
17676 vec![
17677 indent_guide(buffer_id, 1, 6, 0),
17678 indent_guide(buffer_id, 2, 5, 1),
17679 indent_guide(buffer_id, 4, 4, 2),
17680 ],
17681 None,
17682 &mut cx,
17683 );
17684}
17685
17686#[gpui::test]
17687async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17688 let (buffer_id, mut cx) = setup_indent_guides_editor(
17689 &"
17690 block1
17691
17692
17693
17694 block2
17695 "
17696 .unindent(),
17697 cx,
17698 )
17699 .await;
17700
17701 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17702}
17703
17704#[gpui::test]
17705async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17706 let (buffer_id, mut cx) = setup_indent_guides_editor(
17707 &"
17708 def a:
17709 \tb = 3
17710 \tif True:
17711 \t\tc = 4
17712 \t\td = 5
17713 \tprint(b)
17714 "
17715 .unindent(),
17716 cx,
17717 )
17718 .await;
17719
17720 assert_indent_guides(
17721 0..6,
17722 vec![
17723 indent_guide(buffer_id, 1, 5, 0),
17724 indent_guide(buffer_id, 3, 4, 1),
17725 ],
17726 None,
17727 &mut cx,
17728 );
17729}
17730
17731#[gpui::test]
17732async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17733 let (buffer_id, mut cx) = setup_indent_guides_editor(
17734 &"
17735 fn main() {
17736 let a = 1;
17737 }"
17738 .unindent(),
17739 cx,
17740 )
17741 .await;
17742
17743 cx.update_editor(|editor, window, cx| {
17744 editor.change_selections(None, window, cx, |s| {
17745 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17746 });
17747 });
17748
17749 assert_indent_guides(
17750 0..3,
17751 vec![indent_guide(buffer_id, 1, 1, 0)],
17752 Some(vec![0]),
17753 &mut cx,
17754 );
17755}
17756
17757#[gpui::test]
17758async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17759 let (buffer_id, mut cx) = setup_indent_guides_editor(
17760 &"
17761 fn main() {
17762 if 1 == 2 {
17763 let a = 1;
17764 }
17765 }"
17766 .unindent(),
17767 cx,
17768 )
17769 .await;
17770
17771 cx.update_editor(|editor, window, cx| {
17772 editor.change_selections(None, window, cx, |s| {
17773 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17774 });
17775 });
17776
17777 assert_indent_guides(
17778 0..4,
17779 vec![
17780 indent_guide(buffer_id, 1, 3, 0),
17781 indent_guide(buffer_id, 2, 2, 1),
17782 ],
17783 Some(vec![1]),
17784 &mut cx,
17785 );
17786
17787 cx.update_editor(|editor, window, cx| {
17788 editor.change_selections(None, window, cx, |s| {
17789 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17790 });
17791 });
17792
17793 assert_indent_guides(
17794 0..4,
17795 vec![
17796 indent_guide(buffer_id, 1, 3, 0),
17797 indent_guide(buffer_id, 2, 2, 1),
17798 ],
17799 Some(vec![1]),
17800 &mut cx,
17801 );
17802
17803 cx.update_editor(|editor, window, cx| {
17804 editor.change_selections(None, window, cx, |s| {
17805 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17806 });
17807 });
17808
17809 assert_indent_guides(
17810 0..4,
17811 vec![
17812 indent_guide(buffer_id, 1, 3, 0),
17813 indent_guide(buffer_id, 2, 2, 1),
17814 ],
17815 Some(vec![0]),
17816 &mut cx,
17817 );
17818}
17819
17820#[gpui::test]
17821async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17822 let (buffer_id, mut cx) = setup_indent_guides_editor(
17823 &"
17824 fn main() {
17825 let a = 1;
17826
17827 let b = 2;
17828 }"
17829 .unindent(),
17830 cx,
17831 )
17832 .await;
17833
17834 cx.update_editor(|editor, window, cx| {
17835 editor.change_selections(None, window, cx, |s| {
17836 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17837 });
17838 });
17839
17840 assert_indent_guides(
17841 0..5,
17842 vec![indent_guide(buffer_id, 1, 3, 0)],
17843 Some(vec![0]),
17844 &mut cx,
17845 );
17846}
17847
17848#[gpui::test]
17849async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17850 let (buffer_id, mut cx) = setup_indent_guides_editor(
17851 &"
17852 def m:
17853 a = 1
17854 pass"
17855 .unindent(),
17856 cx,
17857 )
17858 .await;
17859
17860 cx.update_editor(|editor, window, cx| {
17861 editor.change_selections(None, window, cx, |s| {
17862 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17863 });
17864 });
17865
17866 assert_indent_guides(
17867 0..3,
17868 vec![indent_guide(buffer_id, 1, 2, 0)],
17869 Some(vec![0]),
17870 &mut cx,
17871 );
17872}
17873
17874#[gpui::test]
17875async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17876 init_test(cx, |_| {});
17877 let mut cx = EditorTestContext::new(cx).await;
17878 let text = indoc! {
17879 "
17880 impl A {
17881 fn b() {
17882 0;
17883 3;
17884 5;
17885 6;
17886 7;
17887 }
17888 }
17889 "
17890 };
17891 let base_text = indoc! {
17892 "
17893 impl A {
17894 fn b() {
17895 0;
17896 1;
17897 2;
17898 3;
17899 4;
17900 }
17901 fn c() {
17902 5;
17903 6;
17904 7;
17905 }
17906 }
17907 "
17908 };
17909
17910 cx.update_editor(|editor, window, cx| {
17911 editor.set_text(text, window, cx);
17912
17913 editor.buffer().update(cx, |multibuffer, cx| {
17914 let buffer = multibuffer.as_singleton().unwrap();
17915 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17916
17917 multibuffer.set_all_diff_hunks_expanded(cx);
17918 multibuffer.add_diff(diff, cx);
17919
17920 buffer.read(cx).remote_id()
17921 })
17922 });
17923 cx.run_until_parked();
17924
17925 cx.assert_state_with_diff(
17926 indoc! { "
17927 impl A {
17928 fn b() {
17929 0;
17930 - 1;
17931 - 2;
17932 3;
17933 - 4;
17934 - }
17935 - fn c() {
17936 5;
17937 6;
17938 7;
17939 }
17940 }
17941 ˇ"
17942 }
17943 .to_string(),
17944 );
17945
17946 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17947 editor
17948 .snapshot(window, cx)
17949 .buffer_snapshot
17950 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17951 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17952 .collect::<Vec<_>>()
17953 });
17954 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17955 assert_eq!(
17956 actual_guides,
17957 vec![
17958 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17959 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17960 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17961 ]
17962 );
17963}
17964
17965#[gpui::test]
17966async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17967 init_test(cx, |_| {});
17968 let mut cx = EditorTestContext::new(cx).await;
17969
17970 let diff_base = r#"
17971 a
17972 b
17973 c
17974 "#
17975 .unindent();
17976
17977 cx.set_state(
17978 &r#"
17979 ˇA
17980 b
17981 C
17982 "#
17983 .unindent(),
17984 );
17985 cx.set_head_text(&diff_base);
17986 cx.update_editor(|editor, window, cx| {
17987 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17988 });
17989 executor.run_until_parked();
17990
17991 let both_hunks_expanded = r#"
17992 - a
17993 + ˇA
17994 b
17995 - c
17996 + C
17997 "#
17998 .unindent();
17999
18000 cx.assert_state_with_diff(both_hunks_expanded.clone());
18001
18002 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18003 let snapshot = editor.snapshot(window, cx);
18004 let hunks = editor
18005 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18006 .collect::<Vec<_>>();
18007 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18008 let buffer_id = hunks[0].buffer_id;
18009 hunks
18010 .into_iter()
18011 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18012 .collect::<Vec<_>>()
18013 });
18014 assert_eq!(hunk_ranges.len(), 2);
18015
18016 cx.update_editor(|editor, _, cx| {
18017 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18018 });
18019 executor.run_until_parked();
18020
18021 let second_hunk_expanded = r#"
18022 ˇA
18023 b
18024 - c
18025 + C
18026 "#
18027 .unindent();
18028
18029 cx.assert_state_with_diff(second_hunk_expanded);
18030
18031 cx.update_editor(|editor, _, cx| {
18032 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18033 });
18034 executor.run_until_parked();
18035
18036 cx.assert_state_with_diff(both_hunks_expanded.clone());
18037
18038 cx.update_editor(|editor, _, cx| {
18039 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18040 });
18041 executor.run_until_parked();
18042
18043 let first_hunk_expanded = r#"
18044 - a
18045 + ˇA
18046 b
18047 C
18048 "#
18049 .unindent();
18050
18051 cx.assert_state_with_diff(first_hunk_expanded);
18052
18053 cx.update_editor(|editor, _, cx| {
18054 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18055 });
18056 executor.run_until_parked();
18057
18058 cx.assert_state_with_diff(both_hunks_expanded);
18059
18060 cx.set_state(
18061 &r#"
18062 ˇA
18063 b
18064 "#
18065 .unindent(),
18066 );
18067 cx.run_until_parked();
18068
18069 // TODO this cursor position seems bad
18070 cx.assert_state_with_diff(
18071 r#"
18072 - ˇa
18073 + A
18074 b
18075 "#
18076 .unindent(),
18077 );
18078
18079 cx.update_editor(|editor, window, cx| {
18080 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18081 });
18082
18083 cx.assert_state_with_diff(
18084 r#"
18085 - ˇa
18086 + A
18087 b
18088 - c
18089 "#
18090 .unindent(),
18091 );
18092
18093 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18094 let snapshot = editor.snapshot(window, cx);
18095 let hunks = editor
18096 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18097 .collect::<Vec<_>>();
18098 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18099 let buffer_id = hunks[0].buffer_id;
18100 hunks
18101 .into_iter()
18102 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18103 .collect::<Vec<_>>()
18104 });
18105 assert_eq!(hunk_ranges.len(), 2);
18106
18107 cx.update_editor(|editor, _, cx| {
18108 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18109 });
18110 executor.run_until_parked();
18111
18112 cx.assert_state_with_diff(
18113 r#"
18114 - ˇa
18115 + A
18116 b
18117 "#
18118 .unindent(),
18119 );
18120}
18121
18122#[gpui::test]
18123async fn test_toggle_deletion_hunk_at_start_of_file(
18124 executor: BackgroundExecutor,
18125 cx: &mut TestAppContext,
18126) {
18127 init_test(cx, |_| {});
18128 let mut cx = EditorTestContext::new(cx).await;
18129
18130 let diff_base = r#"
18131 a
18132 b
18133 c
18134 "#
18135 .unindent();
18136
18137 cx.set_state(
18138 &r#"
18139 ˇb
18140 c
18141 "#
18142 .unindent(),
18143 );
18144 cx.set_head_text(&diff_base);
18145 cx.update_editor(|editor, window, cx| {
18146 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18147 });
18148 executor.run_until_parked();
18149
18150 let hunk_expanded = r#"
18151 - a
18152 ˇb
18153 c
18154 "#
18155 .unindent();
18156
18157 cx.assert_state_with_diff(hunk_expanded.clone());
18158
18159 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18160 let snapshot = editor.snapshot(window, cx);
18161 let hunks = editor
18162 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18163 .collect::<Vec<_>>();
18164 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18165 let buffer_id = hunks[0].buffer_id;
18166 hunks
18167 .into_iter()
18168 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18169 .collect::<Vec<_>>()
18170 });
18171 assert_eq!(hunk_ranges.len(), 1);
18172
18173 cx.update_editor(|editor, _, cx| {
18174 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18175 });
18176 executor.run_until_parked();
18177
18178 let hunk_collapsed = r#"
18179 ˇb
18180 c
18181 "#
18182 .unindent();
18183
18184 cx.assert_state_with_diff(hunk_collapsed);
18185
18186 cx.update_editor(|editor, _, cx| {
18187 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18188 });
18189 executor.run_until_parked();
18190
18191 cx.assert_state_with_diff(hunk_expanded.clone());
18192}
18193
18194#[gpui::test]
18195async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18196 init_test(cx, |_| {});
18197
18198 let fs = FakeFs::new(cx.executor());
18199 fs.insert_tree(
18200 path!("/test"),
18201 json!({
18202 ".git": {},
18203 "file-1": "ONE\n",
18204 "file-2": "TWO\n",
18205 "file-3": "THREE\n",
18206 }),
18207 )
18208 .await;
18209
18210 fs.set_head_for_repo(
18211 path!("/test/.git").as_ref(),
18212 &[
18213 ("file-1".into(), "one\n".into()),
18214 ("file-2".into(), "two\n".into()),
18215 ("file-3".into(), "three\n".into()),
18216 ],
18217 "deadbeef",
18218 );
18219
18220 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18221 let mut buffers = vec![];
18222 for i in 1..=3 {
18223 let buffer = project
18224 .update(cx, |project, cx| {
18225 let path = format!(path!("/test/file-{}"), i);
18226 project.open_local_buffer(path, cx)
18227 })
18228 .await
18229 .unwrap();
18230 buffers.push(buffer);
18231 }
18232
18233 let multibuffer = cx.new(|cx| {
18234 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18235 multibuffer.set_all_diff_hunks_expanded(cx);
18236 for buffer in &buffers {
18237 let snapshot = buffer.read(cx).snapshot();
18238 multibuffer.set_excerpts_for_path(
18239 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18240 buffer.clone(),
18241 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18242 DEFAULT_MULTIBUFFER_CONTEXT,
18243 cx,
18244 );
18245 }
18246 multibuffer
18247 });
18248
18249 let editor = cx.add_window(|window, cx| {
18250 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18251 });
18252 cx.run_until_parked();
18253
18254 let snapshot = editor
18255 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18256 .unwrap();
18257 let hunks = snapshot
18258 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18259 .map(|hunk| match hunk {
18260 DisplayDiffHunk::Unfolded {
18261 display_row_range, ..
18262 } => display_row_range,
18263 DisplayDiffHunk::Folded { .. } => unreachable!(),
18264 })
18265 .collect::<Vec<_>>();
18266 assert_eq!(
18267 hunks,
18268 [
18269 DisplayRow(2)..DisplayRow(4),
18270 DisplayRow(7)..DisplayRow(9),
18271 DisplayRow(12)..DisplayRow(14),
18272 ]
18273 );
18274}
18275
18276#[gpui::test]
18277async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18278 init_test(cx, |_| {});
18279
18280 let mut cx = EditorTestContext::new(cx).await;
18281 cx.set_head_text(indoc! { "
18282 one
18283 two
18284 three
18285 four
18286 five
18287 "
18288 });
18289 cx.set_index_text(indoc! { "
18290 one
18291 two
18292 three
18293 four
18294 five
18295 "
18296 });
18297 cx.set_state(indoc! {"
18298 one
18299 TWO
18300 ˇTHREE
18301 FOUR
18302 five
18303 "});
18304 cx.run_until_parked();
18305 cx.update_editor(|editor, window, cx| {
18306 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18307 });
18308 cx.run_until_parked();
18309 cx.assert_index_text(Some(indoc! {"
18310 one
18311 TWO
18312 THREE
18313 FOUR
18314 five
18315 "}));
18316 cx.set_state(indoc! { "
18317 one
18318 TWO
18319 ˇTHREE-HUNDRED
18320 FOUR
18321 five
18322 "});
18323 cx.run_until_parked();
18324 cx.update_editor(|editor, window, cx| {
18325 let snapshot = editor.snapshot(window, cx);
18326 let hunks = editor
18327 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18328 .collect::<Vec<_>>();
18329 assert_eq!(hunks.len(), 1);
18330 assert_eq!(
18331 hunks[0].status(),
18332 DiffHunkStatus {
18333 kind: DiffHunkStatusKind::Modified,
18334 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18335 }
18336 );
18337
18338 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18339 });
18340 cx.run_until_parked();
18341 cx.assert_index_text(Some(indoc! {"
18342 one
18343 TWO
18344 THREE-HUNDRED
18345 FOUR
18346 five
18347 "}));
18348}
18349
18350#[gpui::test]
18351fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18352 init_test(cx, |_| {});
18353
18354 let editor = cx.add_window(|window, cx| {
18355 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18356 build_editor(buffer, window, cx)
18357 });
18358
18359 let render_args = Arc::new(Mutex::new(None));
18360 let snapshot = editor
18361 .update(cx, |editor, window, cx| {
18362 let snapshot = editor.buffer().read(cx).snapshot(cx);
18363 let range =
18364 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18365
18366 struct RenderArgs {
18367 row: MultiBufferRow,
18368 folded: bool,
18369 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18370 }
18371
18372 let crease = Crease::inline(
18373 range,
18374 FoldPlaceholder::test(),
18375 {
18376 let toggle_callback = render_args.clone();
18377 move |row, folded, callback, _window, _cx| {
18378 *toggle_callback.lock() = Some(RenderArgs {
18379 row,
18380 folded,
18381 callback,
18382 });
18383 div()
18384 }
18385 },
18386 |_row, _folded, _window, _cx| div(),
18387 );
18388
18389 editor.insert_creases(Some(crease), cx);
18390 let snapshot = editor.snapshot(window, cx);
18391 let _div = snapshot.render_crease_toggle(
18392 MultiBufferRow(1),
18393 false,
18394 cx.entity().clone(),
18395 window,
18396 cx,
18397 );
18398 snapshot
18399 })
18400 .unwrap();
18401
18402 let render_args = render_args.lock().take().unwrap();
18403 assert_eq!(render_args.row, MultiBufferRow(1));
18404 assert!(!render_args.folded);
18405 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18406
18407 cx.update_window(*editor, |_, window, cx| {
18408 (render_args.callback)(true, window, cx)
18409 })
18410 .unwrap();
18411 let snapshot = editor
18412 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18413 .unwrap();
18414 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18415
18416 cx.update_window(*editor, |_, window, cx| {
18417 (render_args.callback)(false, window, cx)
18418 })
18419 .unwrap();
18420 let snapshot = editor
18421 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18422 .unwrap();
18423 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18424}
18425
18426#[gpui::test]
18427async fn test_input_text(cx: &mut TestAppContext) {
18428 init_test(cx, |_| {});
18429 let mut cx = EditorTestContext::new(cx).await;
18430
18431 cx.set_state(
18432 &r#"ˇone
18433 two
18434
18435 three
18436 fourˇ
18437 five
18438
18439 siˇx"#
18440 .unindent(),
18441 );
18442
18443 cx.dispatch_action(HandleInput(String::new()));
18444 cx.assert_editor_state(
18445 &r#"ˇone
18446 two
18447
18448 three
18449 fourˇ
18450 five
18451
18452 siˇx"#
18453 .unindent(),
18454 );
18455
18456 cx.dispatch_action(HandleInput("AAAA".to_string()));
18457 cx.assert_editor_state(
18458 &r#"AAAAˇone
18459 two
18460
18461 three
18462 fourAAAAˇ
18463 five
18464
18465 siAAAAˇx"#
18466 .unindent(),
18467 );
18468}
18469
18470#[gpui::test]
18471async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18472 init_test(cx, |_| {});
18473
18474 let mut cx = EditorTestContext::new(cx).await;
18475 cx.set_state(
18476 r#"let foo = 1;
18477let foo = 2;
18478let foo = 3;
18479let fooˇ = 4;
18480let foo = 5;
18481let foo = 6;
18482let foo = 7;
18483let foo = 8;
18484let foo = 9;
18485let foo = 10;
18486let foo = 11;
18487let foo = 12;
18488let foo = 13;
18489let foo = 14;
18490let foo = 15;"#,
18491 );
18492
18493 cx.update_editor(|e, window, cx| {
18494 assert_eq!(
18495 e.next_scroll_position,
18496 NextScrollCursorCenterTopBottom::Center,
18497 "Default next scroll direction is center",
18498 );
18499
18500 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18501 assert_eq!(
18502 e.next_scroll_position,
18503 NextScrollCursorCenterTopBottom::Top,
18504 "After center, next scroll direction should be top",
18505 );
18506
18507 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18508 assert_eq!(
18509 e.next_scroll_position,
18510 NextScrollCursorCenterTopBottom::Bottom,
18511 "After top, next scroll direction should be bottom",
18512 );
18513
18514 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18515 assert_eq!(
18516 e.next_scroll_position,
18517 NextScrollCursorCenterTopBottom::Center,
18518 "After bottom, scrolling should start over",
18519 );
18520
18521 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18522 assert_eq!(
18523 e.next_scroll_position,
18524 NextScrollCursorCenterTopBottom::Top,
18525 "Scrolling continues if retriggered fast enough"
18526 );
18527 });
18528
18529 cx.executor()
18530 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18531 cx.executor().run_until_parked();
18532 cx.update_editor(|e, _, _| {
18533 assert_eq!(
18534 e.next_scroll_position,
18535 NextScrollCursorCenterTopBottom::Center,
18536 "If scrolling is not triggered fast enough, it should reset"
18537 );
18538 });
18539}
18540
18541#[gpui::test]
18542async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18543 init_test(cx, |_| {});
18544 let mut cx = EditorLspTestContext::new_rust(
18545 lsp::ServerCapabilities {
18546 definition_provider: Some(lsp::OneOf::Left(true)),
18547 references_provider: Some(lsp::OneOf::Left(true)),
18548 ..lsp::ServerCapabilities::default()
18549 },
18550 cx,
18551 )
18552 .await;
18553
18554 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18555 let go_to_definition = cx
18556 .lsp
18557 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18558 move |params, _| async move {
18559 if empty_go_to_definition {
18560 Ok(None)
18561 } else {
18562 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18563 uri: params.text_document_position_params.text_document.uri,
18564 range: lsp::Range::new(
18565 lsp::Position::new(4, 3),
18566 lsp::Position::new(4, 6),
18567 ),
18568 })))
18569 }
18570 },
18571 );
18572 let references = cx
18573 .lsp
18574 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18575 Ok(Some(vec![lsp::Location {
18576 uri: params.text_document_position.text_document.uri,
18577 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18578 }]))
18579 });
18580 (go_to_definition, references)
18581 };
18582
18583 cx.set_state(
18584 &r#"fn one() {
18585 let mut a = ˇtwo();
18586 }
18587
18588 fn two() {}"#
18589 .unindent(),
18590 );
18591 set_up_lsp_handlers(false, &mut cx);
18592 let navigated = cx
18593 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18594 .await
18595 .expect("Failed to navigate to definition");
18596 assert_eq!(
18597 navigated,
18598 Navigated::Yes,
18599 "Should have navigated to definition from the GetDefinition response"
18600 );
18601 cx.assert_editor_state(
18602 &r#"fn one() {
18603 let mut a = two();
18604 }
18605
18606 fn «twoˇ»() {}"#
18607 .unindent(),
18608 );
18609
18610 let editors = cx.update_workspace(|workspace, _, cx| {
18611 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18612 });
18613 cx.update_editor(|_, _, test_editor_cx| {
18614 assert_eq!(
18615 editors.len(),
18616 1,
18617 "Initially, only one, test, editor should be open in the workspace"
18618 );
18619 assert_eq!(
18620 test_editor_cx.entity(),
18621 editors.last().expect("Asserted len is 1").clone()
18622 );
18623 });
18624
18625 set_up_lsp_handlers(true, &mut cx);
18626 let navigated = cx
18627 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18628 .await
18629 .expect("Failed to navigate to lookup references");
18630 assert_eq!(
18631 navigated,
18632 Navigated::Yes,
18633 "Should have navigated to references as a fallback after empty GoToDefinition response"
18634 );
18635 // We should not change the selections in the existing file,
18636 // if opening another milti buffer with the references
18637 cx.assert_editor_state(
18638 &r#"fn one() {
18639 let mut a = two();
18640 }
18641
18642 fn «twoˇ»() {}"#
18643 .unindent(),
18644 );
18645 let editors = cx.update_workspace(|workspace, _, cx| {
18646 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18647 });
18648 cx.update_editor(|_, _, test_editor_cx| {
18649 assert_eq!(
18650 editors.len(),
18651 2,
18652 "After falling back to references search, we open a new editor with the results"
18653 );
18654 let references_fallback_text = editors
18655 .into_iter()
18656 .find(|new_editor| *new_editor != test_editor_cx.entity())
18657 .expect("Should have one non-test editor now")
18658 .read(test_editor_cx)
18659 .text(test_editor_cx);
18660 assert_eq!(
18661 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18662 "Should use the range from the references response and not the GoToDefinition one"
18663 );
18664 });
18665}
18666
18667#[gpui::test]
18668async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18669 init_test(cx, |_| {});
18670 cx.update(|cx| {
18671 let mut editor_settings = EditorSettings::get_global(cx).clone();
18672 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18673 EditorSettings::override_global(editor_settings, cx);
18674 });
18675 let mut cx = EditorLspTestContext::new_rust(
18676 lsp::ServerCapabilities {
18677 definition_provider: Some(lsp::OneOf::Left(true)),
18678 references_provider: Some(lsp::OneOf::Left(true)),
18679 ..lsp::ServerCapabilities::default()
18680 },
18681 cx,
18682 )
18683 .await;
18684 let original_state = r#"fn one() {
18685 let mut a = ˇtwo();
18686 }
18687
18688 fn two() {}"#
18689 .unindent();
18690 cx.set_state(&original_state);
18691
18692 let mut go_to_definition = cx
18693 .lsp
18694 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18695 move |_, _| async move { Ok(None) },
18696 );
18697 let _references = cx
18698 .lsp
18699 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18700 panic!("Should not call for references with no go to definition fallback")
18701 });
18702
18703 let navigated = cx
18704 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18705 .await
18706 .expect("Failed to navigate to lookup references");
18707 go_to_definition
18708 .next()
18709 .await
18710 .expect("Should have called the go_to_definition handler");
18711
18712 assert_eq!(
18713 navigated,
18714 Navigated::No,
18715 "Should have navigated to references as a fallback after empty GoToDefinition response"
18716 );
18717 cx.assert_editor_state(&original_state);
18718 let editors = cx.update_workspace(|workspace, _, cx| {
18719 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18720 });
18721 cx.update_editor(|_, _, _| {
18722 assert_eq!(
18723 editors.len(),
18724 1,
18725 "After unsuccessful fallback, no other editor should have been opened"
18726 );
18727 });
18728}
18729
18730#[gpui::test]
18731async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18732 init_test(cx, |_| {});
18733
18734 let language = Arc::new(Language::new(
18735 LanguageConfig::default(),
18736 Some(tree_sitter_rust::LANGUAGE.into()),
18737 ));
18738
18739 let text = r#"
18740 #[cfg(test)]
18741 mod tests() {
18742 #[test]
18743 fn runnable_1() {
18744 let a = 1;
18745 }
18746
18747 #[test]
18748 fn runnable_2() {
18749 let a = 1;
18750 let b = 2;
18751 }
18752 }
18753 "#
18754 .unindent();
18755
18756 let fs = FakeFs::new(cx.executor());
18757 fs.insert_file("/file.rs", Default::default()).await;
18758
18759 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18760 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18761 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18762 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18763 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18764
18765 let editor = cx.new_window_entity(|window, cx| {
18766 Editor::new(
18767 EditorMode::full(),
18768 multi_buffer,
18769 Some(project.clone()),
18770 window,
18771 cx,
18772 )
18773 });
18774
18775 editor.update_in(cx, |editor, window, cx| {
18776 let snapshot = editor.buffer().read(cx).snapshot(cx);
18777 editor.tasks.insert(
18778 (buffer.read(cx).remote_id(), 3),
18779 RunnableTasks {
18780 templates: vec![],
18781 offset: snapshot.anchor_before(43),
18782 column: 0,
18783 extra_variables: HashMap::default(),
18784 context_range: BufferOffset(43)..BufferOffset(85),
18785 },
18786 );
18787 editor.tasks.insert(
18788 (buffer.read(cx).remote_id(), 8),
18789 RunnableTasks {
18790 templates: vec![],
18791 offset: snapshot.anchor_before(86),
18792 column: 0,
18793 extra_variables: HashMap::default(),
18794 context_range: BufferOffset(86)..BufferOffset(191),
18795 },
18796 );
18797
18798 // Test finding task when cursor is inside function body
18799 editor.change_selections(None, window, cx, |s| {
18800 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18801 });
18802 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18803 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18804
18805 // Test finding task when cursor is on function name
18806 editor.change_selections(None, window, cx, |s| {
18807 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18808 });
18809 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18810 assert_eq!(row, 8, "Should find task when cursor is on function name");
18811 });
18812}
18813
18814#[gpui::test]
18815async fn test_folding_buffers(cx: &mut TestAppContext) {
18816 init_test(cx, |_| {});
18817
18818 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18819 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18820 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18821
18822 let fs = FakeFs::new(cx.executor());
18823 fs.insert_tree(
18824 path!("/a"),
18825 json!({
18826 "first.rs": sample_text_1,
18827 "second.rs": sample_text_2,
18828 "third.rs": sample_text_3,
18829 }),
18830 )
18831 .await;
18832 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18833 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18834 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18835 let worktree = project.update(cx, |project, cx| {
18836 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18837 assert_eq!(worktrees.len(), 1);
18838 worktrees.pop().unwrap()
18839 });
18840 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18841
18842 let buffer_1 = project
18843 .update(cx, |project, cx| {
18844 project.open_buffer((worktree_id, "first.rs"), cx)
18845 })
18846 .await
18847 .unwrap();
18848 let buffer_2 = project
18849 .update(cx, |project, cx| {
18850 project.open_buffer((worktree_id, "second.rs"), cx)
18851 })
18852 .await
18853 .unwrap();
18854 let buffer_3 = project
18855 .update(cx, |project, cx| {
18856 project.open_buffer((worktree_id, "third.rs"), cx)
18857 })
18858 .await
18859 .unwrap();
18860
18861 let multi_buffer = cx.new(|cx| {
18862 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18863 multi_buffer.push_excerpts(
18864 buffer_1.clone(),
18865 [
18866 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18867 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18868 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18869 ],
18870 cx,
18871 );
18872 multi_buffer.push_excerpts(
18873 buffer_2.clone(),
18874 [
18875 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18876 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18877 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18878 ],
18879 cx,
18880 );
18881 multi_buffer.push_excerpts(
18882 buffer_3.clone(),
18883 [
18884 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18885 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18886 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18887 ],
18888 cx,
18889 );
18890 multi_buffer
18891 });
18892 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18893 Editor::new(
18894 EditorMode::full(),
18895 multi_buffer.clone(),
18896 Some(project.clone()),
18897 window,
18898 cx,
18899 )
18900 });
18901
18902 assert_eq!(
18903 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18904 "\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",
18905 );
18906
18907 multi_buffer_editor.update(cx, |editor, cx| {
18908 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18909 });
18910 assert_eq!(
18911 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18912 "\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",
18913 "After folding the first buffer, its text should not be displayed"
18914 );
18915
18916 multi_buffer_editor.update(cx, |editor, cx| {
18917 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18918 });
18919 assert_eq!(
18920 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18921 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18922 "After folding the second buffer, its text should not be displayed"
18923 );
18924
18925 multi_buffer_editor.update(cx, |editor, cx| {
18926 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18927 });
18928 assert_eq!(
18929 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18930 "\n\n\n\n\n",
18931 "After folding the third buffer, its text should not be displayed"
18932 );
18933
18934 // Emulate selection inside the fold logic, that should work
18935 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18936 editor
18937 .snapshot(window, cx)
18938 .next_line_boundary(Point::new(0, 4));
18939 });
18940
18941 multi_buffer_editor.update(cx, |editor, cx| {
18942 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18943 });
18944 assert_eq!(
18945 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18946 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18947 "After unfolding the second buffer, its text should be displayed"
18948 );
18949
18950 // Typing inside of buffer 1 causes that buffer to be unfolded.
18951 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18952 assert_eq!(
18953 multi_buffer
18954 .read(cx)
18955 .snapshot(cx)
18956 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18957 .collect::<String>(),
18958 "bbbb"
18959 );
18960 editor.change_selections(None, window, cx, |selections| {
18961 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18962 });
18963 editor.handle_input("B", window, cx);
18964 });
18965
18966 assert_eq!(
18967 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18968 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18969 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18970 );
18971
18972 multi_buffer_editor.update(cx, |editor, cx| {
18973 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18974 });
18975 assert_eq!(
18976 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18977 "\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",
18978 "After unfolding the all buffers, all original text should be displayed"
18979 );
18980}
18981
18982#[gpui::test]
18983async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18984 init_test(cx, |_| {});
18985
18986 let sample_text_1 = "1111\n2222\n3333".to_string();
18987 let sample_text_2 = "4444\n5555\n6666".to_string();
18988 let sample_text_3 = "7777\n8888\n9999".to_string();
18989
18990 let fs = FakeFs::new(cx.executor());
18991 fs.insert_tree(
18992 path!("/a"),
18993 json!({
18994 "first.rs": sample_text_1,
18995 "second.rs": sample_text_2,
18996 "third.rs": sample_text_3,
18997 }),
18998 )
18999 .await;
19000 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19001 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19002 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19003 let worktree = project.update(cx, |project, cx| {
19004 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19005 assert_eq!(worktrees.len(), 1);
19006 worktrees.pop().unwrap()
19007 });
19008 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19009
19010 let buffer_1 = project
19011 .update(cx, |project, cx| {
19012 project.open_buffer((worktree_id, "first.rs"), cx)
19013 })
19014 .await
19015 .unwrap();
19016 let buffer_2 = project
19017 .update(cx, |project, cx| {
19018 project.open_buffer((worktree_id, "second.rs"), cx)
19019 })
19020 .await
19021 .unwrap();
19022 let buffer_3 = project
19023 .update(cx, |project, cx| {
19024 project.open_buffer((worktree_id, "third.rs"), cx)
19025 })
19026 .await
19027 .unwrap();
19028
19029 let multi_buffer = cx.new(|cx| {
19030 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19031 multi_buffer.push_excerpts(
19032 buffer_1.clone(),
19033 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19034 cx,
19035 );
19036 multi_buffer.push_excerpts(
19037 buffer_2.clone(),
19038 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19039 cx,
19040 );
19041 multi_buffer.push_excerpts(
19042 buffer_3.clone(),
19043 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19044 cx,
19045 );
19046 multi_buffer
19047 });
19048
19049 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19050 Editor::new(
19051 EditorMode::full(),
19052 multi_buffer,
19053 Some(project.clone()),
19054 window,
19055 cx,
19056 )
19057 });
19058
19059 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19060 assert_eq!(
19061 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19062 full_text,
19063 );
19064
19065 multi_buffer_editor.update(cx, |editor, cx| {
19066 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19067 });
19068 assert_eq!(
19069 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19070 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19071 "After folding the first buffer, its text should not be displayed"
19072 );
19073
19074 multi_buffer_editor.update(cx, |editor, cx| {
19075 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19076 });
19077
19078 assert_eq!(
19079 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19080 "\n\n\n\n\n\n7777\n8888\n9999",
19081 "After folding the second buffer, its text should not be displayed"
19082 );
19083
19084 multi_buffer_editor.update(cx, |editor, cx| {
19085 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19086 });
19087 assert_eq!(
19088 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19089 "\n\n\n\n\n",
19090 "After folding the third buffer, its text should not be displayed"
19091 );
19092
19093 multi_buffer_editor.update(cx, |editor, cx| {
19094 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19095 });
19096 assert_eq!(
19097 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19098 "\n\n\n\n4444\n5555\n6666\n\n",
19099 "After unfolding the second buffer, its text should be displayed"
19100 );
19101
19102 multi_buffer_editor.update(cx, |editor, cx| {
19103 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19104 });
19105 assert_eq!(
19106 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19107 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19108 "After unfolding the first buffer, its text should be displayed"
19109 );
19110
19111 multi_buffer_editor.update(cx, |editor, cx| {
19112 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19113 });
19114 assert_eq!(
19115 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19116 full_text,
19117 "After unfolding all buffers, all original text should be displayed"
19118 );
19119}
19120
19121#[gpui::test]
19122async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19123 init_test(cx, |_| {});
19124
19125 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19126
19127 let fs = FakeFs::new(cx.executor());
19128 fs.insert_tree(
19129 path!("/a"),
19130 json!({
19131 "main.rs": sample_text,
19132 }),
19133 )
19134 .await;
19135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19136 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19137 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19138 let worktree = project.update(cx, |project, cx| {
19139 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19140 assert_eq!(worktrees.len(), 1);
19141 worktrees.pop().unwrap()
19142 });
19143 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19144
19145 let buffer_1 = project
19146 .update(cx, |project, cx| {
19147 project.open_buffer((worktree_id, "main.rs"), cx)
19148 })
19149 .await
19150 .unwrap();
19151
19152 let multi_buffer = cx.new(|cx| {
19153 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19154 multi_buffer.push_excerpts(
19155 buffer_1.clone(),
19156 [ExcerptRange::new(
19157 Point::new(0, 0)
19158 ..Point::new(
19159 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19160 0,
19161 ),
19162 )],
19163 cx,
19164 );
19165 multi_buffer
19166 });
19167 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19168 Editor::new(
19169 EditorMode::full(),
19170 multi_buffer,
19171 Some(project.clone()),
19172 window,
19173 cx,
19174 )
19175 });
19176
19177 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19178 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19179 enum TestHighlight {}
19180 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19181 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19182 editor.highlight_text::<TestHighlight>(
19183 vec![highlight_range.clone()],
19184 HighlightStyle::color(Hsla::green()),
19185 cx,
19186 );
19187 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19188 });
19189
19190 let full_text = format!("\n\n{sample_text}");
19191 assert_eq!(
19192 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19193 full_text,
19194 );
19195}
19196
19197#[gpui::test]
19198async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19199 init_test(cx, |_| {});
19200 cx.update(|cx| {
19201 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19202 "keymaps/default-linux.json",
19203 cx,
19204 )
19205 .unwrap();
19206 cx.bind_keys(default_key_bindings);
19207 });
19208
19209 let (editor, cx) = cx.add_window_view(|window, cx| {
19210 let multi_buffer = MultiBuffer::build_multi(
19211 [
19212 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19213 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19214 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19215 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19216 ],
19217 cx,
19218 );
19219 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19220
19221 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19222 // fold all but the second buffer, so that we test navigating between two
19223 // adjacent folded buffers, as well as folded buffers at the start and
19224 // end the multibuffer
19225 editor.fold_buffer(buffer_ids[0], cx);
19226 editor.fold_buffer(buffer_ids[2], cx);
19227 editor.fold_buffer(buffer_ids[3], cx);
19228
19229 editor
19230 });
19231 cx.simulate_resize(size(px(1000.), px(1000.)));
19232
19233 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19234 cx.assert_excerpts_with_selections(indoc! {"
19235 [EXCERPT]
19236 ˇ[FOLDED]
19237 [EXCERPT]
19238 a1
19239 b1
19240 [EXCERPT]
19241 [FOLDED]
19242 [EXCERPT]
19243 [FOLDED]
19244 "
19245 });
19246 cx.simulate_keystroke("down");
19247 cx.assert_excerpts_with_selections(indoc! {"
19248 [EXCERPT]
19249 [FOLDED]
19250 [EXCERPT]
19251 ˇa1
19252 b1
19253 [EXCERPT]
19254 [FOLDED]
19255 [EXCERPT]
19256 [FOLDED]
19257 "
19258 });
19259 cx.simulate_keystroke("down");
19260 cx.assert_excerpts_with_selections(indoc! {"
19261 [EXCERPT]
19262 [FOLDED]
19263 [EXCERPT]
19264 a1
19265 ˇb1
19266 [EXCERPT]
19267 [FOLDED]
19268 [EXCERPT]
19269 [FOLDED]
19270 "
19271 });
19272 cx.simulate_keystroke("down");
19273 cx.assert_excerpts_with_selections(indoc! {"
19274 [EXCERPT]
19275 [FOLDED]
19276 [EXCERPT]
19277 a1
19278 b1
19279 ˇ[EXCERPT]
19280 [FOLDED]
19281 [EXCERPT]
19282 [FOLDED]
19283 "
19284 });
19285 cx.simulate_keystroke("down");
19286 cx.assert_excerpts_with_selections(indoc! {"
19287 [EXCERPT]
19288 [FOLDED]
19289 [EXCERPT]
19290 a1
19291 b1
19292 [EXCERPT]
19293 ˇ[FOLDED]
19294 [EXCERPT]
19295 [FOLDED]
19296 "
19297 });
19298 for _ in 0..5 {
19299 cx.simulate_keystroke("down");
19300 cx.assert_excerpts_with_selections(indoc! {"
19301 [EXCERPT]
19302 [FOLDED]
19303 [EXCERPT]
19304 a1
19305 b1
19306 [EXCERPT]
19307 [FOLDED]
19308 [EXCERPT]
19309 ˇ[FOLDED]
19310 "
19311 });
19312 }
19313
19314 cx.simulate_keystroke("up");
19315 cx.assert_excerpts_with_selections(indoc! {"
19316 [EXCERPT]
19317 [FOLDED]
19318 [EXCERPT]
19319 a1
19320 b1
19321 [EXCERPT]
19322 ˇ[FOLDED]
19323 [EXCERPT]
19324 [FOLDED]
19325 "
19326 });
19327 cx.simulate_keystroke("up");
19328 cx.assert_excerpts_with_selections(indoc! {"
19329 [EXCERPT]
19330 [FOLDED]
19331 [EXCERPT]
19332 a1
19333 b1
19334 ˇ[EXCERPT]
19335 [FOLDED]
19336 [EXCERPT]
19337 [FOLDED]
19338 "
19339 });
19340 cx.simulate_keystroke("up");
19341 cx.assert_excerpts_with_selections(indoc! {"
19342 [EXCERPT]
19343 [FOLDED]
19344 [EXCERPT]
19345 a1
19346 ˇb1
19347 [EXCERPT]
19348 [FOLDED]
19349 [EXCERPT]
19350 [FOLDED]
19351 "
19352 });
19353 cx.simulate_keystroke("up");
19354 cx.assert_excerpts_with_selections(indoc! {"
19355 [EXCERPT]
19356 [FOLDED]
19357 [EXCERPT]
19358 ˇa1
19359 b1
19360 [EXCERPT]
19361 [FOLDED]
19362 [EXCERPT]
19363 [FOLDED]
19364 "
19365 });
19366 for _ in 0..5 {
19367 cx.simulate_keystroke("up");
19368 cx.assert_excerpts_with_selections(indoc! {"
19369 [EXCERPT]
19370 ˇ[FOLDED]
19371 [EXCERPT]
19372 a1
19373 b1
19374 [EXCERPT]
19375 [FOLDED]
19376 [EXCERPT]
19377 [FOLDED]
19378 "
19379 });
19380 }
19381}
19382
19383#[gpui::test]
19384async fn test_inline_completion_text(cx: &mut TestAppContext) {
19385 init_test(cx, |_| {});
19386
19387 // Simple insertion
19388 assert_highlighted_edits(
19389 "Hello, world!",
19390 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19391 true,
19392 cx,
19393 |highlighted_edits, cx| {
19394 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19395 assert_eq!(highlighted_edits.highlights.len(), 1);
19396 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19397 assert_eq!(
19398 highlighted_edits.highlights[0].1.background_color,
19399 Some(cx.theme().status().created_background)
19400 );
19401 },
19402 )
19403 .await;
19404
19405 // Replacement
19406 assert_highlighted_edits(
19407 "This is a test.",
19408 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19409 false,
19410 cx,
19411 |highlighted_edits, cx| {
19412 assert_eq!(highlighted_edits.text, "That is a test.");
19413 assert_eq!(highlighted_edits.highlights.len(), 1);
19414 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19415 assert_eq!(
19416 highlighted_edits.highlights[0].1.background_color,
19417 Some(cx.theme().status().created_background)
19418 );
19419 },
19420 )
19421 .await;
19422
19423 // Multiple edits
19424 assert_highlighted_edits(
19425 "Hello, world!",
19426 vec![
19427 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19428 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19429 ],
19430 false,
19431 cx,
19432 |highlighted_edits, cx| {
19433 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19434 assert_eq!(highlighted_edits.highlights.len(), 2);
19435 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19436 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19437 assert_eq!(
19438 highlighted_edits.highlights[0].1.background_color,
19439 Some(cx.theme().status().created_background)
19440 );
19441 assert_eq!(
19442 highlighted_edits.highlights[1].1.background_color,
19443 Some(cx.theme().status().created_background)
19444 );
19445 },
19446 )
19447 .await;
19448
19449 // Multiple lines with edits
19450 assert_highlighted_edits(
19451 "First line\nSecond line\nThird line\nFourth line",
19452 vec![
19453 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19454 (
19455 Point::new(2, 0)..Point::new(2, 10),
19456 "New third line".to_string(),
19457 ),
19458 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19459 ],
19460 false,
19461 cx,
19462 |highlighted_edits, cx| {
19463 assert_eq!(
19464 highlighted_edits.text,
19465 "Second modified\nNew third line\nFourth updated line"
19466 );
19467 assert_eq!(highlighted_edits.highlights.len(), 3);
19468 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19469 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19470 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19471 for highlight in &highlighted_edits.highlights {
19472 assert_eq!(
19473 highlight.1.background_color,
19474 Some(cx.theme().status().created_background)
19475 );
19476 }
19477 },
19478 )
19479 .await;
19480}
19481
19482#[gpui::test]
19483async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19484 init_test(cx, |_| {});
19485
19486 // Deletion
19487 assert_highlighted_edits(
19488 "Hello, world!",
19489 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19490 true,
19491 cx,
19492 |highlighted_edits, cx| {
19493 assert_eq!(highlighted_edits.text, "Hello, world!");
19494 assert_eq!(highlighted_edits.highlights.len(), 1);
19495 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19496 assert_eq!(
19497 highlighted_edits.highlights[0].1.background_color,
19498 Some(cx.theme().status().deleted_background)
19499 );
19500 },
19501 )
19502 .await;
19503
19504 // Insertion
19505 assert_highlighted_edits(
19506 "Hello, world!",
19507 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19508 true,
19509 cx,
19510 |highlighted_edits, cx| {
19511 assert_eq!(highlighted_edits.highlights.len(), 1);
19512 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19513 assert_eq!(
19514 highlighted_edits.highlights[0].1.background_color,
19515 Some(cx.theme().status().created_background)
19516 );
19517 },
19518 )
19519 .await;
19520}
19521
19522async fn assert_highlighted_edits(
19523 text: &str,
19524 edits: Vec<(Range<Point>, String)>,
19525 include_deletions: bool,
19526 cx: &mut TestAppContext,
19527 assertion_fn: impl Fn(HighlightedText, &App),
19528) {
19529 let window = cx.add_window(|window, cx| {
19530 let buffer = MultiBuffer::build_simple(text, cx);
19531 Editor::new(EditorMode::full(), buffer, None, window, cx)
19532 });
19533 let cx = &mut VisualTestContext::from_window(*window, cx);
19534
19535 let (buffer, snapshot) = window
19536 .update(cx, |editor, _window, cx| {
19537 (
19538 editor.buffer().clone(),
19539 editor.buffer().read(cx).snapshot(cx),
19540 )
19541 })
19542 .unwrap();
19543
19544 let edits = edits
19545 .into_iter()
19546 .map(|(range, edit)| {
19547 (
19548 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19549 edit,
19550 )
19551 })
19552 .collect::<Vec<_>>();
19553
19554 let text_anchor_edits = edits
19555 .clone()
19556 .into_iter()
19557 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19558 .collect::<Vec<_>>();
19559
19560 let edit_preview = window
19561 .update(cx, |_, _window, cx| {
19562 buffer
19563 .read(cx)
19564 .as_singleton()
19565 .unwrap()
19566 .read(cx)
19567 .preview_edits(text_anchor_edits.into(), cx)
19568 })
19569 .unwrap()
19570 .await;
19571
19572 cx.update(|_window, cx| {
19573 let highlighted_edits = inline_completion_edit_text(
19574 &snapshot.as_singleton().unwrap().2,
19575 &edits,
19576 &edit_preview,
19577 include_deletions,
19578 cx,
19579 );
19580 assertion_fn(highlighted_edits, cx)
19581 });
19582}
19583
19584#[track_caller]
19585fn assert_breakpoint(
19586 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19587 path: &Arc<Path>,
19588 expected: Vec<(u32, Breakpoint)>,
19589) {
19590 if expected.len() == 0usize {
19591 assert!(!breakpoints.contains_key(path), "{}", path.display());
19592 } else {
19593 let mut breakpoint = breakpoints
19594 .get(path)
19595 .unwrap()
19596 .into_iter()
19597 .map(|breakpoint| {
19598 (
19599 breakpoint.row,
19600 Breakpoint {
19601 message: breakpoint.message.clone(),
19602 state: breakpoint.state,
19603 condition: breakpoint.condition.clone(),
19604 hit_condition: breakpoint.hit_condition.clone(),
19605 },
19606 )
19607 })
19608 .collect::<Vec<_>>();
19609
19610 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19611
19612 assert_eq!(expected, breakpoint);
19613 }
19614}
19615
19616fn add_log_breakpoint_at_cursor(
19617 editor: &mut Editor,
19618 log_message: &str,
19619 window: &mut Window,
19620 cx: &mut Context<Editor>,
19621) {
19622 let (anchor, bp) = editor
19623 .breakpoints_at_cursors(window, cx)
19624 .first()
19625 .and_then(|(anchor, bp)| {
19626 if let Some(bp) = bp {
19627 Some((*anchor, bp.clone()))
19628 } else {
19629 None
19630 }
19631 })
19632 .unwrap_or_else(|| {
19633 let cursor_position: Point = editor.selections.newest(cx).head();
19634
19635 let breakpoint_position = editor
19636 .snapshot(window, cx)
19637 .display_snapshot
19638 .buffer_snapshot
19639 .anchor_before(Point::new(cursor_position.row, 0));
19640
19641 (breakpoint_position, Breakpoint::new_log(&log_message))
19642 });
19643
19644 editor.edit_breakpoint_at_anchor(
19645 anchor,
19646 bp,
19647 BreakpointEditAction::EditLogMessage(log_message.into()),
19648 cx,
19649 );
19650}
19651
19652#[gpui::test]
19653async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19654 init_test(cx, |_| {});
19655
19656 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19657 let fs = FakeFs::new(cx.executor());
19658 fs.insert_tree(
19659 path!("/a"),
19660 json!({
19661 "main.rs": sample_text,
19662 }),
19663 )
19664 .await;
19665 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19666 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19667 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19668
19669 let fs = FakeFs::new(cx.executor());
19670 fs.insert_tree(
19671 path!("/a"),
19672 json!({
19673 "main.rs": sample_text,
19674 }),
19675 )
19676 .await;
19677 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19678 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19679 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19680 let worktree_id = workspace
19681 .update(cx, |workspace, _window, cx| {
19682 workspace.project().update(cx, |project, cx| {
19683 project.worktrees(cx).next().unwrap().read(cx).id()
19684 })
19685 })
19686 .unwrap();
19687
19688 let buffer = project
19689 .update(cx, |project, cx| {
19690 project.open_buffer((worktree_id, "main.rs"), cx)
19691 })
19692 .await
19693 .unwrap();
19694
19695 let (editor, cx) = cx.add_window_view(|window, cx| {
19696 Editor::new(
19697 EditorMode::full(),
19698 MultiBuffer::build_from_buffer(buffer, cx),
19699 Some(project.clone()),
19700 window,
19701 cx,
19702 )
19703 });
19704
19705 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19706 let abs_path = project.read_with(cx, |project, cx| {
19707 project
19708 .absolute_path(&project_path, cx)
19709 .map(|path_buf| Arc::from(path_buf.to_owned()))
19710 .unwrap()
19711 });
19712
19713 // assert we can add breakpoint on the first line
19714 editor.update_in(cx, |editor, window, cx| {
19715 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19716 editor.move_to_end(&MoveToEnd, window, cx);
19717 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19718 });
19719
19720 let breakpoints = editor.update(cx, |editor, cx| {
19721 editor
19722 .breakpoint_store()
19723 .as_ref()
19724 .unwrap()
19725 .read(cx)
19726 .all_source_breakpoints(cx)
19727 .clone()
19728 });
19729
19730 assert_eq!(1, breakpoints.len());
19731 assert_breakpoint(
19732 &breakpoints,
19733 &abs_path,
19734 vec![
19735 (0, Breakpoint::new_standard()),
19736 (3, Breakpoint::new_standard()),
19737 ],
19738 );
19739
19740 editor.update_in(cx, |editor, window, cx| {
19741 editor.move_to_beginning(&MoveToBeginning, window, cx);
19742 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19743 });
19744
19745 let breakpoints = editor.update(cx, |editor, cx| {
19746 editor
19747 .breakpoint_store()
19748 .as_ref()
19749 .unwrap()
19750 .read(cx)
19751 .all_source_breakpoints(cx)
19752 .clone()
19753 });
19754
19755 assert_eq!(1, breakpoints.len());
19756 assert_breakpoint(
19757 &breakpoints,
19758 &abs_path,
19759 vec![(3, Breakpoint::new_standard())],
19760 );
19761
19762 editor.update_in(cx, |editor, window, cx| {
19763 editor.move_to_end(&MoveToEnd, window, cx);
19764 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19765 });
19766
19767 let breakpoints = editor.update(cx, |editor, cx| {
19768 editor
19769 .breakpoint_store()
19770 .as_ref()
19771 .unwrap()
19772 .read(cx)
19773 .all_source_breakpoints(cx)
19774 .clone()
19775 });
19776
19777 assert_eq!(0, breakpoints.len());
19778 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19779}
19780
19781#[gpui::test]
19782async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19783 init_test(cx, |_| {});
19784
19785 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19786
19787 let fs = FakeFs::new(cx.executor());
19788 fs.insert_tree(
19789 path!("/a"),
19790 json!({
19791 "main.rs": sample_text,
19792 }),
19793 )
19794 .await;
19795 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19796 let (workspace, cx) =
19797 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19798
19799 let worktree_id = workspace.update(cx, |workspace, cx| {
19800 workspace.project().update(cx, |project, cx| {
19801 project.worktrees(cx).next().unwrap().read(cx).id()
19802 })
19803 });
19804
19805 let buffer = project
19806 .update(cx, |project, cx| {
19807 project.open_buffer((worktree_id, "main.rs"), cx)
19808 })
19809 .await
19810 .unwrap();
19811
19812 let (editor, cx) = cx.add_window_view(|window, cx| {
19813 Editor::new(
19814 EditorMode::full(),
19815 MultiBuffer::build_from_buffer(buffer, cx),
19816 Some(project.clone()),
19817 window,
19818 cx,
19819 )
19820 });
19821
19822 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19823 let abs_path = project.read_with(cx, |project, cx| {
19824 project
19825 .absolute_path(&project_path, cx)
19826 .map(|path_buf| Arc::from(path_buf.to_owned()))
19827 .unwrap()
19828 });
19829
19830 editor.update_in(cx, |editor, window, cx| {
19831 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19832 });
19833
19834 let breakpoints = editor.update(cx, |editor, cx| {
19835 editor
19836 .breakpoint_store()
19837 .as_ref()
19838 .unwrap()
19839 .read(cx)
19840 .all_source_breakpoints(cx)
19841 .clone()
19842 });
19843
19844 assert_breakpoint(
19845 &breakpoints,
19846 &abs_path,
19847 vec![(0, Breakpoint::new_log("hello world"))],
19848 );
19849
19850 // Removing a log message from a log breakpoint should remove it
19851 editor.update_in(cx, |editor, window, cx| {
19852 add_log_breakpoint_at_cursor(editor, "", window, cx);
19853 });
19854
19855 let breakpoints = editor.update(cx, |editor, cx| {
19856 editor
19857 .breakpoint_store()
19858 .as_ref()
19859 .unwrap()
19860 .read(cx)
19861 .all_source_breakpoints(cx)
19862 .clone()
19863 });
19864
19865 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19866
19867 editor.update_in(cx, |editor, window, cx| {
19868 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19869 editor.move_to_end(&MoveToEnd, window, cx);
19870 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19871 // Not adding a log message to a standard breakpoint shouldn't remove it
19872 add_log_breakpoint_at_cursor(editor, "", window, cx);
19873 });
19874
19875 let breakpoints = editor.update(cx, |editor, cx| {
19876 editor
19877 .breakpoint_store()
19878 .as_ref()
19879 .unwrap()
19880 .read(cx)
19881 .all_source_breakpoints(cx)
19882 .clone()
19883 });
19884
19885 assert_breakpoint(
19886 &breakpoints,
19887 &abs_path,
19888 vec![
19889 (0, Breakpoint::new_standard()),
19890 (3, Breakpoint::new_standard()),
19891 ],
19892 );
19893
19894 editor.update_in(cx, |editor, window, cx| {
19895 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19896 });
19897
19898 let breakpoints = editor.update(cx, |editor, cx| {
19899 editor
19900 .breakpoint_store()
19901 .as_ref()
19902 .unwrap()
19903 .read(cx)
19904 .all_source_breakpoints(cx)
19905 .clone()
19906 });
19907
19908 assert_breakpoint(
19909 &breakpoints,
19910 &abs_path,
19911 vec![
19912 (0, Breakpoint::new_standard()),
19913 (3, Breakpoint::new_log("hello world")),
19914 ],
19915 );
19916
19917 editor.update_in(cx, |editor, window, cx| {
19918 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19919 });
19920
19921 let breakpoints = editor.update(cx, |editor, cx| {
19922 editor
19923 .breakpoint_store()
19924 .as_ref()
19925 .unwrap()
19926 .read(cx)
19927 .all_source_breakpoints(cx)
19928 .clone()
19929 });
19930
19931 assert_breakpoint(
19932 &breakpoints,
19933 &abs_path,
19934 vec![
19935 (0, Breakpoint::new_standard()),
19936 (3, Breakpoint::new_log("hello Earth!!")),
19937 ],
19938 );
19939}
19940
19941/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19942/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19943/// or when breakpoints were placed out of order. This tests for a regression too
19944#[gpui::test]
19945async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19946 init_test(cx, |_| {});
19947
19948 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19949 let fs = FakeFs::new(cx.executor());
19950 fs.insert_tree(
19951 path!("/a"),
19952 json!({
19953 "main.rs": sample_text,
19954 }),
19955 )
19956 .await;
19957 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19958 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19959 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19960
19961 let fs = FakeFs::new(cx.executor());
19962 fs.insert_tree(
19963 path!("/a"),
19964 json!({
19965 "main.rs": sample_text,
19966 }),
19967 )
19968 .await;
19969 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19970 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19971 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19972 let worktree_id = workspace
19973 .update(cx, |workspace, _window, cx| {
19974 workspace.project().update(cx, |project, cx| {
19975 project.worktrees(cx).next().unwrap().read(cx).id()
19976 })
19977 })
19978 .unwrap();
19979
19980 let buffer = project
19981 .update(cx, |project, cx| {
19982 project.open_buffer((worktree_id, "main.rs"), cx)
19983 })
19984 .await
19985 .unwrap();
19986
19987 let (editor, cx) = cx.add_window_view(|window, cx| {
19988 Editor::new(
19989 EditorMode::full(),
19990 MultiBuffer::build_from_buffer(buffer, cx),
19991 Some(project.clone()),
19992 window,
19993 cx,
19994 )
19995 });
19996
19997 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19998 let abs_path = project.read_with(cx, |project, cx| {
19999 project
20000 .absolute_path(&project_path, cx)
20001 .map(|path_buf| Arc::from(path_buf.to_owned()))
20002 .unwrap()
20003 });
20004
20005 // assert we can add breakpoint on the first line
20006 editor.update_in(cx, |editor, window, cx| {
20007 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20008 editor.move_to_end(&MoveToEnd, window, cx);
20009 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20010 editor.move_up(&MoveUp, window, cx);
20011 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20012 });
20013
20014 let breakpoints = editor.update(cx, |editor, cx| {
20015 editor
20016 .breakpoint_store()
20017 .as_ref()
20018 .unwrap()
20019 .read(cx)
20020 .all_source_breakpoints(cx)
20021 .clone()
20022 });
20023
20024 assert_eq!(1, breakpoints.len());
20025 assert_breakpoint(
20026 &breakpoints,
20027 &abs_path,
20028 vec![
20029 (0, Breakpoint::new_standard()),
20030 (2, Breakpoint::new_standard()),
20031 (3, Breakpoint::new_standard()),
20032 ],
20033 );
20034
20035 editor.update_in(cx, |editor, window, cx| {
20036 editor.move_to_beginning(&MoveToBeginning, window, cx);
20037 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20038 editor.move_to_end(&MoveToEnd, window, cx);
20039 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20040 // Disabling a breakpoint that doesn't exist should do nothing
20041 editor.move_up(&MoveUp, window, cx);
20042 editor.move_up(&MoveUp, window, cx);
20043 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20044 });
20045
20046 let breakpoints = editor.update(cx, |editor, cx| {
20047 editor
20048 .breakpoint_store()
20049 .as_ref()
20050 .unwrap()
20051 .read(cx)
20052 .all_source_breakpoints(cx)
20053 .clone()
20054 });
20055
20056 let disable_breakpoint = {
20057 let mut bp = Breakpoint::new_standard();
20058 bp.state = BreakpointState::Disabled;
20059 bp
20060 };
20061
20062 assert_eq!(1, breakpoints.len());
20063 assert_breakpoint(
20064 &breakpoints,
20065 &abs_path,
20066 vec![
20067 (0, disable_breakpoint.clone()),
20068 (2, Breakpoint::new_standard()),
20069 (3, disable_breakpoint.clone()),
20070 ],
20071 );
20072
20073 editor.update_in(cx, |editor, window, cx| {
20074 editor.move_to_beginning(&MoveToBeginning, window, cx);
20075 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20076 editor.move_to_end(&MoveToEnd, window, cx);
20077 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20078 editor.move_up(&MoveUp, window, cx);
20079 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20080 });
20081
20082 let breakpoints = editor.update(cx, |editor, cx| {
20083 editor
20084 .breakpoint_store()
20085 .as_ref()
20086 .unwrap()
20087 .read(cx)
20088 .all_source_breakpoints(cx)
20089 .clone()
20090 });
20091
20092 assert_eq!(1, breakpoints.len());
20093 assert_breakpoint(
20094 &breakpoints,
20095 &abs_path,
20096 vec![
20097 (0, Breakpoint::new_standard()),
20098 (2, disable_breakpoint),
20099 (3, Breakpoint::new_standard()),
20100 ],
20101 );
20102}
20103
20104#[gpui::test]
20105async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20106 init_test(cx, |_| {});
20107 let capabilities = lsp::ServerCapabilities {
20108 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20109 prepare_provider: Some(true),
20110 work_done_progress_options: Default::default(),
20111 })),
20112 ..Default::default()
20113 };
20114 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20115
20116 cx.set_state(indoc! {"
20117 struct Fˇoo {}
20118 "});
20119
20120 cx.update_editor(|editor, _, cx| {
20121 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20122 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20123 editor.highlight_background::<DocumentHighlightRead>(
20124 &[highlight_range],
20125 |c| c.editor_document_highlight_read_background,
20126 cx,
20127 );
20128 });
20129
20130 let mut prepare_rename_handler = cx
20131 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20132 move |_, _, _| async move {
20133 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20134 start: lsp::Position {
20135 line: 0,
20136 character: 7,
20137 },
20138 end: lsp::Position {
20139 line: 0,
20140 character: 10,
20141 },
20142 })))
20143 },
20144 );
20145 let prepare_rename_task = cx
20146 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20147 .expect("Prepare rename was not started");
20148 prepare_rename_handler.next().await.unwrap();
20149 prepare_rename_task.await.expect("Prepare rename failed");
20150
20151 let mut rename_handler =
20152 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20153 let edit = lsp::TextEdit {
20154 range: lsp::Range {
20155 start: lsp::Position {
20156 line: 0,
20157 character: 7,
20158 },
20159 end: lsp::Position {
20160 line: 0,
20161 character: 10,
20162 },
20163 },
20164 new_text: "FooRenamed".to_string(),
20165 };
20166 Ok(Some(lsp::WorkspaceEdit::new(
20167 // Specify the same edit twice
20168 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20169 )))
20170 });
20171 let rename_task = cx
20172 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20173 .expect("Confirm rename was not started");
20174 rename_handler.next().await.unwrap();
20175 rename_task.await.expect("Confirm rename failed");
20176 cx.run_until_parked();
20177
20178 // Despite two edits, only one is actually applied as those are identical
20179 cx.assert_editor_state(indoc! {"
20180 struct FooRenamedˇ {}
20181 "});
20182}
20183
20184#[gpui::test]
20185async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20186 init_test(cx, |_| {});
20187 // These capabilities indicate that the server does not support prepare rename.
20188 let capabilities = lsp::ServerCapabilities {
20189 rename_provider: Some(lsp::OneOf::Left(true)),
20190 ..Default::default()
20191 };
20192 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20193
20194 cx.set_state(indoc! {"
20195 struct Fˇoo {}
20196 "});
20197
20198 cx.update_editor(|editor, _window, cx| {
20199 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20200 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20201 editor.highlight_background::<DocumentHighlightRead>(
20202 &[highlight_range],
20203 |c| c.editor_document_highlight_read_background,
20204 cx,
20205 );
20206 });
20207
20208 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20209 .expect("Prepare rename was not started")
20210 .await
20211 .expect("Prepare rename failed");
20212
20213 let mut rename_handler =
20214 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20215 let edit = lsp::TextEdit {
20216 range: lsp::Range {
20217 start: lsp::Position {
20218 line: 0,
20219 character: 7,
20220 },
20221 end: lsp::Position {
20222 line: 0,
20223 character: 10,
20224 },
20225 },
20226 new_text: "FooRenamed".to_string(),
20227 };
20228 Ok(Some(lsp::WorkspaceEdit::new(
20229 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20230 )))
20231 });
20232 let rename_task = cx
20233 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20234 .expect("Confirm rename was not started");
20235 rename_handler.next().await.unwrap();
20236 rename_task.await.expect("Confirm rename failed");
20237 cx.run_until_parked();
20238
20239 // Correct range is renamed, as `surrounding_word` is used to find it.
20240 cx.assert_editor_state(indoc! {"
20241 struct FooRenamedˇ {}
20242 "});
20243}
20244
20245#[gpui::test]
20246async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20247 init_test(cx, |_| {});
20248 let mut cx = EditorTestContext::new(cx).await;
20249
20250 let language = Arc::new(
20251 Language::new(
20252 LanguageConfig::default(),
20253 Some(tree_sitter_html::LANGUAGE.into()),
20254 )
20255 .with_brackets_query(
20256 r#"
20257 ("<" @open "/>" @close)
20258 ("</" @open ">" @close)
20259 ("<" @open ">" @close)
20260 ("\"" @open "\"" @close)
20261 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20262 "#,
20263 )
20264 .unwrap(),
20265 );
20266 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20267
20268 cx.set_state(indoc! {"
20269 <span>ˇ</span>
20270 "});
20271 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20272 cx.assert_editor_state(indoc! {"
20273 <span>
20274 ˇ
20275 </span>
20276 "});
20277
20278 cx.set_state(indoc! {"
20279 <span><span></span>ˇ</span>
20280 "});
20281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20282 cx.assert_editor_state(indoc! {"
20283 <span><span></span>
20284 ˇ</span>
20285 "});
20286
20287 cx.set_state(indoc! {"
20288 <span>ˇ
20289 </span>
20290 "});
20291 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20292 cx.assert_editor_state(indoc! {"
20293 <span>
20294 ˇ
20295 </span>
20296 "});
20297}
20298
20299#[gpui::test(iterations = 10)]
20300async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20301 init_test(cx, |_| {});
20302
20303 let fs = FakeFs::new(cx.executor());
20304 fs.insert_tree(
20305 path!("/dir"),
20306 json!({
20307 "a.ts": "a",
20308 }),
20309 )
20310 .await;
20311
20312 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20313 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20314 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20315
20316 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20317 language_registry.add(Arc::new(Language::new(
20318 LanguageConfig {
20319 name: "TypeScript".into(),
20320 matcher: LanguageMatcher {
20321 path_suffixes: vec!["ts".to_string()],
20322 ..Default::default()
20323 },
20324 ..Default::default()
20325 },
20326 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20327 )));
20328 let mut fake_language_servers = language_registry.register_fake_lsp(
20329 "TypeScript",
20330 FakeLspAdapter {
20331 capabilities: lsp::ServerCapabilities {
20332 code_lens_provider: Some(lsp::CodeLensOptions {
20333 resolve_provider: Some(true),
20334 }),
20335 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20336 commands: vec!["_the/command".to_string()],
20337 ..lsp::ExecuteCommandOptions::default()
20338 }),
20339 ..lsp::ServerCapabilities::default()
20340 },
20341 ..FakeLspAdapter::default()
20342 },
20343 );
20344
20345 let (buffer, _handle) = project
20346 .update(cx, |p, cx| {
20347 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20348 })
20349 .await
20350 .unwrap();
20351 cx.executor().run_until_parked();
20352
20353 let fake_server = fake_language_servers.next().await.unwrap();
20354
20355 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20356 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20357 drop(buffer_snapshot);
20358 let actions = cx
20359 .update_window(*workspace, |_, window, cx| {
20360 project.code_actions(&buffer, anchor..anchor, window, cx)
20361 })
20362 .unwrap();
20363
20364 fake_server
20365 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20366 Ok(Some(vec![
20367 lsp::CodeLens {
20368 range: lsp::Range::default(),
20369 command: Some(lsp::Command {
20370 title: "Code lens command".to_owned(),
20371 command: "_the/command".to_owned(),
20372 arguments: None,
20373 }),
20374 data: None,
20375 },
20376 lsp::CodeLens {
20377 range: lsp::Range::default(),
20378 command: Some(lsp::Command {
20379 title: "Command not in capabilities".to_owned(),
20380 command: "not in capabilities".to_owned(),
20381 arguments: None,
20382 }),
20383 data: None,
20384 },
20385 lsp::CodeLens {
20386 range: lsp::Range {
20387 start: lsp::Position {
20388 line: 1,
20389 character: 1,
20390 },
20391 end: lsp::Position {
20392 line: 1,
20393 character: 1,
20394 },
20395 },
20396 command: Some(lsp::Command {
20397 title: "Command not in range".to_owned(),
20398 command: "_the/command".to_owned(),
20399 arguments: None,
20400 }),
20401 data: None,
20402 },
20403 ]))
20404 })
20405 .next()
20406 .await;
20407
20408 let actions = actions.await.unwrap();
20409 assert_eq!(
20410 actions.len(),
20411 1,
20412 "Should have only one valid action for the 0..0 range"
20413 );
20414 let action = actions[0].clone();
20415 let apply = project.update(cx, |project, cx| {
20416 project.apply_code_action(buffer.clone(), action, true, cx)
20417 });
20418
20419 // Resolving the code action does not populate its edits. In absence of
20420 // edits, we must execute the given command.
20421 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20422 |mut lens, _| async move {
20423 let lens_command = lens.command.as_mut().expect("should have a command");
20424 assert_eq!(lens_command.title, "Code lens command");
20425 lens_command.arguments = Some(vec![json!("the-argument")]);
20426 Ok(lens)
20427 },
20428 );
20429
20430 // While executing the command, the language server sends the editor
20431 // a `workspaceEdit` request.
20432 fake_server
20433 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20434 let fake = fake_server.clone();
20435 move |params, _| {
20436 assert_eq!(params.command, "_the/command");
20437 let fake = fake.clone();
20438 async move {
20439 fake.server
20440 .request::<lsp::request::ApplyWorkspaceEdit>(
20441 lsp::ApplyWorkspaceEditParams {
20442 label: None,
20443 edit: lsp::WorkspaceEdit {
20444 changes: Some(
20445 [(
20446 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20447 vec![lsp::TextEdit {
20448 range: lsp::Range::new(
20449 lsp::Position::new(0, 0),
20450 lsp::Position::new(0, 0),
20451 ),
20452 new_text: "X".into(),
20453 }],
20454 )]
20455 .into_iter()
20456 .collect(),
20457 ),
20458 ..Default::default()
20459 },
20460 },
20461 )
20462 .await
20463 .into_response()
20464 .unwrap();
20465 Ok(Some(json!(null)))
20466 }
20467 }
20468 })
20469 .next()
20470 .await;
20471
20472 // Applying the code lens command returns a project transaction containing the edits
20473 // sent by the language server in its `workspaceEdit` request.
20474 let transaction = apply.await.unwrap();
20475 assert!(transaction.0.contains_key(&buffer));
20476 buffer.update(cx, |buffer, cx| {
20477 assert_eq!(buffer.text(), "Xa");
20478 buffer.undo(cx);
20479 assert_eq!(buffer.text(), "a");
20480 });
20481}
20482
20483#[gpui::test]
20484async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20485 init_test(cx, |_| {});
20486
20487 let fs = FakeFs::new(cx.executor());
20488 let main_text = r#"fn main() {
20489println!("1");
20490println!("2");
20491println!("3");
20492println!("4");
20493println!("5");
20494}"#;
20495 let lib_text = "mod foo {}";
20496 fs.insert_tree(
20497 path!("/a"),
20498 json!({
20499 "lib.rs": lib_text,
20500 "main.rs": main_text,
20501 }),
20502 )
20503 .await;
20504
20505 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20506 let (workspace, cx) =
20507 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20508 let worktree_id = workspace.update(cx, |workspace, cx| {
20509 workspace.project().update(cx, |project, cx| {
20510 project.worktrees(cx).next().unwrap().read(cx).id()
20511 })
20512 });
20513
20514 let expected_ranges = vec![
20515 Point::new(0, 0)..Point::new(0, 0),
20516 Point::new(1, 0)..Point::new(1, 1),
20517 Point::new(2, 0)..Point::new(2, 2),
20518 Point::new(3, 0)..Point::new(3, 3),
20519 ];
20520
20521 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20522 let editor_1 = workspace
20523 .update_in(cx, |workspace, window, cx| {
20524 workspace.open_path(
20525 (worktree_id, "main.rs"),
20526 Some(pane_1.downgrade()),
20527 true,
20528 window,
20529 cx,
20530 )
20531 })
20532 .unwrap()
20533 .await
20534 .downcast::<Editor>()
20535 .unwrap();
20536 pane_1.update(cx, |pane, cx| {
20537 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20538 open_editor.update(cx, |editor, cx| {
20539 assert_eq!(
20540 editor.display_text(cx),
20541 main_text,
20542 "Original main.rs text on initial open",
20543 );
20544 assert_eq!(
20545 editor
20546 .selections
20547 .all::<Point>(cx)
20548 .into_iter()
20549 .map(|s| s.range())
20550 .collect::<Vec<_>>(),
20551 vec![Point::zero()..Point::zero()],
20552 "Default selections on initial open",
20553 );
20554 })
20555 });
20556 editor_1.update_in(cx, |editor, window, cx| {
20557 editor.change_selections(None, window, cx, |s| {
20558 s.select_ranges(expected_ranges.clone());
20559 });
20560 });
20561
20562 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20563 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20564 });
20565 let editor_2 = workspace
20566 .update_in(cx, |workspace, window, cx| {
20567 workspace.open_path(
20568 (worktree_id, "main.rs"),
20569 Some(pane_2.downgrade()),
20570 true,
20571 window,
20572 cx,
20573 )
20574 })
20575 .unwrap()
20576 .await
20577 .downcast::<Editor>()
20578 .unwrap();
20579 pane_2.update(cx, |pane, cx| {
20580 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20581 open_editor.update(cx, |editor, cx| {
20582 assert_eq!(
20583 editor.display_text(cx),
20584 main_text,
20585 "Original main.rs text on initial open in another panel",
20586 );
20587 assert_eq!(
20588 editor
20589 .selections
20590 .all::<Point>(cx)
20591 .into_iter()
20592 .map(|s| s.range())
20593 .collect::<Vec<_>>(),
20594 vec![Point::zero()..Point::zero()],
20595 "Default selections on initial open in another panel",
20596 );
20597 })
20598 });
20599
20600 editor_2.update_in(cx, |editor, window, cx| {
20601 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20602 });
20603
20604 let _other_editor_1 = workspace
20605 .update_in(cx, |workspace, window, cx| {
20606 workspace.open_path(
20607 (worktree_id, "lib.rs"),
20608 Some(pane_1.downgrade()),
20609 true,
20610 window,
20611 cx,
20612 )
20613 })
20614 .unwrap()
20615 .await
20616 .downcast::<Editor>()
20617 .unwrap();
20618 pane_1
20619 .update_in(cx, |pane, window, cx| {
20620 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20621 })
20622 .await
20623 .unwrap();
20624 drop(editor_1);
20625 pane_1.update(cx, |pane, cx| {
20626 pane.active_item()
20627 .unwrap()
20628 .downcast::<Editor>()
20629 .unwrap()
20630 .update(cx, |editor, cx| {
20631 assert_eq!(
20632 editor.display_text(cx),
20633 lib_text,
20634 "Other file should be open and active",
20635 );
20636 });
20637 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20638 });
20639
20640 let _other_editor_2 = workspace
20641 .update_in(cx, |workspace, window, cx| {
20642 workspace.open_path(
20643 (worktree_id, "lib.rs"),
20644 Some(pane_2.downgrade()),
20645 true,
20646 window,
20647 cx,
20648 )
20649 })
20650 .unwrap()
20651 .await
20652 .downcast::<Editor>()
20653 .unwrap();
20654 pane_2
20655 .update_in(cx, |pane, window, cx| {
20656 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20657 })
20658 .await
20659 .unwrap();
20660 drop(editor_2);
20661 pane_2.update(cx, |pane, cx| {
20662 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20663 open_editor.update(cx, |editor, cx| {
20664 assert_eq!(
20665 editor.display_text(cx),
20666 lib_text,
20667 "Other file should be open and active in another panel too",
20668 );
20669 });
20670 assert_eq!(
20671 pane.items().count(),
20672 1,
20673 "No other editors should be open in another pane",
20674 );
20675 });
20676
20677 let _editor_1_reopened = workspace
20678 .update_in(cx, |workspace, window, cx| {
20679 workspace.open_path(
20680 (worktree_id, "main.rs"),
20681 Some(pane_1.downgrade()),
20682 true,
20683 window,
20684 cx,
20685 )
20686 })
20687 .unwrap()
20688 .await
20689 .downcast::<Editor>()
20690 .unwrap();
20691 let _editor_2_reopened = workspace
20692 .update_in(cx, |workspace, window, cx| {
20693 workspace.open_path(
20694 (worktree_id, "main.rs"),
20695 Some(pane_2.downgrade()),
20696 true,
20697 window,
20698 cx,
20699 )
20700 })
20701 .unwrap()
20702 .await
20703 .downcast::<Editor>()
20704 .unwrap();
20705 pane_1.update(cx, |pane, cx| {
20706 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20707 open_editor.update(cx, |editor, cx| {
20708 assert_eq!(
20709 editor.display_text(cx),
20710 main_text,
20711 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20712 );
20713 assert_eq!(
20714 editor
20715 .selections
20716 .all::<Point>(cx)
20717 .into_iter()
20718 .map(|s| s.range())
20719 .collect::<Vec<_>>(),
20720 expected_ranges,
20721 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20722 );
20723 })
20724 });
20725 pane_2.update(cx, |pane, cx| {
20726 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20727 open_editor.update(cx, |editor, cx| {
20728 assert_eq!(
20729 editor.display_text(cx),
20730 r#"fn main() {
20731⋯rintln!("1");
20732⋯intln!("2");
20733⋯ntln!("3");
20734println!("4");
20735println!("5");
20736}"#,
20737 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20738 );
20739 assert_eq!(
20740 editor
20741 .selections
20742 .all::<Point>(cx)
20743 .into_iter()
20744 .map(|s| s.range())
20745 .collect::<Vec<_>>(),
20746 vec![Point::zero()..Point::zero()],
20747 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20748 );
20749 })
20750 });
20751}
20752
20753#[gpui::test]
20754async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20755 init_test(cx, |_| {});
20756
20757 let fs = FakeFs::new(cx.executor());
20758 let main_text = r#"fn main() {
20759println!("1");
20760println!("2");
20761println!("3");
20762println!("4");
20763println!("5");
20764}"#;
20765 let lib_text = "mod foo {}";
20766 fs.insert_tree(
20767 path!("/a"),
20768 json!({
20769 "lib.rs": lib_text,
20770 "main.rs": main_text,
20771 }),
20772 )
20773 .await;
20774
20775 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20776 let (workspace, cx) =
20777 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20778 let worktree_id = workspace.update(cx, |workspace, cx| {
20779 workspace.project().update(cx, |project, cx| {
20780 project.worktrees(cx).next().unwrap().read(cx).id()
20781 })
20782 });
20783
20784 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20785 let editor = workspace
20786 .update_in(cx, |workspace, window, cx| {
20787 workspace.open_path(
20788 (worktree_id, "main.rs"),
20789 Some(pane.downgrade()),
20790 true,
20791 window,
20792 cx,
20793 )
20794 })
20795 .unwrap()
20796 .await
20797 .downcast::<Editor>()
20798 .unwrap();
20799 pane.update(cx, |pane, cx| {
20800 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20801 open_editor.update(cx, |editor, cx| {
20802 assert_eq!(
20803 editor.display_text(cx),
20804 main_text,
20805 "Original main.rs text on initial open",
20806 );
20807 })
20808 });
20809 editor.update_in(cx, |editor, window, cx| {
20810 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20811 });
20812
20813 cx.update_global(|store: &mut SettingsStore, cx| {
20814 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20815 s.restore_on_file_reopen = Some(false);
20816 });
20817 });
20818 editor.update_in(cx, |editor, window, cx| {
20819 editor.fold_ranges(
20820 vec![
20821 Point::new(1, 0)..Point::new(1, 1),
20822 Point::new(2, 0)..Point::new(2, 2),
20823 Point::new(3, 0)..Point::new(3, 3),
20824 ],
20825 false,
20826 window,
20827 cx,
20828 );
20829 });
20830 pane.update_in(cx, |pane, window, cx| {
20831 pane.close_all_items(&CloseAllItems::default(), window, cx)
20832 })
20833 .await
20834 .unwrap();
20835 pane.update(cx, |pane, _| {
20836 assert!(pane.active_item().is_none());
20837 });
20838 cx.update_global(|store: &mut SettingsStore, cx| {
20839 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20840 s.restore_on_file_reopen = Some(true);
20841 });
20842 });
20843
20844 let _editor_reopened = workspace
20845 .update_in(cx, |workspace, window, cx| {
20846 workspace.open_path(
20847 (worktree_id, "main.rs"),
20848 Some(pane.downgrade()),
20849 true,
20850 window,
20851 cx,
20852 )
20853 })
20854 .unwrap()
20855 .await
20856 .downcast::<Editor>()
20857 .unwrap();
20858 pane.update(cx, |pane, cx| {
20859 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20860 open_editor.update(cx, |editor, cx| {
20861 assert_eq!(
20862 editor.display_text(cx),
20863 main_text,
20864 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20865 );
20866 })
20867 });
20868}
20869
20870#[gpui::test]
20871async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20872 struct EmptyModalView {
20873 focus_handle: gpui::FocusHandle,
20874 }
20875 impl EventEmitter<DismissEvent> for EmptyModalView {}
20876 impl Render for EmptyModalView {
20877 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20878 div()
20879 }
20880 }
20881 impl Focusable for EmptyModalView {
20882 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20883 self.focus_handle.clone()
20884 }
20885 }
20886 impl workspace::ModalView for EmptyModalView {}
20887 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20888 EmptyModalView {
20889 focus_handle: cx.focus_handle(),
20890 }
20891 }
20892
20893 init_test(cx, |_| {});
20894
20895 let fs = FakeFs::new(cx.executor());
20896 let project = Project::test(fs, [], cx).await;
20897 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20898 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20899 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20900 let editor = cx.new_window_entity(|window, cx| {
20901 Editor::new(
20902 EditorMode::full(),
20903 buffer,
20904 Some(project.clone()),
20905 window,
20906 cx,
20907 )
20908 });
20909 workspace
20910 .update(cx, |workspace, window, cx| {
20911 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20912 })
20913 .unwrap();
20914 editor.update_in(cx, |editor, window, cx| {
20915 editor.open_context_menu(&OpenContextMenu, window, cx);
20916 assert!(editor.mouse_context_menu.is_some());
20917 });
20918 workspace
20919 .update(cx, |workspace, window, cx| {
20920 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20921 })
20922 .unwrap();
20923 cx.read(|cx| {
20924 assert!(editor.read(cx).mouse_context_menu.is_none());
20925 });
20926}
20927
20928#[gpui::test]
20929async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20930 init_test(cx, |_| {});
20931
20932 let fs = FakeFs::new(cx.executor());
20933 fs.insert_file(path!("/file.html"), Default::default())
20934 .await;
20935
20936 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20937
20938 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20939 let html_language = Arc::new(Language::new(
20940 LanguageConfig {
20941 name: "HTML".into(),
20942 matcher: LanguageMatcher {
20943 path_suffixes: vec!["html".to_string()],
20944 ..LanguageMatcher::default()
20945 },
20946 brackets: BracketPairConfig {
20947 pairs: vec![BracketPair {
20948 start: "<".into(),
20949 end: ">".into(),
20950 close: true,
20951 ..Default::default()
20952 }],
20953 ..Default::default()
20954 },
20955 ..Default::default()
20956 },
20957 Some(tree_sitter_html::LANGUAGE.into()),
20958 ));
20959 language_registry.add(html_language);
20960 let mut fake_servers = language_registry.register_fake_lsp(
20961 "HTML",
20962 FakeLspAdapter {
20963 capabilities: lsp::ServerCapabilities {
20964 completion_provider: Some(lsp::CompletionOptions {
20965 resolve_provider: Some(true),
20966 ..Default::default()
20967 }),
20968 ..Default::default()
20969 },
20970 ..Default::default()
20971 },
20972 );
20973
20974 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20975 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20976
20977 let worktree_id = workspace
20978 .update(cx, |workspace, _window, cx| {
20979 workspace.project().update(cx, |project, cx| {
20980 project.worktrees(cx).next().unwrap().read(cx).id()
20981 })
20982 })
20983 .unwrap();
20984 project
20985 .update(cx, |project, cx| {
20986 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20987 })
20988 .await
20989 .unwrap();
20990 let editor = workspace
20991 .update(cx, |workspace, window, cx| {
20992 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20993 })
20994 .unwrap()
20995 .await
20996 .unwrap()
20997 .downcast::<Editor>()
20998 .unwrap();
20999
21000 let fake_server = fake_servers.next().await.unwrap();
21001 editor.update_in(cx, |editor, window, cx| {
21002 editor.set_text("<ad></ad>", window, cx);
21003 editor.change_selections(None, window, cx, |selections| {
21004 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21005 });
21006 let Some((buffer, _)) = editor
21007 .buffer
21008 .read(cx)
21009 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21010 else {
21011 panic!("Failed to get buffer for selection position");
21012 };
21013 let buffer = buffer.read(cx);
21014 let buffer_id = buffer.remote_id();
21015 let opening_range =
21016 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21017 let closing_range =
21018 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21019 let mut linked_ranges = HashMap::default();
21020 linked_ranges.insert(
21021 buffer_id,
21022 vec![(opening_range.clone(), vec![closing_range.clone()])],
21023 );
21024 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21025 });
21026 let mut completion_handle =
21027 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21028 Ok(Some(lsp::CompletionResponse::Array(vec![
21029 lsp::CompletionItem {
21030 label: "head".to_string(),
21031 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21032 lsp::InsertReplaceEdit {
21033 new_text: "head".to_string(),
21034 insert: lsp::Range::new(
21035 lsp::Position::new(0, 1),
21036 lsp::Position::new(0, 3),
21037 ),
21038 replace: lsp::Range::new(
21039 lsp::Position::new(0, 1),
21040 lsp::Position::new(0, 3),
21041 ),
21042 },
21043 )),
21044 ..Default::default()
21045 },
21046 ])))
21047 });
21048 editor.update_in(cx, |editor, window, cx| {
21049 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21050 });
21051 cx.run_until_parked();
21052 completion_handle.next().await.unwrap();
21053 editor.update(cx, |editor, _| {
21054 assert!(
21055 editor.context_menu_visible(),
21056 "Completion menu should be visible"
21057 );
21058 });
21059 editor.update_in(cx, |editor, window, cx| {
21060 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21061 });
21062 cx.executor().run_until_parked();
21063 editor.update(cx, |editor, cx| {
21064 assert_eq!(editor.text(cx), "<head></head>");
21065 });
21066}
21067
21068#[gpui::test]
21069async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21070 init_test(cx, |_| {});
21071
21072 let fs = FakeFs::new(cx.executor());
21073 fs.insert_tree(
21074 path!("/root"),
21075 json!({
21076 "a": {
21077 "main.rs": "fn main() {}",
21078 },
21079 "foo": {
21080 "bar": {
21081 "external_file.rs": "pub mod external {}",
21082 }
21083 }
21084 }),
21085 )
21086 .await;
21087
21088 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21089 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21090 language_registry.add(rust_lang());
21091 let _fake_servers = language_registry.register_fake_lsp(
21092 "Rust",
21093 FakeLspAdapter {
21094 ..FakeLspAdapter::default()
21095 },
21096 );
21097 let (workspace, cx) =
21098 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21099 let worktree_id = workspace.update(cx, |workspace, cx| {
21100 workspace.project().update(cx, |project, cx| {
21101 project.worktrees(cx).next().unwrap().read(cx).id()
21102 })
21103 });
21104
21105 let assert_language_servers_count =
21106 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21107 project.update(cx, |project, cx| {
21108 let current = project
21109 .lsp_store()
21110 .read(cx)
21111 .as_local()
21112 .unwrap()
21113 .language_servers
21114 .len();
21115 assert_eq!(expected, current, "{context}");
21116 });
21117 };
21118
21119 assert_language_servers_count(
21120 0,
21121 "No servers should be running before any file is open",
21122 cx,
21123 );
21124 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21125 let main_editor = workspace
21126 .update_in(cx, |workspace, window, cx| {
21127 workspace.open_path(
21128 (worktree_id, "main.rs"),
21129 Some(pane.downgrade()),
21130 true,
21131 window,
21132 cx,
21133 )
21134 })
21135 .unwrap()
21136 .await
21137 .downcast::<Editor>()
21138 .unwrap();
21139 pane.update(cx, |pane, cx| {
21140 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21141 open_editor.update(cx, |editor, cx| {
21142 assert_eq!(
21143 editor.display_text(cx),
21144 "fn main() {}",
21145 "Original main.rs text on initial open",
21146 );
21147 });
21148 assert_eq!(open_editor, main_editor);
21149 });
21150 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21151
21152 let external_editor = workspace
21153 .update_in(cx, |workspace, window, cx| {
21154 workspace.open_abs_path(
21155 PathBuf::from("/root/foo/bar/external_file.rs"),
21156 OpenOptions::default(),
21157 window,
21158 cx,
21159 )
21160 })
21161 .await
21162 .expect("opening external file")
21163 .downcast::<Editor>()
21164 .expect("downcasted external file's open element to editor");
21165 pane.update(cx, |pane, cx| {
21166 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21167 open_editor.update(cx, |editor, cx| {
21168 assert_eq!(
21169 editor.display_text(cx),
21170 "pub mod external {}",
21171 "External file is open now",
21172 );
21173 });
21174 assert_eq!(open_editor, external_editor);
21175 });
21176 assert_language_servers_count(
21177 1,
21178 "Second, external, *.rs file should join the existing server",
21179 cx,
21180 );
21181
21182 pane.update_in(cx, |pane, window, cx| {
21183 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21184 })
21185 .await
21186 .unwrap();
21187 pane.update_in(cx, |pane, window, cx| {
21188 pane.navigate_backward(window, cx);
21189 });
21190 cx.run_until_parked();
21191 pane.update(cx, |pane, cx| {
21192 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21193 open_editor.update(cx, |editor, cx| {
21194 assert_eq!(
21195 editor.display_text(cx),
21196 "pub mod external {}",
21197 "External file is open now",
21198 );
21199 });
21200 });
21201 assert_language_servers_count(
21202 1,
21203 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21204 cx,
21205 );
21206
21207 cx.update(|_, cx| {
21208 workspace::reload(&workspace::Reload::default(), cx);
21209 });
21210 assert_language_servers_count(
21211 1,
21212 "After reloading the worktree with local and external files opened, only one project should be started",
21213 cx,
21214 );
21215}
21216
21217#[gpui::test]
21218async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21219 init_test(cx, |_| {});
21220
21221 let mut cx = EditorTestContext::new(cx).await;
21222 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21223 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21224
21225 // test cursor move to start of each line on tab
21226 // for `if`, `elif`, `else`, `while`, `with` and `for`
21227 cx.set_state(indoc! {"
21228 def main():
21229 ˇ for item in items:
21230 ˇ while item.active:
21231 ˇ if item.value > 10:
21232 ˇ continue
21233 ˇ elif item.value < 0:
21234 ˇ break
21235 ˇ else:
21236 ˇ with item.context() as ctx:
21237 ˇ yield count
21238 ˇ else:
21239 ˇ log('while else')
21240 ˇ else:
21241 ˇ log('for else')
21242 "});
21243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21244 cx.assert_editor_state(indoc! {"
21245 def main():
21246 ˇfor item in items:
21247 ˇwhile item.active:
21248 ˇif item.value > 10:
21249 ˇcontinue
21250 ˇelif item.value < 0:
21251 ˇbreak
21252 ˇelse:
21253 ˇwith item.context() as ctx:
21254 ˇyield count
21255 ˇelse:
21256 ˇlog('while else')
21257 ˇelse:
21258 ˇlog('for else')
21259 "});
21260 // test relative indent is preserved when tab
21261 // for `if`, `elif`, `else`, `while`, `with` and `for`
21262 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21263 cx.assert_editor_state(indoc! {"
21264 def main():
21265 ˇfor item in items:
21266 ˇwhile item.active:
21267 ˇif item.value > 10:
21268 ˇcontinue
21269 ˇelif item.value < 0:
21270 ˇbreak
21271 ˇelse:
21272 ˇwith item.context() as ctx:
21273 ˇyield count
21274 ˇelse:
21275 ˇlog('while else')
21276 ˇelse:
21277 ˇlog('for else')
21278 "});
21279
21280 // test cursor move to start of each line on tab
21281 // for `try`, `except`, `else`, `finally`, `match` and `def`
21282 cx.set_state(indoc! {"
21283 def main():
21284 ˇ try:
21285 ˇ fetch()
21286 ˇ except ValueError:
21287 ˇ handle_error()
21288 ˇ else:
21289 ˇ match value:
21290 ˇ case _:
21291 ˇ finally:
21292 ˇ def status():
21293 ˇ return 0
21294 "});
21295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21296 cx.assert_editor_state(indoc! {"
21297 def main():
21298 ˇtry:
21299 ˇfetch()
21300 ˇexcept ValueError:
21301 ˇhandle_error()
21302 ˇelse:
21303 ˇmatch value:
21304 ˇcase _:
21305 ˇfinally:
21306 ˇdef status():
21307 ˇreturn 0
21308 "});
21309 // test relative indent is preserved when tab
21310 // for `try`, `except`, `else`, `finally`, `match` and `def`
21311 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21312 cx.assert_editor_state(indoc! {"
21313 def main():
21314 ˇtry:
21315 ˇfetch()
21316 ˇexcept ValueError:
21317 ˇhandle_error()
21318 ˇelse:
21319 ˇmatch value:
21320 ˇcase _:
21321 ˇfinally:
21322 ˇdef status():
21323 ˇreturn 0
21324 "});
21325}
21326
21327#[gpui::test]
21328async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21329 init_test(cx, |_| {});
21330
21331 let mut cx = EditorTestContext::new(cx).await;
21332 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21333 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21334
21335 // test `else` auto outdents when typed inside `if` block
21336 cx.set_state(indoc! {"
21337 def main():
21338 if i == 2:
21339 return
21340 ˇ
21341 "});
21342 cx.update_editor(|editor, window, cx| {
21343 editor.handle_input("else:", window, cx);
21344 });
21345 cx.assert_editor_state(indoc! {"
21346 def main():
21347 if i == 2:
21348 return
21349 else:ˇ
21350 "});
21351
21352 // test `except` auto outdents when typed inside `try` block
21353 cx.set_state(indoc! {"
21354 def main():
21355 try:
21356 i = 2
21357 ˇ
21358 "});
21359 cx.update_editor(|editor, window, cx| {
21360 editor.handle_input("except:", window, cx);
21361 });
21362 cx.assert_editor_state(indoc! {"
21363 def main():
21364 try:
21365 i = 2
21366 except:ˇ
21367 "});
21368
21369 // test `else` auto outdents when typed inside `except` block
21370 cx.set_state(indoc! {"
21371 def main():
21372 try:
21373 i = 2
21374 except:
21375 j = 2
21376 ˇ
21377 "});
21378 cx.update_editor(|editor, window, cx| {
21379 editor.handle_input("else:", window, cx);
21380 });
21381 cx.assert_editor_state(indoc! {"
21382 def main():
21383 try:
21384 i = 2
21385 except:
21386 j = 2
21387 else:ˇ
21388 "});
21389
21390 // test `finally` auto outdents when typed inside `else` block
21391 cx.set_state(indoc! {"
21392 def main():
21393 try:
21394 i = 2
21395 except:
21396 j = 2
21397 else:
21398 k = 2
21399 ˇ
21400 "});
21401 cx.update_editor(|editor, window, cx| {
21402 editor.handle_input("finally:", window, cx);
21403 });
21404 cx.assert_editor_state(indoc! {"
21405 def main():
21406 try:
21407 i = 2
21408 except:
21409 j = 2
21410 else:
21411 k = 2
21412 finally:ˇ
21413 "});
21414
21415 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21416 // cx.set_state(indoc! {"
21417 // def main():
21418 // try:
21419 // for i in range(n):
21420 // pass
21421 // ˇ
21422 // "});
21423 // cx.update_editor(|editor, window, cx| {
21424 // editor.handle_input("except:", window, cx);
21425 // });
21426 // cx.assert_editor_state(indoc! {"
21427 // def main():
21428 // try:
21429 // for i in range(n):
21430 // pass
21431 // except:ˇ
21432 // "});
21433
21434 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21435 // cx.set_state(indoc! {"
21436 // def main():
21437 // try:
21438 // i = 2
21439 // except:
21440 // for i in range(n):
21441 // pass
21442 // ˇ
21443 // "});
21444 // cx.update_editor(|editor, window, cx| {
21445 // editor.handle_input("else:", window, cx);
21446 // });
21447 // cx.assert_editor_state(indoc! {"
21448 // def main():
21449 // try:
21450 // i = 2
21451 // except:
21452 // for i in range(n):
21453 // pass
21454 // else:ˇ
21455 // "});
21456
21457 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21458 // cx.set_state(indoc! {"
21459 // def main():
21460 // try:
21461 // i = 2
21462 // except:
21463 // j = 2
21464 // else:
21465 // for i in range(n):
21466 // pass
21467 // ˇ
21468 // "});
21469 // cx.update_editor(|editor, window, cx| {
21470 // editor.handle_input("finally:", window, cx);
21471 // });
21472 // cx.assert_editor_state(indoc! {"
21473 // def main():
21474 // try:
21475 // i = 2
21476 // except:
21477 // j = 2
21478 // else:
21479 // for i in range(n):
21480 // pass
21481 // finally:ˇ
21482 // "});
21483
21484 // test `else` stays at correct indent when typed after `for` block
21485 cx.set_state(indoc! {"
21486 def main():
21487 for i in range(10):
21488 if i == 3:
21489 break
21490 ˇ
21491 "});
21492 cx.update_editor(|editor, window, cx| {
21493 editor.handle_input("else:", window, cx);
21494 });
21495 cx.assert_editor_state(indoc! {"
21496 def main():
21497 for i in range(10):
21498 if i == 3:
21499 break
21500 else:ˇ
21501 "});
21502
21503 // test does not outdent on typing after line with square brackets
21504 cx.set_state(indoc! {"
21505 def f() -> list[str]:
21506 ˇ
21507 "});
21508 cx.update_editor(|editor, window, cx| {
21509 editor.handle_input("a", window, cx);
21510 });
21511 cx.assert_editor_state(indoc! {"
21512 def f() -> list[str]:
21513 aˇ
21514 "});
21515}
21516
21517#[gpui::test]
21518async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21519 init_test(cx, |_| {});
21520 update_test_language_settings(cx, |settings| {
21521 settings.defaults.extend_comment_on_newline = Some(false);
21522 });
21523 let mut cx = EditorTestContext::new(cx).await;
21524 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21525 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21526
21527 // test correct indent after newline on comment
21528 cx.set_state(indoc! {"
21529 # COMMENT:ˇ
21530 "});
21531 cx.update_editor(|editor, window, cx| {
21532 editor.newline(&Newline, window, cx);
21533 });
21534 cx.assert_editor_state(indoc! {"
21535 # COMMENT:
21536 ˇ
21537 "});
21538
21539 // test correct indent after newline in brackets
21540 cx.set_state(indoc! {"
21541 {ˇ}
21542 "});
21543 cx.update_editor(|editor, window, cx| {
21544 editor.newline(&Newline, window, cx);
21545 });
21546 cx.run_until_parked();
21547 cx.assert_editor_state(indoc! {"
21548 {
21549 ˇ
21550 }
21551 "});
21552
21553 cx.set_state(indoc! {"
21554 (ˇ)
21555 "});
21556 cx.update_editor(|editor, window, cx| {
21557 editor.newline(&Newline, window, cx);
21558 });
21559 cx.run_until_parked();
21560 cx.assert_editor_state(indoc! {"
21561 (
21562 ˇ
21563 )
21564 "});
21565
21566 // do not indent after empty lists or dictionaries
21567 cx.set_state(indoc! {"
21568 a = []ˇ
21569 "});
21570 cx.update_editor(|editor, window, cx| {
21571 editor.newline(&Newline, window, cx);
21572 });
21573 cx.run_until_parked();
21574 cx.assert_editor_state(indoc! {"
21575 a = []
21576 ˇ
21577 "});
21578}
21579
21580fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21581 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21582 point..point
21583}
21584
21585#[track_caller]
21586fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21587 let (text, ranges) = marked_text_ranges(marked_text, true);
21588 assert_eq!(editor.text(cx), text);
21589 assert_eq!(
21590 editor.selections.ranges(cx),
21591 ranges,
21592 "Assert selections are {}",
21593 marked_text
21594 );
21595}
21596
21597pub fn handle_signature_help_request(
21598 cx: &mut EditorLspTestContext,
21599 mocked_response: lsp::SignatureHelp,
21600) -> impl Future<Output = ()> + use<> {
21601 let mut request =
21602 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21603 let mocked_response = mocked_response.clone();
21604 async move { Ok(Some(mocked_response)) }
21605 });
21606
21607 async move {
21608 request.next().await;
21609 }
21610}
21611
21612#[track_caller]
21613pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21614 cx.update_editor(|editor, _, _| {
21615 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21616 let entries = menu.entries.borrow();
21617 let entries = entries
21618 .iter()
21619 .map(|entry| entry.string.as_str())
21620 .collect::<Vec<_>>();
21621 assert_eq!(entries, expected);
21622 } else {
21623 panic!("Expected completions menu");
21624 }
21625 });
21626}
21627
21628/// Handle completion request passing a marked string specifying where the completion
21629/// should be triggered from using '|' character, what range should be replaced, and what completions
21630/// should be returned using '<' and '>' to delimit the range.
21631///
21632/// Also see `handle_completion_request_with_insert_and_replace`.
21633#[track_caller]
21634pub fn handle_completion_request(
21635 marked_string: &str,
21636 completions: Vec<&'static str>,
21637 is_incomplete: bool,
21638 counter: Arc<AtomicUsize>,
21639 cx: &mut EditorLspTestContext,
21640) -> impl Future<Output = ()> {
21641 let complete_from_marker: TextRangeMarker = '|'.into();
21642 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21643 let (_, mut marked_ranges) = marked_text_ranges_by(
21644 marked_string,
21645 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21646 );
21647
21648 let complete_from_position =
21649 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21650 let replace_range =
21651 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21652
21653 let mut request =
21654 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21655 let completions = completions.clone();
21656 counter.fetch_add(1, atomic::Ordering::Release);
21657 async move {
21658 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21659 assert_eq!(
21660 params.text_document_position.position,
21661 complete_from_position
21662 );
21663 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21664 is_incomplete: is_incomplete,
21665 item_defaults: None,
21666 items: completions
21667 .iter()
21668 .map(|completion_text| lsp::CompletionItem {
21669 label: completion_text.to_string(),
21670 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21671 range: replace_range,
21672 new_text: completion_text.to_string(),
21673 })),
21674 ..Default::default()
21675 })
21676 .collect(),
21677 })))
21678 }
21679 });
21680
21681 async move {
21682 request.next().await;
21683 }
21684}
21685
21686/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21687/// given instead, which also contains an `insert` range.
21688///
21689/// This function uses markers to define ranges:
21690/// - `|` marks the cursor position
21691/// - `<>` marks the replace range
21692/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21693pub fn handle_completion_request_with_insert_and_replace(
21694 cx: &mut EditorLspTestContext,
21695 marked_string: &str,
21696 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21697 counter: Arc<AtomicUsize>,
21698) -> impl Future<Output = ()> {
21699 let complete_from_marker: TextRangeMarker = '|'.into();
21700 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21701 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21702
21703 let (_, mut marked_ranges) = marked_text_ranges_by(
21704 marked_string,
21705 vec![
21706 complete_from_marker.clone(),
21707 replace_range_marker.clone(),
21708 insert_range_marker.clone(),
21709 ],
21710 );
21711
21712 let complete_from_position =
21713 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21714 let replace_range =
21715 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21716
21717 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21718 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21719 _ => lsp::Range {
21720 start: replace_range.start,
21721 end: complete_from_position,
21722 },
21723 };
21724
21725 let mut request =
21726 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21727 let completions = completions.clone();
21728 counter.fetch_add(1, atomic::Ordering::Release);
21729 async move {
21730 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21731 assert_eq!(
21732 params.text_document_position.position, complete_from_position,
21733 "marker `|` position doesn't match",
21734 );
21735 Ok(Some(lsp::CompletionResponse::Array(
21736 completions
21737 .iter()
21738 .map(|(label, new_text)| lsp::CompletionItem {
21739 label: label.to_string(),
21740 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21741 lsp::InsertReplaceEdit {
21742 insert: insert_range,
21743 replace: replace_range,
21744 new_text: new_text.to_string(),
21745 },
21746 )),
21747 ..Default::default()
21748 })
21749 .collect(),
21750 )))
21751 }
21752 });
21753
21754 async move {
21755 request.next().await;
21756 }
21757}
21758
21759fn handle_resolve_completion_request(
21760 cx: &mut EditorLspTestContext,
21761 edits: Option<Vec<(&'static str, &'static str)>>,
21762) -> impl Future<Output = ()> {
21763 let edits = edits.map(|edits| {
21764 edits
21765 .iter()
21766 .map(|(marked_string, new_text)| {
21767 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21768 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21769 lsp::TextEdit::new(replace_range, new_text.to_string())
21770 })
21771 .collect::<Vec<_>>()
21772 });
21773
21774 let mut request =
21775 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21776 let edits = edits.clone();
21777 async move {
21778 Ok(lsp::CompletionItem {
21779 additional_text_edits: edits,
21780 ..Default::default()
21781 })
21782 }
21783 });
21784
21785 async move {
21786 request.next().await;
21787 }
21788}
21789
21790pub(crate) fn update_test_language_settings(
21791 cx: &mut TestAppContext,
21792 f: impl Fn(&mut AllLanguageSettingsContent),
21793) {
21794 cx.update(|cx| {
21795 SettingsStore::update_global(cx, |store, cx| {
21796 store.update_user_settings::<AllLanguageSettings>(cx, f);
21797 });
21798 });
21799}
21800
21801pub(crate) fn update_test_project_settings(
21802 cx: &mut TestAppContext,
21803 f: impl Fn(&mut ProjectSettings),
21804) {
21805 cx.update(|cx| {
21806 SettingsStore::update_global(cx, |store, cx| {
21807 store.update_user_settings::<ProjectSettings>(cx, f);
21808 });
21809 });
21810}
21811
21812pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21813 cx.update(|cx| {
21814 assets::Assets.load_test_fonts(cx);
21815 let store = SettingsStore::test(cx);
21816 cx.set_global(store);
21817 theme::init(theme::LoadThemes::JustBase, cx);
21818 release_channel::init(SemanticVersion::default(), cx);
21819 client::init_settings(cx);
21820 language::init(cx);
21821 Project::init_settings(cx);
21822 workspace::init_settings(cx);
21823 crate::init(cx);
21824 });
21825
21826 update_test_language_settings(cx, f);
21827}
21828
21829#[track_caller]
21830fn assert_hunk_revert(
21831 not_reverted_text_with_selections: &str,
21832 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21833 expected_reverted_text_with_selections: &str,
21834 base_text: &str,
21835 cx: &mut EditorLspTestContext,
21836) {
21837 cx.set_state(not_reverted_text_with_selections);
21838 cx.set_head_text(base_text);
21839 cx.executor().run_until_parked();
21840
21841 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21842 let snapshot = editor.snapshot(window, cx);
21843 let reverted_hunk_statuses = snapshot
21844 .buffer_snapshot
21845 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21846 .map(|hunk| hunk.status().kind)
21847 .collect::<Vec<_>>();
21848
21849 editor.git_restore(&Default::default(), window, cx);
21850 reverted_hunk_statuses
21851 });
21852 cx.executor().run_until_parked();
21853 cx.assert_editor_state(expected_reverted_text_with_selections);
21854 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21855}
21856
21857#[gpui::test(iterations = 10)]
21858async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
21859 init_test(cx, |_| {});
21860
21861 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
21862 let counter = diagnostic_requests.clone();
21863
21864 let fs = FakeFs::new(cx.executor());
21865 fs.insert_tree(
21866 path!("/a"),
21867 json!({
21868 "first.rs": "fn main() { let a = 5; }",
21869 "second.rs": "// Test file",
21870 }),
21871 )
21872 .await;
21873
21874 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21876 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21877
21878 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21879 language_registry.add(rust_lang());
21880 let mut fake_servers = language_registry.register_fake_lsp(
21881 "Rust",
21882 FakeLspAdapter {
21883 capabilities: lsp::ServerCapabilities {
21884 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
21885 lsp::DiagnosticOptions {
21886 identifier: None,
21887 inter_file_dependencies: true,
21888 workspace_diagnostics: true,
21889 work_done_progress_options: Default::default(),
21890 },
21891 )),
21892 ..Default::default()
21893 },
21894 ..Default::default()
21895 },
21896 );
21897
21898 let editor = workspace
21899 .update(cx, |workspace, window, cx| {
21900 workspace.open_abs_path(
21901 PathBuf::from(path!("/a/first.rs")),
21902 OpenOptions::default(),
21903 window,
21904 cx,
21905 )
21906 })
21907 .unwrap()
21908 .await
21909 .unwrap()
21910 .downcast::<Editor>()
21911 .unwrap();
21912 let fake_server = fake_servers.next().await.unwrap();
21913 let mut first_request = fake_server
21914 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
21915 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
21916 let result_id = Some(new_result_id.to_string());
21917 assert_eq!(
21918 params.text_document.uri,
21919 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
21920 );
21921 async move {
21922 Ok(lsp::DocumentDiagnosticReportResult::Report(
21923 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
21924 related_documents: None,
21925 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
21926 items: Vec::new(),
21927 result_id,
21928 },
21929 }),
21930 ))
21931 }
21932 });
21933
21934 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
21935 project.update(cx, |project, cx| {
21936 let buffer_id = editor
21937 .read(cx)
21938 .buffer()
21939 .read(cx)
21940 .as_singleton()
21941 .expect("created a singleton buffer")
21942 .read(cx)
21943 .remote_id();
21944 let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
21945 assert_eq!(expected, buffer_result_id);
21946 });
21947 };
21948
21949 ensure_result_id(None, cx);
21950 cx.executor().advance_clock(Duration::from_millis(60));
21951 cx.executor().run_until_parked();
21952 assert_eq!(
21953 diagnostic_requests.load(atomic::Ordering::Acquire),
21954 1,
21955 "Opening file should trigger diagnostic request"
21956 );
21957 first_request
21958 .next()
21959 .await
21960 .expect("should have sent the first diagnostics pull request");
21961 ensure_result_id(Some("1".to_string()), cx);
21962
21963 // Editing should trigger diagnostics
21964 editor.update_in(cx, |editor, window, cx| {
21965 editor.handle_input("2", window, cx)
21966 });
21967 cx.executor().advance_clock(Duration::from_millis(60));
21968 cx.executor().run_until_parked();
21969 assert_eq!(
21970 diagnostic_requests.load(atomic::Ordering::Acquire),
21971 2,
21972 "Editing should trigger diagnostic request"
21973 );
21974 ensure_result_id(Some("2".to_string()), cx);
21975
21976 // Moving cursor should not trigger diagnostic request
21977 editor.update_in(cx, |editor, window, cx| {
21978 editor.change_selections(None, window, cx, |s| {
21979 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
21980 });
21981 });
21982 cx.executor().advance_clock(Duration::from_millis(60));
21983 cx.executor().run_until_parked();
21984 assert_eq!(
21985 diagnostic_requests.load(atomic::Ordering::Acquire),
21986 2,
21987 "Cursor movement should not trigger diagnostic request"
21988 );
21989 ensure_result_id(Some("2".to_string()), cx);
21990 // Multiple rapid edits should be debounced
21991 for _ in 0..5 {
21992 editor.update_in(cx, |editor, window, cx| {
21993 editor.handle_input("x", window, cx)
21994 });
21995 }
21996 cx.executor().advance_clock(Duration::from_millis(60));
21997 cx.executor().run_until_parked();
21998
21999 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22000 assert!(
22001 final_requests <= 4,
22002 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22003 );
22004 ensure_result_id(Some(final_requests.to_string()), cx);
22005}
22006
22007#[gpui::test]
22008async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22009 // Regression test for issue #11671
22010 // Previously, adding a cursor after moving multiple cursors would reset
22011 // the cursor count instead of adding to the existing cursors.
22012 init_test(cx, |_| {});
22013 let mut cx = EditorTestContext::new(cx).await;
22014
22015 // Create a simple buffer with cursor at start
22016 cx.set_state(indoc! {"
22017 ˇaaaa
22018 bbbb
22019 cccc
22020 dddd
22021 eeee
22022 ffff
22023 gggg
22024 hhhh"});
22025
22026 // Add 2 cursors below (so we have 3 total)
22027 cx.update_editor(|editor, window, cx| {
22028 editor.add_selection_below(&Default::default(), window, cx);
22029 editor.add_selection_below(&Default::default(), window, cx);
22030 });
22031
22032 // Verify we have 3 cursors
22033 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22034 assert_eq!(
22035 initial_count, 3,
22036 "Should have 3 cursors after adding 2 below"
22037 );
22038
22039 // Move down one line
22040 cx.update_editor(|editor, window, cx| {
22041 editor.move_down(&MoveDown, window, cx);
22042 });
22043
22044 // Add another cursor below
22045 cx.update_editor(|editor, window, cx| {
22046 editor.add_selection_below(&Default::default(), window, cx);
22047 });
22048
22049 // Should now have 4 cursors (3 original + 1 new)
22050 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22051 assert_eq!(
22052 final_count, 4,
22053 "Should have 4 cursors after moving and adding another"
22054 );
22055}