1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
26 Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
280 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
283 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])
1909 });
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 //
2798 // ˇ
2799 "});
2800 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 ˇ )
3298 ˇ)
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
3518 ˇ»four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_toggle_case(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // If all lower case -> upper case
4320 cx.set_state(indoc! {"
4321 «hello worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «HELLO WORLDˇ»
4326 "});
4327
4328 // If all upper case -> lower case
4329 cx.set_state(indoc! {"
4330 «HELLO WORLDˇ»
4331 "});
4332 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4333 cx.assert_editor_state(indoc! {"
4334 «hello worldˇ»
4335 "});
4336
4337 // If any upper case characters are identified -> lower case
4338 // This matches JetBrains IDEs
4339 cx.set_state(indoc! {"
4340 «hEllo worldˇ»
4341 "});
4342 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4343 cx.assert_editor_state(indoc! {"
4344 «hello worldˇ»
4345 "});
4346}
4347
4348#[gpui::test]
4349async fn test_manipulate_text(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 let mut cx = EditorTestContext::new(cx).await;
4353
4354 // Test convert_to_upper_case()
4355 cx.set_state(indoc! {"
4356 «hello worldˇ»
4357 "});
4358 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 «HELLO WORLDˇ»
4361 "});
4362
4363 // Test convert_to_lower_case()
4364 cx.set_state(indoc! {"
4365 «HELLO WORLDˇ»
4366 "});
4367 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «hello worldˇ»
4370 "});
4371
4372 // Test multiple line, single selection case
4373 cx.set_state(indoc! {"
4374 «The quick brown
4375 fox jumps over
4376 the lazy dogˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 «The Quick Brown
4381 Fox Jumps Over
4382 The Lazy Dogˇ»
4383 "});
4384
4385 // Test multiple line, single selection case
4386 cx.set_state(indoc! {"
4387 «The quick brown
4388 fox jumps over
4389 the lazy dogˇ»
4390 "});
4391 cx.update_editor(|e, window, cx| {
4392 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4393 });
4394 cx.assert_editor_state(indoc! {"
4395 «TheQuickBrown
4396 FoxJumpsOver
4397 TheLazyDogˇ»
4398 "});
4399
4400 // From here on out, test more complex cases of manipulate_text()
4401
4402 // Test no selection case - should affect words cursors are in
4403 // Cursor at beginning, middle, and end of word
4404 cx.set_state(indoc! {"
4405 ˇhello big beauˇtiful worldˇ
4406 "});
4407 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4410 "});
4411
4412 // Test multiple selections on a single line and across multiple lines
4413 cx.set_state(indoc! {"
4414 «Theˇ» quick «brown
4415 foxˇ» jumps «overˇ»
4416 the «lazyˇ» dog
4417 "});
4418 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4419 cx.assert_editor_state(indoc! {"
4420 «THEˇ» quick «BROWN
4421 FOXˇ» jumps «OVERˇ»
4422 the «LAZYˇ» dog
4423 "});
4424
4425 // Test case where text length grows
4426 cx.set_state(indoc! {"
4427 «tschüߡ»
4428 "});
4429 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4430 cx.assert_editor_state(indoc! {"
4431 «TSCHÜSSˇ»
4432 "});
4433
4434 // Test to make sure we don't crash when text shrinks
4435 cx.set_state(indoc! {"
4436 aaa_bbbˇ
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «aaaBbbˇ»
4443 "});
4444
4445 // Test to make sure we all aware of the fact that each word can grow and shrink
4446 // Final selections should be aware of this fact
4447 cx.set_state(indoc! {"
4448 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4449 "});
4450 cx.update_editor(|e, window, cx| {
4451 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4452 });
4453 cx.assert_editor_state(indoc! {"
4454 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4455 "});
4456
4457 cx.set_state(indoc! {"
4458 «hElLo, WoRld!ˇ»
4459 "});
4460 cx.update_editor(|e, window, cx| {
4461 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4462 });
4463 cx.assert_editor_state(indoc! {"
4464 «HeLlO, wOrLD!ˇ»
4465 "});
4466}
4467
4468#[gpui::test]
4469fn test_duplicate_line(cx: &mut TestAppContext) {
4470 init_test(cx, |_| {});
4471
4472 let editor = cx.add_window(|window, cx| {
4473 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4474 build_editor(buffer, window, cx)
4475 });
4476 _ = editor.update(cx, |editor, window, cx| {
4477 editor.change_selections(None, window, cx, |s| {
4478 s.select_display_ranges([
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4480 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4481 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4482 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4483 ])
4484 });
4485 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4486 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4487 assert_eq!(
4488 editor.selections.display_ranges(cx),
4489 vec![
4490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4491 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4492 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4493 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4494 ]
4495 );
4496 });
4497
4498 let editor = cx.add_window(|window, cx| {
4499 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4500 build_editor(buffer, window, cx)
4501 });
4502 _ = editor.update(cx, |editor, window, cx| {
4503 editor.change_selections(None, window, cx, |s| {
4504 s.select_display_ranges([
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4507 ])
4508 });
4509 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4510 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4511 assert_eq!(
4512 editor.selections.display_ranges(cx),
4513 vec![
4514 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4515 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4516 ]
4517 );
4518 });
4519
4520 // With `move_upwards` the selections stay in place, except for
4521 // the lines inserted above them
4522 let editor = cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4524 build_editor(buffer, window, cx)
4525 });
4526 _ = editor.update(cx, |editor, window, cx| {
4527 editor.change_selections(None, window, cx, |s| {
4528 s.select_display_ranges([
4529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4531 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4532 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4533 ])
4534 });
4535 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4536 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4537 assert_eq!(
4538 editor.selections.display_ranges(cx),
4539 vec![
4540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4541 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4542 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4543 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4544 ]
4545 );
4546 });
4547
4548 let editor = cx.add_window(|window, cx| {
4549 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4550 build_editor(buffer, window, cx)
4551 });
4552 _ = editor.update(cx, |editor, window, cx| {
4553 editor.change_selections(None, window, cx, |s| {
4554 s.select_display_ranges([
4555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4556 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4557 ])
4558 });
4559 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4560 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4561 assert_eq!(
4562 editor.selections.display_ranges(cx),
4563 vec![
4564 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4566 ]
4567 );
4568 });
4569
4570 let editor = cx.add_window(|window, cx| {
4571 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4572 build_editor(buffer, window, cx)
4573 });
4574 _ = editor.update(cx, |editor, window, cx| {
4575 editor.change_selections(None, window, cx, |s| {
4576 s.select_display_ranges([
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4578 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4579 ])
4580 });
4581 editor.duplicate_selection(&DuplicateSelection, window, cx);
4582 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4587 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4588 ]
4589 );
4590 });
4591}
4592
4593#[gpui::test]
4594fn test_move_line_up_down(cx: &mut TestAppContext) {
4595 init_test(cx, |_| {});
4596
4597 let editor = cx.add_window(|window, cx| {
4598 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4599 build_editor(buffer, window, cx)
4600 });
4601 _ = editor.update(cx, |editor, window, cx| {
4602 editor.fold_creases(
4603 vec![
4604 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4606 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4607 ],
4608 true,
4609 window,
4610 cx,
4611 );
4612 editor.change_selections(None, window, cx, |s| {
4613 s.select_display_ranges([
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4616 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4617 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4618 ])
4619 });
4620 assert_eq!(
4621 editor.display_text(cx),
4622 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4623 );
4624
4625 editor.move_line_up(&MoveLineUp, window, cx);
4626 assert_eq!(
4627 editor.display_text(cx),
4628 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4629 );
4630 assert_eq!(
4631 editor.selections.display_ranges(cx),
4632 vec![
4633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4634 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4635 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4637 ]
4638 );
4639 });
4640
4641 _ = editor.update(cx, |editor, window, cx| {
4642 editor.move_line_down(&MoveLineDown, window, cx);
4643 assert_eq!(
4644 editor.display_text(cx),
4645 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4646 );
4647 assert_eq!(
4648 editor.selections.display_ranges(cx),
4649 vec![
4650 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4651 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4652 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4653 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4654 ]
4655 );
4656 });
4657
4658 _ = editor.update(cx, |editor, window, cx| {
4659 editor.move_line_down(&MoveLineDown, window, cx);
4660 assert_eq!(
4661 editor.display_text(cx),
4662 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4663 );
4664 assert_eq!(
4665 editor.selections.display_ranges(cx),
4666 vec![
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4669 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4670 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4671 ]
4672 );
4673 });
4674
4675 _ = editor.update(cx, |editor, window, cx| {
4676 editor.move_line_up(&MoveLineUp, window, cx);
4677 assert_eq!(
4678 editor.display_text(cx),
4679 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4680 );
4681 assert_eq!(
4682 editor.selections.display_ranges(cx),
4683 vec![
4684 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4685 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4686 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4687 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4688 ]
4689 );
4690 });
4691}
4692
4693#[gpui::test]
4694fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4695 init_test(cx, |_| {});
4696
4697 let editor = cx.add_window(|window, cx| {
4698 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4699 build_editor(buffer, window, cx)
4700 });
4701 _ = editor.update(cx, |editor, window, cx| {
4702 let snapshot = editor.buffer.read(cx).snapshot(cx);
4703 editor.insert_blocks(
4704 [BlockProperties {
4705 style: BlockStyle::Fixed,
4706 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4707 height: Some(1),
4708 render: Arc::new(|_| div().into_any()),
4709 priority: 0,
4710 render_in_minimap: true,
4711 }],
4712 Some(Autoscroll::fit()),
4713 cx,
4714 );
4715 editor.change_selections(None, window, cx, |s| {
4716 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4717 });
4718 editor.move_line_down(&MoveLineDown, window, cx);
4719 });
4720}
4721
4722#[gpui::test]
4723async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727 cx.set_state(
4728 &"
4729 ˇzero
4730 one
4731 two
4732 three
4733 four
4734 five
4735 "
4736 .unindent(),
4737 );
4738
4739 // Create a four-line block that replaces three lines of text.
4740 cx.update_editor(|editor, window, cx| {
4741 let snapshot = editor.snapshot(window, cx);
4742 let snapshot = &snapshot.buffer_snapshot;
4743 let placement = BlockPlacement::Replace(
4744 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4745 );
4746 editor.insert_blocks(
4747 [BlockProperties {
4748 placement,
4749 height: Some(4),
4750 style: BlockStyle::Sticky,
4751 render: Arc::new(|_| gpui::div().into_any_element()),
4752 priority: 0,
4753 render_in_minimap: true,
4754 }],
4755 None,
4756 cx,
4757 );
4758 });
4759
4760 // Move down so that the cursor touches the block.
4761 cx.update_editor(|editor, window, cx| {
4762 editor.move_down(&Default::default(), window, cx);
4763 });
4764 cx.assert_editor_state(
4765 &"
4766 zero
4767 «one
4768 two
4769 threeˇ»
4770 four
4771 five
4772 "
4773 .unindent(),
4774 );
4775
4776 // Move down past the block.
4777 cx.update_editor(|editor, window, cx| {
4778 editor.move_down(&Default::default(), window, cx);
4779 });
4780 cx.assert_editor_state(
4781 &"
4782 zero
4783 one
4784 two
4785 three
4786 ˇfour
4787 five
4788 "
4789 .unindent(),
4790 );
4791}
4792
4793#[gpui::test]
4794fn test_transpose(cx: &mut TestAppContext) {
4795 init_test(cx, |_| {});
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "bac");
4803 assert_eq!(editor.selections.ranges(cx), [2..2]);
4804
4805 editor.transpose(&Default::default(), window, cx);
4806 assert_eq!(editor.text(cx), "bca");
4807 assert_eq!(editor.selections.ranges(cx), [3..3]);
4808
4809 editor.transpose(&Default::default(), window, cx);
4810 assert_eq!(editor.text(cx), "bac");
4811 assert_eq!(editor.selections.ranges(cx), [3..3]);
4812
4813 editor
4814 });
4815
4816 _ = cx.add_window(|window, cx| {
4817 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4818 editor.set_style(EditorStyle::default(), window, cx);
4819 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4820 editor.transpose(&Default::default(), window, cx);
4821 assert_eq!(editor.text(cx), "acb\nde");
4822 assert_eq!(editor.selections.ranges(cx), [3..3]);
4823
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "acbd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "acbde\n");
4831 assert_eq!(editor.selections.ranges(cx), [6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "acbd\ne");
4835 assert_eq!(editor.selections.ranges(cx), [6..6]);
4836
4837 editor
4838 });
4839
4840 _ = cx.add_window(|window, cx| {
4841 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4842 editor.set_style(EditorStyle::default(), window, cx);
4843 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4844 editor.transpose(&Default::default(), window, cx);
4845 assert_eq!(editor.text(cx), "bacd\ne");
4846 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4847
4848 editor.transpose(&Default::default(), window, cx);
4849 assert_eq!(editor.text(cx), "bcade\n");
4850 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4851
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "bcda\ne");
4854 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "bcade\n");
4858 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "bcaed\n");
4862 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4863
4864 editor
4865 });
4866
4867 _ = cx.add_window(|window, cx| {
4868 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4869 editor.set_style(EditorStyle::default(), window, cx);
4870 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4871 editor.transpose(&Default::default(), window, cx);
4872 assert_eq!(editor.text(cx), "🏀🍐✋");
4873 assert_eq!(editor.selections.ranges(cx), [8..8]);
4874
4875 editor.transpose(&Default::default(), window, cx);
4876 assert_eq!(editor.text(cx), "🏀✋🍐");
4877 assert_eq!(editor.selections.ranges(cx), [11..11]);
4878
4879 editor.transpose(&Default::default(), window, cx);
4880 assert_eq!(editor.text(cx), "🏀🍐✋");
4881 assert_eq!(editor.selections.ranges(cx), [11..11]);
4882
4883 editor
4884 });
4885}
4886
4887#[gpui::test]
4888async fn test_rewrap(cx: &mut TestAppContext) {
4889 init_test(cx, |settings| {
4890 settings.languages.extend([
4891 (
4892 "Markdown".into(),
4893 LanguageSettingsContent {
4894 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4895 ..Default::default()
4896 },
4897 ),
4898 (
4899 "Plain Text".into(),
4900 LanguageSettingsContent {
4901 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4902 ..Default::default()
4903 },
4904 ),
4905 ])
4906 });
4907
4908 let mut cx = EditorTestContext::new(cx).await;
4909
4910 let language_with_c_comments = Arc::new(Language::new(
4911 LanguageConfig {
4912 line_comments: vec!["// ".into()],
4913 ..LanguageConfig::default()
4914 },
4915 None,
4916 ));
4917 let language_with_pound_comments = Arc::new(Language::new(
4918 LanguageConfig {
4919 line_comments: vec!["# ".into()],
4920 ..LanguageConfig::default()
4921 },
4922 None,
4923 ));
4924 let markdown_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Markdown".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931 let language_with_doc_comments = Arc::new(Language::new(
4932 LanguageConfig {
4933 line_comments: vec!["// ".into(), "/// ".into()],
4934 ..LanguageConfig::default()
4935 },
4936 Some(tree_sitter_rust::LANGUAGE.into()),
4937 ));
4938
4939 let plaintext_language = Arc::new(Language::new(
4940 LanguageConfig {
4941 name: "Plain Text".into(),
4942 ..LanguageConfig::default()
4943 },
4944 None,
4945 ));
4946
4947 assert_rewrap(
4948 indoc! {"
4949 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4950 "},
4951 indoc! {"
4952 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4953 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4954 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4955 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4956 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4957 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4958 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4959 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4960 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4961 // porttitor id. Aliquam id accumsan eros.
4962 "},
4963 language_with_c_comments.clone(),
4964 &mut cx,
4965 );
4966
4967 // Test that rewrapping works inside of a selection
4968 assert_rewrap(
4969 indoc! {"
4970 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4971 "},
4972 indoc! {"
4973 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4974 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4975 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4976 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4977 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4978 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4979 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4980 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4981 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4982 // porttitor id. Aliquam id accumsan eros.ˇ»
4983 "},
4984 language_with_c_comments.clone(),
4985 &mut cx,
4986 );
4987
4988 // Test that cursors that expand to the same region are collapsed.
4989 assert_rewrap(
4990 indoc! {"
4991 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4992 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4993 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4994 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4995 "},
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4998 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4999 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5000 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5001 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5002 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5003 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5004 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5005 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5006 // porttitor id. Aliquam id accumsan eros.
5007 "},
5008 language_with_c_comments.clone(),
5009 &mut cx,
5010 );
5011
5012 // Test that non-contiguous selections are treated separately.
5013 assert_rewrap(
5014 indoc! {"
5015 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5016 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5017 //
5018 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5019 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5020 "},
5021 indoc! {"
5022 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5023 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5024 // auctor, eu lacinia sapien scelerisque.
5025 //
5026 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5027 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5028 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5029 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5030 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5031 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5032 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5033 "},
5034 language_with_c_comments.clone(),
5035 &mut cx,
5036 );
5037
5038 // Test that different comment prefixes are supported.
5039 assert_rewrap(
5040 indoc! {"
5041 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5042 "},
5043 indoc! {"
5044 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5045 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5046 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5047 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5048 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5049 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5050 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5051 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5052 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5053 # accumsan eros.
5054 "},
5055 language_with_pound_comments.clone(),
5056 &mut cx,
5057 );
5058
5059 // Test that rewrapping is ignored outside of comments in most languages.
5060 assert_rewrap(
5061 indoc! {"
5062 /// Adds two numbers.
5063 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5064 fn add(a: u32, b: u32) -> u32 {
5065 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5066 }
5067 "},
5068 indoc! {"
5069 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5070 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5071 fn add(a: u32, b: u32) -> u32 {
5072 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5073 }
5074 "},
5075 language_with_doc_comments.clone(),
5076 &mut cx,
5077 );
5078
5079 // Test that rewrapping works in Markdown and Plain Text languages.
5080 assert_rewrap(
5081 indoc! {"
5082 # Hello
5083
5084 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5085 "},
5086 indoc! {"
5087 # Hello
5088
5089 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5090 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5091 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5092 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5093 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5094 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5095 Integer sit amet scelerisque nisi.
5096 "},
5097 markdown_language,
5098 &mut cx,
5099 );
5100
5101 assert_rewrap(
5102 indoc! {"
5103 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5104 "},
5105 indoc! {"
5106 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5107 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5108 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5109 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5110 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5111 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5112 Integer sit amet scelerisque nisi.
5113 "},
5114 plaintext_language.clone(),
5115 &mut cx,
5116 );
5117
5118 // Test rewrapping unaligned comments in a selection.
5119 assert_rewrap(
5120 indoc! {"
5121 fn foo() {
5122 if true {
5123 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5124 // Praesent semper egestas tellus id dignissim.ˇ»
5125 do_something();
5126 } else {
5127 //
5128 }
5129 }
5130 "},
5131 indoc! {"
5132 fn foo() {
5133 if true {
5134 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5135 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5136 // egestas tellus id dignissim.ˇ»
5137 do_something();
5138 } else {
5139 //
5140 }
5141 }
5142 "},
5143 language_with_doc_comments.clone(),
5144 &mut cx,
5145 );
5146
5147 assert_rewrap(
5148 indoc! {"
5149 fn foo() {
5150 if true {
5151 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5152 // Praesent semper egestas tellus id dignissim.»
5153 do_something();
5154 } else {
5155 //
5156 }
5157
5158 }
5159 "},
5160 indoc! {"
5161 fn foo() {
5162 if true {
5163 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5164 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5165 // egestas tellus id dignissim.»
5166 do_something();
5167 } else {
5168 //
5169 }
5170
5171 }
5172 "},
5173 language_with_doc_comments.clone(),
5174 &mut cx,
5175 );
5176
5177 assert_rewrap(
5178 indoc! {"
5179 «ˇ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
5180
5181 two»
5182
5183 three
5184
5185 «ˇ\t
5186
5187 four four four four four four four four four four four four four four four four four four four four»
5188
5189 «ˇfive five five five five five five five five five five five five five five five five five five five
5190 \t»
5191 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
5192 "},
5193 indoc! {"
5194 «ˇone one one one one one one one one one one one one one one one one one one one
5195 one one one one one
5196
5197 two»
5198
5199 three
5200
5201 «ˇ\t
5202
5203 four four four four four four four four four four four four four four four four
5204 four four four four»
5205
5206 «ˇfive five five five five five five five five five five five five five five five
5207 five five five five
5208 \t»
5209 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
5210 "},
5211 plaintext_language.clone(),
5212 &mut cx,
5213 );
5214
5215 assert_rewrap(
5216 indoc! {"
5217 //ˇ 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
5218 //ˇ
5219 //ˇ 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
5220 //ˇ short short short
5221 int main(void) {
5222 return 17;
5223 }
5224 "},
5225 indoc! {"
5226 //ˇ long long long long long long long long long long long long long long long
5227 // long long long long long long long long long long long long long
5228 //ˇ
5229 //ˇ long long long long long long long long long long long long long long long
5230 //ˇ long long long long long long long long long long long long long short short
5231 // short
5232 int main(void) {
5233 return 17;
5234 }
5235 "},
5236 language_with_c_comments,
5237 &mut cx,
5238 );
5239
5240 #[track_caller]
5241 fn assert_rewrap(
5242 unwrapped_text: &str,
5243 wrapped_text: &str,
5244 language: Arc<Language>,
5245 cx: &mut EditorTestContext,
5246 ) {
5247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5248 cx.set_state(unwrapped_text);
5249 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5250 cx.assert_editor_state(wrapped_text);
5251 }
5252}
5253
5254#[gpui::test]
5255async fn test_hard_wrap(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5260 cx.update_editor(|editor, _, cx| {
5261 editor.set_hard_wrap(Some(14), cx);
5262 });
5263
5264 cx.set_state(indoc!(
5265 "
5266 one two three ˇ
5267 "
5268 ));
5269 cx.simulate_input("four");
5270 cx.run_until_parked();
5271
5272 cx.assert_editor_state(indoc!(
5273 "
5274 one two three
5275 fourˇ
5276 "
5277 ));
5278
5279 cx.update_editor(|editor, window, cx| {
5280 editor.newline(&Default::default(), window, cx);
5281 });
5282 cx.run_until_parked();
5283 cx.assert_editor_state(indoc!(
5284 "
5285 one two three
5286 four
5287 ˇ
5288 "
5289 ));
5290
5291 cx.simulate_input("five");
5292 cx.run_until_parked();
5293 cx.assert_editor_state(indoc!(
5294 "
5295 one two three
5296 four
5297 fiveˇ
5298 "
5299 ));
5300
5301 cx.update_editor(|editor, window, cx| {
5302 editor.newline(&Default::default(), window, cx);
5303 });
5304 cx.run_until_parked();
5305 cx.simulate_input("# ");
5306 cx.run_until_parked();
5307 cx.assert_editor_state(indoc!(
5308 "
5309 one two three
5310 four
5311 five
5312 # ˇ
5313 "
5314 ));
5315
5316 cx.update_editor(|editor, window, cx| {
5317 editor.newline(&Default::default(), window, cx);
5318 });
5319 cx.run_until_parked();
5320 cx.assert_editor_state(indoc!(
5321 "
5322 one two three
5323 four
5324 five
5325 #\x20
5326 #ˇ
5327 "
5328 ));
5329
5330 cx.simulate_input(" 6");
5331 cx.run_until_parked();
5332 cx.assert_editor_state(indoc!(
5333 "
5334 one two three
5335 four
5336 five
5337 #
5338 # 6ˇ
5339 "
5340 ));
5341}
5342
5343#[gpui::test]
5344async fn test_clipboard(cx: &mut TestAppContext) {
5345 init_test(cx, |_| {});
5346
5347 let mut cx = EditorTestContext::new(cx).await;
5348
5349 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5350 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5351 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5352
5353 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5354 cx.set_state("two ˇfour ˇsix ˇ");
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5357
5358 // Paste again but with only two cursors. Since the number of cursors doesn't
5359 // match the number of slices in the clipboard, the entire clipboard text
5360 // is pasted at each cursor.
5361 cx.set_state("ˇtwo one✅ four three six five ˇ");
5362 cx.update_editor(|e, window, cx| {
5363 e.handle_input("( ", window, cx);
5364 e.paste(&Paste, window, cx);
5365 e.handle_input(") ", window, cx);
5366 });
5367 cx.assert_editor_state(
5368 &([
5369 "( one✅ ",
5370 "three ",
5371 "five ) ˇtwo one✅ four three six five ( one✅ ",
5372 "three ",
5373 "five ) ˇ",
5374 ]
5375 .join("\n")),
5376 );
5377
5378 // Cut with three selections, one of which is full-line.
5379 cx.set_state(indoc! {"
5380 1«2ˇ»3
5381 4ˇ567
5382 «8ˇ»9"});
5383 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5384 cx.assert_editor_state(indoc! {"
5385 1ˇ3
5386 ˇ9"});
5387
5388 // Paste with three selections, noticing how the copied selection that was full-line
5389 // gets inserted before the second cursor.
5390 cx.set_state(indoc! {"
5391 1ˇ3
5392 9ˇ
5393 «oˇ»ne"});
5394 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5395 cx.assert_editor_state(indoc! {"
5396 12ˇ3
5397 4567
5398 9ˇ
5399 8ˇne"});
5400
5401 // Copy with a single cursor only, which writes the whole line into the clipboard.
5402 cx.set_state(indoc! {"
5403 The quick brown
5404 fox juˇmps over
5405 the lazy dog"});
5406 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5407 assert_eq!(
5408 cx.read_from_clipboard()
5409 .and_then(|item| item.text().as_deref().map(str::to_string)),
5410 Some("fox jumps over\n".to_string())
5411 );
5412
5413 // Paste with three selections, noticing how the copied full-line selection is inserted
5414 // before the empty selections but replaces the selection that is non-empty.
5415 cx.set_state(indoc! {"
5416 Tˇhe quick brown
5417 «foˇ»x jumps over
5418 tˇhe lazy dog"});
5419 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5420 cx.assert_editor_state(indoc! {"
5421 fox jumps over
5422 Tˇhe quick brown
5423 fox jumps over
5424 ˇx jumps over
5425 fox jumps over
5426 tˇhe lazy dog"});
5427}
5428
5429#[gpui::test]
5430async fn test_copy_trim(cx: &mut TestAppContext) {
5431 init_test(cx, |_| {});
5432
5433 let mut cx = EditorTestContext::new(cx).await;
5434 cx.set_state(
5435 r#" «for selection in selections.iter() {
5436 let mut start = selection.start;
5437 let mut end = selection.end;
5438 let is_entire_line = selection.is_empty();
5439 if is_entire_line {
5440 start = Point::new(start.row, 0);ˇ»
5441 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5442 }
5443 "#,
5444 );
5445 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5446 assert_eq!(
5447 cx.read_from_clipboard()
5448 .and_then(|item| item.text().as_deref().map(str::to_string)),
5449 Some(
5450 "for selection in selections.iter() {
5451 let mut start = selection.start;
5452 let mut end = selection.end;
5453 let is_entire_line = selection.is_empty();
5454 if is_entire_line {
5455 start = Point::new(start.row, 0);"
5456 .to_string()
5457 ),
5458 "Regular copying preserves all indentation selected",
5459 );
5460 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5461 assert_eq!(
5462 cx.read_from_clipboard()
5463 .and_then(|item| item.text().as_deref().map(str::to_string)),
5464 Some(
5465 "for selection in selections.iter() {
5466let mut start = selection.start;
5467let mut end = selection.end;
5468let is_entire_line = selection.is_empty();
5469if is_entire_line {
5470 start = Point::new(start.row, 0);"
5471 .to_string()
5472 ),
5473 "Copying with stripping should strip all leading whitespaces"
5474 );
5475
5476 cx.set_state(
5477 r#" « for selection in selections.iter() {
5478 let mut start = selection.start;
5479 let mut end = selection.end;
5480 let is_entire_line = selection.is_empty();
5481 if is_entire_line {
5482 start = Point::new(start.row, 0);ˇ»
5483 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5484 }
5485 "#,
5486 );
5487 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5488 assert_eq!(
5489 cx.read_from_clipboard()
5490 .and_then(|item| item.text().as_deref().map(str::to_string)),
5491 Some(
5492 " for selection in selections.iter() {
5493 let mut start = selection.start;
5494 let mut end = selection.end;
5495 let is_entire_line = selection.is_empty();
5496 if is_entire_line {
5497 start = Point::new(start.row, 0);"
5498 .to_string()
5499 ),
5500 "Regular copying preserves all indentation selected",
5501 );
5502 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5503 assert_eq!(
5504 cx.read_from_clipboard()
5505 .and_then(|item| item.text().as_deref().map(str::to_string)),
5506 Some(
5507 "for selection in selections.iter() {
5508let mut start = selection.start;
5509let mut end = selection.end;
5510let is_entire_line = selection.is_empty();
5511if is_entire_line {
5512 start = Point::new(start.row, 0);"
5513 .to_string()
5514 ),
5515 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5516 );
5517
5518 cx.set_state(
5519 r#" «ˇ for selection in selections.iter() {
5520 let mut start = selection.start;
5521 let mut end = selection.end;
5522 let is_entire_line = selection.is_empty();
5523 if is_entire_line {
5524 start = Point::new(start.row, 0);»
5525 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5526 }
5527 "#,
5528 );
5529 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5530 assert_eq!(
5531 cx.read_from_clipboard()
5532 .and_then(|item| item.text().as_deref().map(str::to_string)),
5533 Some(
5534 " for selection in selections.iter() {
5535 let mut start = selection.start;
5536 let mut end = selection.end;
5537 let is_entire_line = selection.is_empty();
5538 if is_entire_line {
5539 start = Point::new(start.row, 0);"
5540 .to_string()
5541 ),
5542 "Regular copying for reverse selection works the same",
5543 );
5544 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5545 assert_eq!(
5546 cx.read_from_clipboard()
5547 .and_then(|item| item.text().as_deref().map(str::to_string)),
5548 Some(
5549 "for selection in selections.iter() {
5550let mut start = selection.start;
5551let mut end = selection.end;
5552let is_entire_line = selection.is_empty();
5553if is_entire_line {
5554 start = Point::new(start.row, 0);"
5555 .to_string()
5556 ),
5557 "Copying with stripping for reverse selection works the same"
5558 );
5559
5560 cx.set_state(
5561 r#" for selection «in selections.iter() {
5562 let mut start = selection.start;
5563 let mut end = selection.end;
5564 let is_entire_line = selection.is_empty();
5565 if is_entire_line {
5566 start = Point::new(start.row, 0);ˇ»
5567 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5568 }
5569 "#,
5570 );
5571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5572 assert_eq!(
5573 cx.read_from_clipboard()
5574 .and_then(|item| item.text().as_deref().map(str::to_string)),
5575 Some(
5576 "in selections.iter() {
5577 let mut start = selection.start;
5578 let mut end = selection.end;
5579 let is_entire_line = selection.is_empty();
5580 if is_entire_line {
5581 start = Point::new(start.row, 0);"
5582 .to_string()
5583 ),
5584 "When selecting past the indent, the copying works as usual",
5585 );
5586 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5587 assert_eq!(
5588 cx.read_from_clipboard()
5589 .and_then(|item| item.text().as_deref().map(str::to_string)),
5590 Some(
5591 "in selections.iter() {
5592 let mut start = selection.start;
5593 let mut end = selection.end;
5594 let is_entire_line = selection.is_empty();
5595 if is_entire_line {
5596 start = Point::new(start.row, 0);"
5597 .to_string()
5598 ),
5599 "When selecting past the indent, nothing is trimmed"
5600 );
5601
5602 cx.set_state(
5603 r#" «for selection in selections.iter() {
5604 let mut start = selection.start;
5605
5606 let mut end = selection.end;
5607 let is_entire_line = selection.is_empty();
5608 if is_entire_line {
5609 start = Point::new(start.row, 0);
5610ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5611 }
5612 "#,
5613 );
5614 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5615 assert_eq!(
5616 cx.read_from_clipboard()
5617 .and_then(|item| item.text().as_deref().map(str::to_string)),
5618 Some(
5619 "for selection in selections.iter() {
5620let mut start = selection.start;
5621
5622let mut end = selection.end;
5623let is_entire_line = selection.is_empty();
5624if is_entire_line {
5625 start = Point::new(start.row, 0);
5626"
5627 .to_string()
5628 ),
5629 "Copying with stripping should ignore empty lines"
5630 );
5631}
5632
5633#[gpui::test]
5634async fn test_paste_multiline(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5639
5640 // Cut an indented block, without the leading whitespace.
5641 cx.set_state(indoc! {"
5642 const a: B = (
5643 c(),
5644 «d(
5645 e,
5646 f
5647 )ˇ»
5648 );
5649 "});
5650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5651 cx.assert_editor_state(indoc! {"
5652 const a: B = (
5653 c(),
5654 ˇ
5655 );
5656 "});
5657
5658 // Paste it at the same position.
5659 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5660 cx.assert_editor_state(indoc! {"
5661 const a: B = (
5662 c(),
5663 d(
5664 e,
5665 f
5666 )ˇ
5667 );
5668 "});
5669
5670 // Paste it at a line with a lower indent level.
5671 cx.set_state(indoc! {"
5672 ˇ
5673 const a: B = (
5674 c(),
5675 );
5676 "});
5677 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5678 cx.assert_editor_state(indoc! {"
5679 d(
5680 e,
5681 f
5682 )ˇ
5683 const a: B = (
5684 c(),
5685 );
5686 "});
5687
5688 // Cut an indented block, with the leading whitespace.
5689 cx.set_state(indoc! {"
5690 const a: B = (
5691 c(),
5692 « d(
5693 e,
5694 f
5695 )
5696 ˇ»);
5697 "});
5698 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5699 cx.assert_editor_state(indoc! {"
5700 const a: B = (
5701 c(),
5702 ˇ);
5703 "});
5704
5705 // Paste it at the same position.
5706 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5707 cx.assert_editor_state(indoc! {"
5708 const a: B = (
5709 c(),
5710 d(
5711 e,
5712 f
5713 )
5714 ˇ);
5715 "});
5716
5717 // Paste it at a line with a higher indent level.
5718 cx.set_state(indoc! {"
5719 const a: B = (
5720 c(),
5721 d(
5722 e,
5723 fˇ
5724 )
5725 );
5726 "});
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 const a: B = (
5730 c(),
5731 d(
5732 e,
5733 f d(
5734 e,
5735 f
5736 )
5737 ˇ
5738 )
5739 );
5740 "});
5741
5742 // Copy an indented block, starting mid-line
5743 cx.set_state(indoc! {"
5744 const a: B = (
5745 c(),
5746 somethin«g(
5747 e,
5748 f
5749 )ˇ»
5750 );
5751 "});
5752 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5753
5754 // Paste it on a line with a lower indent level
5755 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5756 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5757 cx.assert_editor_state(indoc! {"
5758 const a: B = (
5759 c(),
5760 something(
5761 e,
5762 f
5763 )
5764 );
5765 g(
5766 e,
5767 f
5768 )ˇ"});
5769}
5770
5771#[gpui::test]
5772async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5773 init_test(cx, |_| {});
5774
5775 cx.write_to_clipboard(ClipboardItem::new_string(
5776 " d(\n e\n );\n".into(),
5777 ));
5778
5779 let mut cx = EditorTestContext::new(cx).await;
5780 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5781
5782 cx.set_state(indoc! {"
5783 fn a() {
5784 b();
5785 if c() {
5786 ˇ
5787 }
5788 }
5789 "});
5790
5791 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5792 cx.assert_editor_state(indoc! {"
5793 fn a() {
5794 b();
5795 if c() {
5796 d(
5797 e
5798 );
5799 ˇ
5800 }
5801 }
5802 "});
5803
5804 cx.set_state(indoc! {"
5805 fn a() {
5806 b();
5807 ˇ
5808 }
5809 "});
5810
5811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5812 cx.assert_editor_state(indoc! {"
5813 fn a() {
5814 b();
5815 d(
5816 e
5817 );
5818 ˇ
5819 }
5820 "});
5821}
5822
5823#[gpui::test]
5824fn test_select_all(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826
5827 let editor = cx.add_window(|window, cx| {
5828 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5829 build_editor(buffer, window, cx)
5830 });
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.select_all(&SelectAll, window, cx);
5833 assert_eq!(
5834 editor.selections.display_ranges(cx),
5835 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5836 );
5837 });
5838}
5839
5840#[gpui::test]
5841fn test_select_line(cx: &mut TestAppContext) {
5842 init_test(cx, |_| {});
5843
5844 let editor = cx.add_window(|window, cx| {
5845 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5846 build_editor(buffer, window, cx)
5847 });
5848 _ = editor.update(cx, |editor, window, cx| {
5849 editor.change_selections(None, window, cx, |s| {
5850 s.select_display_ranges([
5851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5852 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5854 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5855 ])
5856 });
5857 editor.select_line(&SelectLine, window, cx);
5858 assert_eq!(
5859 editor.selections.display_ranges(cx),
5860 vec![
5861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5863 ]
5864 );
5865 });
5866
5867 _ = editor.update(cx, |editor, window, cx| {
5868 editor.select_line(&SelectLine, window, cx);
5869 assert_eq!(
5870 editor.selections.display_ranges(cx),
5871 vec![
5872 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5873 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5874 ]
5875 );
5876 });
5877
5878 _ = editor.update(cx, |editor, window, cx| {
5879 editor.select_line(&SelectLine, window, cx);
5880 assert_eq!(
5881 editor.selections.display_ranges(cx),
5882 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5883 );
5884 });
5885}
5886
5887#[gpui::test]
5888async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890 let mut cx = EditorTestContext::new(cx).await;
5891
5892 #[track_caller]
5893 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5894 cx.set_state(initial_state);
5895 cx.update_editor(|e, window, cx| {
5896 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5897 });
5898 cx.assert_editor_state(expected_state);
5899 }
5900
5901 // Selection starts and ends at the middle of lines, left-to-right
5902 test(
5903 &mut cx,
5904 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5905 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5906 );
5907 // Same thing, right-to-left
5908 test(
5909 &mut cx,
5910 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5911 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5912 );
5913
5914 // Whole buffer, left-to-right, last line *doesn't* end with newline
5915 test(
5916 &mut cx,
5917 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5918 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5919 );
5920 // Same thing, right-to-left
5921 test(
5922 &mut cx,
5923 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5924 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5925 );
5926
5927 // Whole buffer, left-to-right, last line ends with newline
5928 test(
5929 &mut cx,
5930 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5931 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5932 );
5933 // Same thing, right-to-left
5934 test(
5935 &mut cx,
5936 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5937 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5938 );
5939
5940 // Starts at the end of a line, ends at the start of another
5941 test(
5942 &mut cx,
5943 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5944 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5945 );
5946}
5947
5948#[gpui::test]
5949async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5950 init_test(cx, |_| {});
5951
5952 let editor = cx.add_window(|window, cx| {
5953 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5954 build_editor(buffer, window, cx)
5955 });
5956
5957 // setup
5958 _ = editor.update(cx, |editor, window, cx| {
5959 editor.fold_creases(
5960 vec![
5961 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5963 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5964 ],
5965 true,
5966 window,
5967 cx,
5968 );
5969 assert_eq!(
5970 editor.display_text(cx),
5971 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5972 );
5973 });
5974
5975 _ = editor.update(cx, |editor, window, cx| {
5976 editor.change_selections(None, window, cx, |s| {
5977 s.select_display_ranges([
5978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5980 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5981 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5982 ])
5983 });
5984 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5985 assert_eq!(
5986 editor.display_text(cx),
5987 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5988 );
5989 });
5990 EditorTestContext::for_editor(editor, cx)
5991 .await
5992 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5993
5994 _ = editor.update(cx, |editor, window, cx| {
5995 editor.change_selections(None, window, cx, |s| {
5996 s.select_display_ranges([
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5998 ])
5999 });
6000 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6001 assert_eq!(
6002 editor.display_text(cx),
6003 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6004 );
6005 assert_eq!(
6006 editor.selections.display_ranges(cx),
6007 [
6008 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6009 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6010 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6011 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6012 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6013 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6014 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6015 ]
6016 );
6017 });
6018 EditorTestContext::for_editor(editor, cx)
6019 .await
6020 .assert_editor_state(
6021 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6022 );
6023}
6024
6025#[gpui::test]
6026async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6027 init_test(cx, |_| {});
6028
6029 let mut cx = EditorTestContext::new(cx).await;
6030
6031 cx.set_state(indoc!(
6032 r#"abc
6033 defˇghi
6034
6035 jk
6036 nlmo
6037 "#
6038 ));
6039
6040 cx.update_editor(|editor, window, cx| {
6041 editor.add_selection_above(&Default::default(), window, cx);
6042 });
6043
6044 cx.assert_editor_state(indoc!(
6045 r#"abcˇ
6046 defˇghi
6047
6048 jk
6049 nlmo
6050 "#
6051 ));
6052
6053 cx.update_editor(|editor, window, cx| {
6054 editor.add_selection_above(&Default::default(), window, cx);
6055 });
6056
6057 cx.assert_editor_state(indoc!(
6058 r#"abcˇ
6059 defˇghi
6060
6061 jk
6062 nlmo
6063 "#
6064 ));
6065
6066 cx.update_editor(|editor, window, cx| {
6067 editor.add_selection_below(&Default::default(), window, cx);
6068 });
6069
6070 cx.assert_editor_state(indoc!(
6071 r#"abc
6072 defˇghi
6073
6074 jk
6075 nlmo
6076 "#
6077 ));
6078
6079 cx.update_editor(|editor, window, cx| {
6080 editor.undo_selection(&Default::default(), window, cx);
6081 });
6082
6083 cx.assert_editor_state(indoc!(
6084 r#"abcˇ
6085 defˇghi
6086
6087 jk
6088 nlmo
6089 "#
6090 ));
6091
6092 cx.update_editor(|editor, window, cx| {
6093 editor.redo_selection(&Default::default(), window, cx);
6094 });
6095
6096 cx.assert_editor_state(indoc!(
6097 r#"abc
6098 defˇghi
6099
6100 jk
6101 nlmo
6102 "#
6103 ));
6104
6105 cx.update_editor(|editor, window, cx| {
6106 editor.add_selection_below(&Default::default(), window, cx);
6107 });
6108
6109 cx.assert_editor_state(indoc!(
6110 r#"abc
6111 defˇghi
6112 ˇ
6113 jk
6114 nlmo
6115 "#
6116 ));
6117
6118 cx.update_editor(|editor, window, cx| {
6119 editor.add_selection_below(&Default::default(), window, cx);
6120 });
6121
6122 cx.assert_editor_state(indoc!(
6123 r#"abc
6124 defˇghi
6125 ˇ
6126 jkˇ
6127 nlmo
6128 "#
6129 ));
6130
6131 cx.update_editor(|editor, window, cx| {
6132 editor.add_selection_below(&Default::default(), window, cx);
6133 });
6134
6135 cx.assert_editor_state(indoc!(
6136 r#"abc
6137 defˇghi
6138 ˇ
6139 jkˇ
6140 nlmˇo
6141 "#
6142 ));
6143
6144 cx.update_editor(|editor, window, cx| {
6145 editor.add_selection_below(&Default::default(), window, cx);
6146 });
6147
6148 cx.assert_editor_state(indoc!(
6149 r#"abc
6150 defˇghi
6151 ˇ
6152 jkˇ
6153 nlmˇo
6154 ˇ"#
6155 ));
6156
6157 // change selections
6158 cx.set_state(indoc!(
6159 r#"abc
6160 def«ˇg»hi
6161
6162 jk
6163 nlmo
6164 "#
6165 ));
6166
6167 cx.update_editor(|editor, window, cx| {
6168 editor.add_selection_below(&Default::default(), window, cx);
6169 });
6170
6171 cx.assert_editor_state(indoc!(
6172 r#"abc
6173 def«ˇg»hi
6174
6175 jk
6176 nlm«ˇo»
6177 "#
6178 ));
6179
6180 cx.update_editor(|editor, window, cx| {
6181 editor.add_selection_below(&Default::default(), window, cx);
6182 });
6183
6184 cx.assert_editor_state(indoc!(
6185 r#"abc
6186 def«ˇg»hi
6187
6188 jk
6189 nlm«ˇo»
6190 "#
6191 ));
6192
6193 cx.update_editor(|editor, window, cx| {
6194 editor.add_selection_above(&Default::default(), window, cx);
6195 });
6196
6197 cx.assert_editor_state(indoc!(
6198 r#"abc
6199 def«ˇg»hi
6200
6201 jk
6202 nlmo
6203 "#
6204 ));
6205
6206 cx.update_editor(|editor, window, cx| {
6207 editor.add_selection_above(&Default::default(), window, cx);
6208 });
6209
6210 cx.assert_editor_state(indoc!(
6211 r#"abc
6212 def«ˇg»hi
6213
6214 jk
6215 nlmo
6216 "#
6217 ));
6218
6219 // Change selections again
6220 cx.set_state(indoc!(
6221 r#"a«bc
6222 defgˇ»hi
6223
6224 jk
6225 nlmo
6226 "#
6227 ));
6228
6229 cx.update_editor(|editor, window, cx| {
6230 editor.add_selection_below(&Default::default(), window, cx);
6231 });
6232
6233 cx.assert_editor_state(indoc!(
6234 r#"a«bcˇ»
6235 d«efgˇ»hi
6236
6237 j«kˇ»
6238 nlmo
6239 "#
6240 ));
6241
6242 cx.update_editor(|editor, window, cx| {
6243 editor.add_selection_below(&Default::default(), window, cx);
6244 });
6245 cx.assert_editor_state(indoc!(
6246 r#"a«bcˇ»
6247 d«efgˇ»hi
6248
6249 j«kˇ»
6250 n«lmoˇ»
6251 "#
6252 ));
6253 cx.update_editor(|editor, window, cx| {
6254 editor.add_selection_above(&Default::default(), window, cx);
6255 });
6256
6257 cx.assert_editor_state(indoc!(
6258 r#"a«bcˇ»
6259 d«efgˇ»hi
6260
6261 j«kˇ»
6262 nlmo
6263 "#
6264 ));
6265
6266 // Change selections again
6267 cx.set_state(indoc!(
6268 r#"abc
6269 d«ˇefghi
6270
6271 jk
6272 nlm»o
6273 "#
6274 ));
6275
6276 cx.update_editor(|editor, window, cx| {
6277 editor.add_selection_above(&Default::default(), window, cx);
6278 });
6279
6280 cx.assert_editor_state(indoc!(
6281 r#"a«ˇbc»
6282 d«ˇef»ghi
6283
6284 j«ˇk»
6285 n«ˇlm»o
6286 "#
6287 ));
6288
6289 cx.update_editor(|editor, window, cx| {
6290 editor.add_selection_below(&Default::default(), window, cx);
6291 });
6292
6293 cx.assert_editor_state(indoc!(
6294 r#"abc
6295 d«ˇef»ghi
6296
6297 j«ˇk»
6298 n«ˇlm»o
6299 "#
6300 ));
6301}
6302
6303#[gpui::test]
6304async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306 let mut cx = EditorTestContext::new(cx).await;
6307
6308 cx.set_state(indoc!(
6309 r#"line onˇe
6310 liˇne two
6311 line three
6312 line four"#
6313 ));
6314
6315 cx.update_editor(|editor, window, cx| {
6316 editor.add_selection_below(&Default::default(), window, cx);
6317 });
6318
6319 // test multiple cursors expand in the same direction
6320 cx.assert_editor_state(indoc!(
6321 r#"line onˇe
6322 liˇne twˇo
6323 liˇne three
6324 line four"#
6325 ));
6326
6327 cx.update_editor(|editor, window, cx| {
6328 editor.add_selection_below(&Default::default(), window, cx);
6329 });
6330
6331 cx.update_editor(|editor, window, cx| {
6332 editor.add_selection_below(&Default::default(), window, cx);
6333 });
6334
6335 // test multiple cursors expand below overflow
6336 cx.assert_editor_state(indoc!(
6337 r#"line onˇe
6338 liˇne twˇo
6339 liˇne thˇree
6340 liˇne foˇur"#
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.add_selection_above(&Default::default(), window, cx);
6345 });
6346
6347 // test multiple cursors retrieves back correctly
6348 cx.assert_editor_state(indoc!(
6349 r#"line onˇe
6350 liˇne twˇo
6351 liˇne thˇree
6352 line four"#
6353 ));
6354
6355 cx.update_editor(|editor, window, cx| {
6356 editor.add_selection_above(&Default::default(), window, cx);
6357 });
6358
6359 cx.update_editor(|editor, window, cx| {
6360 editor.add_selection_above(&Default::default(), window, cx);
6361 });
6362
6363 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6364 cx.assert_editor_state(indoc!(
6365 r#"liˇne onˇe
6366 liˇne two
6367 line three
6368 line four"#
6369 ));
6370
6371 cx.update_editor(|editor, window, cx| {
6372 editor.undo_selection(&Default::default(), window, cx);
6373 });
6374
6375 // test undo
6376 cx.assert_editor_state(indoc!(
6377 r#"line onˇe
6378 liˇne twˇo
6379 line three
6380 line four"#
6381 ));
6382
6383 cx.update_editor(|editor, window, cx| {
6384 editor.redo_selection(&Default::default(), window, cx);
6385 });
6386
6387 // test redo
6388 cx.assert_editor_state(indoc!(
6389 r#"liˇne onˇe
6390 liˇne two
6391 line three
6392 line four"#
6393 ));
6394
6395 cx.set_state(indoc!(
6396 r#"abcd
6397 ef«ghˇ»
6398 ijkl
6399 «mˇ»nop"#
6400 ));
6401
6402 cx.update_editor(|editor, window, cx| {
6403 editor.add_selection_above(&Default::default(), window, cx);
6404 });
6405
6406 // test multiple selections expand in the same direction
6407 cx.assert_editor_state(indoc!(
6408 r#"ab«cdˇ»
6409 ef«ghˇ»
6410 «iˇ»jkl
6411 «mˇ»nop"#
6412 ));
6413
6414 cx.update_editor(|editor, window, cx| {
6415 editor.add_selection_above(&Default::default(), window, cx);
6416 });
6417
6418 // test multiple selection upward overflow
6419 cx.assert_editor_state(indoc!(
6420 r#"ab«cdˇ»
6421 «eˇ»f«ghˇ»
6422 «iˇ»jkl
6423 «mˇ»nop"#
6424 ));
6425
6426 cx.update_editor(|editor, window, cx| {
6427 editor.add_selection_below(&Default::default(), window, cx);
6428 });
6429
6430 // test multiple selection retrieves back correctly
6431 cx.assert_editor_state(indoc!(
6432 r#"abcd
6433 ef«ghˇ»
6434 «iˇ»jkl
6435 «mˇ»nop"#
6436 ));
6437
6438 cx.update_editor(|editor, window, cx| {
6439 editor.add_selection_below(&Default::default(), window, cx);
6440 });
6441
6442 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6443 cx.assert_editor_state(indoc!(
6444 r#"abcd
6445 ef«ghˇ»
6446 ij«klˇ»
6447 «mˇ»nop"#
6448 ));
6449
6450 cx.update_editor(|editor, window, cx| {
6451 editor.undo_selection(&Default::default(), window, cx);
6452 });
6453
6454 // test undo
6455 cx.assert_editor_state(indoc!(
6456 r#"abcd
6457 ef«ghˇ»
6458 «iˇ»jkl
6459 «mˇ»nop"#
6460 ));
6461
6462 cx.update_editor(|editor, window, cx| {
6463 editor.redo_selection(&Default::default(), window, cx);
6464 });
6465
6466 // test redo
6467 cx.assert_editor_state(indoc!(
6468 r#"abcd
6469 ef«ghˇ»
6470 ij«klˇ»
6471 «mˇ»nop"#
6472 ));
6473}
6474
6475#[gpui::test]
6476async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6477 init_test(cx, |_| {});
6478 let mut cx = EditorTestContext::new(cx).await;
6479
6480 cx.set_state(indoc!(
6481 r#"line onˇe
6482 liˇne two
6483 line three
6484 line four"#
6485 ));
6486
6487 cx.update_editor(|editor, window, cx| {
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 editor.add_selection_below(&Default::default(), window, cx);
6491 });
6492
6493 // initial state with two multi cursor groups
6494 cx.assert_editor_state(indoc!(
6495 r#"line onˇe
6496 liˇne twˇo
6497 liˇne thˇree
6498 liˇne foˇur"#
6499 ));
6500
6501 // add single cursor in middle - simulate opt click
6502 cx.update_editor(|editor, window, cx| {
6503 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6504 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6505 editor.end_selection(window, cx);
6506 });
6507
6508 cx.assert_editor_state(indoc!(
6509 r#"line onˇe
6510 liˇne twˇo
6511 liˇneˇ thˇree
6512 liˇne foˇur"#
6513 ));
6514
6515 cx.update_editor(|editor, window, cx| {
6516 editor.add_selection_above(&Default::default(), window, cx);
6517 });
6518
6519 // test new added selection expands above and existing selection shrinks
6520 cx.assert_editor_state(indoc!(
6521 r#"line onˇe
6522 liˇneˇ twˇo
6523 liˇneˇ thˇree
6524 line four"#
6525 ));
6526
6527 cx.update_editor(|editor, window, cx| {
6528 editor.add_selection_above(&Default::default(), window, cx);
6529 });
6530
6531 // test new added selection expands above and existing selection shrinks
6532 cx.assert_editor_state(indoc!(
6533 r#"lineˇ onˇe
6534 liˇneˇ twˇo
6535 lineˇ three
6536 line four"#
6537 ));
6538
6539 // intial state with two selection groups
6540 cx.set_state(indoc!(
6541 r#"abcd
6542 ef«ghˇ»
6543 ijkl
6544 «mˇ»nop"#
6545 ));
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 editor.add_selection_above(&Default::default(), window, cx);
6550 });
6551
6552 cx.assert_editor_state(indoc!(
6553 r#"ab«cdˇ»
6554 «eˇ»f«ghˇ»
6555 «iˇ»jkl
6556 «mˇ»nop"#
6557 ));
6558
6559 // add single selection in middle - simulate opt drag
6560 cx.update_editor(|editor, window, cx| {
6561 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6562 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6563 editor.update_selection(
6564 DisplayPoint::new(DisplayRow(2), 4),
6565 0,
6566 gpui::Point::<f32>::default(),
6567 window,
6568 cx,
6569 );
6570 editor.end_selection(window, cx);
6571 });
6572
6573 cx.assert_editor_state(indoc!(
6574 r#"ab«cdˇ»
6575 «eˇ»f«ghˇ»
6576 «iˇ»jk«lˇ»
6577 «mˇ»nop"#
6578 ));
6579
6580 cx.update_editor(|editor, window, cx| {
6581 editor.add_selection_below(&Default::default(), window, cx);
6582 });
6583
6584 // test new added selection expands below, others shrinks from above
6585 cx.assert_editor_state(indoc!(
6586 r#"abcd
6587 ef«ghˇ»
6588 «iˇ»jk«lˇ»
6589 «mˇ»no«pˇ»"#
6590 ));
6591}
6592
6593#[gpui::test]
6594async fn test_select_next(cx: &mut TestAppContext) {
6595 init_test(cx, |_| {});
6596
6597 let mut cx = EditorTestContext::new(cx).await;
6598 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6599
6600 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6601 .unwrap();
6602 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6603
6604 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6605 .unwrap();
6606 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6607
6608 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6609 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6610
6611 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6612 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6613
6614 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6615 .unwrap();
6616 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6617
6618 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6619 .unwrap();
6620 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6621
6622 // Test selection direction should be preserved
6623 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6624
6625 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6626 .unwrap();
6627 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6628}
6629
6630#[gpui::test]
6631async fn test_select_all_matches(cx: &mut TestAppContext) {
6632 init_test(cx, |_| {});
6633
6634 let mut cx = EditorTestContext::new(cx).await;
6635
6636 // Test caret-only selections
6637 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6638 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6639 .unwrap();
6640 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6641
6642 // Test left-to-right selections
6643 cx.set_state("abc\n«abcˇ»\nabc");
6644 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6645 .unwrap();
6646 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6647
6648 // Test right-to-left selections
6649 cx.set_state("abc\n«ˇabc»\nabc");
6650 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6651 .unwrap();
6652 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6653
6654 // Test selecting whitespace with caret selection
6655 cx.set_state("abc\nˇ abc\nabc");
6656 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6657 .unwrap();
6658 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6659
6660 // Test selecting whitespace with left-to-right selection
6661 cx.set_state("abc\n«ˇ »abc\nabc");
6662 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6663 .unwrap();
6664 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6665
6666 // Test no matches with right-to-left selection
6667 cx.set_state("abc\n« ˇ»abc\nabc");
6668 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6669 .unwrap();
6670 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6671}
6672
6673#[gpui::test]
6674async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6675 init_test(cx, |_| {});
6676
6677 let mut cx = EditorTestContext::new(cx).await;
6678
6679 let large_body_1 = "\nd".repeat(200);
6680 let large_body_2 = "\ne".repeat(200);
6681
6682 cx.set_state(&format!(
6683 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6684 ));
6685 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6686 let scroll_position = editor.scroll_position(cx);
6687 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6688 scroll_position
6689 });
6690
6691 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6692 .unwrap();
6693 cx.assert_editor_state(&format!(
6694 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6695 ));
6696 let scroll_position_after_selection =
6697 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6698 assert_eq!(
6699 initial_scroll_position, scroll_position_after_selection,
6700 "Scroll position should not change after selecting all matches"
6701 );
6702}
6703
6704#[gpui::test]
6705async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6706 init_test(cx, |_| {});
6707
6708 let mut cx = EditorLspTestContext::new_rust(
6709 lsp::ServerCapabilities {
6710 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6711 ..Default::default()
6712 },
6713 cx,
6714 )
6715 .await;
6716
6717 cx.set_state(indoc! {"
6718 line 1
6719 line 2
6720 linˇe 3
6721 line 4
6722 line 5
6723 "});
6724
6725 // Make an edit
6726 cx.update_editor(|editor, window, cx| {
6727 editor.handle_input("X", window, cx);
6728 });
6729
6730 // Move cursor to a different position
6731 cx.update_editor(|editor, window, cx| {
6732 editor.change_selections(None, window, cx, |s| {
6733 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6734 });
6735 });
6736
6737 cx.assert_editor_state(indoc! {"
6738 line 1
6739 line 2
6740 linXe 3
6741 line 4
6742 liˇne 5
6743 "});
6744
6745 cx.lsp
6746 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6747 Ok(Some(vec![lsp::TextEdit::new(
6748 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6749 "PREFIX ".to_string(),
6750 )]))
6751 });
6752
6753 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6754 .unwrap()
6755 .await
6756 .unwrap();
6757
6758 cx.assert_editor_state(indoc! {"
6759 PREFIX line 1
6760 line 2
6761 linXe 3
6762 line 4
6763 liˇne 5
6764 "});
6765
6766 // Undo formatting
6767 cx.update_editor(|editor, window, cx| {
6768 editor.undo(&Default::default(), window, cx);
6769 });
6770
6771 // Verify cursor moved back to position after edit
6772 cx.assert_editor_state(indoc! {"
6773 line 1
6774 line 2
6775 linXˇe 3
6776 line 4
6777 line 5
6778 "});
6779}
6780
6781#[gpui::test]
6782async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6783 init_test(cx, |_| {});
6784
6785 let mut cx = EditorTestContext::new(cx).await;
6786
6787 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6788 cx.update_editor(|editor, window, cx| {
6789 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6790 });
6791
6792 cx.set_state(indoc! {"
6793 line 1
6794 line 2
6795 linˇe 3
6796 line 4
6797 line 5
6798 line 6
6799 line 7
6800 line 8
6801 line 9
6802 line 10
6803 "});
6804
6805 let snapshot = cx.buffer_snapshot();
6806 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6807
6808 cx.update(|_, cx| {
6809 provider.update(cx, |provider, _| {
6810 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6811 id: None,
6812 edits: vec![(edit_position..edit_position, "X".into())],
6813 edit_preview: None,
6814 }))
6815 })
6816 });
6817
6818 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6819 cx.update_editor(|editor, window, cx| {
6820 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6821 });
6822
6823 cx.assert_editor_state(indoc! {"
6824 line 1
6825 line 2
6826 lineXˇ 3
6827 line 4
6828 line 5
6829 line 6
6830 line 7
6831 line 8
6832 line 9
6833 line 10
6834 "});
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.change_selections(None, window, cx, |s| {
6838 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6839 });
6840 });
6841
6842 cx.assert_editor_state(indoc! {"
6843 line 1
6844 line 2
6845 lineX 3
6846 line 4
6847 line 5
6848 line 6
6849 line 7
6850 line 8
6851 line 9
6852 liˇne 10
6853 "});
6854
6855 cx.update_editor(|editor, window, cx| {
6856 editor.undo(&Default::default(), window, cx);
6857 });
6858
6859 cx.assert_editor_state(indoc! {"
6860 line 1
6861 line 2
6862 lineˇ 3
6863 line 4
6864 line 5
6865 line 6
6866 line 7
6867 line 8
6868 line 9
6869 line 10
6870 "});
6871}
6872
6873#[gpui::test]
6874async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let mut cx = EditorTestContext::new(cx).await;
6878 cx.set_state(
6879 r#"let foo = 2;
6880lˇet foo = 2;
6881let fooˇ = 2;
6882let foo = 2;
6883let foo = ˇ2;"#,
6884 );
6885
6886 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6887 .unwrap();
6888 cx.assert_editor_state(
6889 r#"let foo = 2;
6890«letˇ» foo = 2;
6891let «fooˇ» = 2;
6892let foo = 2;
6893let foo = «2ˇ»;"#,
6894 );
6895
6896 // noop for multiple selections with different contents
6897 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6898 .unwrap();
6899 cx.assert_editor_state(
6900 r#"let foo = 2;
6901«letˇ» foo = 2;
6902let «fooˇ» = 2;
6903let foo = 2;
6904let foo = «2ˇ»;"#,
6905 );
6906
6907 // Test last selection direction should be preserved
6908 cx.set_state(
6909 r#"let foo = 2;
6910let foo = 2;
6911let «fooˇ» = 2;
6912let «ˇfoo» = 2;
6913let foo = 2;"#,
6914 );
6915
6916 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6917 .unwrap();
6918 cx.assert_editor_state(
6919 r#"let foo = 2;
6920let foo = 2;
6921let «fooˇ» = 2;
6922let «ˇfoo» = 2;
6923let «ˇfoo» = 2;"#,
6924 );
6925}
6926
6927#[gpui::test]
6928async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6929 init_test(cx, |_| {});
6930
6931 let mut cx =
6932 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6933
6934 cx.assert_editor_state(indoc! {"
6935 ˇbbb
6936 ccc
6937
6938 bbb
6939 ccc
6940 "});
6941 cx.dispatch_action(SelectPrevious::default());
6942 cx.assert_editor_state(indoc! {"
6943 «bbbˇ»
6944 ccc
6945
6946 bbb
6947 ccc
6948 "});
6949 cx.dispatch_action(SelectPrevious::default());
6950 cx.assert_editor_state(indoc! {"
6951 «bbbˇ»
6952 ccc
6953
6954 «bbbˇ»
6955 ccc
6956 "});
6957}
6958
6959#[gpui::test]
6960async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6961 init_test(cx, |_| {});
6962
6963 let mut cx = EditorTestContext::new(cx).await;
6964 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6965
6966 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6967 .unwrap();
6968 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6969
6970 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6971 .unwrap();
6972 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6973
6974 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6975 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6976
6977 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6978 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6979
6980 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6981 .unwrap();
6982 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6983
6984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6985 .unwrap();
6986 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6987}
6988
6989#[gpui::test]
6990async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6991 init_test(cx, |_| {});
6992
6993 let mut cx = EditorTestContext::new(cx).await;
6994 cx.set_state("aˇ");
6995
6996 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6997 .unwrap();
6998 cx.assert_editor_state("«aˇ»");
6999 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7000 .unwrap();
7001 cx.assert_editor_state("«aˇ»");
7002}
7003
7004#[gpui::test]
7005async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009 cx.set_state(
7010 r#"let foo = 2;
7011lˇet foo = 2;
7012let fooˇ = 2;
7013let foo = 2;
7014let foo = ˇ2;"#,
7015 );
7016
7017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7018 .unwrap();
7019 cx.assert_editor_state(
7020 r#"let foo = 2;
7021«letˇ» foo = 2;
7022let «fooˇ» = 2;
7023let foo = 2;
7024let foo = «2ˇ»;"#,
7025 );
7026
7027 // noop for multiple selections with different contents
7028 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state(
7031 r#"let foo = 2;
7032«letˇ» foo = 2;
7033let «fooˇ» = 2;
7034let foo = 2;
7035let foo = «2ˇ»;"#,
7036 );
7037}
7038
7039#[gpui::test]
7040async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7041 init_test(cx, |_| {});
7042
7043 let mut cx = EditorTestContext::new(cx).await;
7044 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7045
7046 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7047 .unwrap();
7048 // selection direction is preserved
7049 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7050
7051 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7052 .unwrap();
7053 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7054
7055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7056 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7057
7058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7059 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7060
7061 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7062 .unwrap();
7063 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7064
7065 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7066 .unwrap();
7067 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7068}
7069
7070#[gpui::test]
7071async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let language = Arc::new(Language::new(
7075 LanguageConfig::default(),
7076 Some(tree_sitter_rust::LANGUAGE.into()),
7077 ));
7078
7079 let text = r#"
7080 use mod1::mod2::{mod3, mod4};
7081
7082 fn fn_1(param1: bool, param2: &str) {
7083 let var1 = "text";
7084 }
7085 "#
7086 .unindent();
7087
7088 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7089 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7090 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7091
7092 editor
7093 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7094 .await;
7095
7096 editor.update_in(cx, |editor, window, cx| {
7097 editor.change_selections(None, window, cx, |s| {
7098 s.select_display_ranges([
7099 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7100 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7101 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7102 ]);
7103 });
7104 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7105 });
7106 editor.update(cx, |editor, cx| {
7107 assert_text_with_selections(
7108 editor,
7109 indoc! {r#"
7110 use mod1::mod2::{mod3, «mod4ˇ»};
7111
7112 fn fn_1«ˇ(param1: bool, param2: &str)» {
7113 let var1 = "«ˇtext»";
7114 }
7115 "#},
7116 cx,
7117 );
7118 });
7119
7120 editor.update_in(cx, |editor, window, cx| {
7121 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7122 });
7123 editor.update(cx, |editor, cx| {
7124 assert_text_with_selections(
7125 editor,
7126 indoc! {r#"
7127 use mod1::mod2::«{mod3, mod4}ˇ»;
7128
7129 «ˇfn fn_1(param1: bool, param2: &str) {
7130 let var1 = "text";
7131 }»
7132 "#},
7133 cx,
7134 );
7135 });
7136
7137 editor.update_in(cx, |editor, window, cx| {
7138 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7139 });
7140 assert_eq!(
7141 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7142 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7143 );
7144
7145 // Trying to expand the selected syntax node one more time has no effect.
7146 editor.update_in(cx, |editor, window, cx| {
7147 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7148 });
7149 assert_eq!(
7150 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7151 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7152 );
7153
7154 editor.update_in(cx, |editor, window, cx| {
7155 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7156 });
7157 editor.update(cx, |editor, cx| {
7158 assert_text_with_selections(
7159 editor,
7160 indoc! {r#"
7161 use mod1::mod2::«{mod3, mod4}ˇ»;
7162
7163 «ˇfn fn_1(param1: bool, param2: &str) {
7164 let var1 = "text";
7165 }»
7166 "#},
7167 cx,
7168 );
7169 });
7170
7171 editor.update_in(cx, |editor, window, cx| {
7172 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7173 });
7174 editor.update(cx, |editor, cx| {
7175 assert_text_with_selections(
7176 editor,
7177 indoc! {r#"
7178 use mod1::mod2::{mod3, «mod4ˇ»};
7179
7180 fn fn_1«ˇ(param1: bool, param2: &str)» {
7181 let var1 = "«ˇtext»";
7182 }
7183 "#},
7184 cx,
7185 );
7186 });
7187
7188 editor.update_in(cx, |editor, window, cx| {
7189 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7190 });
7191 editor.update(cx, |editor, cx| {
7192 assert_text_with_selections(
7193 editor,
7194 indoc! {r#"
7195 use mod1::mod2::{mod3, mo«ˇ»d4};
7196
7197 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7198 let var1 = "te«ˇ»xt";
7199 }
7200 "#},
7201 cx,
7202 );
7203 });
7204
7205 // Trying to shrink the selected syntax node one more time has no effect.
7206 editor.update_in(cx, |editor, window, cx| {
7207 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7208 });
7209 editor.update_in(cx, |editor, _, cx| {
7210 assert_text_with_selections(
7211 editor,
7212 indoc! {r#"
7213 use mod1::mod2::{mod3, mo«ˇ»d4};
7214
7215 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7216 let var1 = "te«ˇ»xt";
7217 }
7218 "#},
7219 cx,
7220 );
7221 });
7222
7223 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7224 // a fold.
7225 editor.update_in(cx, |editor, window, cx| {
7226 editor.fold_creases(
7227 vec![
7228 Crease::simple(
7229 Point::new(0, 21)..Point::new(0, 24),
7230 FoldPlaceholder::test(),
7231 ),
7232 Crease::simple(
7233 Point::new(3, 20)..Point::new(3, 22),
7234 FoldPlaceholder::test(),
7235 ),
7236 ],
7237 true,
7238 window,
7239 cx,
7240 );
7241 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7242 });
7243 editor.update(cx, |editor, cx| {
7244 assert_text_with_selections(
7245 editor,
7246 indoc! {r#"
7247 use mod1::mod2::«{mod3, mod4}ˇ»;
7248
7249 fn fn_1«ˇ(param1: bool, param2: &str)» {
7250 let var1 = "«ˇtext»";
7251 }
7252 "#},
7253 cx,
7254 );
7255 });
7256}
7257
7258#[gpui::test]
7259async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7260 init_test(cx, |_| {});
7261
7262 let language = Arc::new(Language::new(
7263 LanguageConfig::default(),
7264 Some(tree_sitter_rust::LANGUAGE.into()),
7265 ));
7266
7267 let text = "let a = 2;";
7268
7269 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7270 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7271 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7272
7273 editor
7274 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7275 .await;
7276
7277 // Test case 1: Cursor at end of word
7278 editor.update_in(cx, |editor, window, cx| {
7279 editor.change_selections(None, window, cx, |s| {
7280 s.select_display_ranges([
7281 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7282 ]);
7283 });
7284 });
7285 editor.update(cx, |editor, cx| {
7286 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7287 });
7288 editor.update_in(cx, |editor, window, cx| {
7289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7290 });
7291 editor.update(cx, |editor, cx| {
7292 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7293 });
7294 editor.update_in(cx, |editor, window, cx| {
7295 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7296 });
7297 editor.update(cx, |editor, cx| {
7298 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7299 });
7300
7301 // Test case 2: Cursor at end of statement
7302 editor.update_in(cx, |editor, window, cx| {
7303 editor.change_selections(None, window, cx, |s| {
7304 s.select_display_ranges([
7305 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7306 ]);
7307 });
7308 });
7309 editor.update(cx, |editor, cx| {
7310 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7311 });
7312 editor.update_in(cx, |editor, window, cx| {
7313 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7314 });
7315 editor.update(cx, |editor, cx| {
7316 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7317 });
7318}
7319
7320#[gpui::test]
7321async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7322 init_test(cx, |_| {});
7323
7324 let language = Arc::new(Language::new(
7325 LanguageConfig::default(),
7326 Some(tree_sitter_rust::LANGUAGE.into()),
7327 ));
7328
7329 let text = r#"
7330 use mod1::mod2::{mod3, mod4};
7331
7332 fn fn_1(param1: bool, param2: &str) {
7333 let var1 = "hello world";
7334 }
7335 "#
7336 .unindent();
7337
7338 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7339 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7340 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7341
7342 editor
7343 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7344 .await;
7345
7346 // Test 1: Cursor on a letter of a string word
7347 editor.update_in(cx, |editor, window, cx| {
7348 editor.change_selections(None, window, cx, |s| {
7349 s.select_display_ranges([
7350 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7351 ]);
7352 });
7353 });
7354 editor.update_in(cx, |editor, window, cx| {
7355 assert_text_with_selections(
7356 editor,
7357 indoc! {r#"
7358 use mod1::mod2::{mod3, mod4};
7359
7360 fn fn_1(param1: bool, param2: &str) {
7361 let var1 = "hˇello world";
7362 }
7363 "#},
7364 cx,
7365 );
7366 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7367 assert_text_with_selections(
7368 editor,
7369 indoc! {r#"
7370 use mod1::mod2::{mod3, mod4};
7371
7372 fn fn_1(param1: bool, param2: &str) {
7373 let var1 = "«ˇhello» world";
7374 }
7375 "#},
7376 cx,
7377 );
7378 });
7379
7380 // Test 2: Partial selection within a word
7381 editor.update_in(cx, |editor, window, cx| {
7382 editor.change_selections(None, window, cx, |s| {
7383 s.select_display_ranges([
7384 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7385 ]);
7386 });
7387 });
7388 editor.update_in(cx, |editor, window, cx| {
7389 assert_text_with_selections(
7390 editor,
7391 indoc! {r#"
7392 use mod1::mod2::{mod3, mod4};
7393
7394 fn fn_1(param1: bool, param2: &str) {
7395 let var1 = "h«elˇ»lo world";
7396 }
7397 "#},
7398 cx,
7399 );
7400 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7401 assert_text_with_selections(
7402 editor,
7403 indoc! {r#"
7404 use mod1::mod2::{mod3, mod4};
7405
7406 fn fn_1(param1: bool, param2: &str) {
7407 let var1 = "«ˇhello» world";
7408 }
7409 "#},
7410 cx,
7411 );
7412 });
7413
7414 // Test 3: Complete word already selected
7415 editor.update_in(cx, |editor, window, cx| {
7416 editor.change_selections(None, window, cx, |s| {
7417 s.select_display_ranges([
7418 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7419 ]);
7420 });
7421 });
7422 editor.update_in(cx, |editor, window, cx| {
7423 assert_text_with_selections(
7424 editor,
7425 indoc! {r#"
7426 use mod1::mod2::{mod3, mod4};
7427
7428 fn fn_1(param1: bool, param2: &str) {
7429 let var1 = "«helloˇ» world";
7430 }
7431 "#},
7432 cx,
7433 );
7434 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7435 assert_text_with_selections(
7436 editor,
7437 indoc! {r#"
7438 use mod1::mod2::{mod3, mod4};
7439
7440 fn fn_1(param1: bool, param2: &str) {
7441 let var1 = "«hello worldˇ»";
7442 }
7443 "#},
7444 cx,
7445 );
7446 });
7447
7448 // Test 4: Selection spanning across words
7449 editor.update_in(cx, |editor, window, cx| {
7450 editor.change_selections(None, window, cx, |s| {
7451 s.select_display_ranges([
7452 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7453 ]);
7454 });
7455 });
7456 editor.update_in(cx, |editor, window, cx| {
7457 assert_text_with_selections(
7458 editor,
7459 indoc! {r#"
7460 use mod1::mod2::{mod3, mod4};
7461
7462 fn fn_1(param1: bool, param2: &str) {
7463 let var1 = "hel«lo woˇ»rld";
7464 }
7465 "#},
7466 cx,
7467 );
7468 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7469 assert_text_with_selections(
7470 editor,
7471 indoc! {r#"
7472 use mod1::mod2::{mod3, mod4};
7473
7474 fn fn_1(param1: bool, param2: &str) {
7475 let var1 = "«ˇhello world»";
7476 }
7477 "#},
7478 cx,
7479 );
7480 });
7481
7482 // Test 5: Expansion beyond string
7483 editor.update_in(cx, |editor, window, cx| {
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7486 assert_text_with_selections(
7487 editor,
7488 indoc! {r#"
7489 use mod1::mod2::{mod3, mod4};
7490
7491 fn fn_1(param1: bool, param2: &str) {
7492 «ˇlet var1 = "hello world";»
7493 }
7494 "#},
7495 cx,
7496 );
7497 });
7498}
7499
7500#[gpui::test]
7501async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7502 init_test(cx, |_| {});
7503
7504 let base_text = r#"
7505 impl A {
7506 // this is an uncommitted comment
7507
7508 fn b() {
7509 c();
7510 }
7511
7512 // this is another uncommitted comment
7513
7514 fn d() {
7515 // e
7516 // f
7517 }
7518 }
7519
7520 fn g() {
7521 // h
7522 }
7523 "#
7524 .unindent();
7525
7526 let text = r#"
7527 ˇimpl A {
7528
7529 fn b() {
7530 c();
7531 }
7532
7533 fn d() {
7534 // e
7535 // f
7536 }
7537 }
7538
7539 fn g() {
7540 // h
7541 }
7542 "#
7543 .unindent();
7544
7545 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7546 cx.set_state(&text);
7547 cx.set_head_text(&base_text);
7548 cx.update_editor(|editor, window, cx| {
7549 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_state_with_diff(
7553 "
7554 ˇimpl A {
7555 - // this is an uncommitted comment
7556
7557 fn b() {
7558 c();
7559 }
7560
7561 - // this is another uncommitted comment
7562 -
7563 fn d() {
7564 // e
7565 // f
7566 }
7567 }
7568
7569 fn g() {
7570 // h
7571 }
7572 "
7573 .unindent(),
7574 );
7575
7576 let expected_display_text = "
7577 impl A {
7578 // this is an uncommitted comment
7579
7580 fn b() {
7581 ⋯
7582 }
7583
7584 // this is another uncommitted comment
7585
7586 fn d() {
7587 ⋯
7588 }
7589 }
7590
7591 fn g() {
7592 ⋯
7593 }
7594 "
7595 .unindent();
7596
7597 cx.update_editor(|editor, window, cx| {
7598 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7599 assert_eq!(editor.display_text(cx), expected_display_text);
7600 });
7601}
7602
7603#[gpui::test]
7604async fn test_autoindent(cx: &mut TestAppContext) {
7605 init_test(cx, |_| {});
7606
7607 let language = Arc::new(
7608 Language::new(
7609 LanguageConfig {
7610 brackets: BracketPairConfig {
7611 pairs: vec![
7612 BracketPair {
7613 start: "{".to_string(),
7614 end: "}".to_string(),
7615 close: false,
7616 surround: false,
7617 newline: true,
7618 },
7619 BracketPair {
7620 start: "(".to_string(),
7621 end: ")".to_string(),
7622 close: false,
7623 surround: false,
7624 newline: true,
7625 },
7626 ],
7627 ..Default::default()
7628 },
7629 ..Default::default()
7630 },
7631 Some(tree_sitter_rust::LANGUAGE.into()),
7632 )
7633 .with_indents_query(
7634 r#"
7635 (_ "(" ")" @end) @indent
7636 (_ "{" "}" @end) @indent
7637 "#,
7638 )
7639 .unwrap(),
7640 );
7641
7642 let text = "fn a() {}";
7643
7644 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7645 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7646 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7647 editor
7648 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7649 .await;
7650
7651 editor.update_in(cx, |editor, window, cx| {
7652 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7653 editor.newline(&Newline, window, cx);
7654 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7655 assert_eq!(
7656 editor.selections.ranges(cx),
7657 &[
7658 Point::new(1, 4)..Point::new(1, 4),
7659 Point::new(3, 4)..Point::new(3, 4),
7660 Point::new(5, 0)..Point::new(5, 0)
7661 ]
7662 );
7663 });
7664}
7665
7666#[gpui::test]
7667async fn test_autoindent_selections(cx: &mut TestAppContext) {
7668 init_test(cx, |_| {});
7669
7670 {
7671 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7672 cx.set_state(indoc! {"
7673 impl A {
7674
7675 fn b() {}
7676
7677 «fn c() {
7678
7679 }ˇ»
7680 }
7681 "});
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.autoindent(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc! {"
7688 impl A {
7689
7690 fn b() {}
7691
7692 «fn c() {
7693
7694 }ˇ»
7695 }
7696 "});
7697 }
7698
7699 {
7700 let mut cx = EditorTestContext::new_multibuffer(
7701 cx,
7702 [indoc! { "
7703 impl A {
7704 «
7705 // a
7706 fn b(){}
7707 »
7708 «
7709 }
7710 fn c(){}
7711 »
7712 "}],
7713 );
7714
7715 let buffer = cx.update_editor(|editor, _, cx| {
7716 let buffer = editor.buffer().update(cx, |buffer, _| {
7717 buffer.all_buffers().iter().next().unwrap().clone()
7718 });
7719 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7720 buffer
7721 });
7722
7723 cx.run_until_parked();
7724 cx.update_editor(|editor, window, cx| {
7725 editor.select_all(&Default::default(), window, cx);
7726 editor.autoindent(&Default::default(), window, cx)
7727 });
7728 cx.run_until_parked();
7729
7730 cx.update(|_, cx| {
7731 assert_eq!(
7732 buffer.read(cx).text(),
7733 indoc! { "
7734 impl A {
7735
7736 // a
7737 fn b(){}
7738
7739
7740 }
7741 fn c(){}
7742
7743 " }
7744 )
7745 });
7746 }
7747}
7748
7749#[gpui::test]
7750async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7751 init_test(cx, |_| {});
7752
7753 let mut cx = EditorTestContext::new(cx).await;
7754
7755 let language = Arc::new(Language::new(
7756 LanguageConfig {
7757 brackets: BracketPairConfig {
7758 pairs: vec![
7759 BracketPair {
7760 start: "{".to_string(),
7761 end: "}".to_string(),
7762 close: true,
7763 surround: true,
7764 newline: true,
7765 },
7766 BracketPair {
7767 start: "(".to_string(),
7768 end: ")".to_string(),
7769 close: true,
7770 surround: true,
7771 newline: true,
7772 },
7773 BracketPair {
7774 start: "/*".to_string(),
7775 end: " */".to_string(),
7776 close: true,
7777 surround: true,
7778 newline: true,
7779 },
7780 BracketPair {
7781 start: "[".to_string(),
7782 end: "]".to_string(),
7783 close: false,
7784 surround: false,
7785 newline: true,
7786 },
7787 BracketPair {
7788 start: "\"".to_string(),
7789 end: "\"".to_string(),
7790 close: true,
7791 surround: true,
7792 newline: false,
7793 },
7794 BracketPair {
7795 start: "<".to_string(),
7796 end: ">".to_string(),
7797 close: false,
7798 surround: true,
7799 newline: true,
7800 },
7801 ],
7802 ..Default::default()
7803 },
7804 autoclose_before: "})]".to_string(),
7805 ..Default::default()
7806 },
7807 Some(tree_sitter_rust::LANGUAGE.into()),
7808 ));
7809
7810 cx.language_registry().add(language.clone());
7811 cx.update_buffer(|buffer, cx| {
7812 buffer.set_language(Some(language), cx);
7813 });
7814
7815 cx.set_state(
7816 &r#"
7817 🏀ˇ
7818 εˇ
7819 ❤️ˇ
7820 "#
7821 .unindent(),
7822 );
7823
7824 // autoclose multiple nested brackets at multiple cursors
7825 cx.update_editor(|editor, window, cx| {
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 editor.handle_input("{", window, cx);
7829 });
7830 cx.assert_editor_state(
7831 &"
7832 🏀{{{ˇ}}}
7833 ε{{{ˇ}}}
7834 ❤️{{{ˇ}}}
7835 "
7836 .unindent(),
7837 );
7838
7839 // insert a different closing bracket
7840 cx.update_editor(|editor, window, cx| {
7841 editor.handle_input(")", window, cx);
7842 });
7843 cx.assert_editor_state(
7844 &"
7845 🏀{{{)ˇ}}}
7846 ε{{{)ˇ}}}
7847 ❤️{{{)ˇ}}}
7848 "
7849 .unindent(),
7850 );
7851
7852 // skip over the auto-closed brackets when typing a closing bracket
7853 cx.update_editor(|editor, window, cx| {
7854 editor.move_right(&MoveRight, window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 editor.handle_input("}", window, cx);
7858 });
7859 cx.assert_editor_state(
7860 &"
7861 🏀{{{)}}}}ˇ
7862 ε{{{)}}}}ˇ
7863 ❤️{{{)}}}}ˇ
7864 "
7865 .unindent(),
7866 );
7867
7868 // autoclose multi-character pairs
7869 cx.set_state(
7870 &"
7871 ˇ
7872 ˇ
7873 "
7874 .unindent(),
7875 );
7876 cx.update_editor(|editor, window, cx| {
7877 editor.handle_input("/", window, cx);
7878 editor.handle_input("*", window, cx);
7879 });
7880 cx.assert_editor_state(
7881 &"
7882 /*ˇ */
7883 /*ˇ */
7884 "
7885 .unindent(),
7886 );
7887
7888 // one cursor autocloses a multi-character pair, one cursor
7889 // does not autoclose.
7890 cx.set_state(
7891 &"
7892 /ˇ
7893 ˇ
7894 "
7895 .unindent(),
7896 );
7897 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7898 cx.assert_editor_state(
7899 &"
7900 /*ˇ */
7901 *ˇ
7902 "
7903 .unindent(),
7904 );
7905
7906 // Don't autoclose if the next character isn't whitespace and isn't
7907 // listed in the language's "autoclose_before" section.
7908 cx.set_state("ˇa b");
7909 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7910 cx.assert_editor_state("{ˇa b");
7911
7912 // Don't autoclose if `close` is false for the bracket pair
7913 cx.set_state("ˇ");
7914 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7915 cx.assert_editor_state("[ˇ");
7916
7917 // Surround with brackets if text is selected
7918 cx.set_state("«aˇ» b");
7919 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7920 cx.assert_editor_state("{«aˇ»} b");
7921
7922 // Autoclose when not immediately after a word character
7923 cx.set_state("a ˇ");
7924 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7925 cx.assert_editor_state("a \"ˇ\"");
7926
7927 // Autoclose pair where the start and end characters are the same
7928 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7929 cx.assert_editor_state("a \"\"ˇ");
7930
7931 // Don't autoclose when immediately after a word character
7932 cx.set_state("aˇ");
7933 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7934 cx.assert_editor_state("a\"ˇ");
7935
7936 // Do autoclose when after a non-word character
7937 cx.set_state("{ˇ");
7938 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7939 cx.assert_editor_state("{\"ˇ\"");
7940
7941 // Non identical pairs autoclose regardless of preceding character
7942 cx.set_state("aˇ");
7943 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7944 cx.assert_editor_state("a{ˇ}");
7945
7946 // Don't autoclose pair if autoclose is disabled
7947 cx.set_state("ˇ");
7948 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7949 cx.assert_editor_state("<ˇ");
7950
7951 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7952 cx.set_state("«aˇ» b");
7953 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7954 cx.assert_editor_state("<«aˇ»> b");
7955}
7956
7957#[gpui::test]
7958async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7959 init_test(cx, |settings| {
7960 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7961 });
7962
7963 let mut cx = EditorTestContext::new(cx).await;
7964
7965 let language = Arc::new(Language::new(
7966 LanguageConfig {
7967 brackets: BracketPairConfig {
7968 pairs: vec![
7969 BracketPair {
7970 start: "{".to_string(),
7971 end: "}".to_string(),
7972 close: true,
7973 surround: true,
7974 newline: true,
7975 },
7976 BracketPair {
7977 start: "(".to_string(),
7978 end: ")".to_string(),
7979 close: true,
7980 surround: true,
7981 newline: true,
7982 },
7983 BracketPair {
7984 start: "[".to_string(),
7985 end: "]".to_string(),
7986 close: false,
7987 surround: false,
7988 newline: true,
7989 },
7990 ],
7991 ..Default::default()
7992 },
7993 autoclose_before: "})]".to_string(),
7994 ..Default::default()
7995 },
7996 Some(tree_sitter_rust::LANGUAGE.into()),
7997 ));
7998
7999 cx.language_registry().add(language.clone());
8000 cx.update_buffer(|buffer, cx| {
8001 buffer.set_language(Some(language), cx);
8002 });
8003
8004 cx.set_state(
8005 &"
8006 ˇ
8007 ˇ
8008 ˇ
8009 "
8010 .unindent(),
8011 );
8012
8013 // ensure only matching closing brackets are skipped over
8014 cx.update_editor(|editor, window, cx| {
8015 editor.handle_input("}", window, cx);
8016 editor.move_left(&MoveLeft, window, cx);
8017 editor.handle_input(")", window, cx);
8018 editor.move_left(&MoveLeft, window, cx);
8019 });
8020 cx.assert_editor_state(
8021 &"
8022 ˇ)}
8023 ˇ)}
8024 ˇ)}
8025 "
8026 .unindent(),
8027 );
8028
8029 // skip-over closing brackets at multiple cursors
8030 cx.update_editor(|editor, window, cx| {
8031 editor.handle_input(")", window, cx);
8032 editor.handle_input("}", window, cx);
8033 });
8034 cx.assert_editor_state(
8035 &"
8036 )}ˇ
8037 )}ˇ
8038 )}ˇ
8039 "
8040 .unindent(),
8041 );
8042
8043 // ignore non-close brackets
8044 cx.update_editor(|editor, window, cx| {
8045 editor.handle_input("]", window, cx);
8046 editor.move_left(&MoveLeft, window, cx);
8047 editor.handle_input("]", window, cx);
8048 });
8049 cx.assert_editor_state(
8050 &"
8051 )}]ˇ]
8052 )}]ˇ]
8053 )}]ˇ]
8054 "
8055 .unindent(),
8056 );
8057}
8058
8059#[gpui::test]
8060async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8061 init_test(cx, |_| {});
8062
8063 let mut cx = EditorTestContext::new(cx).await;
8064
8065 let html_language = Arc::new(
8066 Language::new(
8067 LanguageConfig {
8068 name: "HTML".into(),
8069 brackets: BracketPairConfig {
8070 pairs: vec![
8071 BracketPair {
8072 start: "<".into(),
8073 end: ">".into(),
8074 close: true,
8075 ..Default::default()
8076 },
8077 BracketPair {
8078 start: "{".into(),
8079 end: "}".into(),
8080 close: true,
8081 ..Default::default()
8082 },
8083 BracketPair {
8084 start: "(".into(),
8085 end: ")".into(),
8086 close: true,
8087 ..Default::default()
8088 },
8089 ],
8090 ..Default::default()
8091 },
8092 autoclose_before: "})]>".into(),
8093 ..Default::default()
8094 },
8095 Some(tree_sitter_html::LANGUAGE.into()),
8096 )
8097 .with_injection_query(
8098 r#"
8099 (script_element
8100 (raw_text) @injection.content
8101 (#set! injection.language "javascript"))
8102 "#,
8103 )
8104 .unwrap(),
8105 );
8106
8107 let javascript_language = Arc::new(Language::new(
8108 LanguageConfig {
8109 name: "JavaScript".into(),
8110 brackets: BracketPairConfig {
8111 pairs: vec![
8112 BracketPair {
8113 start: "/*".into(),
8114 end: " */".into(),
8115 close: true,
8116 ..Default::default()
8117 },
8118 BracketPair {
8119 start: "{".into(),
8120 end: "}".into(),
8121 close: true,
8122 ..Default::default()
8123 },
8124 BracketPair {
8125 start: "(".into(),
8126 end: ")".into(),
8127 close: true,
8128 ..Default::default()
8129 },
8130 ],
8131 ..Default::default()
8132 },
8133 autoclose_before: "})]>".into(),
8134 ..Default::default()
8135 },
8136 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8137 ));
8138
8139 cx.language_registry().add(html_language.clone());
8140 cx.language_registry().add(javascript_language.clone());
8141
8142 cx.update_buffer(|buffer, cx| {
8143 buffer.set_language(Some(html_language), cx);
8144 });
8145
8146 cx.set_state(
8147 &r#"
8148 <body>ˇ
8149 <script>
8150 var x = 1;ˇ
8151 </script>
8152 </body>ˇ
8153 "#
8154 .unindent(),
8155 );
8156
8157 // Precondition: different languages are active at different locations.
8158 cx.update_editor(|editor, window, cx| {
8159 let snapshot = editor.snapshot(window, cx);
8160 let cursors = editor.selections.ranges::<usize>(cx);
8161 let languages = cursors
8162 .iter()
8163 .map(|c| snapshot.language_at(c.start).unwrap().name())
8164 .collect::<Vec<_>>();
8165 assert_eq!(
8166 languages,
8167 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8168 );
8169 });
8170
8171 // Angle brackets autoclose in HTML, but not JavaScript.
8172 cx.update_editor(|editor, window, cx| {
8173 editor.handle_input("<", window, cx);
8174 editor.handle_input("a", window, cx);
8175 });
8176 cx.assert_editor_state(
8177 &r#"
8178 <body><aˇ>
8179 <script>
8180 var x = 1;<aˇ
8181 </script>
8182 </body><aˇ>
8183 "#
8184 .unindent(),
8185 );
8186
8187 // Curly braces and parens autoclose in both HTML and JavaScript.
8188 cx.update_editor(|editor, window, cx| {
8189 editor.handle_input(" b=", window, cx);
8190 editor.handle_input("{", window, cx);
8191 editor.handle_input("c", window, cx);
8192 editor.handle_input("(", window, cx);
8193 });
8194 cx.assert_editor_state(
8195 &r#"
8196 <body><a b={c(ˇ)}>
8197 <script>
8198 var x = 1;<a b={c(ˇ)}
8199 </script>
8200 </body><a b={c(ˇ)}>
8201 "#
8202 .unindent(),
8203 );
8204
8205 // Brackets that were already autoclosed are skipped.
8206 cx.update_editor(|editor, window, cx| {
8207 editor.handle_input(")", window, cx);
8208 editor.handle_input("d", window, cx);
8209 editor.handle_input("}", window, cx);
8210 });
8211 cx.assert_editor_state(
8212 &r#"
8213 <body><a b={c()d}ˇ>
8214 <script>
8215 var x = 1;<a b={c()d}ˇ
8216 </script>
8217 </body><a b={c()d}ˇ>
8218 "#
8219 .unindent(),
8220 );
8221 cx.update_editor(|editor, window, cx| {
8222 editor.handle_input(">", window, cx);
8223 });
8224 cx.assert_editor_state(
8225 &r#"
8226 <body><a b={c()d}>ˇ
8227 <script>
8228 var x = 1;<a b={c()d}>ˇ
8229 </script>
8230 </body><a b={c()d}>ˇ
8231 "#
8232 .unindent(),
8233 );
8234
8235 // Reset
8236 cx.set_state(
8237 &r#"
8238 <body>ˇ
8239 <script>
8240 var x = 1;ˇ
8241 </script>
8242 </body>ˇ
8243 "#
8244 .unindent(),
8245 );
8246
8247 cx.update_editor(|editor, window, cx| {
8248 editor.handle_input("<", window, cx);
8249 });
8250 cx.assert_editor_state(
8251 &r#"
8252 <body><ˇ>
8253 <script>
8254 var x = 1;<ˇ
8255 </script>
8256 </body><ˇ>
8257 "#
8258 .unindent(),
8259 );
8260
8261 // When backspacing, the closing angle brackets are removed.
8262 cx.update_editor(|editor, window, cx| {
8263 editor.backspace(&Backspace, window, cx);
8264 });
8265 cx.assert_editor_state(
8266 &r#"
8267 <body>ˇ
8268 <script>
8269 var x = 1;ˇ
8270 </script>
8271 </body>ˇ
8272 "#
8273 .unindent(),
8274 );
8275
8276 // Block comments autoclose in JavaScript, but not HTML.
8277 cx.update_editor(|editor, window, cx| {
8278 editor.handle_input("/", window, cx);
8279 editor.handle_input("*", window, cx);
8280 });
8281 cx.assert_editor_state(
8282 &r#"
8283 <body>/*ˇ
8284 <script>
8285 var x = 1;/*ˇ */
8286 </script>
8287 </body>/*ˇ
8288 "#
8289 .unindent(),
8290 );
8291}
8292
8293#[gpui::test]
8294async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8295 init_test(cx, |_| {});
8296
8297 let mut cx = EditorTestContext::new(cx).await;
8298
8299 let rust_language = Arc::new(
8300 Language::new(
8301 LanguageConfig {
8302 name: "Rust".into(),
8303 brackets: serde_json::from_value(json!([
8304 { "start": "{", "end": "}", "close": true, "newline": true },
8305 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8306 ]))
8307 .unwrap(),
8308 autoclose_before: "})]>".into(),
8309 ..Default::default()
8310 },
8311 Some(tree_sitter_rust::LANGUAGE.into()),
8312 )
8313 .with_override_query("(string_literal) @string")
8314 .unwrap(),
8315 );
8316
8317 cx.language_registry().add(rust_language.clone());
8318 cx.update_buffer(|buffer, cx| {
8319 buffer.set_language(Some(rust_language), cx);
8320 });
8321
8322 cx.set_state(
8323 &r#"
8324 let x = ˇ
8325 "#
8326 .unindent(),
8327 );
8328
8329 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8330 cx.update_editor(|editor, window, cx| {
8331 editor.handle_input("\"", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &r#"
8335 let x = "ˇ"
8336 "#
8337 .unindent(),
8338 );
8339
8340 // Inserting another quotation mark. The cursor moves across the existing
8341 // automatically-inserted quotation mark.
8342 cx.update_editor(|editor, window, cx| {
8343 editor.handle_input("\"", window, cx);
8344 });
8345 cx.assert_editor_state(
8346 &r#"
8347 let x = ""ˇ
8348 "#
8349 .unindent(),
8350 );
8351
8352 // Reset
8353 cx.set_state(
8354 &r#"
8355 let x = ˇ
8356 "#
8357 .unindent(),
8358 );
8359
8360 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8361 cx.update_editor(|editor, window, cx| {
8362 editor.handle_input("\"", window, cx);
8363 editor.handle_input(" ", window, cx);
8364 editor.move_left(&Default::default(), window, cx);
8365 editor.handle_input("\\", window, cx);
8366 editor.handle_input("\"", window, cx);
8367 });
8368 cx.assert_editor_state(
8369 &r#"
8370 let x = "\"ˇ "
8371 "#
8372 .unindent(),
8373 );
8374
8375 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8376 // mark. Nothing is inserted.
8377 cx.update_editor(|editor, window, cx| {
8378 editor.move_right(&Default::default(), window, cx);
8379 editor.handle_input("\"", window, cx);
8380 });
8381 cx.assert_editor_state(
8382 &r#"
8383 let x = "\" "ˇ
8384 "#
8385 .unindent(),
8386 );
8387}
8388
8389#[gpui::test]
8390async fn test_surround_with_pair(cx: &mut TestAppContext) {
8391 init_test(cx, |_| {});
8392
8393 let language = Arc::new(Language::new(
8394 LanguageConfig {
8395 brackets: BracketPairConfig {
8396 pairs: vec![
8397 BracketPair {
8398 start: "{".to_string(),
8399 end: "}".to_string(),
8400 close: true,
8401 surround: true,
8402 newline: true,
8403 },
8404 BracketPair {
8405 start: "/* ".to_string(),
8406 end: "*/".to_string(),
8407 close: true,
8408 surround: true,
8409 ..Default::default()
8410 },
8411 ],
8412 ..Default::default()
8413 },
8414 ..Default::default()
8415 },
8416 Some(tree_sitter_rust::LANGUAGE.into()),
8417 ));
8418
8419 let text = r#"
8420 a
8421 b
8422 c
8423 "#
8424 .unindent();
8425
8426 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8429 editor
8430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8431 .await;
8432
8433 editor.update_in(cx, |editor, window, cx| {
8434 editor.change_selections(None, window, cx, |s| {
8435 s.select_display_ranges([
8436 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8437 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8438 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8439 ])
8440 });
8441
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 editor.handle_input("{", window, cx);
8445 assert_eq!(
8446 editor.text(cx),
8447 "
8448 {{{a}}}
8449 {{{b}}}
8450 {{{c}}}
8451 "
8452 .unindent()
8453 );
8454 assert_eq!(
8455 editor.selections.display_ranges(cx),
8456 [
8457 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8458 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8459 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8460 ]
8461 );
8462
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 editor.undo(&Undo, window, cx);
8466 assert_eq!(
8467 editor.text(cx),
8468 "
8469 a
8470 b
8471 c
8472 "
8473 .unindent()
8474 );
8475 assert_eq!(
8476 editor.selections.display_ranges(cx),
8477 [
8478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8479 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8480 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8481 ]
8482 );
8483
8484 // Ensure inserting the first character of a multi-byte bracket pair
8485 // doesn't surround the selections with the bracket.
8486 editor.handle_input("/", window, cx);
8487 assert_eq!(
8488 editor.text(cx),
8489 "
8490 /
8491 /
8492 /
8493 "
8494 .unindent()
8495 );
8496 assert_eq!(
8497 editor.selections.display_ranges(cx),
8498 [
8499 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8500 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8502 ]
8503 );
8504
8505 editor.undo(&Undo, window, cx);
8506 assert_eq!(
8507 editor.text(cx),
8508 "
8509 a
8510 b
8511 c
8512 "
8513 .unindent()
8514 );
8515 assert_eq!(
8516 editor.selections.display_ranges(cx),
8517 [
8518 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8519 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8520 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8521 ]
8522 );
8523
8524 // Ensure inserting the last character of a multi-byte bracket pair
8525 // doesn't surround the selections with the bracket.
8526 editor.handle_input("*", window, cx);
8527 assert_eq!(
8528 editor.text(cx),
8529 "
8530 *
8531 *
8532 *
8533 "
8534 .unindent()
8535 );
8536 assert_eq!(
8537 editor.selections.display_ranges(cx),
8538 [
8539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8540 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8541 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8542 ]
8543 );
8544 });
8545}
8546
8547#[gpui::test]
8548async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8549 init_test(cx, |_| {});
8550
8551 let language = Arc::new(Language::new(
8552 LanguageConfig {
8553 brackets: BracketPairConfig {
8554 pairs: vec![BracketPair {
8555 start: "{".to_string(),
8556 end: "}".to_string(),
8557 close: true,
8558 surround: true,
8559 newline: true,
8560 }],
8561 ..Default::default()
8562 },
8563 autoclose_before: "}".to_string(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_rust::LANGUAGE.into()),
8567 ));
8568
8569 let text = r#"
8570 a
8571 b
8572 c
8573 "#
8574 .unindent();
8575
8576 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8577 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8578 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8579 editor
8580 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8581 .await;
8582
8583 editor.update_in(cx, |editor, window, cx| {
8584 editor.change_selections(None, window, cx, |s| {
8585 s.select_ranges([
8586 Point::new(0, 1)..Point::new(0, 1),
8587 Point::new(1, 1)..Point::new(1, 1),
8588 Point::new(2, 1)..Point::new(2, 1),
8589 ])
8590 });
8591
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("{", window, cx);
8594 editor.handle_input("_", window, cx);
8595 assert_eq!(
8596 editor.text(cx),
8597 "
8598 a{{_}}
8599 b{{_}}
8600 c{{_}}
8601 "
8602 .unindent()
8603 );
8604 assert_eq!(
8605 editor.selections.ranges::<Point>(cx),
8606 [
8607 Point::new(0, 4)..Point::new(0, 4),
8608 Point::new(1, 4)..Point::new(1, 4),
8609 Point::new(2, 4)..Point::new(2, 4)
8610 ]
8611 );
8612
8613 editor.backspace(&Default::default(), window, cx);
8614 editor.backspace(&Default::default(), window, cx);
8615 assert_eq!(
8616 editor.text(cx),
8617 "
8618 a{}
8619 b{}
8620 c{}
8621 "
8622 .unindent()
8623 );
8624 assert_eq!(
8625 editor.selections.ranges::<Point>(cx),
8626 [
8627 Point::new(0, 2)..Point::new(0, 2),
8628 Point::new(1, 2)..Point::new(1, 2),
8629 Point::new(2, 2)..Point::new(2, 2)
8630 ]
8631 );
8632
8633 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8634 assert_eq!(
8635 editor.text(cx),
8636 "
8637 a
8638 b
8639 c
8640 "
8641 .unindent()
8642 );
8643 assert_eq!(
8644 editor.selections.ranges::<Point>(cx),
8645 [
8646 Point::new(0, 1)..Point::new(0, 1),
8647 Point::new(1, 1)..Point::new(1, 1),
8648 Point::new(2, 1)..Point::new(2, 1)
8649 ]
8650 );
8651 });
8652}
8653
8654#[gpui::test]
8655async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8656 init_test(cx, |settings| {
8657 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8658 });
8659
8660 let mut cx = EditorTestContext::new(cx).await;
8661
8662 let language = Arc::new(Language::new(
8663 LanguageConfig {
8664 brackets: BracketPairConfig {
8665 pairs: vec![
8666 BracketPair {
8667 start: "{".to_string(),
8668 end: "}".to_string(),
8669 close: true,
8670 surround: true,
8671 newline: true,
8672 },
8673 BracketPair {
8674 start: "(".to_string(),
8675 end: ")".to_string(),
8676 close: true,
8677 surround: true,
8678 newline: true,
8679 },
8680 BracketPair {
8681 start: "[".to_string(),
8682 end: "]".to_string(),
8683 close: false,
8684 surround: true,
8685 newline: true,
8686 },
8687 ],
8688 ..Default::default()
8689 },
8690 autoclose_before: "})]".to_string(),
8691 ..Default::default()
8692 },
8693 Some(tree_sitter_rust::LANGUAGE.into()),
8694 ));
8695
8696 cx.language_registry().add(language.clone());
8697 cx.update_buffer(|buffer, cx| {
8698 buffer.set_language(Some(language), cx);
8699 });
8700
8701 cx.set_state(
8702 &"
8703 {(ˇ)}
8704 [[ˇ]]
8705 {(ˇ)}
8706 "
8707 .unindent(),
8708 );
8709
8710 cx.update_editor(|editor, window, cx| {
8711 editor.backspace(&Default::default(), window, cx);
8712 editor.backspace(&Default::default(), window, cx);
8713 });
8714
8715 cx.assert_editor_state(
8716 &"
8717 ˇ
8718 ˇ]]
8719 ˇ
8720 "
8721 .unindent(),
8722 );
8723
8724 cx.update_editor(|editor, window, cx| {
8725 editor.handle_input("{", window, cx);
8726 editor.handle_input("{", window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_right(&MoveRight, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.move_left(&MoveLeft, window, cx);
8731 editor.backspace(&Default::default(), window, cx);
8732 });
8733
8734 cx.assert_editor_state(
8735 &"
8736 {ˇ}
8737 {ˇ}]]
8738 {ˇ}
8739 "
8740 .unindent(),
8741 );
8742
8743 cx.update_editor(|editor, window, cx| {
8744 editor.backspace(&Default::default(), window, cx);
8745 });
8746
8747 cx.assert_editor_state(
8748 &"
8749 ˇ
8750 ˇ]]
8751 ˇ
8752 "
8753 .unindent(),
8754 );
8755}
8756
8757#[gpui::test]
8758async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8759 init_test(cx, |_| {});
8760
8761 let language = Arc::new(Language::new(
8762 LanguageConfig::default(),
8763 Some(tree_sitter_rust::LANGUAGE.into()),
8764 ));
8765
8766 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8769 editor
8770 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8771 .await;
8772
8773 editor.update_in(cx, |editor, window, cx| {
8774 editor.set_auto_replace_emoji_shortcode(true);
8775
8776 editor.handle_input("Hello ", window, cx);
8777 editor.handle_input(":wave", window, cx);
8778 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8779
8780 editor.handle_input(":", window, cx);
8781 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8782
8783 editor.handle_input(" :smile", window, cx);
8784 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8785
8786 editor.handle_input(":", window, cx);
8787 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8788
8789 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8790 editor.handle_input(":wave", window, cx);
8791 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8792
8793 editor.handle_input(":", window, cx);
8794 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8795
8796 editor.handle_input(":1", window, cx);
8797 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8798
8799 editor.handle_input(":", window, cx);
8800 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8801
8802 // Ensure shortcode does not get replaced when it is part of a word
8803 editor.handle_input(" Test:wave", window, cx);
8804 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8805
8806 editor.handle_input(":", window, cx);
8807 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8808
8809 editor.set_auto_replace_emoji_shortcode(false);
8810
8811 // Ensure shortcode does not get replaced when auto replace is off
8812 editor.handle_input(" :wave", window, cx);
8813 assert_eq!(
8814 editor.text(cx),
8815 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8816 );
8817
8818 editor.handle_input(":", window, cx);
8819 assert_eq!(
8820 editor.text(cx),
8821 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8822 );
8823 });
8824}
8825
8826#[gpui::test]
8827async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8828 init_test(cx, |_| {});
8829
8830 let (text, insertion_ranges) = marked_text_ranges(
8831 indoc! {"
8832 ˇ
8833 "},
8834 false,
8835 );
8836
8837 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8838 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8839
8840 _ = editor.update_in(cx, |editor, window, cx| {
8841 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8842
8843 editor
8844 .insert_snippet(&insertion_ranges, snippet, window, cx)
8845 .unwrap();
8846
8847 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8848 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8849 assert_eq!(editor.text(cx), expected_text);
8850 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8851 }
8852
8853 assert(
8854 editor,
8855 cx,
8856 indoc! {"
8857 type «» =•
8858 "},
8859 );
8860
8861 assert!(editor.context_menu_visible(), "There should be a matches");
8862 });
8863}
8864
8865#[gpui::test]
8866async fn test_snippets(cx: &mut TestAppContext) {
8867 init_test(cx, |_| {});
8868
8869 let mut cx = EditorTestContext::new(cx).await;
8870
8871 cx.set_state(indoc! {"
8872 a.ˇ b
8873 a.ˇ b
8874 a.ˇ b
8875 "});
8876
8877 cx.update_editor(|editor, window, cx| {
8878 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8879 let insertion_ranges = editor
8880 .selections
8881 .all(cx)
8882 .iter()
8883 .map(|s| s.range().clone())
8884 .collect::<Vec<_>>();
8885 editor
8886 .insert_snippet(&insertion_ranges, snippet, window, cx)
8887 .unwrap();
8888 });
8889
8890 cx.assert_editor_state(indoc! {"
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 a.f(«oneˇ», two, «threeˇ») b
8894 "});
8895
8896 // Can't move earlier than the first tab stop
8897 cx.update_editor(|editor, window, cx| {
8898 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8899 });
8900 cx.assert_editor_state(indoc! {"
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 a.f(«oneˇ», two, «threeˇ») b
8904 "});
8905
8906 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8907 cx.assert_editor_state(indoc! {"
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 a.f(one, «twoˇ», three) b
8911 "});
8912
8913 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8914 cx.assert_editor_state(indoc! {"
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 a.f(«oneˇ», two, «threeˇ») b
8918 "});
8919
8920 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8921 cx.assert_editor_state(indoc! {"
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 a.f(one, «twoˇ», three) b
8925 "});
8926 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8927 cx.assert_editor_state(indoc! {"
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 a.f(one, two, three)ˇ b
8931 "});
8932
8933 // As soon as the last tab stop is reached, snippet state is gone
8934 cx.update_editor(|editor, window, cx| {
8935 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8936 });
8937 cx.assert_editor_state(indoc! {"
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 a.f(one, two, three)ˇ b
8941 "});
8942}
8943
8944#[gpui::test]
8945async fn test_snippet_indentation(cx: &mut TestAppContext) {
8946 init_test(cx, |_| {});
8947
8948 let mut cx = EditorTestContext::new(cx).await;
8949
8950 cx.update_editor(|editor, window, cx| {
8951 let snippet = Snippet::parse(indoc! {"
8952 /*
8953 * Multiline comment with leading indentation
8954 *
8955 * $1
8956 */
8957 $0"})
8958 .unwrap();
8959 let insertion_ranges = editor
8960 .selections
8961 .all(cx)
8962 .iter()
8963 .map(|s| s.range().clone())
8964 .collect::<Vec<_>>();
8965 editor
8966 .insert_snippet(&insertion_ranges, snippet, window, cx)
8967 .unwrap();
8968 });
8969
8970 cx.assert_editor_state(indoc! {"
8971 /*
8972 * Multiline comment with leading indentation
8973 *
8974 * ˇ
8975 */
8976 "});
8977
8978 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8979 cx.assert_editor_state(indoc! {"
8980 /*
8981 * Multiline comment with leading indentation
8982 *
8983 *•
8984 */
8985 ˇ"});
8986}
8987
8988#[gpui::test]
8989async fn test_document_format_during_save(cx: &mut TestAppContext) {
8990 init_test(cx, |_| {});
8991
8992 let fs = FakeFs::new(cx.executor());
8993 fs.insert_file(path!("/file.rs"), Default::default()).await;
8994
8995 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8996
8997 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8998 language_registry.add(rust_lang());
8999 let mut fake_servers = language_registry.register_fake_lsp(
9000 "Rust",
9001 FakeLspAdapter {
9002 capabilities: lsp::ServerCapabilities {
9003 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9004 ..Default::default()
9005 },
9006 ..Default::default()
9007 },
9008 );
9009
9010 let buffer = project
9011 .update(cx, |project, cx| {
9012 project.open_local_buffer(path!("/file.rs"), cx)
9013 })
9014 .await
9015 .unwrap();
9016
9017 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9018 let (editor, cx) = cx.add_window_view(|window, cx| {
9019 build_editor_with_project(project.clone(), buffer, window, cx)
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.set_text("one\ntwo\nthree\n", window, cx)
9023 });
9024 assert!(cx.read(|cx| editor.is_dirty(cx)));
9025
9026 cx.executor().start_waiting();
9027 let fake_server = fake_servers.next().await.unwrap();
9028
9029 {
9030 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9031 move |params, _| async move {
9032 assert_eq!(
9033 params.text_document.uri,
9034 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9035 );
9036 assert_eq!(params.options.tab_size, 4);
9037 Ok(Some(vec![lsp::TextEdit::new(
9038 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9039 ", ".to_string(),
9040 )]))
9041 },
9042 );
9043 let save = editor
9044 .update_in(cx, |editor, window, cx| {
9045 editor.save(true, project.clone(), window, cx)
9046 })
9047 .unwrap();
9048 cx.executor().start_waiting();
9049 save.await;
9050
9051 assert_eq!(
9052 editor.update(cx, |editor, cx| editor.text(cx)),
9053 "one, two\nthree\n"
9054 );
9055 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9056 }
9057
9058 {
9059 editor.update_in(cx, |editor, window, cx| {
9060 editor.set_text("one\ntwo\nthree\n", window, cx)
9061 });
9062 assert!(cx.read(|cx| editor.is_dirty(cx)));
9063
9064 // Ensure we can still save even if formatting hangs.
9065 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9066 move |params, _| async move {
9067 assert_eq!(
9068 params.text_document.uri,
9069 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9070 );
9071 futures::future::pending::<()>().await;
9072 unreachable!()
9073 },
9074 );
9075 let save = editor
9076 .update_in(cx, |editor, window, cx| {
9077 editor.save(true, project.clone(), window, cx)
9078 })
9079 .unwrap();
9080 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9081 cx.executor().start_waiting();
9082 save.await;
9083 assert_eq!(
9084 editor.update(cx, |editor, cx| editor.text(cx)),
9085 "one\ntwo\nthree\n"
9086 );
9087 }
9088
9089 // For non-dirty buffer, no formatting request should be sent
9090 {
9091 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9092
9093 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9094 panic!("Should not be invoked on non-dirty buffer");
9095 });
9096 let save = editor
9097 .update_in(cx, |editor, window, cx| {
9098 editor.save(true, project.clone(), window, cx)
9099 })
9100 .unwrap();
9101 cx.executor().start_waiting();
9102 save.await;
9103 }
9104
9105 // Set rust language override and assert overridden tabsize is sent to language server
9106 update_test_language_settings(cx, |settings| {
9107 settings.languages.insert(
9108 "Rust".into(),
9109 LanguageSettingsContent {
9110 tab_size: NonZeroU32::new(8),
9111 ..Default::default()
9112 },
9113 );
9114 });
9115
9116 {
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.set_text("somehting_new\n", window, cx)
9119 });
9120 assert!(cx.read(|cx| editor.is_dirty(cx)));
9121 let _formatting_request_signal = fake_server
9122 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9123 assert_eq!(
9124 params.text_document.uri,
9125 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9126 );
9127 assert_eq!(params.options.tab_size, 8);
9128 Ok(Some(vec![]))
9129 });
9130 let save = editor
9131 .update_in(cx, |editor, window, cx| {
9132 editor.save(true, project.clone(), window, cx)
9133 })
9134 .unwrap();
9135 cx.executor().start_waiting();
9136 save.await;
9137 }
9138}
9139
9140#[gpui::test]
9141async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9142 init_test(cx, |_| {});
9143
9144 let cols = 4;
9145 let rows = 10;
9146 let sample_text_1 = sample_text(rows, cols, 'a');
9147 assert_eq!(
9148 sample_text_1,
9149 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9150 );
9151 let sample_text_2 = sample_text(rows, cols, 'l');
9152 assert_eq!(
9153 sample_text_2,
9154 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9155 );
9156 let sample_text_3 = sample_text(rows, cols, 'v');
9157 assert_eq!(
9158 sample_text_3,
9159 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9160 );
9161
9162 let fs = FakeFs::new(cx.executor());
9163 fs.insert_tree(
9164 path!("/a"),
9165 json!({
9166 "main.rs": sample_text_1,
9167 "other.rs": sample_text_2,
9168 "lib.rs": sample_text_3,
9169 }),
9170 )
9171 .await;
9172
9173 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9174 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9175 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9176
9177 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9178 language_registry.add(rust_lang());
9179 let mut fake_servers = language_registry.register_fake_lsp(
9180 "Rust",
9181 FakeLspAdapter {
9182 capabilities: lsp::ServerCapabilities {
9183 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9184 ..Default::default()
9185 },
9186 ..Default::default()
9187 },
9188 );
9189
9190 let worktree = project.update(cx, |project, cx| {
9191 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9192 assert_eq!(worktrees.len(), 1);
9193 worktrees.pop().unwrap()
9194 });
9195 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9196
9197 let buffer_1 = project
9198 .update(cx, |project, cx| {
9199 project.open_buffer((worktree_id, "main.rs"), cx)
9200 })
9201 .await
9202 .unwrap();
9203 let buffer_2 = project
9204 .update(cx, |project, cx| {
9205 project.open_buffer((worktree_id, "other.rs"), cx)
9206 })
9207 .await
9208 .unwrap();
9209 let buffer_3 = project
9210 .update(cx, |project, cx| {
9211 project.open_buffer((worktree_id, "lib.rs"), cx)
9212 })
9213 .await
9214 .unwrap();
9215
9216 let multi_buffer = cx.new(|cx| {
9217 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9218 multi_buffer.push_excerpts(
9219 buffer_1.clone(),
9220 [
9221 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9222 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9223 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9224 ],
9225 cx,
9226 );
9227 multi_buffer.push_excerpts(
9228 buffer_2.clone(),
9229 [
9230 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9231 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9232 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9233 ],
9234 cx,
9235 );
9236 multi_buffer.push_excerpts(
9237 buffer_3.clone(),
9238 [
9239 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9240 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9241 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9242 ],
9243 cx,
9244 );
9245 multi_buffer
9246 });
9247 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9248 Editor::new(
9249 EditorMode::full(),
9250 multi_buffer,
9251 Some(project.clone()),
9252 window,
9253 cx,
9254 )
9255 });
9256
9257 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9258 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9259 s.select_ranges(Some(1..2))
9260 });
9261 editor.insert("|one|two|three|", window, cx);
9262 });
9263 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9264 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9265 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9266 s.select_ranges(Some(60..70))
9267 });
9268 editor.insert("|four|five|six|", window, cx);
9269 });
9270 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9271
9272 // First two buffers should be edited, but not the third one.
9273 assert_eq!(
9274 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9275 "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}",
9276 );
9277 buffer_1.update(cx, |buffer, _| {
9278 assert!(buffer.is_dirty());
9279 assert_eq!(
9280 buffer.text(),
9281 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9282 )
9283 });
9284 buffer_2.update(cx, |buffer, _| {
9285 assert!(buffer.is_dirty());
9286 assert_eq!(
9287 buffer.text(),
9288 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9289 )
9290 });
9291 buffer_3.update(cx, |buffer, _| {
9292 assert!(!buffer.is_dirty());
9293 assert_eq!(buffer.text(), sample_text_3,)
9294 });
9295 cx.executor().run_until_parked();
9296
9297 cx.executor().start_waiting();
9298 let save = multi_buffer_editor
9299 .update_in(cx, |editor, window, cx| {
9300 editor.save(true, project.clone(), window, cx)
9301 })
9302 .unwrap();
9303
9304 let fake_server = fake_servers.next().await.unwrap();
9305 fake_server
9306 .server
9307 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9308 Ok(Some(vec![lsp::TextEdit::new(
9309 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9310 format!("[{} formatted]", params.text_document.uri),
9311 )]))
9312 })
9313 .detach();
9314 save.await;
9315
9316 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9317 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9318 assert_eq!(
9319 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9320 uri!(
9321 "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}"
9322 ),
9323 );
9324 buffer_1.update(cx, |buffer, _| {
9325 assert!(!buffer.is_dirty());
9326 assert_eq!(
9327 buffer.text(),
9328 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9329 )
9330 });
9331 buffer_2.update(cx, |buffer, _| {
9332 assert!(!buffer.is_dirty());
9333 assert_eq!(
9334 buffer.text(),
9335 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9336 )
9337 });
9338 buffer_3.update(cx, |buffer, _| {
9339 assert!(!buffer.is_dirty());
9340 assert_eq!(buffer.text(), sample_text_3,)
9341 });
9342}
9343
9344#[gpui::test]
9345async fn test_range_format_during_save(cx: &mut TestAppContext) {
9346 init_test(cx, |_| {});
9347
9348 let fs = FakeFs::new(cx.executor());
9349 fs.insert_file(path!("/file.rs"), Default::default()).await;
9350
9351 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9352
9353 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9354 language_registry.add(rust_lang());
9355 let mut fake_servers = language_registry.register_fake_lsp(
9356 "Rust",
9357 FakeLspAdapter {
9358 capabilities: lsp::ServerCapabilities {
9359 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9360 ..Default::default()
9361 },
9362 ..Default::default()
9363 },
9364 );
9365
9366 let buffer = project
9367 .update(cx, |project, cx| {
9368 project.open_local_buffer(path!("/file.rs"), cx)
9369 })
9370 .await
9371 .unwrap();
9372
9373 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9374 let (editor, cx) = cx.add_window_view(|window, cx| {
9375 build_editor_with_project(project.clone(), buffer, window, cx)
9376 });
9377 editor.update_in(cx, |editor, window, cx| {
9378 editor.set_text("one\ntwo\nthree\n", window, cx)
9379 });
9380 assert!(cx.read(|cx| editor.is_dirty(cx)));
9381
9382 cx.executor().start_waiting();
9383 let fake_server = fake_servers.next().await.unwrap();
9384
9385 let save = editor
9386 .update_in(cx, |editor, window, cx| {
9387 editor.save(true, project.clone(), window, cx)
9388 })
9389 .unwrap();
9390 fake_server
9391 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9392 assert_eq!(
9393 params.text_document.uri,
9394 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9395 );
9396 assert_eq!(params.options.tab_size, 4);
9397 Ok(Some(vec![lsp::TextEdit::new(
9398 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9399 ", ".to_string(),
9400 )]))
9401 })
9402 .next()
9403 .await;
9404 cx.executor().start_waiting();
9405 save.await;
9406 assert_eq!(
9407 editor.update(cx, |editor, cx| editor.text(cx)),
9408 "one, two\nthree\n"
9409 );
9410 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9411
9412 editor.update_in(cx, |editor, window, cx| {
9413 editor.set_text("one\ntwo\nthree\n", window, cx)
9414 });
9415 assert!(cx.read(|cx| editor.is_dirty(cx)));
9416
9417 // Ensure we can still save even if formatting hangs.
9418 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9419 move |params, _| async move {
9420 assert_eq!(
9421 params.text_document.uri,
9422 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9423 );
9424 futures::future::pending::<()>().await;
9425 unreachable!()
9426 },
9427 );
9428 let save = editor
9429 .update_in(cx, |editor, window, cx| {
9430 editor.save(true, project.clone(), window, cx)
9431 })
9432 .unwrap();
9433 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9434 cx.executor().start_waiting();
9435 save.await;
9436 assert_eq!(
9437 editor.update(cx, |editor, cx| editor.text(cx)),
9438 "one\ntwo\nthree\n"
9439 );
9440 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9441
9442 // For non-dirty buffer, no formatting request should be sent
9443 let save = editor
9444 .update_in(cx, |editor, window, cx| {
9445 editor.save(true, project.clone(), window, cx)
9446 })
9447 .unwrap();
9448 let _pending_format_request = fake_server
9449 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9450 panic!("Should not be invoked on non-dirty buffer");
9451 })
9452 .next();
9453 cx.executor().start_waiting();
9454 save.await;
9455
9456 // Set Rust language override and assert overridden tabsize is sent to language server
9457 update_test_language_settings(cx, |settings| {
9458 settings.languages.insert(
9459 "Rust".into(),
9460 LanguageSettingsContent {
9461 tab_size: NonZeroU32::new(8),
9462 ..Default::default()
9463 },
9464 );
9465 });
9466
9467 editor.update_in(cx, |editor, window, cx| {
9468 editor.set_text("somehting_new\n", window, cx)
9469 });
9470 assert!(cx.read(|cx| editor.is_dirty(cx)));
9471 let save = editor
9472 .update_in(cx, |editor, window, cx| {
9473 editor.save(true, project.clone(), window, cx)
9474 })
9475 .unwrap();
9476 fake_server
9477 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9478 assert_eq!(
9479 params.text_document.uri,
9480 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9481 );
9482 assert_eq!(params.options.tab_size, 8);
9483 Ok(Some(Vec::new()))
9484 })
9485 .next()
9486 .await;
9487 save.await;
9488}
9489
9490#[gpui::test]
9491async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9492 init_test(cx, |settings| {
9493 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9494 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9495 ))
9496 });
9497
9498 let fs = FakeFs::new(cx.executor());
9499 fs.insert_file(path!("/file.rs"), Default::default()).await;
9500
9501 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9502
9503 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9504 language_registry.add(Arc::new(Language::new(
9505 LanguageConfig {
9506 name: "Rust".into(),
9507 matcher: LanguageMatcher {
9508 path_suffixes: vec!["rs".to_string()],
9509 ..Default::default()
9510 },
9511 ..LanguageConfig::default()
9512 },
9513 Some(tree_sitter_rust::LANGUAGE.into()),
9514 )));
9515 update_test_language_settings(cx, |settings| {
9516 // Enable Prettier formatting for the same buffer, and ensure
9517 // LSP is called instead of Prettier.
9518 settings.defaults.prettier = Some(PrettierSettings {
9519 allowed: true,
9520 ..PrettierSettings::default()
9521 });
9522 });
9523 let mut fake_servers = language_registry.register_fake_lsp(
9524 "Rust",
9525 FakeLspAdapter {
9526 capabilities: lsp::ServerCapabilities {
9527 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9528 ..Default::default()
9529 },
9530 ..Default::default()
9531 },
9532 );
9533
9534 let buffer = project
9535 .update(cx, |project, cx| {
9536 project.open_local_buffer(path!("/file.rs"), cx)
9537 })
9538 .await
9539 .unwrap();
9540
9541 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9542 let (editor, cx) = cx.add_window_view(|window, cx| {
9543 build_editor_with_project(project.clone(), buffer, window, cx)
9544 });
9545 editor.update_in(cx, |editor, window, cx| {
9546 editor.set_text("one\ntwo\nthree\n", window, cx)
9547 });
9548
9549 cx.executor().start_waiting();
9550 let fake_server = fake_servers.next().await.unwrap();
9551
9552 let format = editor
9553 .update_in(cx, |editor, window, cx| {
9554 editor.perform_format(
9555 project.clone(),
9556 FormatTrigger::Manual,
9557 FormatTarget::Buffers,
9558 window,
9559 cx,
9560 )
9561 })
9562 .unwrap();
9563 fake_server
9564 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9565 assert_eq!(
9566 params.text_document.uri,
9567 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9568 );
9569 assert_eq!(params.options.tab_size, 4);
9570 Ok(Some(vec![lsp::TextEdit::new(
9571 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9572 ", ".to_string(),
9573 )]))
9574 })
9575 .next()
9576 .await;
9577 cx.executor().start_waiting();
9578 format.await;
9579 assert_eq!(
9580 editor.update(cx, |editor, cx| editor.text(cx)),
9581 "one, two\nthree\n"
9582 );
9583
9584 editor.update_in(cx, |editor, window, cx| {
9585 editor.set_text("one\ntwo\nthree\n", window, cx)
9586 });
9587 // Ensure we don't lock if formatting hangs.
9588 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9589 move |params, _| async move {
9590 assert_eq!(
9591 params.text_document.uri,
9592 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9593 );
9594 futures::future::pending::<()>().await;
9595 unreachable!()
9596 },
9597 );
9598 let format = editor
9599 .update_in(cx, |editor, window, cx| {
9600 editor.perform_format(
9601 project,
9602 FormatTrigger::Manual,
9603 FormatTarget::Buffers,
9604 window,
9605 cx,
9606 )
9607 })
9608 .unwrap();
9609 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9610 cx.executor().start_waiting();
9611 format.await;
9612 assert_eq!(
9613 editor.update(cx, |editor, cx| editor.text(cx)),
9614 "one\ntwo\nthree\n"
9615 );
9616}
9617
9618#[gpui::test]
9619async fn test_multiple_formatters(cx: &mut TestAppContext) {
9620 init_test(cx, |settings| {
9621 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9622 settings.defaults.formatter =
9623 Some(language_settings::SelectedFormatter::List(FormatterList(
9624 vec![
9625 Formatter::LanguageServer { name: None },
9626 Formatter::CodeActions(
9627 [
9628 ("code-action-1".into(), true),
9629 ("code-action-2".into(), true),
9630 ]
9631 .into_iter()
9632 .collect(),
9633 ),
9634 ]
9635 .into(),
9636 )))
9637 });
9638
9639 let fs = FakeFs::new(cx.executor());
9640 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9641 .await;
9642
9643 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9644 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9645 language_registry.add(rust_lang());
9646
9647 let mut fake_servers = language_registry.register_fake_lsp(
9648 "Rust",
9649 FakeLspAdapter {
9650 capabilities: lsp::ServerCapabilities {
9651 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9652 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9653 commands: vec!["the-command-for-code-action-1".into()],
9654 ..Default::default()
9655 }),
9656 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9657 ..Default::default()
9658 },
9659 ..Default::default()
9660 },
9661 );
9662
9663 let buffer = project
9664 .update(cx, |project, cx| {
9665 project.open_local_buffer(path!("/file.rs"), cx)
9666 })
9667 .await
9668 .unwrap();
9669
9670 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9671 let (editor, cx) = cx.add_window_view(|window, cx| {
9672 build_editor_with_project(project.clone(), buffer, window, cx)
9673 });
9674
9675 cx.executor().start_waiting();
9676
9677 let fake_server = fake_servers.next().await.unwrap();
9678 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9679 move |_params, _| async move {
9680 Ok(Some(vec![lsp::TextEdit::new(
9681 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9682 "applied-formatting\n".to_string(),
9683 )]))
9684 },
9685 );
9686 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9687 move |params, _| async move {
9688 assert_eq!(
9689 params.context.only,
9690 Some(vec!["code-action-1".into(), "code-action-2".into()])
9691 );
9692 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9693 Ok(Some(vec![
9694 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9695 kind: Some("code-action-1".into()),
9696 edit: Some(lsp::WorkspaceEdit::new(
9697 [(
9698 uri.clone(),
9699 vec![lsp::TextEdit::new(
9700 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9701 "applied-code-action-1-edit\n".to_string(),
9702 )],
9703 )]
9704 .into_iter()
9705 .collect(),
9706 )),
9707 command: Some(lsp::Command {
9708 command: "the-command-for-code-action-1".into(),
9709 ..Default::default()
9710 }),
9711 ..Default::default()
9712 }),
9713 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9714 kind: Some("code-action-2".into()),
9715 edit: Some(lsp::WorkspaceEdit::new(
9716 [(
9717 uri.clone(),
9718 vec![lsp::TextEdit::new(
9719 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9720 "applied-code-action-2-edit\n".to_string(),
9721 )],
9722 )]
9723 .into_iter()
9724 .collect(),
9725 )),
9726 ..Default::default()
9727 }),
9728 ]))
9729 },
9730 );
9731
9732 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9733 move |params, _| async move { Ok(params) }
9734 });
9735
9736 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9737 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9738 let fake = fake_server.clone();
9739 let lock = command_lock.clone();
9740 move |params, _| {
9741 assert_eq!(params.command, "the-command-for-code-action-1");
9742 let fake = fake.clone();
9743 let lock = lock.clone();
9744 async move {
9745 lock.lock().await;
9746 fake.server
9747 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9748 label: None,
9749 edit: lsp::WorkspaceEdit {
9750 changes: Some(
9751 [(
9752 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9753 vec![lsp::TextEdit {
9754 range: lsp::Range::new(
9755 lsp::Position::new(0, 0),
9756 lsp::Position::new(0, 0),
9757 ),
9758 new_text: "applied-code-action-1-command\n".into(),
9759 }],
9760 )]
9761 .into_iter()
9762 .collect(),
9763 ),
9764 ..Default::default()
9765 },
9766 })
9767 .await
9768 .into_response()
9769 .unwrap();
9770 Ok(Some(json!(null)))
9771 }
9772 }
9773 });
9774
9775 cx.executor().start_waiting();
9776 editor
9777 .update_in(cx, |editor, window, cx| {
9778 editor.perform_format(
9779 project.clone(),
9780 FormatTrigger::Manual,
9781 FormatTarget::Buffers,
9782 window,
9783 cx,
9784 )
9785 })
9786 .unwrap()
9787 .await;
9788 editor.update(cx, |editor, cx| {
9789 assert_eq!(
9790 editor.text(cx),
9791 r#"
9792 applied-code-action-2-edit
9793 applied-code-action-1-command
9794 applied-code-action-1-edit
9795 applied-formatting
9796 one
9797 two
9798 three
9799 "#
9800 .unindent()
9801 );
9802 });
9803
9804 editor.update_in(cx, |editor, window, cx| {
9805 editor.undo(&Default::default(), window, cx);
9806 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9807 });
9808
9809 // Perform a manual edit while waiting for an LSP command
9810 // that's being run as part of a formatting code action.
9811 let lock_guard = command_lock.lock().await;
9812 let format = editor
9813 .update_in(cx, |editor, window, cx| {
9814 editor.perform_format(
9815 project.clone(),
9816 FormatTrigger::Manual,
9817 FormatTarget::Buffers,
9818 window,
9819 cx,
9820 )
9821 })
9822 .unwrap();
9823 cx.run_until_parked();
9824 editor.update(cx, |editor, cx| {
9825 assert_eq!(
9826 editor.text(cx),
9827 r#"
9828 applied-code-action-1-edit
9829 applied-formatting
9830 one
9831 two
9832 three
9833 "#
9834 .unindent()
9835 );
9836
9837 editor.buffer.update(cx, |buffer, cx| {
9838 let ix = buffer.len(cx);
9839 buffer.edit([(ix..ix, "edited\n")], None, cx);
9840 });
9841 });
9842
9843 // Allow the LSP command to proceed. Because the buffer was edited,
9844 // the second code action will not be run.
9845 drop(lock_guard);
9846 format.await;
9847 editor.update_in(cx, |editor, window, cx| {
9848 assert_eq!(
9849 editor.text(cx),
9850 r#"
9851 applied-code-action-1-command
9852 applied-code-action-1-edit
9853 applied-formatting
9854 one
9855 two
9856 three
9857 edited
9858 "#
9859 .unindent()
9860 );
9861
9862 // The manual edit is undone first, because it is the last thing the user did
9863 // (even though the command completed afterwards).
9864 editor.undo(&Default::default(), window, cx);
9865 assert_eq!(
9866 editor.text(cx),
9867 r#"
9868 applied-code-action-1-command
9869 applied-code-action-1-edit
9870 applied-formatting
9871 one
9872 two
9873 three
9874 "#
9875 .unindent()
9876 );
9877
9878 // All the formatting (including the command, which completed after the manual edit)
9879 // is undone together.
9880 editor.undo(&Default::default(), window, cx);
9881 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9882 });
9883}
9884
9885#[gpui::test]
9886async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9887 init_test(cx, |settings| {
9888 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9889 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9890 ))
9891 });
9892
9893 let fs = FakeFs::new(cx.executor());
9894 fs.insert_file(path!("/file.ts"), Default::default()).await;
9895
9896 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9897
9898 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9899 language_registry.add(Arc::new(Language::new(
9900 LanguageConfig {
9901 name: "TypeScript".into(),
9902 matcher: LanguageMatcher {
9903 path_suffixes: vec!["ts".to_string()],
9904 ..Default::default()
9905 },
9906 ..LanguageConfig::default()
9907 },
9908 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9909 )));
9910 update_test_language_settings(cx, |settings| {
9911 settings.defaults.prettier = Some(PrettierSettings {
9912 allowed: true,
9913 ..PrettierSettings::default()
9914 });
9915 });
9916 let mut fake_servers = language_registry.register_fake_lsp(
9917 "TypeScript",
9918 FakeLspAdapter {
9919 capabilities: lsp::ServerCapabilities {
9920 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9921 ..Default::default()
9922 },
9923 ..Default::default()
9924 },
9925 );
9926
9927 let buffer = project
9928 .update(cx, |project, cx| {
9929 project.open_local_buffer(path!("/file.ts"), cx)
9930 })
9931 .await
9932 .unwrap();
9933
9934 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9935 let (editor, cx) = cx.add_window_view(|window, cx| {
9936 build_editor_with_project(project.clone(), buffer, window, cx)
9937 });
9938 editor.update_in(cx, |editor, window, cx| {
9939 editor.set_text(
9940 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9941 window,
9942 cx,
9943 )
9944 });
9945
9946 cx.executor().start_waiting();
9947 let fake_server = fake_servers.next().await.unwrap();
9948
9949 let format = editor
9950 .update_in(cx, |editor, window, cx| {
9951 editor.perform_code_action_kind(
9952 project.clone(),
9953 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9954 window,
9955 cx,
9956 )
9957 })
9958 .unwrap();
9959 fake_server
9960 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9961 assert_eq!(
9962 params.text_document.uri,
9963 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9964 );
9965 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9966 lsp::CodeAction {
9967 title: "Organize Imports".to_string(),
9968 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9969 edit: Some(lsp::WorkspaceEdit {
9970 changes: Some(
9971 [(
9972 params.text_document.uri.clone(),
9973 vec![lsp::TextEdit::new(
9974 lsp::Range::new(
9975 lsp::Position::new(1, 0),
9976 lsp::Position::new(2, 0),
9977 ),
9978 "".to_string(),
9979 )],
9980 )]
9981 .into_iter()
9982 .collect(),
9983 ),
9984 ..Default::default()
9985 }),
9986 ..Default::default()
9987 },
9988 )]))
9989 })
9990 .next()
9991 .await;
9992 cx.executor().start_waiting();
9993 format.await;
9994 assert_eq!(
9995 editor.update(cx, |editor, cx| editor.text(cx)),
9996 "import { a } from 'module';\n\nconst x = a;\n"
9997 );
9998
9999 editor.update_in(cx, |editor, window, cx| {
10000 editor.set_text(
10001 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10002 window,
10003 cx,
10004 )
10005 });
10006 // Ensure we don't lock if code action hangs.
10007 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10008 move |params, _| async move {
10009 assert_eq!(
10010 params.text_document.uri,
10011 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10012 );
10013 futures::future::pending::<()>().await;
10014 unreachable!()
10015 },
10016 );
10017 let format = editor
10018 .update_in(cx, |editor, window, cx| {
10019 editor.perform_code_action_kind(
10020 project,
10021 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10022 window,
10023 cx,
10024 )
10025 })
10026 .unwrap();
10027 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10028 cx.executor().start_waiting();
10029 format.await;
10030 assert_eq!(
10031 editor.update(cx, |editor, cx| editor.text(cx)),
10032 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10033 );
10034}
10035
10036#[gpui::test]
10037async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10038 init_test(cx, |_| {});
10039
10040 let mut cx = EditorLspTestContext::new_rust(
10041 lsp::ServerCapabilities {
10042 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10043 ..Default::default()
10044 },
10045 cx,
10046 )
10047 .await;
10048
10049 cx.set_state(indoc! {"
10050 one.twoˇ
10051 "});
10052
10053 // The format request takes a long time. When it completes, it inserts
10054 // a newline and an indent before the `.`
10055 cx.lsp
10056 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10057 let executor = cx.background_executor().clone();
10058 async move {
10059 executor.timer(Duration::from_millis(100)).await;
10060 Ok(Some(vec![lsp::TextEdit {
10061 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10062 new_text: "\n ".into(),
10063 }]))
10064 }
10065 });
10066
10067 // Submit a format request.
10068 let format_1 = cx
10069 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10070 .unwrap();
10071 cx.executor().run_until_parked();
10072
10073 // Submit a second format request.
10074 let format_2 = cx
10075 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10076 .unwrap();
10077 cx.executor().run_until_parked();
10078
10079 // Wait for both format requests to complete
10080 cx.executor().advance_clock(Duration::from_millis(200));
10081 cx.executor().start_waiting();
10082 format_1.await.unwrap();
10083 cx.executor().start_waiting();
10084 format_2.await.unwrap();
10085
10086 // The formatting edits only happens once.
10087 cx.assert_editor_state(indoc! {"
10088 one
10089 .twoˇ
10090 "});
10091}
10092
10093#[gpui::test]
10094async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10095 init_test(cx, |settings| {
10096 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10097 });
10098
10099 let mut cx = EditorLspTestContext::new_rust(
10100 lsp::ServerCapabilities {
10101 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10102 ..Default::default()
10103 },
10104 cx,
10105 )
10106 .await;
10107
10108 // Set up a buffer white some trailing whitespace and no trailing newline.
10109 cx.set_state(
10110 &[
10111 "one ", //
10112 "twoˇ", //
10113 "three ", //
10114 "four", //
10115 ]
10116 .join("\n"),
10117 );
10118
10119 // Submit a format request.
10120 let format = cx
10121 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10122 .unwrap();
10123
10124 // Record which buffer changes have been sent to the language server
10125 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10126 cx.lsp
10127 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10128 let buffer_changes = buffer_changes.clone();
10129 move |params, _| {
10130 buffer_changes.lock().extend(
10131 params
10132 .content_changes
10133 .into_iter()
10134 .map(|e| (e.range.unwrap(), e.text)),
10135 );
10136 }
10137 });
10138
10139 // Handle formatting requests to the language server.
10140 cx.lsp
10141 .set_request_handler::<lsp::request::Formatting, _, _>({
10142 let buffer_changes = buffer_changes.clone();
10143 move |_, _| {
10144 // When formatting is requested, trailing whitespace has already been stripped,
10145 // and the trailing newline has already been added.
10146 assert_eq!(
10147 &buffer_changes.lock()[1..],
10148 &[
10149 (
10150 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10151 "".into()
10152 ),
10153 (
10154 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10155 "".into()
10156 ),
10157 (
10158 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10159 "\n".into()
10160 ),
10161 ]
10162 );
10163
10164 // Insert blank lines between each line of the buffer.
10165 async move {
10166 Ok(Some(vec![
10167 lsp::TextEdit {
10168 range: lsp::Range::new(
10169 lsp::Position::new(1, 0),
10170 lsp::Position::new(1, 0),
10171 ),
10172 new_text: "\n".into(),
10173 },
10174 lsp::TextEdit {
10175 range: lsp::Range::new(
10176 lsp::Position::new(2, 0),
10177 lsp::Position::new(2, 0),
10178 ),
10179 new_text: "\n".into(),
10180 },
10181 ]))
10182 }
10183 }
10184 });
10185
10186 // After formatting the buffer, the trailing whitespace is stripped,
10187 // a newline is appended, and the edits provided by the language server
10188 // have been applied.
10189 format.await.unwrap();
10190 cx.assert_editor_state(
10191 &[
10192 "one", //
10193 "", //
10194 "twoˇ", //
10195 "", //
10196 "three", //
10197 "four", //
10198 "", //
10199 ]
10200 .join("\n"),
10201 );
10202
10203 // Undoing the formatting undoes the trailing whitespace removal, the
10204 // trailing newline, and the LSP edits.
10205 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10206 cx.assert_editor_state(
10207 &[
10208 "one ", //
10209 "twoˇ", //
10210 "three ", //
10211 "four", //
10212 ]
10213 .join("\n"),
10214 );
10215}
10216
10217#[gpui::test]
10218async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10219 cx: &mut TestAppContext,
10220) {
10221 init_test(cx, |_| {});
10222
10223 cx.update(|cx| {
10224 cx.update_global::<SettingsStore, _>(|settings, cx| {
10225 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10226 settings.auto_signature_help = Some(true);
10227 });
10228 });
10229 });
10230
10231 let mut cx = EditorLspTestContext::new_rust(
10232 lsp::ServerCapabilities {
10233 signature_help_provider: Some(lsp::SignatureHelpOptions {
10234 ..Default::default()
10235 }),
10236 ..Default::default()
10237 },
10238 cx,
10239 )
10240 .await;
10241
10242 let language = Language::new(
10243 LanguageConfig {
10244 name: "Rust".into(),
10245 brackets: BracketPairConfig {
10246 pairs: vec![
10247 BracketPair {
10248 start: "{".to_string(),
10249 end: "}".to_string(),
10250 close: true,
10251 surround: true,
10252 newline: true,
10253 },
10254 BracketPair {
10255 start: "(".to_string(),
10256 end: ")".to_string(),
10257 close: true,
10258 surround: true,
10259 newline: true,
10260 },
10261 BracketPair {
10262 start: "/*".to_string(),
10263 end: " */".to_string(),
10264 close: true,
10265 surround: true,
10266 newline: true,
10267 },
10268 BracketPair {
10269 start: "[".to_string(),
10270 end: "]".to_string(),
10271 close: false,
10272 surround: false,
10273 newline: true,
10274 },
10275 BracketPair {
10276 start: "\"".to_string(),
10277 end: "\"".to_string(),
10278 close: true,
10279 surround: true,
10280 newline: false,
10281 },
10282 BracketPair {
10283 start: "<".to_string(),
10284 end: ">".to_string(),
10285 close: false,
10286 surround: true,
10287 newline: true,
10288 },
10289 ],
10290 ..Default::default()
10291 },
10292 autoclose_before: "})]".to_string(),
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_rust::LANGUAGE.into()),
10296 );
10297 let language = Arc::new(language);
10298
10299 cx.language_registry().add(language.clone());
10300 cx.update_buffer(|buffer, cx| {
10301 buffer.set_language(Some(language), cx);
10302 });
10303
10304 cx.set_state(
10305 &r#"
10306 fn main() {
10307 sampleˇ
10308 }
10309 "#
10310 .unindent(),
10311 );
10312
10313 cx.update_editor(|editor, window, cx| {
10314 editor.handle_input("(", window, cx);
10315 });
10316 cx.assert_editor_state(
10317 &"
10318 fn main() {
10319 sample(ˇ)
10320 }
10321 "
10322 .unindent(),
10323 );
10324
10325 let mocked_response = lsp::SignatureHelp {
10326 signatures: vec![lsp::SignatureInformation {
10327 label: "fn sample(param1: u8, param2: u8)".to_string(),
10328 documentation: None,
10329 parameters: Some(vec![
10330 lsp::ParameterInformation {
10331 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10332 documentation: None,
10333 },
10334 lsp::ParameterInformation {
10335 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10336 documentation: None,
10337 },
10338 ]),
10339 active_parameter: None,
10340 }],
10341 active_signature: Some(0),
10342 active_parameter: Some(0),
10343 };
10344 handle_signature_help_request(&mut cx, mocked_response).await;
10345
10346 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10347 .await;
10348
10349 cx.editor(|editor, _, _| {
10350 let signature_help_state = editor.signature_help_state.popover().cloned();
10351 assert_eq!(
10352 signature_help_state.unwrap().label,
10353 "param1: u8, param2: u8"
10354 );
10355 });
10356}
10357
10358#[gpui::test]
10359async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10360 init_test(cx, |_| {});
10361
10362 cx.update(|cx| {
10363 cx.update_global::<SettingsStore, _>(|settings, cx| {
10364 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10365 settings.auto_signature_help = Some(false);
10366 settings.show_signature_help_after_edits = Some(false);
10367 });
10368 });
10369 });
10370
10371 let mut cx = EditorLspTestContext::new_rust(
10372 lsp::ServerCapabilities {
10373 signature_help_provider: Some(lsp::SignatureHelpOptions {
10374 ..Default::default()
10375 }),
10376 ..Default::default()
10377 },
10378 cx,
10379 )
10380 .await;
10381
10382 let language = Language::new(
10383 LanguageConfig {
10384 name: "Rust".into(),
10385 brackets: BracketPairConfig {
10386 pairs: vec![
10387 BracketPair {
10388 start: "{".to_string(),
10389 end: "}".to_string(),
10390 close: true,
10391 surround: true,
10392 newline: true,
10393 },
10394 BracketPair {
10395 start: "(".to_string(),
10396 end: ")".to_string(),
10397 close: true,
10398 surround: true,
10399 newline: true,
10400 },
10401 BracketPair {
10402 start: "/*".to_string(),
10403 end: " */".to_string(),
10404 close: true,
10405 surround: true,
10406 newline: true,
10407 },
10408 BracketPair {
10409 start: "[".to_string(),
10410 end: "]".to_string(),
10411 close: false,
10412 surround: false,
10413 newline: true,
10414 },
10415 BracketPair {
10416 start: "\"".to_string(),
10417 end: "\"".to_string(),
10418 close: true,
10419 surround: true,
10420 newline: false,
10421 },
10422 BracketPair {
10423 start: "<".to_string(),
10424 end: ">".to_string(),
10425 close: false,
10426 surround: true,
10427 newline: true,
10428 },
10429 ],
10430 ..Default::default()
10431 },
10432 autoclose_before: "})]".to_string(),
10433 ..Default::default()
10434 },
10435 Some(tree_sitter_rust::LANGUAGE.into()),
10436 );
10437 let language = Arc::new(language);
10438
10439 cx.language_registry().add(language.clone());
10440 cx.update_buffer(|buffer, cx| {
10441 buffer.set_language(Some(language), cx);
10442 });
10443
10444 // Ensure that signature_help is not called when no signature help is enabled.
10445 cx.set_state(
10446 &r#"
10447 fn main() {
10448 sampleˇ
10449 }
10450 "#
10451 .unindent(),
10452 );
10453 cx.update_editor(|editor, window, cx| {
10454 editor.handle_input("(", window, cx);
10455 });
10456 cx.assert_editor_state(
10457 &"
10458 fn main() {
10459 sample(ˇ)
10460 }
10461 "
10462 .unindent(),
10463 );
10464 cx.editor(|editor, _, _| {
10465 assert!(editor.signature_help_state.task().is_none());
10466 });
10467
10468 let mocked_response = lsp::SignatureHelp {
10469 signatures: vec![lsp::SignatureInformation {
10470 label: "fn sample(param1: u8, param2: u8)".to_string(),
10471 documentation: None,
10472 parameters: Some(vec![
10473 lsp::ParameterInformation {
10474 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10475 documentation: None,
10476 },
10477 lsp::ParameterInformation {
10478 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10479 documentation: None,
10480 },
10481 ]),
10482 active_parameter: None,
10483 }],
10484 active_signature: Some(0),
10485 active_parameter: Some(0),
10486 };
10487
10488 // Ensure that signature_help is called when enabled afte edits
10489 cx.update(|_, cx| {
10490 cx.update_global::<SettingsStore, _>(|settings, cx| {
10491 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10492 settings.auto_signature_help = Some(false);
10493 settings.show_signature_help_after_edits = Some(true);
10494 });
10495 });
10496 });
10497 cx.set_state(
10498 &r#"
10499 fn main() {
10500 sampleˇ
10501 }
10502 "#
10503 .unindent(),
10504 );
10505 cx.update_editor(|editor, window, cx| {
10506 editor.handle_input("(", window, cx);
10507 });
10508 cx.assert_editor_state(
10509 &"
10510 fn main() {
10511 sample(ˇ)
10512 }
10513 "
10514 .unindent(),
10515 );
10516 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10517 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10518 .await;
10519 cx.update_editor(|editor, _, _| {
10520 let signature_help_state = editor.signature_help_state.popover().cloned();
10521 assert!(signature_help_state.is_some());
10522 assert_eq!(
10523 signature_help_state.unwrap().label,
10524 "param1: u8, param2: u8"
10525 );
10526 editor.signature_help_state = SignatureHelpState::default();
10527 });
10528
10529 // Ensure that signature_help is called when auto signature help override is enabled
10530 cx.update(|_, cx| {
10531 cx.update_global::<SettingsStore, _>(|settings, cx| {
10532 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10533 settings.auto_signature_help = Some(true);
10534 settings.show_signature_help_after_edits = Some(false);
10535 });
10536 });
10537 });
10538 cx.set_state(
10539 &r#"
10540 fn main() {
10541 sampleˇ
10542 }
10543 "#
10544 .unindent(),
10545 );
10546 cx.update_editor(|editor, window, cx| {
10547 editor.handle_input("(", window, cx);
10548 });
10549 cx.assert_editor_state(
10550 &"
10551 fn main() {
10552 sample(ˇ)
10553 }
10554 "
10555 .unindent(),
10556 );
10557 handle_signature_help_request(&mut cx, mocked_response).await;
10558 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10559 .await;
10560 cx.editor(|editor, _, _| {
10561 let signature_help_state = editor.signature_help_state.popover().cloned();
10562 assert!(signature_help_state.is_some());
10563 assert_eq!(
10564 signature_help_state.unwrap().label,
10565 "param1: u8, param2: u8"
10566 );
10567 });
10568}
10569
10570#[gpui::test]
10571async fn test_signature_help(cx: &mut TestAppContext) {
10572 init_test(cx, |_| {});
10573 cx.update(|cx| {
10574 cx.update_global::<SettingsStore, _>(|settings, cx| {
10575 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10576 settings.auto_signature_help = Some(true);
10577 });
10578 });
10579 });
10580
10581 let mut cx = EditorLspTestContext::new_rust(
10582 lsp::ServerCapabilities {
10583 signature_help_provider: Some(lsp::SignatureHelpOptions {
10584 ..Default::default()
10585 }),
10586 ..Default::default()
10587 },
10588 cx,
10589 )
10590 .await;
10591
10592 // A test that directly calls `show_signature_help`
10593 cx.update_editor(|editor, window, cx| {
10594 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10595 });
10596
10597 let mocked_response = lsp::SignatureHelp {
10598 signatures: vec![lsp::SignatureInformation {
10599 label: "fn sample(param1: u8, param2: u8)".to_string(),
10600 documentation: None,
10601 parameters: Some(vec![
10602 lsp::ParameterInformation {
10603 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10604 documentation: None,
10605 },
10606 lsp::ParameterInformation {
10607 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10608 documentation: None,
10609 },
10610 ]),
10611 active_parameter: None,
10612 }],
10613 active_signature: Some(0),
10614 active_parameter: Some(0),
10615 };
10616 handle_signature_help_request(&mut cx, mocked_response).await;
10617
10618 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10619 .await;
10620
10621 cx.editor(|editor, _, _| {
10622 let signature_help_state = editor.signature_help_state.popover().cloned();
10623 assert!(signature_help_state.is_some());
10624 assert_eq!(
10625 signature_help_state.unwrap().label,
10626 "param1: u8, param2: u8"
10627 );
10628 });
10629
10630 // When exiting outside from inside the brackets, `signature_help` is closed.
10631 cx.set_state(indoc! {"
10632 fn main() {
10633 sample(ˇ);
10634 }
10635
10636 fn sample(param1: u8, param2: u8) {}
10637 "});
10638
10639 cx.update_editor(|editor, window, cx| {
10640 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10641 });
10642
10643 let mocked_response = lsp::SignatureHelp {
10644 signatures: Vec::new(),
10645 active_signature: None,
10646 active_parameter: None,
10647 };
10648 handle_signature_help_request(&mut cx, mocked_response).await;
10649
10650 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10651 .await;
10652
10653 cx.editor(|editor, _, _| {
10654 assert!(!editor.signature_help_state.is_shown());
10655 });
10656
10657 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10658 cx.set_state(indoc! {"
10659 fn main() {
10660 sample(ˇ);
10661 }
10662
10663 fn sample(param1: u8, param2: u8) {}
10664 "});
10665
10666 let mocked_response = lsp::SignatureHelp {
10667 signatures: vec![lsp::SignatureInformation {
10668 label: "fn sample(param1: u8, param2: u8)".to_string(),
10669 documentation: None,
10670 parameters: Some(vec![
10671 lsp::ParameterInformation {
10672 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10673 documentation: None,
10674 },
10675 lsp::ParameterInformation {
10676 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10677 documentation: None,
10678 },
10679 ]),
10680 active_parameter: None,
10681 }],
10682 active_signature: Some(0),
10683 active_parameter: Some(0),
10684 };
10685 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10686 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10687 .await;
10688 cx.editor(|editor, _, _| {
10689 assert!(editor.signature_help_state.is_shown());
10690 });
10691
10692 // Restore the popover with more parameter input
10693 cx.set_state(indoc! {"
10694 fn main() {
10695 sample(param1, param2ˇ);
10696 }
10697
10698 fn sample(param1: u8, param2: u8) {}
10699 "});
10700
10701 let mocked_response = lsp::SignatureHelp {
10702 signatures: vec![lsp::SignatureInformation {
10703 label: "fn sample(param1: u8, param2: u8)".to_string(),
10704 documentation: None,
10705 parameters: Some(vec![
10706 lsp::ParameterInformation {
10707 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10708 documentation: None,
10709 },
10710 lsp::ParameterInformation {
10711 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10712 documentation: None,
10713 },
10714 ]),
10715 active_parameter: None,
10716 }],
10717 active_signature: Some(0),
10718 active_parameter: Some(1),
10719 };
10720 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10721 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10722 .await;
10723
10724 // When selecting a range, the popover is gone.
10725 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10726 cx.update_editor(|editor, window, cx| {
10727 editor.change_selections(None, window, cx, |s| {
10728 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10729 })
10730 });
10731 cx.assert_editor_state(indoc! {"
10732 fn main() {
10733 sample(param1, «ˇparam2»);
10734 }
10735
10736 fn sample(param1: u8, param2: u8) {}
10737 "});
10738 cx.editor(|editor, _, _| {
10739 assert!(!editor.signature_help_state.is_shown());
10740 });
10741
10742 // When unselecting again, the popover is back if within the brackets.
10743 cx.update_editor(|editor, window, cx| {
10744 editor.change_selections(None, window, cx, |s| {
10745 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10746 })
10747 });
10748 cx.assert_editor_state(indoc! {"
10749 fn main() {
10750 sample(param1, ˇparam2);
10751 }
10752
10753 fn sample(param1: u8, param2: u8) {}
10754 "});
10755 handle_signature_help_request(&mut cx, mocked_response).await;
10756 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10757 .await;
10758 cx.editor(|editor, _, _| {
10759 assert!(editor.signature_help_state.is_shown());
10760 });
10761
10762 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10763 cx.update_editor(|editor, window, cx| {
10764 editor.change_selections(None, window, cx, |s| {
10765 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10766 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10767 })
10768 });
10769 cx.assert_editor_state(indoc! {"
10770 fn main() {
10771 sample(param1, ˇparam2);
10772 }
10773
10774 fn sample(param1: u8, param2: u8) {}
10775 "});
10776
10777 let mocked_response = lsp::SignatureHelp {
10778 signatures: vec![lsp::SignatureInformation {
10779 label: "fn sample(param1: u8, param2: u8)".to_string(),
10780 documentation: None,
10781 parameters: Some(vec![
10782 lsp::ParameterInformation {
10783 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10784 documentation: None,
10785 },
10786 lsp::ParameterInformation {
10787 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10788 documentation: None,
10789 },
10790 ]),
10791 active_parameter: None,
10792 }],
10793 active_signature: Some(0),
10794 active_parameter: Some(1),
10795 };
10796 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10797 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10798 .await;
10799 cx.update_editor(|editor, _, cx| {
10800 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10801 });
10802 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10803 .await;
10804 cx.update_editor(|editor, window, cx| {
10805 editor.change_selections(None, window, cx, |s| {
10806 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10807 })
10808 });
10809 cx.assert_editor_state(indoc! {"
10810 fn main() {
10811 sample(param1, «ˇparam2»);
10812 }
10813
10814 fn sample(param1: u8, param2: u8) {}
10815 "});
10816 cx.update_editor(|editor, window, cx| {
10817 editor.change_selections(None, window, cx, |s| {
10818 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10819 })
10820 });
10821 cx.assert_editor_state(indoc! {"
10822 fn main() {
10823 sample(param1, ˇparam2);
10824 }
10825
10826 fn sample(param1: u8, param2: u8) {}
10827 "});
10828 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10829 .await;
10830}
10831
10832#[gpui::test]
10833async fn test_completion_mode(cx: &mut TestAppContext) {
10834 init_test(cx, |_| {});
10835 let mut cx = EditorLspTestContext::new_rust(
10836 lsp::ServerCapabilities {
10837 completion_provider: Some(lsp::CompletionOptions {
10838 resolve_provider: Some(true),
10839 ..Default::default()
10840 }),
10841 ..Default::default()
10842 },
10843 cx,
10844 )
10845 .await;
10846
10847 struct Run {
10848 run_description: &'static str,
10849 initial_state: String,
10850 buffer_marked_text: String,
10851 completion_label: &'static str,
10852 completion_text: &'static str,
10853 expected_with_insert_mode: String,
10854 expected_with_replace_mode: String,
10855 expected_with_replace_subsequence_mode: String,
10856 expected_with_replace_suffix_mode: String,
10857 }
10858
10859 let runs = [
10860 Run {
10861 run_description: "Start of word matches completion text",
10862 initial_state: "before ediˇ after".into(),
10863 buffer_marked_text: "before <edi|> after".into(),
10864 completion_label: "editor",
10865 completion_text: "editor",
10866 expected_with_insert_mode: "before editorˇ after".into(),
10867 expected_with_replace_mode: "before editorˇ after".into(),
10868 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10869 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10870 },
10871 Run {
10872 run_description: "Accept same text at the middle of the word",
10873 initial_state: "before ediˇtor after".into(),
10874 buffer_marked_text: "before <edi|tor> after".into(),
10875 completion_label: "editor",
10876 completion_text: "editor",
10877 expected_with_insert_mode: "before editorˇtor after".into(),
10878 expected_with_replace_mode: "before editorˇ after".into(),
10879 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10880 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10881 },
10882 Run {
10883 run_description: "End of word matches completion text -- cursor at end",
10884 initial_state: "before torˇ after".into(),
10885 buffer_marked_text: "before <tor|> after".into(),
10886 completion_label: "editor",
10887 completion_text: "editor",
10888 expected_with_insert_mode: "before editorˇ after".into(),
10889 expected_with_replace_mode: "before editorˇ after".into(),
10890 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10891 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10892 },
10893 Run {
10894 run_description: "End of word matches completion text -- cursor at start",
10895 initial_state: "before ˇtor after".into(),
10896 buffer_marked_text: "before <|tor> after".into(),
10897 completion_label: "editor",
10898 completion_text: "editor",
10899 expected_with_insert_mode: "before editorˇtor after".into(),
10900 expected_with_replace_mode: "before editorˇ after".into(),
10901 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10902 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10903 },
10904 Run {
10905 run_description: "Prepend text containing whitespace",
10906 initial_state: "pˇfield: bool".into(),
10907 buffer_marked_text: "<p|field>: bool".into(),
10908 completion_label: "pub ",
10909 completion_text: "pub ",
10910 expected_with_insert_mode: "pub ˇfield: bool".into(),
10911 expected_with_replace_mode: "pub ˇ: bool".into(),
10912 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10913 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10914 },
10915 Run {
10916 run_description: "Add element to start of list",
10917 initial_state: "[element_ˇelement_2]".into(),
10918 buffer_marked_text: "[<element_|element_2>]".into(),
10919 completion_label: "element_1",
10920 completion_text: "element_1",
10921 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10922 expected_with_replace_mode: "[element_1ˇ]".into(),
10923 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10924 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10925 },
10926 Run {
10927 run_description: "Add element to start of list -- first and second elements are equal",
10928 initial_state: "[elˇelement]".into(),
10929 buffer_marked_text: "[<el|element>]".into(),
10930 completion_label: "element",
10931 completion_text: "element",
10932 expected_with_insert_mode: "[elementˇelement]".into(),
10933 expected_with_replace_mode: "[elementˇ]".into(),
10934 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10935 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10936 },
10937 Run {
10938 run_description: "Ends with matching suffix",
10939 initial_state: "SubˇError".into(),
10940 buffer_marked_text: "<Sub|Error>".into(),
10941 completion_label: "SubscriptionError",
10942 completion_text: "SubscriptionError",
10943 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10944 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10945 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10946 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10947 },
10948 Run {
10949 run_description: "Suffix is a subsequence -- contiguous",
10950 initial_state: "SubˇErr".into(),
10951 buffer_marked_text: "<Sub|Err>".into(),
10952 completion_label: "SubscriptionError",
10953 completion_text: "SubscriptionError",
10954 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10955 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10956 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10957 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10958 },
10959 Run {
10960 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10961 initial_state: "Suˇscrirr".into(),
10962 buffer_marked_text: "<Su|scrirr>".into(),
10963 completion_label: "SubscriptionError",
10964 completion_text: "SubscriptionError",
10965 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10966 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10967 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10968 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10969 },
10970 Run {
10971 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10972 initial_state: "foo(indˇix)".into(),
10973 buffer_marked_text: "foo(<ind|ix>)".into(),
10974 completion_label: "node_index",
10975 completion_text: "node_index",
10976 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10977 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10978 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10979 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10980 },
10981 Run {
10982 run_description: "Replace range ends before cursor - should extend to cursor",
10983 initial_state: "before editˇo after".into(),
10984 buffer_marked_text: "before <{ed}>it|o after".into(),
10985 completion_label: "editor",
10986 completion_text: "editor",
10987 expected_with_insert_mode: "before editorˇo after".into(),
10988 expected_with_replace_mode: "before editorˇo after".into(),
10989 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10990 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10991 },
10992 Run {
10993 run_description: "Uses label for suffix matching",
10994 initial_state: "before ediˇtor after".into(),
10995 buffer_marked_text: "before <edi|tor> after".into(),
10996 completion_label: "editor",
10997 completion_text: "editor()",
10998 expected_with_insert_mode: "before editor()ˇtor after".into(),
10999 expected_with_replace_mode: "before editor()ˇ after".into(),
11000 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11001 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11002 },
11003 Run {
11004 run_description: "Case insensitive subsequence and suffix matching",
11005 initial_state: "before EDiˇtoR after".into(),
11006 buffer_marked_text: "before <EDi|toR> after".into(),
11007 completion_label: "editor",
11008 completion_text: "editor",
11009 expected_with_insert_mode: "before editorˇtoR after".into(),
11010 expected_with_replace_mode: "before editorˇ after".into(),
11011 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11012 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11013 },
11014 ];
11015
11016 for run in runs {
11017 let run_variations = [
11018 (LspInsertMode::Insert, run.expected_with_insert_mode),
11019 (LspInsertMode::Replace, run.expected_with_replace_mode),
11020 (
11021 LspInsertMode::ReplaceSubsequence,
11022 run.expected_with_replace_subsequence_mode,
11023 ),
11024 (
11025 LspInsertMode::ReplaceSuffix,
11026 run.expected_with_replace_suffix_mode,
11027 ),
11028 ];
11029
11030 for (lsp_insert_mode, expected_text) in run_variations {
11031 eprintln!(
11032 "run = {:?}, mode = {lsp_insert_mode:.?}",
11033 run.run_description,
11034 );
11035
11036 update_test_language_settings(&mut cx, |settings| {
11037 settings.defaults.completions = Some(CompletionSettings {
11038 lsp_insert_mode,
11039 words: WordsCompletionMode::Disabled,
11040 lsp: true,
11041 lsp_fetch_timeout_ms: 0,
11042 });
11043 });
11044
11045 cx.set_state(&run.initial_state);
11046 cx.update_editor(|editor, window, cx| {
11047 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11048 });
11049
11050 let counter = Arc::new(AtomicUsize::new(0));
11051 handle_completion_request_with_insert_and_replace(
11052 &mut cx,
11053 &run.buffer_marked_text,
11054 vec![(run.completion_label, run.completion_text)],
11055 counter.clone(),
11056 )
11057 .await;
11058 cx.condition(|editor, _| editor.context_menu_visible())
11059 .await;
11060 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11061
11062 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11063 editor
11064 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11065 .unwrap()
11066 });
11067 cx.assert_editor_state(&expected_text);
11068 handle_resolve_completion_request(&mut cx, None).await;
11069 apply_additional_edits.await.unwrap();
11070 }
11071 }
11072}
11073
11074#[gpui::test]
11075async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11076 init_test(cx, |_| {});
11077 let mut cx = EditorLspTestContext::new_rust(
11078 lsp::ServerCapabilities {
11079 completion_provider: Some(lsp::CompletionOptions {
11080 resolve_provider: Some(true),
11081 ..Default::default()
11082 }),
11083 ..Default::default()
11084 },
11085 cx,
11086 )
11087 .await;
11088
11089 let initial_state = "SubˇError";
11090 let buffer_marked_text = "<Sub|Error>";
11091 let completion_text = "SubscriptionError";
11092 let expected_with_insert_mode = "SubscriptionErrorˇError";
11093 let expected_with_replace_mode = "SubscriptionErrorˇ";
11094
11095 update_test_language_settings(&mut cx, |settings| {
11096 settings.defaults.completions = Some(CompletionSettings {
11097 words: WordsCompletionMode::Disabled,
11098 // set the opposite here to ensure that the action is overriding the default behavior
11099 lsp_insert_mode: LspInsertMode::Insert,
11100 lsp: true,
11101 lsp_fetch_timeout_ms: 0,
11102 });
11103 });
11104
11105 cx.set_state(initial_state);
11106 cx.update_editor(|editor, window, cx| {
11107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11108 });
11109
11110 let counter = Arc::new(AtomicUsize::new(0));
11111 handle_completion_request_with_insert_and_replace(
11112 &mut cx,
11113 &buffer_marked_text,
11114 vec![(completion_text, completion_text)],
11115 counter.clone(),
11116 )
11117 .await;
11118 cx.condition(|editor, _| editor.context_menu_visible())
11119 .await;
11120 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11121
11122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11123 editor
11124 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11125 .unwrap()
11126 });
11127 cx.assert_editor_state(&expected_with_replace_mode);
11128 handle_resolve_completion_request(&mut cx, None).await;
11129 apply_additional_edits.await.unwrap();
11130
11131 update_test_language_settings(&mut cx, |settings| {
11132 settings.defaults.completions = Some(CompletionSettings {
11133 words: WordsCompletionMode::Disabled,
11134 // set the opposite here to ensure that the action is overriding the default behavior
11135 lsp_insert_mode: LspInsertMode::Replace,
11136 lsp: true,
11137 lsp_fetch_timeout_ms: 0,
11138 });
11139 });
11140
11141 cx.set_state(initial_state);
11142 cx.update_editor(|editor, window, cx| {
11143 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11144 });
11145 handle_completion_request_with_insert_and_replace(
11146 &mut cx,
11147 &buffer_marked_text,
11148 vec![(completion_text, completion_text)],
11149 counter.clone(),
11150 )
11151 .await;
11152 cx.condition(|editor, _| editor.context_menu_visible())
11153 .await;
11154 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11155
11156 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11157 editor
11158 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11159 .unwrap()
11160 });
11161 cx.assert_editor_state(&expected_with_insert_mode);
11162 handle_resolve_completion_request(&mut cx, None).await;
11163 apply_additional_edits.await.unwrap();
11164}
11165
11166#[gpui::test]
11167async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11168 init_test(cx, |_| {});
11169 let mut cx = EditorLspTestContext::new_rust(
11170 lsp::ServerCapabilities {
11171 completion_provider: Some(lsp::CompletionOptions {
11172 resolve_provider: Some(true),
11173 ..Default::default()
11174 }),
11175 ..Default::default()
11176 },
11177 cx,
11178 )
11179 .await;
11180
11181 // scenario: surrounding text matches completion text
11182 let completion_text = "to_offset";
11183 let initial_state = indoc! {"
11184 1. buf.to_offˇsuffix
11185 2. buf.to_offˇsuf
11186 3. buf.to_offˇfix
11187 4. buf.to_offˇ
11188 5. into_offˇensive
11189 6. ˇsuffix
11190 7. let ˇ //
11191 8. aaˇzz
11192 9. buf.to_off«zzzzzˇ»suffix
11193 10. buf.«ˇzzzzz»suffix
11194 11. to_off«ˇzzzzz»
11195
11196 buf.to_offˇsuffix // newest cursor
11197 "};
11198 let completion_marked_buffer = indoc! {"
11199 1. buf.to_offsuffix
11200 2. buf.to_offsuf
11201 3. buf.to_offfix
11202 4. buf.to_off
11203 5. into_offensive
11204 6. suffix
11205 7. let //
11206 8. aazz
11207 9. buf.to_offzzzzzsuffix
11208 10. buf.zzzzzsuffix
11209 11. to_offzzzzz
11210
11211 buf.<to_off|suffix> // newest cursor
11212 "};
11213 let expected = indoc! {"
11214 1. buf.to_offsetˇ
11215 2. buf.to_offsetˇsuf
11216 3. buf.to_offsetˇfix
11217 4. buf.to_offsetˇ
11218 5. into_offsetˇensive
11219 6. to_offsetˇsuffix
11220 7. let to_offsetˇ //
11221 8. aato_offsetˇzz
11222 9. buf.to_offsetˇ
11223 10. buf.to_offsetˇsuffix
11224 11. to_offsetˇ
11225
11226 buf.to_offsetˇ // newest cursor
11227 "};
11228 cx.set_state(initial_state);
11229 cx.update_editor(|editor, window, cx| {
11230 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11231 });
11232 handle_completion_request_with_insert_and_replace(
11233 &mut cx,
11234 completion_marked_buffer,
11235 vec![(completion_text, completion_text)],
11236 Arc::new(AtomicUsize::new(0)),
11237 )
11238 .await;
11239 cx.condition(|editor, _| editor.context_menu_visible())
11240 .await;
11241 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11242 editor
11243 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11244 .unwrap()
11245 });
11246 cx.assert_editor_state(expected);
11247 handle_resolve_completion_request(&mut cx, None).await;
11248 apply_additional_edits.await.unwrap();
11249
11250 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11251 let completion_text = "foo_and_bar";
11252 let initial_state = indoc! {"
11253 1. ooanbˇ
11254 2. zooanbˇ
11255 3. ooanbˇz
11256 4. zooanbˇz
11257 5. ooanˇ
11258 6. oanbˇ
11259
11260 ooanbˇ
11261 "};
11262 let completion_marked_buffer = indoc! {"
11263 1. ooanb
11264 2. zooanb
11265 3. ooanbz
11266 4. zooanbz
11267 5. ooan
11268 6. oanb
11269
11270 <ooanb|>
11271 "};
11272 let expected = indoc! {"
11273 1. foo_and_barˇ
11274 2. zfoo_and_barˇ
11275 3. foo_and_barˇz
11276 4. zfoo_and_barˇz
11277 5. ooanfoo_and_barˇ
11278 6. oanbfoo_and_barˇ
11279
11280 foo_and_barˇ
11281 "};
11282 cx.set_state(initial_state);
11283 cx.update_editor(|editor, window, cx| {
11284 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11285 });
11286 handle_completion_request_with_insert_and_replace(
11287 &mut cx,
11288 completion_marked_buffer,
11289 vec![(completion_text, completion_text)],
11290 Arc::new(AtomicUsize::new(0)),
11291 )
11292 .await;
11293 cx.condition(|editor, _| editor.context_menu_visible())
11294 .await;
11295 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11296 editor
11297 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11298 .unwrap()
11299 });
11300 cx.assert_editor_state(expected);
11301 handle_resolve_completion_request(&mut cx, None).await;
11302 apply_additional_edits.await.unwrap();
11303
11304 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11305 // (expects the same as if it was inserted at the end)
11306 let completion_text = "foo_and_bar";
11307 let initial_state = indoc! {"
11308 1. ooˇanb
11309 2. zooˇanb
11310 3. ooˇanbz
11311 4. zooˇanbz
11312
11313 ooˇanb
11314 "};
11315 let completion_marked_buffer = indoc! {"
11316 1. ooanb
11317 2. zooanb
11318 3. ooanbz
11319 4. zooanbz
11320
11321 <oo|anb>
11322 "};
11323 let expected = indoc! {"
11324 1. foo_and_barˇ
11325 2. zfoo_and_barˇ
11326 3. foo_and_barˇz
11327 4. zfoo_and_barˇz
11328
11329 foo_and_barˇ
11330 "};
11331 cx.set_state(initial_state);
11332 cx.update_editor(|editor, window, cx| {
11333 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11334 });
11335 handle_completion_request_with_insert_and_replace(
11336 &mut cx,
11337 completion_marked_buffer,
11338 vec![(completion_text, completion_text)],
11339 Arc::new(AtomicUsize::new(0)),
11340 )
11341 .await;
11342 cx.condition(|editor, _| editor.context_menu_visible())
11343 .await;
11344 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11345 editor
11346 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11347 .unwrap()
11348 });
11349 cx.assert_editor_state(expected);
11350 handle_resolve_completion_request(&mut cx, None).await;
11351 apply_additional_edits.await.unwrap();
11352}
11353
11354// This used to crash
11355#[gpui::test]
11356async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11357 init_test(cx, |_| {});
11358
11359 let buffer_text = indoc! {"
11360 fn main() {
11361 10.satu;
11362
11363 //
11364 // separate cursors so they open in different excerpts (manually reproducible)
11365 //
11366
11367 10.satu20;
11368 }
11369 "};
11370 let multibuffer_text_with_selections = indoc! {"
11371 fn main() {
11372 10.satuˇ;
11373
11374 //
11375
11376 //
11377
11378 10.satuˇ20;
11379 }
11380 "};
11381 let expected_multibuffer = indoc! {"
11382 fn main() {
11383 10.saturating_sub()ˇ;
11384
11385 //
11386
11387 //
11388
11389 10.saturating_sub()ˇ;
11390 }
11391 "};
11392
11393 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11394 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11395
11396 let fs = FakeFs::new(cx.executor());
11397 fs.insert_tree(
11398 path!("/a"),
11399 json!({
11400 "main.rs": buffer_text,
11401 }),
11402 )
11403 .await;
11404
11405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11406 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11407 language_registry.add(rust_lang());
11408 let mut fake_servers = language_registry.register_fake_lsp(
11409 "Rust",
11410 FakeLspAdapter {
11411 capabilities: lsp::ServerCapabilities {
11412 completion_provider: Some(lsp::CompletionOptions {
11413 resolve_provider: None,
11414 ..lsp::CompletionOptions::default()
11415 }),
11416 ..lsp::ServerCapabilities::default()
11417 },
11418 ..FakeLspAdapter::default()
11419 },
11420 );
11421 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11422 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11423 let buffer = project
11424 .update(cx, |project, cx| {
11425 project.open_local_buffer(path!("/a/main.rs"), cx)
11426 })
11427 .await
11428 .unwrap();
11429
11430 let multi_buffer = cx.new(|cx| {
11431 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11432 multi_buffer.push_excerpts(
11433 buffer.clone(),
11434 [ExcerptRange::new(0..first_excerpt_end)],
11435 cx,
11436 );
11437 multi_buffer.push_excerpts(
11438 buffer.clone(),
11439 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11440 cx,
11441 );
11442 multi_buffer
11443 });
11444
11445 let editor = workspace
11446 .update(cx, |_, window, cx| {
11447 cx.new(|cx| {
11448 Editor::new(
11449 EditorMode::Full {
11450 scale_ui_elements_with_buffer_font_size: false,
11451 show_active_line_background: false,
11452 sized_by_content: false,
11453 },
11454 multi_buffer.clone(),
11455 Some(project.clone()),
11456 window,
11457 cx,
11458 )
11459 })
11460 })
11461 .unwrap();
11462
11463 let pane = workspace
11464 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11465 .unwrap();
11466 pane.update_in(cx, |pane, window, cx| {
11467 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11468 });
11469
11470 let fake_server = fake_servers.next().await.unwrap();
11471
11472 editor.update_in(cx, |editor, window, cx| {
11473 editor.change_selections(None, window, cx, |s| {
11474 s.select_ranges([
11475 Point::new(1, 11)..Point::new(1, 11),
11476 Point::new(7, 11)..Point::new(7, 11),
11477 ])
11478 });
11479
11480 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11481 });
11482
11483 editor.update_in(cx, |editor, window, cx| {
11484 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11485 });
11486
11487 fake_server
11488 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11489 let completion_item = lsp::CompletionItem {
11490 label: "saturating_sub()".into(),
11491 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11492 lsp::InsertReplaceEdit {
11493 new_text: "saturating_sub()".to_owned(),
11494 insert: lsp::Range::new(
11495 lsp::Position::new(7, 7),
11496 lsp::Position::new(7, 11),
11497 ),
11498 replace: lsp::Range::new(
11499 lsp::Position::new(7, 7),
11500 lsp::Position::new(7, 13),
11501 ),
11502 },
11503 )),
11504 ..lsp::CompletionItem::default()
11505 };
11506
11507 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11508 })
11509 .next()
11510 .await
11511 .unwrap();
11512
11513 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11514 .await;
11515
11516 editor
11517 .update_in(cx, |editor, window, cx| {
11518 editor
11519 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11520 .unwrap()
11521 })
11522 .await
11523 .unwrap();
11524
11525 editor.update(cx, |editor, cx| {
11526 assert_text_with_selections(editor, expected_multibuffer, cx);
11527 })
11528}
11529
11530#[gpui::test]
11531async fn test_completion(cx: &mut TestAppContext) {
11532 init_test(cx, |_| {});
11533
11534 let mut cx = EditorLspTestContext::new_rust(
11535 lsp::ServerCapabilities {
11536 completion_provider: Some(lsp::CompletionOptions {
11537 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11538 resolve_provider: Some(true),
11539 ..Default::default()
11540 }),
11541 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11542 ..Default::default()
11543 },
11544 cx,
11545 )
11546 .await;
11547 let counter = Arc::new(AtomicUsize::new(0));
11548
11549 cx.set_state(indoc! {"
11550 oneˇ
11551 two
11552 three
11553 "});
11554 cx.simulate_keystroke(".");
11555 handle_completion_request(
11556 indoc! {"
11557 one.|<>
11558 two
11559 three
11560 "},
11561 vec!["first_completion", "second_completion"],
11562 true,
11563 counter.clone(),
11564 &mut cx,
11565 )
11566 .await;
11567 cx.condition(|editor, _| editor.context_menu_visible())
11568 .await;
11569 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11570
11571 let _handler = handle_signature_help_request(
11572 &mut cx,
11573 lsp::SignatureHelp {
11574 signatures: vec![lsp::SignatureInformation {
11575 label: "test signature".to_string(),
11576 documentation: None,
11577 parameters: Some(vec![lsp::ParameterInformation {
11578 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11579 documentation: None,
11580 }]),
11581 active_parameter: None,
11582 }],
11583 active_signature: None,
11584 active_parameter: None,
11585 },
11586 );
11587 cx.update_editor(|editor, window, cx| {
11588 assert!(
11589 !editor.signature_help_state.is_shown(),
11590 "No signature help was called for"
11591 );
11592 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11593 });
11594 cx.run_until_parked();
11595 cx.update_editor(|editor, _, _| {
11596 assert!(
11597 !editor.signature_help_state.is_shown(),
11598 "No signature help should be shown when completions menu is open"
11599 );
11600 });
11601
11602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11603 editor.context_menu_next(&Default::default(), window, cx);
11604 editor
11605 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11606 .unwrap()
11607 });
11608 cx.assert_editor_state(indoc! {"
11609 one.second_completionˇ
11610 two
11611 three
11612 "});
11613
11614 handle_resolve_completion_request(
11615 &mut cx,
11616 Some(vec![
11617 (
11618 //This overlaps with the primary completion edit which is
11619 //misbehavior from the LSP spec, test that we filter it out
11620 indoc! {"
11621 one.second_ˇcompletion
11622 two
11623 threeˇ
11624 "},
11625 "overlapping additional edit",
11626 ),
11627 (
11628 indoc! {"
11629 one.second_completion
11630 two
11631 threeˇ
11632 "},
11633 "\nadditional edit",
11634 ),
11635 ]),
11636 )
11637 .await;
11638 apply_additional_edits.await.unwrap();
11639 cx.assert_editor_state(indoc! {"
11640 one.second_completionˇ
11641 two
11642 three
11643 additional edit
11644 "});
11645
11646 cx.set_state(indoc! {"
11647 one.second_completion
11648 twoˇ
11649 threeˇ
11650 additional edit
11651 "});
11652 cx.simulate_keystroke(" ");
11653 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11654 cx.simulate_keystroke("s");
11655 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11656
11657 cx.assert_editor_state(indoc! {"
11658 one.second_completion
11659 two sˇ
11660 three sˇ
11661 additional edit
11662 "});
11663 handle_completion_request(
11664 indoc! {"
11665 one.second_completion
11666 two s
11667 three <s|>
11668 additional edit
11669 "},
11670 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11671 true,
11672 counter.clone(),
11673 &mut cx,
11674 )
11675 .await;
11676 cx.condition(|editor, _| editor.context_menu_visible())
11677 .await;
11678 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11679
11680 cx.simulate_keystroke("i");
11681
11682 handle_completion_request(
11683 indoc! {"
11684 one.second_completion
11685 two si
11686 three <si|>
11687 additional edit
11688 "},
11689 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11690 true,
11691 counter.clone(),
11692 &mut cx,
11693 )
11694 .await;
11695 cx.condition(|editor, _| editor.context_menu_visible())
11696 .await;
11697 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11698
11699 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11700 editor
11701 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11702 .unwrap()
11703 });
11704 cx.assert_editor_state(indoc! {"
11705 one.second_completion
11706 two sixth_completionˇ
11707 three sixth_completionˇ
11708 additional edit
11709 "});
11710
11711 apply_additional_edits.await.unwrap();
11712
11713 update_test_language_settings(&mut cx, |settings| {
11714 settings.defaults.show_completions_on_input = Some(false);
11715 });
11716 cx.set_state("editorˇ");
11717 cx.simulate_keystroke(".");
11718 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11719 cx.simulate_keystrokes("c l o");
11720 cx.assert_editor_state("editor.cloˇ");
11721 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11722 cx.update_editor(|editor, window, cx| {
11723 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11724 });
11725 handle_completion_request(
11726 "editor.<clo|>",
11727 vec!["close", "clobber"],
11728 true,
11729 counter.clone(),
11730 &mut cx,
11731 )
11732 .await;
11733 cx.condition(|editor, _| editor.context_menu_visible())
11734 .await;
11735 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11736
11737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11738 editor
11739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11740 .unwrap()
11741 });
11742 cx.assert_editor_state("editor.closeˇ");
11743 handle_resolve_completion_request(&mut cx, None).await;
11744 apply_additional_edits.await.unwrap();
11745}
11746
11747#[gpui::test]
11748async fn test_completion_reuse(cx: &mut TestAppContext) {
11749 init_test(cx, |_| {});
11750
11751 let mut cx = EditorLspTestContext::new_rust(
11752 lsp::ServerCapabilities {
11753 completion_provider: Some(lsp::CompletionOptions {
11754 trigger_characters: Some(vec![".".to_string()]),
11755 ..Default::default()
11756 }),
11757 ..Default::default()
11758 },
11759 cx,
11760 )
11761 .await;
11762
11763 let counter = Arc::new(AtomicUsize::new(0));
11764 cx.set_state("objˇ");
11765 cx.simulate_keystroke(".");
11766
11767 // Initial completion request returns complete results
11768 let is_incomplete = false;
11769 handle_completion_request(
11770 "obj.|<>",
11771 vec!["a", "ab", "abc"],
11772 is_incomplete,
11773 counter.clone(),
11774 &mut cx,
11775 )
11776 .await;
11777 cx.run_until_parked();
11778 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11779 cx.assert_editor_state("obj.ˇ");
11780 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11781
11782 // Type "a" - filters existing completions
11783 cx.simulate_keystroke("a");
11784 cx.run_until_parked();
11785 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11786 cx.assert_editor_state("obj.aˇ");
11787 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11788
11789 // Type "b" - filters existing completions
11790 cx.simulate_keystroke("b");
11791 cx.run_until_parked();
11792 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11793 cx.assert_editor_state("obj.abˇ");
11794 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11795
11796 // Type "c" - filters existing completions
11797 cx.simulate_keystroke("c");
11798 cx.run_until_parked();
11799 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11800 cx.assert_editor_state("obj.abcˇ");
11801 check_displayed_completions(vec!["abc"], &mut cx);
11802
11803 // Backspace to delete "c" - filters existing completions
11804 cx.update_editor(|editor, window, cx| {
11805 editor.backspace(&Backspace, window, cx);
11806 });
11807 cx.run_until_parked();
11808 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11809 cx.assert_editor_state("obj.abˇ");
11810 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11811
11812 // Moving cursor to the left dismisses menu.
11813 cx.update_editor(|editor, window, cx| {
11814 editor.move_left(&MoveLeft, window, cx);
11815 });
11816 cx.run_until_parked();
11817 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11818 cx.assert_editor_state("obj.aˇb");
11819 cx.update_editor(|editor, _, _| {
11820 assert_eq!(editor.context_menu_visible(), false);
11821 });
11822
11823 // Type "b" - new request
11824 cx.simulate_keystroke("b");
11825 let is_incomplete = false;
11826 handle_completion_request(
11827 "obj.<ab|>a",
11828 vec!["ab", "abc"],
11829 is_incomplete,
11830 counter.clone(),
11831 &mut cx,
11832 )
11833 .await;
11834 cx.run_until_parked();
11835 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11836 cx.assert_editor_state("obj.abˇb");
11837 check_displayed_completions(vec!["ab", "abc"], &mut cx);
11838
11839 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
11840 cx.update_editor(|editor, window, cx| {
11841 editor.backspace(&Backspace, window, cx);
11842 });
11843 let is_incomplete = false;
11844 handle_completion_request(
11845 "obj.<a|>b",
11846 vec!["a", "ab", "abc"],
11847 is_incomplete,
11848 counter.clone(),
11849 &mut cx,
11850 )
11851 .await;
11852 cx.run_until_parked();
11853 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11854 cx.assert_editor_state("obj.aˇb");
11855 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11856
11857 // Backspace to delete "a" - dismisses menu.
11858 cx.update_editor(|editor, window, cx| {
11859 editor.backspace(&Backspace, window, cx);
11860 });
11861 cx.run_until_parked();
11862 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11863 cx.assert_editor_state("obj.ˇb");
11864 cx.update_editor(|editor, _, _| {
11865 assert_eq!(editor.context_menu_visible(), false);
11866 });
11867}
11868
11869#[gpui::test]
11870async fn test_word_completion(cx: &mut TestAppContext) {
11871 let lsp_fetch_timeout_ms = 10;
11872 init_test(cx, |language_settings| {
11873 language_settings.defaults.completions = Some(CompletionSettings {
11874 words: WordsCompletionMode::Fallback,
11875 lsp: true,
11876 lsp_fetch_timeout_ms: 10,
11877 lsp_insert_mode: LspInsertMode::Insert,
11878 });
11879 });
11880
11881 let mut cx = EditorLspTestContext::new_rust(
11882 lsp::ServerCapabilities {
11883 completion_provider: Some(lsp::CompletionOptions {
11884 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11885 ..lsp::CompletionOptions::default()
11886 }),
11887 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11888 ..lsp::ServerCapabilities::default()
11889 },
11890 cx,
11891 )
11892 .await;
11893
11894 let throttle_completions = Arc::new(AtomicBool::new(false));
11895
11896 let lsp_throttle_completions = throttle_completions.clone();
11897 let _completion_requests_handler =
11898 cx.lsp
11899 .server
11900 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11901 let lsp_throttle_completions = lsp_throttle_completions.clone();
11902 let cx = cx.clone();
11903 async move {
11904 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11905 cx.background_executor()
11906 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11907 .await;
11908 }
11909 Ok(Some(lsp::CompletionResponse::Array(vec![
11910 lsp::CompletionItem {
11911 label: "first".into(),
11912 ..lsp::CompletionItem::default()
11913 },
11914 lsp::CompletionItem {
11915 label: "last".into(),
11916 ..lsp::CompletionItem::default()
11917 },
11918 ])))
11919 }
11920 });
11921
11922 cx.set_state(indoc! {"
11923 oneˇ
11924 two
11925 three
11926 "});
11927 cx.simulate_keystroke(".");
11928 cx.executor().run_until_parked();
11929 cx.condition(|editor, _| editor.context_menu_visible())
11930 .await;
11931 cx.update_editor(|editor, window, cx| {
11932 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11933 {
11934 assert_eq!(
11935 completion_menu_entries(&menu),
11936 &["first", "last"],
11937 "When LSP server is fast to reply, no fallback word completions are used"
11938 );
11939 } else {
11940 panic!("expected completion menu to be open");
11941 }
11942 editor.cancel(&Cancel, window, cx);
11943 });
11944 cx.executor().run_until_parked();
11945 cx.condition(|editor, _| !editor.context_menu_visible())
11946 .await;
11947
11948 throttle_completions.store(true, atomic::Ordering::Release);
11949 cx.simulate_keystroke(".");
11950 cx.executor()
11951 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11952 cx.executor().run_until_parked();
11953 cx.condition(|editor, _| editor.context_menu_visible())
11954 .await;
11955 cx.update_editor(|editor, _, _| {
11956 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11957 {
11958 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11959 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11960 } else {
11961 panic!("expected completion menu to be open");
11962 }
11963 });
11964}
11965
11966#[gpui::test]
11967async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11968 init_test(cx, |language_settings| {
11969 language_settings.defaults.completions = Some(CompletionSettings {
11970 words: WordsCompletionMode::Enabled,
11971 lsp: true,
11972 lsp_fetch_timeout_ms: 0,
11973 lsp_insert_mode: LspInsertMode::Insert,
11974 });
11975 });
11976
11977 let mut cx = EditorLspTestContext::new_rust(
11978 lsp::ServerCapabilities {
11979 completion_provider: Some(lsp::CompletionOptions {
11980 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11981 ..lsp::CompletionOptions::default()
11982 }),
11983 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11984 ..lsp::ServerCapabilities::default()
11985 },
11986 cx,
11987 )
11988 .await;
11989
11990 let _completion_requests_handler =
11991 cx.lsp
11992 .server
11993 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11994 Ok(Some(lsp::CompletionResponse::Array(vec![
11995 lsp::CompletionItem {
11996 label: "first".into(),
11997 ..lsp::CompletionItem::default()
11998 },
11999 lsp::CompletionItem {
12000 label: "last".into(),
12001 ..lsp::CompletionItem::default()
12002 },
12003 ])))
12004 });
12005
12006 cx.set_state(indoc! {"ˇ
12007 first
12008 last
12009 second
12010 "});
12011 cx.simulate_keystroke(".");
12012 cx.executor().run_until_parked();
12013 cx.condition(|editor, _| editor.context_menu_visible())
12014 .await;
12015 cx.update_editor(|editor, _, _| {
12016 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12017 {
12018 assert_eq!(
12019 completion_menu_entries(&menu),
12020 &["first", "last", "second"],
12021 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12022 );
12023 } else {
12024 panic!("expected completion menu to be open");
12025 }
12026 });
12027}
12028
12029#[gpui::test]
12030async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12031 init_test(cx, |language_settings| {
12032 language_settings.defaults.completions = Some(CompletionSettings {
12033 words: WordsCompletionMode::Disabled,
12034 lsp: true,
12035 lsp_fetch_timeout_ms: 0,
12036 lsp_insert_mode: LspInsertMode::Insert,
12037 });
12038 });
12039
12040 let mut cx = EditorLspTestContext::new_rust(
12041 lsp::ServerCapabilities {
12042 completion_provider: Some(lsp::CompletionOptions {
12043 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12044 ..lsp::CompletionOptions::default()
12045 }),
12046 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12047 ..lsp::ServerCapabilities::default()
12048 },
12049 cx,
12050 )
12051 .await;
12052
12053 let _completion_requests_handler =
12054 cx.lsp
12055 .server
12056 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12057 panic!("LSP completions should not be queried when dealing with word completions")
12058 });
12059
12060 cx.set_state(indoc! {"ˇ
12061 first
12062 last
12063 second
12064 "});
12065 cx.update_editor(|editor, window, cx| {
12066 editor.show_word_completions(&ShowWordCompletions, window, cx);
12067 });
12068 cx.executor().run_until_parked();
12069 cx.condition(|editor, _| editor.context_menu_visible())
12070 .await;
12071 cx.update_editor(|editor, _, _| {
12072 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12073 {
12074 assert_eq!(
12075 completion_menu_entries(&menu),
12076 &["first", "last", "second"],
12077 "`ShowWordCompletions` action should show word completions"
12078 );
12079 } else {
12080 panic!("expected completion menu to be open");
12081 }
12082 });
12083
12084 cx.simulate_keystroke("l");
12085 cx.executor().run_until_parked();
12086 cx.condition(|editor, _| editor.context_menu_visible())
12087 .await;
12088 cx.update_editor(|editor, _, _| {
12089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12090 {
12091 assert_eq!(
12092 completion_menu_entries(&menu),
12093 &["last"],
12094 "After showing word completions, further editing should filter them and not query the LSP"
12095 );
12096 } else {
12097 panic!("expected completion menu to be open");
12098 }
12099 });
12100}
12101
12102#[gpui::test]
12103async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12104 init_test(cx, |language_settings| {
12105 language_settings.defaults.completions = Some(CompletionSettings {
12106 words: WordsCompletionMode::Fallback,
12107 lsp: false,
12108 lsp_fetch_timeout_ms: 0,
12109 lsp_insert_mode: LspInsertMode::Insert,
12110 });
12111 });
12112
12113 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12114
12115 cx.set_state(indoc! {"ˇ
12116 0_usize
12117 let
12118 33
12119 4.5f32
12120 "});
12121 cx.update_editor(|editor, window, cx| {
12122 editor.show_completions(&ShowCompletions::default(), window, cx);
12123 });
12124 cx.executor().run_until_parked();
12125 cx.condition(|editor, _| editor.context_menu_visible())
12126 .await;
12127 cx.update_editor(|editor, window, cx| {
12128 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12129 {
12130 assert_eq!(
12131 completion_menu_entries(&menu),
12132 &["let"],
12133 "With no digits in the completion query, no digits should be in the word completions"
12134 );
12135 } else {
12136 panic!("expected completion menu to be open");
12137 }
12138 editor.cancel(&Cancel, window, cx);
12139 });
12140
12141 cx.set_state(indoc! {"3ˇ
12142 0_usize
12143 let
12144 3
12145 33.35f32
12146 "});
12147 cx.update_editor(|editor, window, cx| {
12148 editor.show_completions(&ShowCompletions::default(), window, cx);
12149 });
12150 cx.executor().run_until_parked();
12151 cx.condition(|editor, _| editor.context_menu_visible())
12152 .await;
12153 cx.update_editor(|editor, _, _| {
12154 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12155 {
12156 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12157 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12158 } else {
12159 panic!("expected completion menu to be open");
12160 }
12161 });
12162}
12163
12164fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12165 let position = || lsp::Position {
12166 line: params.text_document_position.position.line,
12167 character: params.text_document_position.position.character,
12168 };
12169 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12170 range: lsp::Range {
12171 start: position(),
12172 end: position(),
12173 },
12174 new_text: text.to_string(),
12175 }))
12176}
12177
12178#[gpui::test]
12179async fn test_multiline_completion(cx: &mut TestAppContext) {
12180 init_test(cx, |_| {});
12181
12182 let fs = FakeFs::new(cx.executor());
12183 fs.insert_tree(
12184 path!("/a"),
12185 json!({
12186 "main.ts": "a",
12187 }),
12188 )
12189 .await;
12190
12191 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12193 let typescript_language = Arc::new(Language::new(
12194 LanguageConfig {
12195 name: "TypeScript".into(),
12196 matcher: LanguageMatcher {
12197 path_suffixes: vec!["ts".to_string()],
12198 ..LanguageMatcher::default()
12199 },
12200 line_comments: vec!["// ".into()],
12201 ..LanguageConfig::default()
12202 },
12203 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12204 ));
12205 language_registry.add(typescript_language.clone());
12206 let mut fake_servers = language_registry.register_fake_lsp(
12207 "TypeScript",
12208 FakeLspAdapter {
12209 capabilities: lsp::ServerCapabilities {
12210 completion_provider: Some(lsp::CompletionOptions {
12211 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12212 ..lsp::CompletionOptions::default()
12213 }),
12214 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12215 ..lsp::ServerCapabilities::default()
12216 },
12217 // Emulate vtsls label generation
12218 label_for_completion: Some(Box::new(|item, _| {
12219 let text = if let Some(description) = item
12220 .label_details
12221 .as_ref()
12222 .and_then(|label_details| label_details.description.as_ref())
12223 {
12224 format!("{} {}", item.label, description)
12225 } else if let Some(detail) = &item.detail {
12226 format!("{} {}", item.label, detail)
12227 } else {
12228 item.label.clone()
12229 };
12230 let len = text.len();
12231 Some(language::CodeLabel {
12232 text,
12233 runs: Vec::new(),
12234 filter_range: 0..len,
12235 })
12236 })),
12237 ..FakeLspAdapter::default()
12238 },
12239 );
12240 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12241 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12242 let worktree_id = workspace
12243 .update(cx, |workspace, _window, cx| {
12244 workspace.project().update(cx, |project, cx| {
12245 project.worktrees(cx).next().unwrap().read(cx).id()
12246 })
12247 })
12248 .unwrap();
12249 let _buffer = project
12250 .update(cx, |project, cx| {
12251 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12252 })
12253 .await
12254 .unwrap();
12255 let editor = workspace
12256 .update(cx, |workspace, window, cx| {
12257 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12258 })
12259 .unwrap()
12260 .await
12261 .unwrap()
12262 .downcast::<Editor>()
12263 .unwrap();
12264 let fake_server = fake_servers.next().await.unwrap();
12265
12266 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12267 let multiline_label_2 = "a\nb\nc\n";
12268 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12269 let multiline_description = "d\ne\nf\n";
12270 let multiline_detail_2 = "g\nh\ni\n";
12271
12272 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12273 move |params, _| async move {
12274 Ok(Some(lsp::CompletionResponse::Array(vec![
12275 lsp::CompletionItem {
12276 label: multiline_label.to_string(),
12277 text_edit: gen_text_edit(¶ms, "new_text_1"),
12278 ..lsp::CompletionItem::default()
12279 },
12280 lsp::CompletionItem {
12281 label: "single line label 1".to_string(),
12282 detail: Some(multiline_detail.to_string()),
12283 text_edit: gen_text_edit(¶ms, "new_text_2"),
12284 ..lsp::CompletionItem::default()
12285 },
12286 lsp::CompletionItem {
12287 label: "single line label 2".to_string(),
12288 label_details: Some(lsp::CompletionItemLabelDetails {
12289 description: Some(multiline_description.to_string()),
12290 detail: None,
12291 }),
12292 text_edit: gen_text_edit(¶ms, "new_text_2"),
12293 ..lsp::CompletionItem::default()
12294 },
12295 lsp::CompletionItem {
12296 label: multiline_label_2.to_string(),
12297 detail: Some(multiline_detail_2.to_string()),
12298 text_edit: gen_text_edit(¶ms, "new_text_3"),
12299 ..lsp::CompletionItem::default()
12300 },
12301 lsp::CompletionItem {
12302 label: "Label with many spaces and \t but without newlines".to_string(),
12303 detail: Some(
12304 "Details with many spaces and \t but without newlines".to_string(),
12305 ),
12306 text_edit: gen_text_edit(¶ms, "new_text_4"),
12307 ..lsp::CompletionItem::default()
12308 },
12309 ])))
12310 },
12311 );
12312
12313 editor.update_in(cx, |editor, window, cx| {
12314 cx.focus_self(window);
12315 editor.move_to_end(&MoveToEnd, window, cx);
12316 editor.handle_input(".", window, cx);
12317 });
12318 cx.run_until_parked();
12319 completion_handle.next().await.unwrap();
12320
12321 editor.update(cx, |editor, _| {
12322 assert!(editor.context_menu_visible());
12323 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12324 {
12325 let completion_labels = menu
12326 .completions
12327 .borrow()
12328 .iter()
12329 .map(|c| c.label.text.clone())
12330 .collect::<Vec<_>>();
12331 assert_eq!(
12332 completion_labels,
12333 &[
12334 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12335 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12336 "single line label 2 d e f ",
12337 "a b c g h i ",
12338 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12339 ],
12340 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12341 );
12342
12343 for completion in menu
12344 .completions
12345 .borrow()
12346 .iter() {
12347 assert_eq!(
12348 completion.label.filter_range,
12349 0..completion.label.text.len(),
12350 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12351 );
12352 }
12353 } else {
12354 panic!("expected completion menu to be open");
12355 }
12356 });
12357}
12358
12359#[gpui::test]
12360async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12361 init_test(cx, |_| {});
12362 let mut cx = EditorLspTestContext::new_rust(
12363 lsp::ServerCapabilities {
12364 completion_provider: Some(lsp::CompletionOptions {
12365 trigger_characters: Some(vec![".".to_string()]),
12366 ..Default::default()
12367 }),
12368 ..Default::default()
12369 },
12370 cx,
12371 )
12372 .await;
12373 cx.lsp
12374 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12375 Ok(Some(lsp::CompletionResponse::Array(vec![
12376 lsp::CompletionItem {
12377 label: "first".into(),
12378 ..Default::default()
12379 },
12380 lsp::CompletionItem {
12381 label: "last".into(),
12382 ..Default::default()
12383 },
12384 ])))
12385 });
12386 cx.set_state("variableˇ");
12387 cx.simulate_keystroke(".");
12388 cx.executor().run_until_parked();
12389
12390 cx.update_editor(|editor, _, _| {
12391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12392 {
12393 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12394 } else {
12395 panic!("expected completion menu to be open");
12396 }
12397 });
12398
12399 cx.update_editor(|editor, window, cx| {
12400 editor.move_page_down(&MovePageDown::default(), window, cx);
12401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12402 {
12403 assert!(
12404 menu.selected_item == 1,
12405 "expected PageDown to select the last item from the context menu"
12406 );
12407 } else {
12408 panic!("expected completion menu to stay open after PageDown");
12409 }
12410 });
12411
12412 cx.update_editor(|editor, window, cx| {
12413 editor.move_page_up(&MovePageUp::default(), window, cx);
12414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12415 {
12416 assert!(
12417 menu.selected_item == 0,
12418 "expected PageUp to select the first item from the context menu"
12419 );
12420 } else {
12421 panic!("expected completion menu to stay open after PageUp");
12422 }
12423 });
12424}
12425
12426#[gpui::test]
12427async fn test_as_is_completions(cx: &mut TestAppContext) {
12428 init_test(cx, |_| {});
12429 let mut cx = EditorLspTestContext::new_rust(
12430 lsp::ServerCapabilities {
12431 completion_provider: Some(lsp::CompletionOptions {
12432 ..Default::default()
12433 }),
12434 ..Default::default()
12435 },
12436 cx,
12437 )
12438 .await;
12439 cx.lsp
12440 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12441 Ok(Some(lsp::CompletionResponse::Array(vec![
12442 lsp::CompletionItem {
12443 label: "unsafe".into(),
12444 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12445 range: lsp::Range {
12446 start: lsp::Position {
12447 line: 1,
12448 character: 2,
12449 },
12450 end: lsp::Position {
12451 line: 1,
12452 character: 3,
12453 },
12454 },
12455 new_text: "unsafe".to_string(),
12456 })),
12457 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12458 ..Default::default()
12459 },
12460 ])))
12461 });
12462 cx.set_state("fn a() {}\n nˇ");
12463 cx.executor().run_until_parked();
12464 cx.update_editor(|editor, window, cx| {
12465 editor.show_completions(
12466 &ShowCompletions {
12467 trigger: Some("\n".into()),
12468 },
12469 window,
12470 cx,
12471 );
12472 });
12473 cx.executor().run_until_parked();
12474
12475 cx.update_editor(|editor, window, cx| {
12476 editor.confirm_completion(&Default::default(), window, cx)
12477 });
12478 cx.executor().run_until_parked();
12479 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12480}
12481
12482#[gpui::test]
12483async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12484 init_test(cx, |_| {});
12485
12486 let mut cx = EditorLspTestContext::new_rust(
12487 lsp::ServerCapabilities {
12488 completion_provider: Some(lsp::CompletionOptions {
12489 trigger_characters: Some(vec![".".to_string()]),
12490 resolve_provider: Some(true),
12491 ..Default::default()
12492 }),
12493 ..Default::default()
12494 },
12495 cx,
12496 )
12497 .await;
12498
12499 cx.set_state("fn main() { let a = 2ˇ; }");
12500 cx.simulate_keystroke(".");
12501 let completion_item = lsp::CompletionItem {
12502 label: "Some".into(),
12503 kind: Some(lsp::CompletionItemKind::SNIPPET),
12504 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12505 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12506 kind: lsp::MarkupKind::Markdown,
12507 value: "```rust\nSome(2)\n```".to_string(),
12508 })),
12509 deprecated: Some(false),
12510 sort_text: Some("Some".to_string()),
12511 filter_text: Some("Some".to_string()),
12512 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12513 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12514 range: lsp::Range {
12515 start: lsp::Position {
12516 line: 0,
12517 character: 22,
12518 },
12519 end: lsp::Position {
12520 line: 0,
12521 character: 22,
12522 },
12523 },
12524 new_text: "Some(2)".to_string(),
12525 })),
12526 additional_text_edits: Some(vec![lsp::TextEdit {
12527 range: lsp::Range {
12528 start: lsp::Position {
12529 line: 0,
12530 character: 20,
12531 },
12532 end: lsp::Position {
12533 line: 0,
12534 character: 22,
12535 },
12536 },
12537 new_text: "".to_string(),
12538 }]),
12539 ..Default::default()
12540 };
12541
12542 let closure_completion_item = completion_item.clone();
12543 let counter = Arc::new(AtomicUsize::new(0));
12544 let counter_clone = counter.clone();
12545 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12546 let task_completion_item = closure_completion_item.clone();
12547 counter_clone.fetch_add(1, atomic::Ordering::Release);
12548 async move {
12549 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12550 is_incomplete: true,
12551 item_defaults: None,
12552 items: vec![task_completion_item],
12553 })))
12554 }
12555 });
12556
12557 cx.condition(|editor, _| editor.context_menu_visible())
12558 .await;
12559 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12560 assert!(request.next().await.is_some());
12561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12562
12563 cx.simulate_keystrokes("S o m");
12564 cx.condition(|editor, _| editor.context_menu_visible())
12565 .await;
12566 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12567 assert!(request.next().await.is_some());
12568 assert!(request.next().await.is_some());
12569 assert!(request.next().await.is_some());
12570 request.close();
12571 assert!(request.next().await.is_none());
12572 assert_eq!(
12573 counter.load(atomic::Ordering::Acquire),
12574 4,
12575 "With the completions menu open, only one LSP request should happen per input"
12576 );
12577}
12578
12579#[gpui::test]
12580async fn test_toggle_comment(cx: &mut TestAppContext) {
12581 init_test(cx, |_| {});
12582 let mut cx = EditorTestContext::new(cx).await;
12583 let language = Arc::new(Language::new(
12584 LanguageConfig {
12585 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12586 ..Default::default()
12587 },
12588 Some(tree_sitter_rust::LANGUAGE.into()),
12589 ));
12590 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12591
12592 // If multiple selections intersect a line, the line is only toggled once.
12593 cx.set_state(indoc! {"
12594 fn a() {
12595 «//b();
12596 ˇ»// «c();
12597 //ˇ» d();
12598 }
12599 "});
12600
12601 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12602
12603 cx.assert_editor_state(indoc! {"
12604 fn a() {
12605 «b();
12606 c();
12607 ˇ» d();
12608 }
12609 "});
12610
12611 // The comment prefix is inserted at the same column for every line in a
12612 // selection.
12613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12614
12615 cx.assert_editor_state(indoc! {"
12616 fn a() {
12617 // «b();
12618 // c();
12619 ˇ»// d();
12620 }
12621 "});
12622
12623 // If a selection ends at the beginning of a line, that line is not toggled.
12624 cx.set_selections_state(indoc! {"
12625 fn a() {
12626 // b();
12627 «// c();
12628 ˇ» // d();
12629 }
12630 "});
12631
12632 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12633
12634 cx.assert_editor_state(indoc! {"
12635 fn a() {
12636 // b();
12637 «c();
12638 ˇ» // d();
12639 }
12640 "});
12641
12642 // If a selection span a single line and is empty, the line is toggled.
12643 cx.set_state(indoc! {"
12644 fn a() {
12645 a();
12646 b();
12647 ˇ
12648 }
12649 "});
12650
12651 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12652
12653 cx.assert_editor_state(indoc! {"
12654 fn a() {
12655 a();
12656 b();
12657 //•ˇ
12658 }
12659 "});
12660
12661 // If a selection span multiple lines, empty lines are not toggled.
12662 cx.set_state(indoc! {"
12663 fn a() {
12664 «a();
12665
12666 c();ˇ»
12667 }
12668 "});
12669
12670 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12671
12672 cx.assert_editor_state(indoc! {"
12673 fn a() {
12674 // «a();
12675
12676 // c();ˇ»
12677 }
12678 "});
12679
12680 // If a selection includes multiple comment prefixes, all lines are uncommented.
12681 cx.set_state(indoc! {"
12682 fn a() {
12683 «// a();
12684 /// b();
12685 //! c();ˇ»
12686 }
12687 "});
12688
12689 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12690
12691 cx.assert_editor_state(indoc! {"
12692 fn a() {
12693 «a();
12694 b();
12695 c();ˇ»
12696 }
12697 "});
12698}
12699
12700#[gpui::test]
12701async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12702 init_test(cx, |_| {});
12703 let mut cx = EditorTestContext::new(cx).await;
12704 let language = Arc::new(Language::new(
12705 LanguageConfig {
12706 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12707 ..Default::default()
12708 },
12709 Some(tree_sitter_rust::LANGUAGE.into()),
12710 ));
12711 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12712
12713 let toggle_comments = &ToggleComments {
12714 advance_downwards: false,
12715 ignore_indent: true,
12716 };
12717
12718 // If multiple selections intersect a line, the line is only toggled once.
12719 cx.set_state(indoc! {"
12720 fn a() {
12721 // «b();
12722 // c();
12723 // ˇ» d();
12724 }
12725 "});
12726
12727 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12728
12729 cx.assert_editor_state(indoc! {"
12730 fn a() {
12731 «b();
12732 c();
12733 ˇ» d();
12734 }
12735 "});
12736
12737 // The comment prefix is inserted at the beginning of each line
12738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12739
12740 cx.assert_editor_state(indoc! {"
12741 fn a() {
12742 // «b();
12743 // c();
12744 // ˇ» d();
12745 }
12746 "});
12747
12748 // If a selection ends at the beginning of a line, that line is not toggled.
12749 cx.set_selections_state(indoc! {"
12750 fn a() {
12751 // b();
12752 // «c();
12753 ˇ»// d();
12754 }
12755 "});
12756
12757 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12758
12759 cx.assert_editor_state(indoc! {"
12760 fn a() {
12761 // b();
12762 «c();
12763 ˇ»// d();
12764 }
12765 "});
12766
12767 // If a selection span a single line and is empty, the line is toggled.
12768 cx.set_state(indoc! {"
12769 fn a() {
12770 a();
12771 b();
12772 ˇ
12773 }
12774 "});
12775
12776 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12777
12778 cx.assert_editor_state(indoc! {"
12779 fn a() {
12780 a();
12781 b();
12782 //ˇ
12783 }
12784 "});
12785
12786 // If a selection span multiple lines, empty lines are not toggled.
12787 cx.set_state(indoc! {"
12788 fn a() {
12789 «a();
12790
12791 c();ˇ»
12792 }
12793 "});
12794
12795 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12796
12797 cx.assert_editor_state(indoc! {"
12798 fn a() {
12799 // «a();
12800
12801 // c();ˇ»
12802 }
12803 "});
12804
12805 // If a selection includes multiple comment prefixes, all lines are uncommented.
12806 cx.set_state(indoc! {"
12807 fn a() {
12808 // «a();
12809 /// b();
12810 //! c();ˇ»
12811 }
12812 "});
12813
12814 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12815
12816 cx.assert_editor_state(indoc! {"
12817 fn a() {
12818 «a();
12819 b();
12820 c();ˇ»
12821 }
12822 "});
12823}
12824
12825#[gpui::test]
12826async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12827 init_test(cx, |_| {});
12828
12829 let language = Arc::new(Language::new(
12830 LanguageConfig {
12831 line_comments: vec!["// ".into()],
12832 ..Default::default()
12833 },
12834 Some(tree_sitter_rust::LANGUAGE.into()),
12835 ));
12836
12837 let mut cx = EditorTestContext::new(cx).await;
12838
12839 cx.language_registry().add(language.clone());
12840 cx.update_buffer(|buffer, cx| {
12841 buffer.set_language(Some(language), cx);
12842 });
12843
12844 let toggle_comments = &ToggleComments {
12845 advance_downwards: true,
12846 ignore_indent: false,
12847 };
12848
12849 // Single cursor on one line -> advance
12850 // Cursor moves horizontally 3 characters as well on non-blank line
12851 cx.set_state(indoc!(
12852 "fn a() {
12853 ˇdog();
12854 cat();
12855 }"
12856 ));
12857 cx.update_editor(|editor, window, cx| {
12858 editor.toggle_comments(toggle_comments, window, cx);
12859 });
12860 cx.assert_editor_state(indoc!(
12861 "fn a() {
12862 // dog();
12863 catˇ();
12864 }"
12865 ));
12866
12867 // Single selection on one line -> don't advance
12868 cx.set_state(indoc!(
12869 "fn a() {
12870 «dog()ˇ»;
12871 cat();
12872 }"
12873 ));
12874 cx.update_editor(|editor, window, cx| {
12875 editor.toggle_comments(toggle_comments, window, cx);
12876 });
12877 cx.assert_editor_state(indoc!(
12878 "fn a() {
12879 // «dog()ˇ»;
12880 cat();
12881 }"
12882 ));
12883
12884 // Multiple cursors on one line -> advance
12885 cx.set_state(indoc!(
12886 "fn a() {
12887 ˇdˇog();
12888 cat();
12889 }"
12890 ));
12891 cx.update_editor(|editor, window, cx| {
12892 editor.toggle_comments(toggle_comments, window, cx);
12893 });
12894 cx.assert_editor_state(indoc!(
12895 "fn a() {
12896 // dog();
12897 catˇ(ˇ);
12898 }"
12899 ));
12900
12901 // Multiple cursors on one line, with selection -> don't advance
12902 cx.set_state(indoc!(
12903 "fn a() {
12904 ˇdˇog«()ˇ»;
12905 cat();
12906 }"
12907 ));
12908 cx.update_editor(|editor, window, cx| {
12909 editor.toggle_comments(toggle_comments, window, cx);
12910 });
12911 cx.assert_editor_state(indoc!(
12912 "fn a() {
12913 // ˇdˇog«()ˇ»;
12914 cat();
12915 }"
12916 ));
12917
12918 // Single cursor on one line -> advance
12919 // Cursor moves to column 0 on blank line
12920 cx.set_state(indoc!(
12921 "fn a() {
12922 ˇdog();
12923
12924 cat();
12925 }"
12926 ));
12927 cx.update_editor(|editor, window, cx| {
12928 editor.toggle_comments(toggle_comments, window, cx);
12929 });
12930 cx.assert_editor_state(indoc!(
12931 "fn a() {
12932 // dog();
12933 ˇ
12934 cat();
12935 }"
12936 ));
12937
12938 // Single cursor on one line -> advance
12939 // Cursor starts and ends at column 0
12940 cx.set_state(indoc!(
12941 "fn a() {
12942 ˇ dog();
12943 cat();
12944 }"
12945 ));
12946 cx.update_editor(|editor, window, cx| {
12947 editor.toggle_comments(toggle_comments, window, cx);
12948 });
12949 cx.assert_editor_state(indoc!(
12950 "fn a() {
12951 // dog();
12952 ˇ cat();
12953 }"
12954 ));
12955}
12956
12957#[gpui::test]
12958async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12959 init_test(cx, |_| {});
12960
12961 let mut cx = EditorTestContext::new(cx).await;
12962
12963 let html_language = Arc::new(
12964 Language::new(
12965 LanguageConfig {
12966 name: "HTML".into(),
12967 block_comment: Some(("<!-- ".into(), " -->".into())),
12968 ..Default::default()
12969 },
12970 Some(tree_sitter_html::LANGUAGE.into()),
12971 )
12972 .with_injection_query(
12973 r#"
12974 (script_element
12975 (raw_text) @injection.content
12976 (#set! injection.language "javascript"))
12977 "#,
12978 )
12979 .unwrap(),
12980 );
12981
12982 let javascript_language = Arc::new(Language::new(
12983 LanguageConfig {
12984 name: "JavaScript".into(),
12985 line_comments: vec!["// ".into()],
12986 ..Default::default()
12987 },
12988 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12989 ));
12990
12991 cx.language_registry().add(html_language.clone());
12992 cx.language_registry().add(javascript_language.clone());
12993 cx.update_buffer(|buffer, cx| {
12994 buffer.set_language(Some(html_language), cx);
12995 });
12996
12997 // Toggle comments for empty selections
12998 cx.set_state(
12999 &r#"
13000 <p>A</p>ˇ
13001 <p>B</p>ˇ
13002 <p>C</p>ˇ
13003 "#
13004 .unindent(),
13005 );
13006 cx.update_editor(|editor, window, cx| {
13007 editor.toggle_comments(&ToggleComments::default(), window, cx)
13008 });
13009 cx.assert_editor_state(
13010 &r#"
13011 <!-- <p>A</p>ˇ -->
13012 <!-- <p>B</p>ˇ -->
13013 <!-- <p>C</p>ˇ -->
13014 "#
13015 .unindent(),
13016 );
13017 cx.update_editor(|editor, window, cx| {
13018 editor.toggle_comments(&ToggleComments::default(), window, cx)
13019 });
13020 cx.assert_editor_state(
13021 &r#"
13022 <p>A</p>ˇ
13023 <p>B</p>ˇ
13024 <p>C</p>ˇ
13025 "#
13026 .unindent(),
13027 );
13028
13029 // Toggle comments for mixture of empty and non-empty selections, where
13030 // multiple selections occupy a given line.
13031 cx.set_state(
13032 &r#"
13033 <p>A«</p>
13034 <p>ˇ»B</p>ˇ
13035 <p>C«</p>
13036 <p>ˇ»D</p>ˇ
13037 "#
13038 .unindent(),
13039 );
13040
13041 cx.update_editor(|editor, window, cx| {
13042 editor.toggle_comments(&ToggleComments::default(), window, cx)
13043 });
13044 cx.assert_editor_state(
13045 &r#"
13046 <!-- <p>A«</p>
13047 <p>ˇ»B</p>ˇ -->
13048 <!-- <p>C«</p>
13049 <p>ˇ»D</p>ˇ -->
13050 "#
13051 .unindent(),
13052 );
13053 cx.update_editor(|editor, window, cx| {
13054 editor.toggle_comments(&ToggleComments::default(), window, cx)
13055 });
13056 cx.assert_editor_state(
13057 &r#"
13058 <p>A«</p>
13059 <p>ˇ»B</p>ˇ
13060 <p>C«</p>
13061 <p>ˇ»D</p>ˇ
13062 "#
13063 .unindent(),
13064 );
13065
13066 // Toggle comments when different languages are active for different
13067 // selections.
13068 cx.set_state(
13069 &r#"
13070 ˇ<script>
13071 ˇvar x = new Y();
13072 ˇ</script>
13073 "#
13074 .unindent(),
13075 );
13076 cx.executor().run_until_parked();
13077 cx.update_editor(|editor, window, cx| {
13078 editor.toggle_comments(&ToggleComments::default(), window, cx)
13079 });
13080 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13081 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13082 cx.assert_editor_state(
13083 &r#"
13084 <!-- ˇ<script> -->
13085 // ˇvar x = new Y();
13086 <!-- ˇ</script> -->
13087 "#
13088 .unindent(),
13089 );
13090}
13091
13092#[gpui::test]
13093fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13094 init_test(cx, |_| {});
13095
13096 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13097 let multibuffer = cx.new(|cx| {
13098 let mut multibuffer = MultiBuffer::new(ReadWrite);
13099 multibuffer.push_excerpts(
13100 buffer.clone(),
13101 [
13102 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13103 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13104 ],
13105 cx,
13106 );
13107 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13108 multibuffer
13109 });
13110
13111 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13112 editor.update_in(cx, |editor, window, cx| {
13113 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13114 editor.change_selections(None, window, cx, |s| {
13115 s.select_ranges([
13116 Point::new(0, 0)..Point::new(0, 0),
13117 Point::new(1, 0)..Point::new(1, 0),
13118 ])
13119 });
13120
13121 editor.handle_input("X", window, cx);
13122 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13123 assert_eq!(
13124 editor.selections.ranges(cx),
13125 [
13126 Point::new(0, 1)..Point::new(0, 1),
13127 Point::new(1, 1)..Point::new(1, 1),
13128 ]
13129 );
13130
13131 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13132 editor.change_selections(None, window, cx, |s| {
13133 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13134 });
13135 editor.backspace(&Default::default(), window, cx);
13136 assert_eq!(editor.text(cx), "Xa\nbbb");
13137 assert_eq!(
13138 editor.selections.ranges(cx),
13139 [Point::new(1, 0)..Point::new(1, 0)]
13140 );
13141
13142 editor.change_selections(None, window, cx, |s| {
13143 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13144 });
13145 editor.backspace(&Default::default(), window, cx);
13146 assert_eq!(editor.text(cx), "X\nbb");
13147 assert_eq!(
13148 editor.selections.ranges(cx),
13149 [Point::new(0, 1)..Point::new(0, 1)]
13150 );
13151 });
13152}
13153
13154#[gpui::test]
13155fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13156 init_test(cx, |_| {});
13157
13158 let markers = vec![('[', ']').into(), ('(', ')').into()];
13159 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13160 indoc! {"
13161 [aaaa
13162 (bbbb]
13163 cccc)",
13164 },
13165 markers.clone(),
13166 );
13167 let excerpt_ranges = markers.into_iter().map(|marker| {
13168 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13169 ExcerptRange::new(context.clone())
13170 });
13171 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13172 let multibuffer = cx.new(|cx| {
13173 let mut multibuffer = MultiBuffer::new(ReadWrite);
13174 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13175 multibuffer
13176 });
13177
13178 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13179 editor.update_in(cx, |editor, window, cx| {
13180 let (expected_text, selection_ranges) = marked_text_ranges(
13181 indoc! {"
13182 aaaa
13183 bˇbbb
13184 bˇbbˇb
13185 cccc"
13186 },
13187 true,
13188 );
13189 assert_eq!(editor.text(cx), expected_text);
13190 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13191
13192 editor.handle_input("X", window, cx);
13193
13194 let (expected_text, expected_selections) = marked_text_ranges(
13195 indoc! {"
13196 aaaa
13197 bXˇbbXb
13198 bXˇbbXˇb
13199 cccc"
13200 },
13201 false,
13202 );
13203 assert_eq!(editor.text(cx), expected_text);
13204 assert_eq!(editor.selections.ranges(cx), expected_selections);
13205
13206 editor.newline(&Newline, window, cx);
13207 let (expected_text, expected_selections) = marked_text_ranges(
13208 indoc! {"
13209 aaaa
13210 bX
13211 ˇbbX
13212 b
13213 bX
13214 ˇbbX
13215 ˇb
13216 cccc"
13217 },
13218 false,
13219 );
13220 assert_eq!(editor.text(cx), expected_text);
13221 assert_eq!(editor.selections.ranges(cx), expected_selections);
13222 });
13223}
13224
13225#[gpui::test]
13226fn test_refresh_selections(cx: &mut TestAppContext) {
13227 init_test(cx, |_| {});
13228
13229 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13230 let mut excerpt1_id = None;
13231 let multibuffer = cx.new(|cx| {
13232 let mut multibuffer = MultiBuffer::new(ReadWrite);
13233 excerpt1_id = multibuffer
13234 .push_excerpts(
13235 buffer.clone(),
13236 [
13237 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13238 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13239 ],
13240 cx,
13241 )
13242 .into_iter()
13243 .next();
13244 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13245 multibuffer
13246 });
13247
13248 let editor = cx.add_window(|window, cx| {
13249 let mut editor = build_editor(multibuffer.clone(), window, cx);
13250 let snapshot = editor.snapshot(window, cx);
13251 editor.change_selections(None, window, cx, |s| {
13252 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13253 });
13254 editor.begin_selection(
13255 Point::new(2, 1).to_display_point(&snapshot),
13256 true,
13257 1,
13258 window,
13259 cx,
13260 );
13261 assert_eq!(
13262 editor.selections.ranges(cx),
13263 [
13264 Point::new(1, 3)..Point::new(1, 3),
13265 Point::new(2, 1)..Point::new(2, 1),
13266 ]
13267 );
13268 editor
13269 });
13270
13271 // Refreshing selections is a no-op when excerpts haven't changed.
13272 _ = editor.update(cx, |editor, window, cx| {
13273 editor.change_selections(None, window, cx, |s| s.refresh());
13274 assert_eq!(
13275 editor.selections.ranges(cx),
13276 [
13277 Point::new(1, 3)..Point::new(1, 3),
13278 Point::new(2, 1)..Point::new(2, 1),
13279 ]
13280 );
13281 });
13282
13283 multibuffer.update(cx, |multibuffer, cx| {
13284 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13285 });
13286 _ = editor.update(cx, |editor, window, cx| {
13287 // Removing an excerpt causes the first selection to become degenerate.
13288 assert_eq!(
13289 editor.selections.ranges(cx),
13290 [
13291 Point::new(0, 0)..Point::new(0, 0),
13292 Point::new(0, 1)..Point::new(0, 1)
13293 ]
13294 );
13295
13296 // Refreshing selections will relocate the first selection to the original buffer
13297 // location.
13298 editor.change_selections(None, window, cx, |s| s.refresh());
13299 assert_eq!(
13300 editor.selections.ranges(cx),
13301 [
13302 Point::new(0, 1)..Point::new(0, 1),
13303 Point::new(0, 3)..Point::new(0, 3)
13304 ]
13305 );
13306 assert!(editor.selections.pending_anchor().is_some());
13307 });
13308}
13309
13310#[gpui::test]
13311fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13312 init_test(cx, |_| {});
13313
13314 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13315 let mut excerpt1_id = None;
13316 let multibuffer = cx.new(|cx| {
13317 let mut multibuffer = MultiBuffer::new(ReadWrite);
13318 excerpt1_id = multibuffer
13319 .push_excerpts(
13320 buffer.clone(),
13321 [
13322 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13323 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13324 ],
13325 cx,
13326 )
13327 .into_iter()
13328 .next();
13329 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13330 multibuffer
13331 });
13332
13333 let editor = cx.add_window(|window, cx| {
13334 let mut editor = build_editor(multibuffer.clone(), window, cx);
13335 let snapshot = editor.snapshot(window, cx);
13336 editor.begin_selection(
13337 Point::new(1, 3).to_display_point(&snapshot),
13338 false,
13339 1,
13340 window,
13341 cx,
13342 );
13343 assert_eq!(
13344 editor.selections.ranges(cx),
13345 [Point::new(1, 3)..Point::new(1, 3)]
13346 );
13347 editor
13348 });
13349
13350 multibuffer.update(cx, |multibuffer, cx| {
13351 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13352 });
13353 _ = editor.update(cx, |editor, window, cx| {
13354 assert_eq!(
13355 editor.selections.ranges(cx),
13356 [Point::new(0, 0)..Point::new(0, 0)]
13357 );
13358
13359 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13360 editor.change_selections(None, window, cx, |s| s.refresh());
13361 assert_eq!(
13362 editor.selections.ranges(cx),
13363 [Point::new(0, 3)..Point::new(0, 3)]
13364 );
13365 assert!(editor.selections.pending_anchor().is_some());
13366 });
13367}
13368
13369#[gpui::test]
13370async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13371 init_test(cx, |_| {});
13372
13373 let language = Arc::new(
13374 Language::new(
13375 LanguageConfig {
13376 brackets: BracketPairConfig {
13377 pairs: vec![
13378 BracketPair {
13379 start: "{".to_string(),
13380 end: "}".to_string(),
13381 close: true,
13382 surround: true,
13383 newline: true,
13384 },
13385 BracketPair {
13386 start: "/* ".to_string(),
13387 end: " */".to_string(),
13388 close: true,
13389 surround: true,
13390 newline: true,
13391 },
13392 ],
13393 ..Default::default()
13394 },
13395 ..Default::default()
13396 },
13397 Some(tree_sitter_rust::LANGUAGE.into()),
13398 )
13399 .with_indents_query("")
13400 .unwrap(),
13401 );
13402
13403 let text = concat!(
13404 "{ }\n", //
13405 " x\n", //
13406 " /* */\n", //
13407 "x\n", //
13408 "{{} }\n", //
13409 );
13410
13411 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13412 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13413 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13414 editor
13415 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13416 .await;
13417
13418 editor.update_in(cx, |editor, window, cx| {
13419 editor.change_selections(None, window, cx, |s| {
13420 s.select_display_ranges([
13421 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13422 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13423 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13424 ])
13425 });
13426 editor.newline(&Newline, window, cx);
13427
13428 assert_eq!(
13429 editor.buffer().read(cx).read(cx).text(),
13430 concat!(
13431 "{ \n", // Suppress rustfmt
13432 "\n", //
13433 "}\n", //
13434 " x\n", //
13435 " /* \n", //
13436 " \n", //
13437 " */\n", //
13438 "x\n", //
13439 "{{} \n", //
13440 "}\n", //
13441 )
13442 );
13443 });
13444}
13445
13446#[gpui::test]
13447fn test_highlighted_ranges(cx: &mut TestAppContext) {
13448 init_test(cx, |_| {});
13449
13450 let editor = cx.add_window(|window, cx| {
13451 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13452 build_editor(buffer.clone(), window, cx)
13453 });
13454
13455 _ = editor.update(cx, |editor, window, cx| {
13456 struct Type1;
13457 struct Type2;
13458
13459 let buffer = editor.buffer.read(cx).snapshot(cx);
13460
13461 let anchor_range =
13462 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13463
13464 editor.highlight_background::<Type1>(
13465 &[
13466 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13467 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13468 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13469 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13470 ],
13471 |_| Hsla::red(),
13472 cx,
13473 );
13474 editor.highlight_background::<Type2>(
13475 &[
13476 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13477 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13478 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13479 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13480 ],
13481 |_| Hsla::green(),
13482 cx,
13483 );
13484
13485 let snapshot = editor.snapshot(window, cx);
13486 let mut highlighted_ranges = editor.background_highlights_in_range(
13487 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13488 &snapshot,
13489 cx.theme().colors(),
13490 );
13491 // Enforce a consistent ordering based on color without relying on the ordering of the
13492 // highlight's `TypeId` which is non-executor.
13493 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13494 assert_eq!(
13495 highlighted_ranges,
13496 &[
13497 (
13498 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13499 Hsla::red(),
13500 ),
13501 (
13502 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13503 Hsla::red(),
13504 ),
13505 (
13506 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13507 Hsla::green(),
13508 ),
13509 (
13510 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13511 Hsla::green(),
13512 ),
13513 ]
13514 );
13515 assert_eq!(
13516 editor.background_highlights_in_range(
13517 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13518 &snapshot,
13519 cx.theme().colors(),
13520 ),
13521 &[(
13522 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13523 Hsla::red(),
13524 )]
13525 );
13526 });
13527}
13528
13529#[gpui::test]
13530async fn test_following(cx: &mut TestAppContext) {
13531 init_test(cx, |_| {});
13532
13533 let fs = FakeFs::new(cx.executor());
13534 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13535
13536 let buffer = project.update(cx, |project, cx| {
13537 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13538 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13539 });
13540 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13541 let follower = cx.update(|cx| {
13542 cx.open_window(
13543 WindowOptions {
13544 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13545 gpui::Point::new(px(0.), px(0.)),
13546 gpui::Point::new(px(10.), px(80.)),
13547 ))),
13548 ..Default::default()
13549 },
13550 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13551 )
13552 .unwrap()
13553 });
13554
13555 let is_still_following = Rc::new(RefCell::new(true));
13556 let follower_edit_event_count = Rc::new(RefCell::new(0));
13557 let pending_update = Rc::new(RefCell::new(None));
13558 let leader_entity = leader.root(cx).unwrap();
13559 let follower_entity = follower.root(cx).unwrap();
13560 _ = follower.update(cx, {
13561 let update = pending_update.clone();
13562 let is_still_following = is_still_following.clone();
13563 let follower_edit_event_count = follower_edit_event_count.clone();
13564 |_, window, cx| {
13565 cx.subscribe_in(
13566 &leader_entity,
13567 window,
13568 move |_, leader, event, window, cx| {
13569 leader.read(cx).add_event_to_update_proto(
13570 event,
13571 &mut update.borrow_mut(),
13572 window,
13573 cx,
13574 );
13575 },
13576 )
13577 .detach();
13578
13579 cx.subscribe_in(
13580 &follower_entity,
13581 window,
13582 move |_, _, event: &EditorEvent, _window, _cx| {
13583 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13584 *is_still_following.borrow_mut() = false;
13585 }
13586
13587 if let EditorEvent::BufferEdited = event {
13588 *follower_edit_event_count.borrow_mut() += 1;
13589 }
13590 },
13591 )
13592 .detach();
13593 }
13594 });
13595
13596 // Update the selections only
13597 _ = leader.update(cx, |leader, window, cx| {
13598 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13599 });
13600 follower
13601 .update(cx, |follower, window, cx| {
13602 follower.apply_update_proto(
13603 &project,
13604 pending_update.borrow_mut().take().unwrap(),
13605 window,
13606 cx,
13607 )
13608 })
13609 .unwrap()
13610 .await
13611 .unwrap();
13612 _ = follower.update(cx, |follower, _, cx| {
13613 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13614 });
13615 assert!(*is_still_following.borrow());
13616 assert_eq!(*follower_edit_event_count.borrow(), 0);
13617
13618 // Update the scroll position only
13619 _ = leader.update(cx, |leader, window, cx| {
13620 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13621 });
13622 follower
13623 .update(cx, |follower, window, cx| {
13624 follower.apply_update_proto(
13625 &project,
13626 pending_update.borrow_mut().take().unwrap(),
13627 window,
13628 cx,
13629 )
13630 })
13631 .unwrap()
13632 .await
13633 .unwrap();
13634 assert_eq!(
13635 follower
13636 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13637 .unwrap(),
13638 gpui::Point::new(1.5, 3.5)
13639 );
13640 assert!(*is_still_following.borrow());
13641 assert_eq!(*follower_edit_event_count.borrow(), 0);
13642
13643 // Update the selections and scroll position. The follower's scroll position is updated
13644 // via autoscroll, not via the leader's exact scroll position.
13645 _ = leader.update(cx, |leader, window, cx| {
13646 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13647 leader.request_autoscroll(Autoscroll::newest(), cx);
13648 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13649 });
13650 follower
13651 .update(cx, |follower, window, cx| {
13652 follower.apply_update_proto(
13653 &project,
13654 pending_update.borrow_mut().take().unwrap(),
13655 window,
13656 cx,
13657 )
13658 })
13659 .unwrap()
13660 .await
13661 .unwrap();
13662 _ = follower.update(cx, |follower, _, cx| {
13663 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13664 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13665 });
13666 assert!(*is_still_following.borrow());
13667
13668 // Creating a pending selection that precedes another selection
13669 _ = leader.update(cx, |leader, window, cx| {
13670 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13671 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13672 });
13673 follower
13674 .update(cx, |follower, window, cx| {
13675 follower.apply_update_proto(
13676 &project,
13677 pending_update.borrow_mut().take().unwrap(),
13678 window,
13679 cx,
13680 )
13681 })
13682 .unwrap()
13683 .await
13684 .unwrap();
13685 _ = follower.update(cx, |follower, _, cx| {
13686 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13687 });
13688 assert!(*is_still_following.borrow());
13689
13690 // Extend the pending selection so that it surrounds another selection
13691 _ = leader.update(cx, |leader, window, cx| {
13692 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13693 });
13694 follower
13695 .update(cx, |follower, window, cx| {
13696 follower.apply_update_proto(
13697 &project,
13698 pending_update.borrow_mut().take().unwrap(),
13699 window,
13700 cx,
13701 )
13702 })
13703 .unwrap()
13704 .await
13705 .unwrap();
13706 _ = follower.update(cx, |follower, _, cx| {
13707 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13708 });
13709
13710 // Scrolling locally breaks the follow
13711 _ = follower.update(cx, |follower, window, cx| {
13712 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13713 follower.set_scroll_anchor(
13714 ScrollAnchor {
13715 anchor: top_anchor,
13716 offset: gpui::Point::new(0.0, 0.5),
13717 },
13718 window,
13719 cx,
13720 );
13721 });
13722 assert!(!(*is_still_following.borrow()));
13723}
13724
13725#[gpui::test]
13726async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13727 init_test(cx, |_| {});
13728
13729 let fs = FakeFs::new(cx.executor());
13730 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13732 let pane = workspace
13733 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13734 .unwrap();
13735
13736 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13737
13738 let leader = pane.update_in(cx, |_, window, cx| {
13739 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13740 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13741 });
13742
13743 // Start following the editor when it has no excerpts.
13744 let mut state_message =
13745 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13746 let workspace_entity = workspace.root(cx).unwrap();
13747 let follower_1 = cx
13748 .update_window(*workspace.deref(), |_, window, cx| {
13749 Editor::from_state_proto(
13750 workspace_entity,
13751 ViewId {
13752 creator: CollaboratorId::PeerId(PeerId::default()),
13753 id: 0,
13754 },
13755 &mut state_message,
13756 window,
13757 cx,
13758 )
13759 })
13760 .unwrap()
13761 .unwrap()
13762 .await
13763 .unwrap();
13764
13765 let update_message = Rc::new(RefCell::new(None));
13766 follower_1.update_in(cx, {
13767 let update = update_message.clone();
13768 |_, window, cx| {
13769 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13770 leader.read(cx).add_event_to_update_proto(
13771 event,
13772 &mut update.borrow_mut(),
13773 window,
13774 cx,
13775 );
13776 })
13777 .detach();
13778 }
13779 });
13780
13781 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13782 (
13783 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13784 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13785 )
13786 });
13787
13788 // Insert some excerpts.
13789 leader.update(cx, |leader, cx| {
13790 leader.buffer.update(cx, |multibuffer, cx| {
13791 multibuffer.set_excerpts_for_path(
13792 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13793 buffer_1.clone(),
13794 vec![
13795 Point::row_range(0..3),
13796 Point::row_range(1..6),
13797 Point::row_range(12..15),
13798 ],
13799 0,
13800 cx,
13801 );
13802 multibuffer.set_excerpts_for_path(
13803 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13804 buffer_2.clone(),
13805 vec![Point::row_range(0..6), Point::row_range(8..12)],
13806 0,
13807 cx,
13808 );
13809 });
13810 });
13811
13812 // Apply the update of adding the excerpts.
13813 follower_1
13814 .update_in(cx, |follower, window, cx| {
13815 follower.apply_update_proto(
13816 &project,
13817 update_message.borrow().clone().unwrap(),
13818 window,
13819 cx,
13820 )
13821 })
13822 .await
13823 .unwrap();
13824 assert_eq!(
13825 follower_1.update(cx, |editor, cx| editor.text(cx)),
13826 leader.update(cx, |editor, cx| editor.text(cx))
13827 );
13828 update_message.borrow_mut().take();
13829
13830 // Start following separately after it already has excerpts.
13831 let mut state_message =
13832 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13833 let workspace_entity = workspace.root(cx).unwrap();
13834 let follower_2 = cx
13835 .update_window(*workspace.deref(), |_, window, cx| {
13836 Editor::from_state_proto(
13837 workspace_entity,
13838 ViewId {
13839 creator: CollaboratorId::PeerId(PeerId::default()),
13840 id: 0,
13841 },
13842 &mut state_message,
13843 window,
13844 cx,
13845 )
13846 })
13847 .unwrap()
13848 .unwrap()
13849 .await
13850 .unwrap();
13851 assert_eq!(
13852 follower_2.update(cx, |editor, cx| editor.text(cx)),
13853 leader.update(cx, |editor, cx| editor.text(cx))
13854 );
13855
13856 // Remove some excerpts.
13857 leader.update(cx, |leader, cx| {
13858 leader.buffer.update(cx, |multibuffer, cx| {
13859 let excerpt_ids = multibuffer.excerpt_ids();
13860 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13861 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13862 });
13863 });
13864
13865 // Apply the update of removing the excerpts.
13866 follower_1
13867 .update_in(cx, |follower, window, cx| {
13868 follower.apply_update_proto(
13869 &project,
13870 update_message.borrow().clone().unwrap(),
13871 window,
13872 cx,
13873 )
13874 })
13875 .await
13876 .unwrap();
13877 follower_2
13878 .update_in(cx, |follower, window, cx| {
13879 follower.apply_update_proto(
13880 &project,
13881 update_message.borrow().clone().unwrap(),
13882 window,
13883 cx,
13884 )
13885 })
13886 .await
13887 .unwrap();
13888 update_message.borrow_mut().take();
13889 assert_eq!(
13890 follower_1.update(cx, |editor, cx| editor.text(cx)),
13891 leader.update(cx, |editor, cx| editor.text(cx))
13892 );
13893}
13894
13895#[gpui::test]
13896async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13897 init_test(cx, |_| {});
13898
13899 let mut cx = EditorTestContext::new(cx).await;
13900 let lsp_store =
13901 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13902
13903 cx.set_state(indoc! {"
13904 ˇfn func(abc def: i32) -> u32 {
13905 }
13906 "});
13907
13908 cx.update(|_, cx| {
13909 lsp_store.update(cx, |lsp_store, cx| {
13910 lsp_store
13911 .update_diagnostics(
13912 LanguageServerId(0),
13913 lsp::PublishDiagnosticsParams {
13914 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13915 version: None,
13916 diagnostics: vec![
13917 lsp::Diagnostic {
13918 range: lsp::Range::new(
13919 lsp::Position::new(0, 11),
13920 lsp::Position::new(0, 12),
13921 ),
13922 severity: Some(lsp::DiagnosticSeverity::ERROR),
13923 ..Default::default()
13924 },
13925 lsp::Diagnostic {
13926 range: lsp::Range::new(
13927 lsp::Position::new(0, 12),
13928 lsp::Position::new(0, 15),
13929 ),
13930 severity: Some(lsp::DiagnosticSeverity::ERROR),
13931 ..Default::default()
13932 },
13933 lsp::Diagnostic {
13934 range: lsp::Range::new(
13935 lsp::Position::new(0, 25),
13936 lsp::Position::new(0, 28),
13937 ),
13938 severity: Some(lsp::DiagnosticSeverity::ERROR),
13939 ..Default::default()
13940 },
13941 ],
13942 },
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]
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 counter.fetch_add(1, atomic::Ordering::Release);
21916 assert_eq!(
21917 params.text_document.uri,
21918 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
21919 );
21920 async move {
21921 Ok(lsp::DocumentDiagnosticReportResult::Report(
21922 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
21923 related_documents: None,
21924 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
21925 items: Vec::new(),
21926 result_id: None,
21927 },
21928 }),
21929 ))
21930 }
21931 });
21932
21933 cx.executor().advance_clock(Duration::from_millis(60));
21934 cx.executor().run_until_parked();
21935 assert_eq!(
21936 diagnostic_requests.load(atomic::Ordering::Acquire),
21937 1,
21938 "Opening file should trigger diagnostic request"
21939 );
21940 first_request
21941 .next()
21942 .await
21943 .expect("should have sent the first diagnostics pull request");
21944
21945 // Editing should trigger diagnostics
21946 editor.update_in(cx, |editor, window, cx| {
21947 editor.handle_input("2", window, cx)
21948 });
21949 cx.executor().advance_clock(Duration::from_millis(60));
21950 cx.executor().run_until_parked();
21951 assert_eq!(
21952 diagnostic_requests.load(atomic::Ordering::Acquire),
21953 2,
21954 "Editing should trigger diagnostic request"
21955 );
21956
21957 // Moving cursor should not trigger diagnostic request
21958 editor.update_in(cx, |editor, window, cx| {
21959 editor.change_selections(None, window, cx, |s| {
21960 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
21961 });
21962 });
21963 cx.executor().advance_clock(Duration::from_millis(60));
21964 cx.executor().run_until_parked();
21965 assert_eq!(
21966 diagnostic_requests.load(atomic::Ordering::Acquire),
21967 2,
21968 "Cursor movement should not trigger diagnostic request"
21969 );
21970
21971 // Multiple rapid edits should be debounced
21972 for _ in 0..5 {
21973 editor.update_in(cx, |editor, window, cx| {
21974 editor.handle_input("x", window, cx)
21975 });
21976 }
21977 cx.executor().advance_clock(Duration::from_millis(60));
21978 cx.executor().run_until_parked();
21979
21980 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
21981 assert!(
21982 final_requests <= 4,
21983 "Multiple rapid edits should be debounced (got {} requests)",
21984 final_requests
21985 );
21986}